mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 10:08:57 +00:00
Compare commits
233 Commits
rel/2.0.0-
...
2.1.0-prev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22297a4db | ||
|
|
c42db123bd | ||
|
|
8583f205f9 | ||
|
|
ae4c4d6e8f | ||
|
|
8553647ce8 | ||
|
|
67560266ab | ||
|
|
ce49379afb | ||
|
|
4dc05d96ac | ||
|
|
53742ad649 | ||
|
|
91a1d83acd | ||
|
|
1d504e4565 | ||
|
|
873cfa9adf | ||
|
|
41b8642c2d | ||
|
|
d6b67ca75d | ||
|
|
4af70a0907 | ||
|
|
bf5f40b1ed | ||
|
|
a515f6bb0a | ||
|
|
12a2314a5e | ||
|
|
f35c814fc7 | ||
|
|
dbaa453d18 | ||
|
|
b2373e157e | ||
|
|
08c2f231ea | ||
|
|
6274733565 | ||
|
|
5f6f288056 | ||
|
|
78e583d0fb | ||
|
|
7c07beb494 | ||
|
|
e7ffb8bb71 | ||
|
|
3e6f7f3e45 | ||
|
|
0d83504863 | ||
|
|
370b5f7341 | ||
|
|
0b53b92bc6 | ||
|
|
116f33c66c | ||
|
|
c3964a0437 | ||
|
|
f291f87dfc | ||
|
|
a1c2c18326 | ||
|
|
d6588c31bf | ||
|
|
15d2f5a898 | ||
|
|
814441c933 | ||
|
|
a98c1459b5 | ||
|
|
975d537a0a | ||
|
|
4af2e8670e | ||
|
|
160c91f1b9 | ||
|
|
8ded472fe9 | ||
|
|
f9c62ebb5a | ||
|
|
d5a664e481 | ||
|
|
74512dc3b2 | ||
|
|
e6285f30ae | ||
|
|
bd5793e284 | ||
|
|
02bbcb68f1 | ||
|
|
18140929e7 | ||
|
|
50ba6114ee | ||
|
|
cd3e3c667c | ||
|
|
0de9f0e3ce | ||
|
|
9b1509a52b | ||
|
|
a8809f9a96 | ||
|
|
68c4620a55 | ||
|
|
296435e40c | ||
|
|
aeabbdcada | ||
|
|
96d7f85327 | ||
|
|
30333e250a | ||
|
|
08002e961b | ||
|
|
0c77224f46 | ||
|
|
a83ec3a053 | ||
|
|
a16343681b | ||
|
|
c8b337ebaa | ||
|
|
7bf5516bb2 | ||
|
|
2d98a1808c | ||
|
|
e583a17ef8 | ||
|
|
ba0d82d801 | ||
|
|
64389a9bbe | ||
|
|
86e94d7812 | ||
|
|
9f05a3d34b | ||
|
|
63e0af2ee8 | ||
|
|
e67a30132f | ||
|
|
d51bef194c | ||
|
|
dc5e980efa | ||
|
|
e0ab3ddcca | ||
|
|
0c058894c2 | ||
|
|
98385cbcb0 | ||
|
|
77cac3b6be | ||
|
|
051150475f | ||
|
|
78436adb08 | ||
|
|
09317b83a8 | ||
|
|
a0269fb0ad | ||
|
|
64ed1c7945 | ||
|
|
04fe1204a9 | ||
|
|
e2030fb1fa | ||
|
|
e5f1299239 | ||
|
|
c922eee1d6 | ||
|
|
8b243e8cc7 | ||
|
|
28920c7691 | ||
|
|
412ec1b427 | ||
|
|
c62a3b491c | ||
|
|
559832bb6d | ||
|
|
45d645931b | ||
|
|
8d6119f31d | ||
|
|
0291686b20 | ||
|
|
7c52be5e42 | ||
|
|
900e9ca835 | ||
|
|
ad758b1060 | ||
|
|
cd9ad38a99 | ||
|
|
e057cb35ec | ||
|
|
eea2066a6d | ||
|
|
d6ae8829b6 | ||
|
|
a94ac6f37e | ||
|
|
a40adab38d | ||
|
|
c2a284d5b8 | ||
|
|
fc398d602a | ||
|
|
90c59ff4e7 | ||
|
|
a7e715c88f | ||
|
|
6dddc9d01d | ||
|
|
128683be0e | ||
|
|
5ed1a35ce0 | ||
|
|
680ba7497a | ||
|
|
287c10fd2e | ||
|
|
63f7ac9330 | ||
|
|
d2858beaa1 | ||
|
|
5f1450c9ba | ||
|
|
c83605baff | ||
|
|
fc12d722b8 | ||
|
|
372e597f34 | ||
|
|
3715ec7c3f | ||
|
|
d2eaa36372 | ||
|
|
169ef12cd8 | ||
|
|
f5d58f3f2e | ||
|
|
c2e4d4f261 | ||
|
|
50481fe23f | ||
|
|
895a61160e | ||
|
|
b8b769aa74 | ||
|
|
c4aad6bcab | ||
|
|
27f1d07d21 | ||
|
|
0cb14a3c68 | ||
|
|
2457b4ee5d | ||
|
|
789ea5a320 | ||
|
|
a902874754 | ||
|
|
f43ea777eb | ||
|
|
c79db4e8e2 | ||
|
|
d1198aeab2 | ||
|
|
9528dd7432 | ||
|
|
b8c006a3e9 | ||
|
|
a9ddf1413f | ||
|
|
8b37dc8561 | ||
|
|
7b07fb66eb | ||
|
|
cdb04c74f9 | ||
|
|
a74941e3c8 | ||
|
|
a0a710a0df | ||
|
|
576e3debf1 | ||
|
|
d5f5ad7fdc | ||
|
|
dab0faea66 | ||
|
|
2df0febfba | ||
|
|
e65ecebac6 | ||
|
|
bb0727c34c | ||
|
|
4903e12373 | ||
|
|
56c806b34e | ||
|
|
8acba88160 | ||
|
|
b434eefd83 | ||
|
|
44f86eb8a1 | ||
|
|
44360b6955 | ||
|
|
22ff88ab79 | ||
|
|
70d89b9ad3 | ||
|
|
72b1e627b0 | ||
|
|
c94563c700 | ||
|
|
781c5dc37c | ||
|
|
814df07034 | ||
|
|
c2f63f21fd | ||
|
|
4492d3eab7 | ||
|
|
efdfbf5dbb | ||
|
|
6f2e408083 | ||
|
|
70a4a68e7c | ||
|
|
a1942edbfe | ||
|
|
fd90301b1c | ||
|
|
58145d33e3 | ||
|
|
2135c30d66 | ||
|
|
5191531813 | ||
|
|
03018f32b7 | ||
|
|
e632d2bb69 | ||
|
|
7a56f643f7 | ||
|
|
056b955f52 | ||
|
|
0993f46c9e | ||
|
|
4ba0358987 | ||
|
|
721e3c45fd | ||
|
|
27f59c6971 | ||
|
|
943fb2f482 | ||
|
|
2c4bc302de | ||
|
|
5b634ee33c | ||
|
|
6e1fa2b2c3 | ||
|
|
f72ebbd66e | ||
|
|
391ceec559 | ||
|
|
076b5304e0 | ||
|
|
94fc84a9b4 | ||
|
|
9cce26ebd8 | ||
|
|
513d288a5d | ||
|
|
429a432dd3 | ||
|
|
76928bcbaa | ||
|
|
95c81729cd | ||
|
|
8352ec7626 | ||
|
|
bebb82eb69 | ||
|
|
3da87dd3d3 | ||
|
|
117c1a6cbd | ||
|
|
53f5a77490 | ||
|
|
5fd9b93324 | ||
|
|
55bd0c05cb | ||
|
|
f9068819be | ||
|
|
85bf621386 | ||
|
|
328eb0451b | ||
|
|
bc7742148e | ||
|
|
4ceed817a3 | ||
|
|
6d85e752e1 | ||
|
|
460f202832 | ||
|
|
73054559cc | ||
|
|
d007a3fbba | ||
|
|
fcd3cf9dd7 | ||
|
|
48bca777fe | ||
|
|
aadcfa1fb7 | ||
|
|
f683b39806 | ||
|
|
ed7e07a057 | ||
|
|
749d112b08 | ||
|
|
90922fb3c6 | ||
|
|
bcb359e355 | ||
|
|
a7a3a359fa | ||
|
|
627da65692 | ||
|
|
67ba6271a0 | ||
|
|
32d9bdee6c | ||
|
|
e50d4825c3 | ||
|
|
06c94d54eb | ||
|
|
3685592b01 | ||
|
|
2acdad38cd | ||
|
|
e331355c1d | ||
|
|
d8c0bd8898 | ||
|
|
0626bb1594 | ||
|
|
d9e75c89ae | ||
|
|
ebe9d8ae92 | ||
|
|
d42cd10975 |
19
.appveyor.yml
Executable file
19
.appveyor.yml
Executable file
@@ -0,0 +1,19 @@
|
||||
init:
|
||||
- git config --global core.autocrlf true
|
||||
install:
|
||||
- ps: Install-Product node 6.9.2 x64
|
||||
branches:
|
||||
only:
|
||||
- dev
|
||||
- /^release\/.*$/
|
||||
- /^(.*\/)?ci-.*$/
|
||||
build_script:
|
||||
- ps: .\run.ps1 default-build
|
||||
clone_depth: 1
|
||||
environment:
|
||||
global:
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
test: 'off'
|
||||
deploy: 'off'
|
||||
os: Visual Studio 2017
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -24,19 +24,8 @@ nuget.exe
|
||||
*.ncrunchsolution
|
||||
*.*sdf
|
||||
*.ipch
|
||||
|
||||
.vs/
|
||||
npm-debug.log
|
||||
/.build/
|
||||
|
||||
# The templates can't contain their own .gitignore files, because Yeoman has strange default handling for
|
||||
# files with that name (https://github.com/npm/npm/issues/1862). So, each template instead has a template_gitignore
|
||||
# file which gets renamed after the files are copied. And so any files that need to be excluded in the source
|
||||
# repo have to be excluded here.
|
||||
/templates/*/node_modules/
|
||||
/templates/*/wwwroot/dist/
|
||||
/templates/*/ClientApp/dist/
|
||||
/templates/*/yarn.lock
|
||||
.vscode/
|
||||
|
||||
/templates/*/Properties/launchSettings.json
|
||||
global.json
|
||||
|
||||
@@ -17,3 +17,8 @@ os:
|
||||
osx_image: xcode7.1
|
||||
script:
|
||||
- ./build.sh
|
||||
branches:
|
||||
only:
|
||||
- dev
|
||||
- /^release\/.*$/
|
||||
- /^(.*\/)?ci-.*$/
|
||||
|
||||
16
Directory.Build.props
Normal file
16
Directory.Build.props
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project>
|
||||
<Import Project="version.props" />
|
||||
<Import Project="build\dependencies.props" />
|
||||
<Import Project="build\sources.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Product>Microsoft ASP.NET Core</Product>
|
||||
<RepositoryUrl>https://github.com/aspnet/javascriptservices</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
|
||||
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
6
Directory.Build.targets
Normal file
6
Directory.Build.targets
Normal file
@@ -0,0 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
|
||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,9 +1,12 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26430.4
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
VisualStudioVersion = 15.0.26730.16
|
||||
MinimumVisualStudioVersion = 15.0.26730.03
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{27304DDE-AFB2-4F8B-B765-E3E2F11E886C}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
src\Directory.Build.props = src\Directory.Build.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.NodeServices", "src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj", "{66B77203-1469-41DF-92F2-2BE6900BD36F}"
|
||||
EndProject
|
||||
@@ -28,14 +31,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Webpack", "samples\misc\Web
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodeServicesExamples", "samples\misc\NodeServicesExamples\NodeServicesExamples.csproj", "{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{1598B415-73F1-4B37-B3B4-0A10677ABB2D}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E415FE14-13B0-469F-836D-95059E6BAA6E}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{645F7363-1240-4FB6-9422-B32A327C979F}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
src\build\common.props = src\build\common.props
|
||||
src\build\Key.snk = src\build\Key.snk
|
||||
Directory.Build.props = Directory.Build.props
|
||||
Directory.Build.targets = Directory.Build.targets
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Extensions", "src\Microsoft.AspNetCore.SpaServices.Extensions\Microsoft.AspNetCore.SpaServices.Extensions.csproj", "{D40BD1C4-6A6F-4213-8535-1057F3EB3400}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -66,6 +69,10 @@ Global
|
||||
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D40BD1C4-6A6F-4213-8535-1057F3EB3400}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -78,5 +85,9 @@ Global
|
||||
{1931B19A-EC42-4D56-B2D0-FB06D17244DA} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
|
||||
{DE479DC3-1461-4EAD-A188-4AF7AA4AE344} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
|
||||
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
|
||||
{D40BD1C4-6A6F-4213-8535-1057F3EB3400} = {27304DDE-AFB2-4F8B-B765-E3E2F11E886C}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DDF59B0D-2DEC-45D6-8667-DCB767487101}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
Copyright (c) .NET Foundation. All rights reserved.
|
||||
Copyright (c) .NET Foundation and Contributors
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
these files except in compliance with the License. You may obtain a copy of the
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="AspNetCorePreview2Final" value="https://dotnet.myget.org/F/dotnet-2-0-0-preview2-final/api/v3/index.json" />
|
||||
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-release/api/v3/index.json" />
|
||||
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
|
||||
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
||||
<!-- Restore sources should be defined in build/sources.props. -->
|
||||
</packageSources>
|
||||
</configuration>
|
||||
7
NuGetPackageVerifier.json
Normal file
7
NuGetPackageVerifier.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"Default": {
|
||||
"rules": [
|
||||
"DefaultCompositeRule"
|
||||
]
|
||||
}
|
||||
}
|
||||
48
README.md
48
README.md
@@ -24,29 +24,37 @@ This repo contains:
|
||||
* Server-side and client-side routing integration ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#routing-helper-mapspafallbackroute))
|
||||
* Server-side and client-side validation integration
|
||||
* "Lazy loading" for Knockout apps
|
||||
* A Yeoman generator that creates preconfigured app starting points ([guide](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/))
|
||||
* Samples and docs
|
||||
|
||||
It's cross-platform (Windows, Linux, or macOS) and works with .NET Core 1.0.1 or later.
|
||||
It's cross-platform (Windows, Linux, or macOS) and works with .NET Core 2.0 or later.
|
||||
|
||||
## Creating new applications
|
||||
|
||||
If you want to build a brand-new ASP.NET Core app that uses Angular / React / Knockout on the client, consider starting with the `aspnetcore-spa` generator. This lets you choose your client-side framework. It generates a starting point that includes applicable features such as Webpack dev middleware, server-side prerendering, and efficient production builds. It's much easier than configuring everything to work together manually!
|
||||
Prerequisites:
|
||||
|
||||
To do this, install Yeoman and these generator templates:
|
||||
* [.NET Core 2.0](https://www.microsoft.com/net/core) (or later) SDK
|
||||
* [Node.js](https://nodejs.org/) version 6 (or later)
|
||||
|
||||
npm install -g yo generator-aspnetcore-spa
|
||||
With these prerequisites, you can immediately create new ASP.NET Core applications that use Angular, React, or React+Redux without having to install anything extra.
|
||||
|
||||
Generate your new application starting point:
|
||||
### Option 1: Creating Angular/React/Redux applications from the command line (cross-platform)
|
||||
|
||||
cd some-empty-directory
|
||||
yo aspnetcore-spa
|
||||
In an empty directory, run (for example) `dotnet new angular`. Other supported SPA frameworks include React and React+Redux. You can see the list of available SPA templates by running `dotnet new spa`.
|
||||
|
||||
Once the generator has run and restored all the dependencies, you can start up your new ASP.NET Core SPA:
|
||||
|
||||
npm install
|
||||
dotnet run
|
||||
|
||||
For a more detailed walkthrough, see [getting started with the `aspnetcore-spa` generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/).
|
||||
### Option 2: Creating Angular/React/Redux applications using Visual Studio 2017 Update 3 or later (Windows only)
|
||||
|
||||
Using the `File`->`New Project` dialog, select *ASP.NET Core Web Application*. You will then be offered the option to create an application with Angular, React, or React+Redux. When the application is created, you can build and run it in the normal way.
|
||||
|
||||
### More info and other SPA frameworks
|
||||
|
||||
For a more detailed (albeit somewhat outdated) walkthrough, see [getting started with the `aspnetcore-spa` generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/).
|
||||
|
||||
If you want to build an ASP.NET Core application with Aurelia, Knockout, or Vue, you can use the `Microsoft.AspNetCore.SpaTemplates` package. On the command line, run `dotnet new --install Microsoft.AspNetCore.SpaTemplates`. Then you will be able to run `dotnet new aurelia` (or `dotnet new vue`, etc.) to create your new application.
|
||||
|
||||
## Adding to existing applications
|
||||
|
||||
@@ -58,18 +66,13 @@ If you have an existing ASP.NET Core application, or if you just want to use the
|
||||
* Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.NodeServices#microsoftaspnetcorenodeservices).
|
||||
* `Microsoft.AspNetCore.SpaServices`
|
||||
* This provides infrastructure that's generally useful when building Single Page Applications (SPAs) with technologies such as Angular or React (for example, server-side prerendering and webpack middleware). Internally, it uses the `NodeServices` package to implement its features.
|
||||
* Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#microsoftaspnetcorespaservices).
|
||||
* `Microsoft.AspNetCore.AngularServices`
|
||||
* This builds on the `SpaServices` package and includes features specific to Angular. Currently, this includes validation helpers.
|
||||
* The code is [here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.AngularServices). You'll find a usage example for [the validation helper here](https://github.com/aspnet/JavaScriptServices/blob/dev/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts).
|
||||
* Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#microsoftaspnetcorespaservices)
|
||||
|
||||
There was previously a `Microsoft.AspNetCore.ReactServices` but this is not currently needed - all applicable functionality is in `Microsoft.AspNetCore.SpaServices`, because it's sufficiently general. We might add a new `Microsoft.AspNetCore.ReactServices` package in the future if new React-specific requirements emerge.
|
||||
There were previously other packages called `Microsoft.AspNetCore.AngularServices` and `Microsoft.AspNetCore.ReactServices` but these are not currently needed - all applicable functionality is in `Microsoft.AspNetCore.SpaServices`, because it's sufficiently general.
|
||||
|
||||
If you want to build a helper library for some other SPA framework, you can do so by taking a dependency on `Microsoft.AspNetCore.SpaServices` and wrapping its functionality in whatever way is most useful for your SPA framework.
|
||||
|
||||
## Samples and templates
|
||||
|
||||
Inside this repo, [the `templates` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/templates) contains the application starting points that the `aspnetcore-spa` generator emits. You can clone this repo and run those applications directly. But it's easier to [use the Yeoman tool to run the generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/).
|
||||
## Samples
|
||||
|
||||
The [`samples` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/samples) contains examples of:
|
||||
|
||||
@@ -88,13 +91,6 @@ The [`samples` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing to the various packages, samples, and project templates in this repo, that's great! You can run the code in this repo as follows:
|
||||
If you're interested in contributing to the various packages, samples, and project templates in this repo, that's great!
|
||||
|
||||
* Clone the repo
|
||||
* Run `dotnet restore` at the repo root dir
|
||||
* Go to whatever sample or template you want to run (for example, `cd templates/AngularSpa`)
|
||||
* Restore NPM dependencies (run `npm install`)
|
||||
* If the sample/template you're trying to run has a file called `webpack.config.vendor.js` at its root, run `webpack --config webpack.config.vendor.js`. It it has a file called `webpack.config.js`, run `webpack` (no args). You might need to install webpack first, by running `npm install -g webpack`.
|
||||
* Launch it (`dotnet run`)
|
||||
|
||||
If you're planning to submit a pull request, and if it's more than a trivial fix (for example, for a typo), it's usually a good idea first to file an issue describing what you're proposing to do and how it will work. Then you can find out if it's likely that such a pull request will be accepted, and how it fits into wider ongoing plans.
|
||||
Before working on a pull request, especially if it's more than a trivial fix (for example, for a typo), it's usually a good idea first to file an issue describing what you're proposing to do and how it will work. Then you can find out if it's likely that such a pull request will be accepted, and how it fits into wider ongoing plans.
|
||||
|
||||
40
appveyor.yml
40
appveyor.yml
@@ -1,40 +0,0 @@
|
||||
init:
|
||||
- git config --global core.autocrlf true
|
||||
install:
|
||||
- ps: Install-Product node 6.9.2 x64
|
||||
# .NET Core SDK binaries
|
||||
# Download .NET Core 2.0 Preview 1 SDK and add to PATH
|
||||
- ps: $urlCurrent = "https://download.microsoft.com/download/0/6/5/0656B047-5F2F-4281-A851-F30776F8616D/dotnet-dev-win-x64.2.0.0-preview1-005977.zip"
|
||||
- ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk"
|
||||
- ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null
|
||||
- ps: $tempFileCurrent = [System.IO.Path]::GetTempFileName()
|
||||
- ps: (New-Object System.Net.WebClient).DownloadFile($urlCurrent, $tempFileCurrent)
|
||||
- ps: Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory($tempFileCurrent, $env:DOTNET_INSTALL_DIR)
|
||||
- ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path"
|
||||
build_script:
|
||||
- ps: Push-Location
|
||||
- cd templates/package-builder
|
||||
- npm install
|
||||
- npm run build
|
||||
- ps: Pop-Location
|
||||
artifacts:
|
||||
- path: templates\package-builder\dist\artifacts\generator-aspnetcore-spa.tar.gz
|
||||
name: generator-aspnetcore-spa
|
||||
- path: templates\package-builder\dist\artifacts\*.nupkg
|
||||
name: Microsoft.AspNetCore.SpaTemplates
|
||||
type: NuGetPackage
|
||||
# - ps: .\build.ps1
|
||||
clone_depth: 1
|
||||
test_script:
|
||||
- dotnet restore
|
||||
- ps: Push-Location
|
||||
- cd test
|
||||
- npm install selenium-standalone
|
||||
- ps: Start-Process node './start-selenium.js'
|
||||
- npm install
|
||||
- npm test
|
||||
on_finish :
|
||||
- ps: Pop-Location
|
||||
# After running tests, upload results to Appveyor
|
||||
- ps: (new-object net.webclient).UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test\tmp\junit\*.xml))
|
||||
deploy: off
|
||||
@@ -1,2 +1,2 @@
|
||||
@ECHO OFF
|
||||
PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE"
|
||||
PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE"
|
||||
|
||||
67
build.ps1
67
build.ps1
@@ -1,67 +0,0 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries)
|
||||
{
|
||||
while($true)
|
||||
{
|
||||
try
|
||||
{
|
||||
Invoke-WebRequest $url -OutFile $downloadLocation
|
||||
break
|
||||
}
|
||||
catch
|
||||
{
|
||||
$exceptionMessage = $_.Exception.Message
|
||||
Write-Host "Failed to download '$url': $exceptionMessage"
|
||||
if ($retries -gt 0) {
|
||||
$retries--
|
||||
Write-Host "Waiting 10 seconds before retrying. Retries left: $retries"
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
$exception = $_.Exception
|
||||
throw $exception
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cd $PSScriptRoot
|
||||
|
||||
$repoFolder = $PSScriptRoot
|
||||
$env:REPO_FOLDER = $repoFolder
|
||||
|
||||
$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0-preview2.zip"
|
||||
if ($env:KOREBUILD_ZIP)
|
||||
{
|
||||
$koreBuildZip=$env:KOREBUILD_ZIP
|
||||
}
|
||||
|
||||
$buildFolder = ".build"
|
||||
$buildFile="$buildFolder\KoreBuild.ps1"
|
||||
|
||||
if (!(Test-Path $buildFolder)) {
|
||||
Write-Host "Downloading KoreBuild from $koreBuildZip"
|
||||
|
||||
$tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid()
|
||||
New-Item -Path "$tempFolder" -Type directory | Out-Null
|
||||
|
||||
$localZipFile="$tempFolder\korebuild.zip"
|
||||
|
||||
DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder)
|
||||
|
||||
New-Item -Path "$buildFolder" -Type directory | Out-Null
|
||||
copy-item "$tempFolder\**\build\*" $buildFolder -Recurse
|
||||
|
||||
# Cleanup
|
||||
if (Test-Path $tempFolder) {
|
||||
Remove-Item -Recurse -Force $tempFolder
|
||||
}
|
||||
}
|
||||
|
||||
&"$buildFile" @args
|
||||
48
build.sh
48
build.sh
@@ -1,46 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd $repoFolder
|
||||
|
||||
koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0-preview2.zip"
|
||||
if [ ! -z $KOREBUILD_ZIP ]; then
|
||||
koreBuildZip=$KOREBUILD_ZIP
|
||||
fi
|
||||
set -euo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
buildFolder=".build"
|
||||
buildFile="$buildFolder/KoreBuild.sh"
|
||||
|
||||
if test ! -d $buildFolder; then
|
||||
echo "Downloading KoreBuild from $koreBuildZip"
|
||||
|
||||
tempFolder="/tmp/KoreBuild-$(uuidgen)"
|
||||
mkdir $tempFolder
|
||||
|
||||
localZipFile="$tempFolder/korebuild.zip"
|
||||
|
||||
retries=6
|
||||
until (wget -O $localZipFile $koreBuildZip 2>/dev/null || curl -o $localZipFile --location $koreBuildZip 2>/dev/null)
|
||||
do
|
||||
echo "Failed to download '$koreBuildZip'"
|
||||
if [ "$retries" -le 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
echo "Waiting 10 seconds before retrying. Retries left: $retries"
|
||||
sleep 10s
|
||||
done
|
||||
|
||||
unzip -q -d $tempFolder $localZipFile
|
||||
|
||||
mkdir $buildFolder
|
||||
cp -r $tempFolder/**/build/** $buildFolder
|
||||
|
||||
chmod +x $buildFile
|
||||
|
||||
# Cleanup
|
||||
if test -d $tempFolder; then
|
||||
rm -rf $tempFolder
|
||||
fi
|
||||
fi
|
||||
|
||||
$buildFile -r $repoFolder "$@"
|
||||
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
||||
chmod +x "$DIR/run.sh"; sync
|
||||
"$DIR/run.sh" default-build "$@"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<Project>
|
||||
<Import Project="dependencies.props" />
|
||||
<Import Project="..\version.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Product>Microsoft ASP.NET Core</Product>
|
||||
<RepositoryUrl>https://github.com/aspnet/javascriptservices</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)Key.snk</AssemblyOriginatorKeyFile>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||
<VersionSuffix Condition="'$(VersionSuffix)'!='' AND '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Internal.AspNetCore.Sdk" Version="$(InternalAspNetCoreSdkVersion)" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETFramework'">
|
||||
<PackageReference Include="NETStandard.Library" Version="$(BundledNETStandardPackageVersion)" />
|
||||
<PackageReference Include="NETStandard.Library.NETFramework" Version="$(NETStandardLibraryNETFrameworkVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,12 +1,27 @@
|
||||
<Project>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<AspNetCoreVersion>2.0.0-preview2-*</AspNetCoreVersion>
|
||||
<AutoMapperVersion>5.0.2</AutoMapperVersion>
|
||||
<InternalAspNetCoreSdkVersion>2.1.0-*</InternalAspNetCoreSdkVersion>
|
||||
<JsonNetVersion>10.0.1</JsonNetVersion>
|
||||
<NETStandardImplicitPackageVersion>$(BundledNETStandardPackageVersion)</NETStandardImplicitPackageVersion>
|
||||
<NETStandardLibraryNETFrameworkVersion>2.0.0-*</NETStandardLibraryNETFrameworkVersion>
|
||||
<RuntimeFrameworkVersion Condition="'$(TargetFramework)'=='netcoreapp2.0'">2.0.0-*</RuntimeFrameworkVersion>
|
||||
<ThreadingDataflowVersion>4.7.0</ThreadingDataflowVersion>
|
||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Package Versions">
|
||||
<InternalAspNetCoreSdkPackageVersion>2.1.0-preview2-15749</InternalAspNetCoreSdkPackageVersion>
|
||||
<MicrosoftAspNetCoreDiagnosticsPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreDiagnosticsPackageVersion>
|
||||
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
|
||||
<MicrosoftAspNetCoreHostingPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreHostingPackageVersion>
|
||||
<MicrosoftAspNetCoreMvcPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreMvcPackageVersion>
|
||||
<MicrosoftAspNetCoreMvcTagHelpersPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreMvcTagHelpersPackageVersion>
|
||||
<MicrosoftAspNetCoreMvcViewFeaturesPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreMvcViewFeaturesPackageVersion>
|
||||
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
||||
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
||||
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreStaticFilesPackageVersion>
|
||||
<MicrosoftAspNetCoreWebSocketsPackageVersion>2.1.0-preview2-30478</MicrosoftAspNetCoreWebSocketsPackageVersion>
|
||||
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.0-preview2-30478</MicrosoftExtensionsDependencyInjectionPackageVersion>
|
||||
<MicrosoftExtensionsFileProvidersPhysicalPackageVersion>2.1.0-preview2-30478</MicrosoftExtensionsFileProvidersPhysicalPackageVersion>
|
||||
<MicrosoftExtensionsLoggingConsolePackageVersion>2.1.0-preview2-30478</MicrosoftExtensionsLoggingConsolePackageVersion>
|
||||
<MicrosoftExtensionsLoggingDebugPackageVersion>2.1.0-preview2-30478</MicrosoftExtensionsLoggingDebugPackageVersion>
|
||||
<MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
|
||||
<MicrosoftNETCoreApp21PackageVersion>2.1.0-preview2-26326-03</MicrosoftNETCoreApp21PackageVersion>
|
||||
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
|
||||
<SystemThreadingTasksDataflowPackageVersion>4.9.0-preview2-26326-04</SystemThreadingTasksDataflowPackageVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||
</Project>
|
||||
|
||||
14
build/repo.props
Normal file
14
build/repo.props
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project>
|
||||
<Import Project="dependencies.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- These properties are use by the automation that updates dependencies.props -->
|
||||
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
|
||||
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-release/api/v3/index.json</LineupPackageRestoreSource>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
|
||||
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
17
build/sources.props
Normal file
17
build/sources.props
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project>
|
||||
<Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
|
||||
|
||||
<PropertyGroup Label="RestoreSources">
|
||||
<RestoreSources>$(DotNetRestoreSources)</RestoreSources>
|
||||
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
|
||||
$(RestoreSources);
|
||||
https://dotnet.myget.org/F/dotnet-core/api/v3/index.json;
|
||||
https://dotnet.myget.org/F/aspnetcore-release/api/v3/index.json;
|
||||
https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json;
|
||||
</RestoreSources>
|
||||
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
|
||||
$(RestoreSources);
|
||||
https://api.nuget.org/v3/index.json;
|
||||
</RestoreSources>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
2
korebuild-lock.txt
Normal file
2
korebuild-lock.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
version:2.1.0-preview2-15749
|
||||
commithash:5544c9ab20fa5e24b9e155d8958a3c3b6f5f9df9
|
||||
10
korebuild.json
Normal file
10
korebuild.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json",
|
||||
"channel": "release/2.1",
|
||||
"toolsets": {
|
||||
"nodejs": {
|
||||
"required": true,
|
||||
"minVersion": "6.9"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
run.cmd
Normal file
2
run.cmd
Normal file
@@ -0,0 +1,2 @@
|
||||
@ECHO OFF
|
||||
PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE"
|
||||
196
run.ps1
Normal file
196
run.ps1
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env powershell
|
||||
#requires -version 4
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Executes KoreBuild commands.
|
||||
|
||||
.DESCRIPTION
|
||||
Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`.
|
||||
|
||||
.PARAMETER Command
|
||||
The KoreBuild command to run.
|
||||
|
||||
.PARAMETER Path
|
||||
The folder to build. Defaults to the folder containing this script.
|
||||
|
||||
.PARAMETER Channel
|
||||
The channel of KoreBuild to download. Overrides the value from the config file.
|
||||
|
||||
.PARAMETER DotNetHome
|
||||
The directory where .NET Core tools will be stored.
|
||||
|
||||
.PARAMETER ToolsSource
|
||||
The base url where build tools can be downloaded. Overrides the value from the config file.
|
||||
|
||||
.PARAMETER Update
|
||||
Updates KoreBuild to the latest version even if a lock file is present.
|
||||
|
||||
.PARAMETER ConfigFile
|
||||
The path to the configuration file that stores values. Defaults to korebuild.json.
|
||||
|
||||
.PARAMETER ToolsSourceSuffix
|
||||
The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores.
|
||||
|
||||
.PARAMETER Arguments
|
||||
Arguments to be passed to the command
|
||||
|
||||
.NOTES
|
||||
This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be.
|
||||
When the lockfile is not present, KoreBuild will create one using latest available version from $Channel.
|
||||
|
||||
The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set
|
||||
in the file are overridden by command line parameters.
|
||||
|
||||
.EXAMPLE
|
||||
Example config file:
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json",
|
||||
"channel": "dev",
|
||||
"toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools"
|
||||
}
|
||||
```
|
||||
#>
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[string]$Command,
|
||||
[string]$Path = $PSScriptRoot,
|
||||
[Alias('c')]
|
||||
[string]$Channel,
|
||||
[Alias('d')]
|
||||
[string]$DotNetHome,
|
||||
[Alias('s')]
|
||||
[string]$ToolsSource,
|
||||
[Alias('u')]
|
||||
[switch]$Update,
|
||||
[string]$ConfigFile,
|
||||
[string]$ToolsSourceSuffix,
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Arguments
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
#
|
||||
# Functions
|
||||
#
|
||||
|
||||
function Get-KoreBuild {
|
||||
|
||||
$lockFile = Join-Path $Path 'korebuild-lock.txt'
|
||||
|
||||
if (!(Test-Path $lockFile) -or $Update) {
|
||||
Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix
|
||||
}
|
||||
|
||||
$version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
|
||||
if (!$version) {
|
||||
Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'"
|
||||
}
|
||||
$version = $version.TrimStart('version:').Trim()
|
||||
$korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version)
|
||||
|
||||
if (!(Test-Path $korebuildPath)) {
|
||||
Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version"
|
||||
New-Item -ItemType Directory -Path $korebuildPath | Out-Null
|
||||
$remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip"
|
||||
|
||||
try {
|
||||
$tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip"
|
||||
Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix
|
||||
if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) {
|
||||
# Use built-in commands where possible as they are cross-plat compatible
|
||||
Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath
|
||||
}
|
||||
else {
|
||||
# Fallback to old approach for old installations of PowerShell
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
[System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
Remove-Item $tmpfile -ErrorAction Ignore
|
||||
}
|
||||
}
|
||||
|
||||
return $korebuildPath
|
||||
}
|
||||
|
||||
function Join-Paths([string]$path, [string[]]$childPaths) {
|
||||
$childPaths | ForEach-Object { $path = Join-Path $path $_ }
|
||||
return $path
|
||||
}
|
||||
|
||||
function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) {
|
||||
if ($RemotePath -notlike 'http*') {
|
||||
Copy-Item $RemotePath $LocalPath
|
||||
return
|
||||
}
|
||||
|
||||
$retries = 10
|
||||
while ($retries -gt 0) {
|
||||
$retries -= 1
|
||||
try {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath
|
||||
return
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Request failed. $retries retries remaining"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Error "Download failed: '$RemotePath'."
|
||||
}
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
|
||||
# Load configuration or set defaults
|
||||
|
||||
$Path = Resolve-Path $Path
|
||||
if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' }
|
||||
|
||||
if (Test-Path $ConfigFile) {
|
||||
try {
|
||||
$config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json
|
||||
if ($config) {
|
||||
if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel }
|
||||
if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "$ConfigFile could not be read. Its settings will be ignored."
|
||||
Write-Warning $Error[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!$DotNetHome) {
|
||||
$DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } `
|
||||
elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} `
|
||||
elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}`
|
||||
else { Join-Path $PSScriptRoot '.dotnet'}
|
||||
}
|
||||
|
||||
if (!$Channel) { $Channel = 'dev' }
|
||||
if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' }
|
||||
|
||||
# Execute
|
||||
|
||||
$korebuildPath = Get-KoreBuild
|
||||
Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1')
|
||||
|
||||
try {
|
||||
Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile
|
||||
Invoke-KoreBuildCommand $Command @Arguments
|
||||
}
|
||||
finally {
|
||||
Remove-Module 'KoreBuild' -ErrorAction Ignore
|
||||
}
|
||||
231
run.sh
Executable file
231
run.sh
Executable file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
#
|
||||
# variables
|
||||
#
|
||||
|
||||
RESET="\033[0m"
|
||||
RED="\033[0;31m"
|
||||
YELLOW="\033[0;33m"
|
||||
MAGENTA="\033[0;95m"
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet"
|
||||
verbose=false
|
||||
update=false
|
||||
repo_path="$DIR"
|
||||
channel=''
|
||||
tools_source=''
|
||||
tools_source_suffix=''
|
||||
|
||||
#
|
||||
# Functions
|
||||
#
|
||||
__usage() {
|
||||
echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] <Arguments>...]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " command The command to be run."
|
||||
echo " <Arguments>... Arguments passed to the command. Variable number of arguments allowed."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --verbose Show verbose output."
|
||||
echo " -c|--channel <CHANNEL> The channel of KoreBuild to download. Overrides the value from the config file.."
|
||||
echo " --config-file <FILE> The path to the configuration file that stores values. Defaults to korebuild.json."
|
||||
echo " -d|--dotnet-home <DIR> The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet."
|
||||
echo " --path <PATH> The directory to build. Defaults to the directory containing the script."
|
||||
echo " -s|--tools-source|-ToolsSource <URL> The base url where build tools can be downloaded. Overrides the value from the config file."
|
||||
echo " --tools-source-suffix|-ToolsSourceSuffix <SUFFIX> The suffix to append to tools-source. Useful for query strings."
|
||||
echo " -u|--update Update to the latest KoreBuild even if the lock file is present."
|
||||
echo ""
|
||||
echo "Description:"
|
||||
echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be."
|
||||
echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel."
|
||||
|
||||
if [[ "${1:-}" != '--no-exit' ]]; then
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
get_korebuild() {
|
||||
local version
|
||||
local lock_file="$repo_path/korebuild-lock.txt"
|
||||
if [ ! -f "$lock_file" ] || [ "$update" = true ]; then
|
||||
__get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix"
|
||||
fi
|
||||
version="$(grep 'version:*' -m 1 "$lock_file")"
|
||||
if [[ "$version" == '' ]]; then
|
||||
__error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'"
|
||||
return 1
|
||||
fi
|
||||
version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version"
|
||||
|
||||
{
|
||||
if [ ! -d "$korebuild_path" ]; then
|
||||
mkdir -p "$korebuild_path"
|
||||
local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip"
|
||||
tmpfile="$(mktemp)"
|
||||
echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}"
|
||||
if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then
|
||||
unzip -q -d "$korebuild_path" "$tmpfile"
|
||||
fi
|
||||
rm "$tmpfile" || true
|
||||
fi
|
||||
|
||||
source "$korebuild_path/KoreBuild.sh"
|
||||
} || {
|
||||
if [ -d "$korebuild_path" ]; then
|
||||
echo "Cleaning up after failed installation"
|
||||
rm -rf "$korebuild_path" || true
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
__error() {
|
||||
echo -e "${RED}error: $*${RESET}" 1>&2
|
||||
}
|
||||
|
||||
__warn() {
|
||||
echo -e "${YELLOW}warning: $*${RESET}"
|
||||
}
|
||||
|
||||
__machine_has() {
|
||||
hash "$1" > /dev/null 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
__get_remote_file() {
|
||||
local remote_path=$1
|
||||
local local_path=$2
|
||||
local remote_path_suffix=$3
|
||||
|
||||
if [[ "$remote_path" != 'http'* ]]; then
|
||||
cp "$remote_path" "$local_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local failed=false
|
||||
if __machine_has wget; then
|
||||
wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true
|
||||
else
|
||||
failed=true
|
||||
fi
|
||||
|
||||
if [ "$failed" = true ] && __machine_has curl; then
|
||||
failed=false
|
||||
curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true
|
||||
fi
|
||||
|
||||
if [ "$failed" = true ]; then
|
||||
__error "Download failed: $remote_path" 1>&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# main
|
||||
#
|
||||
|
||||
command="${1:-}"
|
||||
shift
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-\?|-h|--help)
|
||||
__usage --no-exit
|
||||
exit 0
|
||||
;;
|
||||
-c|--channel|-Channel)
|
||||
shift
|
||||
channel="${1:-}"
|
||||
[ -z "$channel" ] && __usage
|
||||
;;
|
||||
--config-file|-ConfigFile)
|
||||
shift
|
||||
config_file="${1:-}"
|
||||
[ -z "$config_file" ] && __usage
|
||||
if [ ! -f "$config_file" ]; then
|
||||
__error "Invalid value for --config-file. $config_file does not exist."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
-d|--dotnet-home|-DotNetHome)
|
||||
shift
|
||||
DOTNET_HOME="${1:-}"
|
||||
[ -z "$DOTNET_HOME" ] && __usage
|
||||
;;
|
||||
--path|-Path)
|
||||
shift
|
||||
repo_path="${1:-}"
|
||||
[ -z "$repo_path" ] && __usage
|
||||
;;
|
||||
-s|--tools-source|-ToolsSource)
|
||||
shift
|
||||
tools_source="${1:-}"
|
||||
[ -z "$tools_source" ] && __usage
|
||||
;;
|
||||
--tools-source-suffix|-ToolsSourceSuffix)
|
||||
shift
|
||||
tools_source_suffix="${1:-}"
|
||||
[ -z "$tools_source_suffix" ] && __usage
|
||||
;;
|
||||
-u|--update|-Update)
|
||||
update=true
|
||||
;;
|
||||
--verbose|-Verbose)
|
||||
verbose=true
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if ! __machine_has unzip; then
|
||||
__error 'Missing required command: unzip'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! __machine_has curl && ! __machine_has wget; then
|
||||
__error 'Missing required command. Either wget or curl is required.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json"
|
||||
if [ -f "$config_file" ]; then
|
||||
if __machine_has jq ; then
|
||||
if jq '.' "$config_file" >/dev/null ; then
|
||||
config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")"
|
||||
config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")"
|
||||
else
|
||||
__warn "$config_file is invalid JSON. Its settings will be ignored."
|
||||
fi
|
||||
elif __machine_has python ; then
|
||||
if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then
|
||||
config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")"
|
||||
config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")"
|
||||
else
|
||||
__warn "$config_file is invalid JSON. Its settings will be ignored."
|
||||
fi
|
||||
else
|
||||
__warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.'
|
||||
fi
|
||||
|
||||
[ ! -z "${config_channel:-}" ] && channel="$config_channel"
|
||||
[ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source"
|
||||
fi
|
||||
|
||||
[ -z "$channel" ] && channel='dev'
|
||||
[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
|
||||
|
||||
get_korebuild
|
||||
set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file"
|
||||
invoke_korebuild_command "$command" "$@"
|
||||
@@ -1,9 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\build\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>exe</OutputType>
|
||||
</PropertyGroup>
|
||||
@@ -14,7 +12,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace ConsoleApplication
|
||||
// Since .NET Core 1.1, the HTTP hosting model has become basically as fast as the Socket hosting model
|
||||
//options.UseSocketHosting();
|
||||
|
||||
options.ProjectPath = Directory.GetCurrentDirectory();
|
||||
options.WatchFileExtensions = new string[] {}; // Don't watch anything
|
||||
});
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
|
||||
namespace NodeServicesExamples.Controllers
|
||||
{
|
||||
@@ -34,6 +35,20 @@ namespace NodeServicesExamples.Controllers
|
||||
return View();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Prerendering([FromServices] ISpaPrerenderer prerenderer)
|
||||
{
|
||||
var result = await prerenderer.RenderToString("./Node/prerenderPage");
|
||||
|
||||
if (!string.IsNullOrEmpty(result.RedirectUrl))
|
||||
{
|
||||
return Redirect(result.RedirectUrl);
|
||||
}
|
||||
|
||||
ViewData["PrerenderedHtml"] = result.Html;
|
||||
ViewData["PrerenderedGlobals"] = result.CreateGlobalsAssignmentScript();
|
||||
return View();
|
||||
}
|
||||
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View("~/Views/Shared/Error.cshtml");
|
||||
|
||||
14
samples/misc/NodeServicesExamples/Node/prerenderPage.js
Normal file
14
samples/misc/NodeServicesExamples/Node/prerenderPage.js
Normal file
@@ -0,0 +1,14 @@
|
||||
var createServerRenderer = require('aspnet-prerendering').createServerRenderer;
|
||||
|
||||
module.exports = createServerRenderer(function(params) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var message = 'The HTML was returned by the prerendering boot function. '
|
||||
+ 'The boot function received the following params:'
|
||||
+ '<pre>' + JSON.stringify(params, null, 4) + '</pre>';
|
||||
|
||||
resolve({
|
||||
html: '<h3>Hello, world!</h3>' + message,
|
||||
globals: { sampleData: { nodeVersion: process.version } }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<Import Project="..\..\..\build\common.props" />
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.SpaServices\Microsoft.AspNetCore.SpaServices.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace NodeServicesExamples
|
||||
|
||||
// Enable Node Services
|
||||
services.AddNodeServices();
|
||||
services.AddSpaPrerenderer();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
||||
@@ -9,4 +9,5 @@
|
||||
<ul>
|
||||
<li><a asp-action="ES2015Transpilation">ES2015 transpilation</a></li>
|
||||
<li><a asp-action="Chart">Server-side chart rendering</a></li>
|
||||
<li><a asp-action="Prerendering">Server-side SPA prerendering</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<h1>Server-side prerendering</h1>
|
||||
|
||||
<p>
|
||||
This sample demonstrates how you can invoke a JavaScript module that contains
|
||||
prerendering logic for a Single-Page Application framework.
|
||||
</p>
|
||||
</p>
|
||||
Your prerendering boot function will receive parameters that describe the page
|
||||
being rendered and any data supplied by the .NET code. The return value should be
|
||||
a promise that resolves with data to be injected into the page, such as the
|
||||
rendered HTML and any global data that should be made available to client-side code.
|
||||
</p>
|
||||
|
||||
@Html.Raw(ViewData["PrerenderedHtml"])
|
||||
|
||||
<script>@Html.Raw(ViewData["PrerenderedGlobals"])</script>
|
||||
|
||||
<script>
|
||||
// Demonstrates how client-side code can receive data from the prerendering process
|
||||
console.log('Received Node version from prerendering logic: ' + sampleData.nodeVersion);
|
||||
</script>
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "nodeservicesexamples",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"aspnet-prerendering": "^2.0.6",
|
||||
"babel-core": "^6.7.4",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"node-chartist": "^1.0.2"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<Import Project="..\..\..\build\common.props" />
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
@@ -13,13 +11,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||
|
||||
13
src/Directory.Build.props
Normal file
13
src/Directory.Build.props
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project>
|
||||
<Import Project="..\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,21 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\build\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Socket-based RPC for Microsoft.AspNetCore.NodeServices.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="node_modules\**\*" />
|
||||
<EmbeddedResource Include="Content\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="$(ThreadingDataflowVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="$(SystemThreadingTasksDataflowPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||
|
||||
@@ -14,8 +14,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets
|
||||
/// <summary>
|
||||
/// A specialisation of the OutOfProcessNodeInstance base class that uses a lightweight binary streaming protocol
|
||||
/// to perform RPC invocations. The physical transport is Named Pipes on Windows, or Domain Sockets on Linux/Mac.
|
||||
/// For details on the binary streaming protocol, see
|
||||
/// Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections.VirtualConnectionClient.
|
||||
/// For details on the binary streaming protocol, see <see cref="Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections.VirtualConnectionClient" />
|
||||
/// The advantage versus using HTTP for RPC is that this is faster (not surprisingly - there's much less overhead
|
||||
/// because we don't need most of the functionality of HTTP.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.NodeServices.Sockets, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.Sockets.NodeServicesOptionsExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseSocketHosting",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.NodeServices.NodeServicesOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections.VirtualConnectionReadErrorHandler",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Sealed": true,
|
||||
"BaseType": "System.MulticastDelegate",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Invoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "ex",
|
||||
"Type": "System.Exception"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "BeginInvoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "ex",
|
||||
"Type": "System.Exception"
|
||||
},
|
||||
{
|
||||
"Name": "callback",
|
||||
"Type": "System.AsyncCallback"
|
||||
},
|
||||
{
|
||||
"Name": "object",
|
||||
"Type": "System.Object"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.IAsyncResult",
|
||||
"Virtual": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "EndInvoke",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "result",
|
||||
"Type": "System.IAsyncResult"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "object",
|
||||
"Type": "System.Object"
|
||||
},
|
||||
{
|
||||
"Name": "method",
|
||||
"Type": "System.IntPtr"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.NodeServices.HostingModels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -34,14 +35,18 @@ namespace Microsoft.AspNetCore.NodeServices
|
||||
InvocationTimeoutMilliseconds = DefaultInvocationTimeoutMilliseconds;
|
||||
WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone();
|
||||
|
||||
// In an ASP.NET environment, we can use the IHostingEnvironment data to auto-populate a few
|
||||
// things that you'd otherwise have to specify manually
|
||||
var hostEnv = serviceProvider.GetService<IHostingEnvironment>();
|
||||
if (hostEnv != null)
|
||||
{
|
||||
// In an ASP.NET environment, we can use the IHostingEnvironment data to auto-populate a few
|
||||
// things that you'd otherwise have to specify manually
|
||||
ProjectPath = hostEnv.ContentRootPath;
|
||||
EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node
|
||||
}
|
||||
else
|
||||
{
|
||||
ProjectPath = Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
var applicationLifetime = serviceProvider.GetService<IApplicationLifetime>();
|
||||
if (applicationLifetime != null)
|
||||
|
||||
@@ -55,10 +55,11 @@
|
||||
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
||||
// but simplifies things for the consumer of this module.
|
||||
__webpack_require__(2);
|
||||
var http = __webpack_require__(3);
|
||||
var path = __webpack_require__(4);
|
||||
var ArgsUtil_1 = __webpack_require__(5);
|
||||
var ExitWhenParentExits_1 = __webpack_require__(6);
|
||||
__webpack_require__(4);
|
||||
var http = __webpack_require__(5);
|
||||
var path = __webpack_require__(3);
|
||||
var ArgsUtil_1 = __webpack_require__(6);
|
||||
var ExitWhenParentExits_1 = __webpack_require__(7);
|
||||
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
|
||||
// reference to Node's runtime 'require' function.
|
||||
var dynamicRequire = eval('require');
|
||||
@@ -133,12 +134,73 @@
|
||||
}
|
||||
function respondWithError(res, errorValue) {
|
||||
res.statusCode = 500;
|
||||
res.end(errorValue.stack || errorValue.toString());
|
||||
res.end(JSON.stringify({
|
||||
errorMessage: errorValue.message || errorValue,
|
||||
errorDetails: errorValue.stack || null
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 2 */
|
||||
/***/ function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
var path = __webpack_require__(3);
|
||||
var startsWith = function (str, prefix) { return str.substring(0, prefix.length) === prefix; };
|
||||
var appRootDir = process.cwd();
|
||||
function patchedLStat(pathToStatLong, fsReqWrap) {
|
||||
try {
|
||||
// If the lstat completes without errors, we don't modify its behavior at all
|
||||
return origLStat.apply(this, arguments);
|
||||
}
|
||||
catch (ex) {
|
||||
var shouldOverrideError = startsWith(ex.message, 'EPERM') // It's a permissions error
|
||||
&& typeof appRootDirLong === 'string'
|
||||
&& startsWith(appRootDirLong, pathToStatLong) // ... for an ancestor directory
|
||||
&& ex.stack.indexOf('Object.realpathSync ') >= 0; // ... during symlink resolution
|
||||
if (shouldOverrideError) {
|
||||
// Fake the result to give the same result as an 'lstat' on the app root dir.
|
||||
// This stops Node failing to load modules just because it doesn't know whether
|
||||
// ancestor directories are symlinks or not. If there's a genuine file
|
||||
// permissions issue, it will still surface later when Node actually
|
||||
// tries to read the file.
|
||||
return origLStat.call(this, appRootDir, fsReqWrap);
|
||||
}
|
||||
else {
|
||||
// In any other case, preserve the original error
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
;
|
||||
// It's only necessary to apply this workaround on Windows
|
||||
var appRootDirLong = null;
|
||||
var origLStat = null;
|
||||
if (/^win/.test(process.platform)) {
|
||||
try {
|
||||
// Get the app's root dir in Node's internal "long" format (e.g., \\?\C:\dir\subdir)
|
||||
appRootDirLong = path._makeLong(appRootDir);
|
||||
// Actually apply the patch, being as defensive as possible
|
||||
var bindingFs = process.binding('fs');
|
||||
origLStat = bindingFs.lstat;
|
||||
if (typeof origLStat === 'function') {
|
||||
bindingFs.lstat = patchedLStat;
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 3 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("path");
|
||||
|
||||
/***/ },
|
||||
/* 4 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
// When Node writes to stdout/strerr, we capture that and convert the lines into calls on the
|
||||
@@ -179,19 +241,13 @@
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 3 */
|
||||
/* 5 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("http");
|
||||
|
||||
/***/ },
|
||||
/* 4 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
module.exports = require("path");
|
||||
|
||||
/***/ },
|
||||
/* 5 */
|
||||
/* 6 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
"use strict";
|
||||
@@ -217,7 +273,7 @@
|
||||
|
||||
|
||||
/***/ },
|
||||
/* 6 */
|
||||
/* 7 */
|
||||
/***/ function(module, exports) {
|
||||
|
||||
/*
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
options.DebuggingPort)
|
||||
{
|
||||
_client = new HttpClient();
|
||||
_client.Timeout = TimeSpan.FromMilliseconds(options.InvocationTimeoutMilliseconds + 1000);
|
||||
}
|
||||
|
||||
private static string MakeCommandLineOptions(int port)
|
||||
@@ -67,8 +68,10 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
// Unfortunately there's no true way to cancel ReadAsStringAsync calls, hence AbandonIfCancelled
|
||||
var responseErrorString = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken);
|
||||
throw new Exception("Call to Node module failed with error: " + responseErrorString);
|
||||
var responseJson = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken);
|
||||
var responseError = JsonConvert.DeserializeObject<RpcJsonResponse>(responseJson, jsonSerializerSettings);
|
||||
|
||||
throw new NodeInvocationException(responseError.ErrorMessage, responseError.ErrorDetails);
|
||||
}
|
||||
|
||||
var responseContentType = response.Content.Headers.ContentType;
|
||||
@@ -136,5 +139,13 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable 649 // These properties are populated via JSON deserialization
|
||||
private class RpcJsonResponse
|
||||
{
|
||||
public string ErrorMessage { get; set; }
|
||||
public string ErrorDetails { get; set; }
|
||||
}
|
||||
#pragma warning restore 649
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
||||
|
||||
// Make sure the Node process is finished
|
||||
// TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
|
||||
if (!_nodeProcess.HasExited)
|
||||
if (_nodeProcess != null && !_nodeProcess.HasExited)
|
||||
{
|
||||
_nodeProcess.Kill();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\build\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Invoke Node.js modules at runtime in ASP.NET Core applications.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="node_modules\**\*" />
|
||||
<EmbeddedResource Include="Content\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||
|
||||
@@ -317,7 +317,7 @@ module.exports = {
|
||||
|
||||
## Hosting models
|
||||
|
||||
NodeServices has a pluggable hosting/transport mechanism, because it is an abstraction over various possible ways to invoke Node.js from .NET. This allows more high-level facilities (e.g., for Angular prerendering) to be agnostic to the details of launching Node and communicating it - those high-level facilities can just trust that *somehow* we can invoke code in Node for them.
|
||||
NodeServices has a pluggable hosting/transport mechanism, because it is an abstraction over various possible ways to invoke Node.js from .NET. This allows more high-level facilities (e.g., for Angular prerendering) to be agnostic to the details of launching Node and communicating with it - those high-level facilities can just trust that *somehow* we can invoke code in Node for them.
|
||||
|
||||
Using this abstraction, we could run Node inside the .NET process, in a separate process on the same machine, or even on a different machine altogether. At the time of writing, all the built-in hosting mechanisms work by launching Node as a separate process on the same machine as your .NET code.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
||||
// but simplifies things for the consumer of this module.
|
||||
import './Util/PatchModuleResolutionLStat';
|
||||
import './Util/OverrideStdOutputs';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
@@ -86,5 +87,8 @@ function readRequestBodyAsJson(request, callback) {
|
||||
|
||||
function respondWithError(res: http.ServerResponse, errorValue: any) {
|
||||
res.statusCode = 500;
|
||||
res.end(errorValue.stack || errorValue.toString());
|
||||
res.end(JSON.stringify({
|
||||
errorMessage: errorValue.message || errorValue,
|
||||
errorDetails: errorValue.stack || null
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as path from 'path';
|
||||
const startsWith = (str: string, prefix: string) => str.substring(0, prefix.length) === prefix;
|
||||
const appRootDir = process.cwd();
|
||||
|
||||
function patchedLStat(pathToStatLong: string, fsReqWrap?: any) {
|
||||
try {
|
||||
// If the lstat completes without errors, we don't modify its behavior at all
|
||||
return origLStat.apply(this, arguments);
|
||||
} catch(ex) {
|
||||
const shouldOverrideError =
|
||||
startsWith(ex.message, 'EPERM') // It's a permissions error
|
||||
&& typeof appRootDirLong === 'string'
|
||||
&& startsWith(appRootDirLong, pathToStatLong) // ... for an ancestor directory
|
||||
&& ex.stack.indexOf('Object.realpathSync ') >= 0; // ... during symlink resolution
|
||||
|
||||
if (shouldOverrideError) {
|
||||
// Fake the result to give the same result as an 'lstat' on the app root dir.
|
||||
// This stops Node failing to load modules just because it doesn't know whether
|
||||
// ancestor directories are symlinks or not. If there's a genuine file
|
||||
// permissions issue, it will still surface later when Node actually
|
||||
// tries to read the file.
|
||||
return origLStat.call(this, appRootDir, fsReqWrap);
|
||||
} else {
|
||||
// In any other case, preserve the original error
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// It's only necessary to apply this workaround on Windows
|
||||
let appRootDirLong: string = null;
|
||||
let origLStat: Function = null;
|
||||
if (/^win/.test(process.platform)) {
|
||||
try {
|
||||
// Get the app's root dir in Node's internal "long" format (e.g., \\?\C:\dir\subdir)
|
||||
appRootDirLong = (path as any)._makeLong(appRootDir);
|
||||
|
||||
// Actually apply the patch, being as defensive as possible
|
||||
const bindingFs = (process as any).binding('fs');
|
||||
origLStat = bindingFs.lstat;
|
||||
if (typeof origLStat === 'function') {
|
||||
bindingFs.lstat = patchedLStat;
|
||||
}
|
||||
} catch(ex) {
|
||||
// If some future version of Node throws (e.g., to prevent use of process.binding()),
|
||||
// don't apply the patch, but still let the application run.
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.NodeServices
|
||||
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
|
||||
public StringAsTempFile(string content, CancellationToken applicationStoppingToken)
|
||||
{
|
||||
FileName = Path.GetTempFileName();
|
||||
FileName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
File.WriteAllText(FileName, content);
|
||||
|
||||
// Because .NET finalizers don't reliably run when the process is terminating, also
|
||||
|
||||
935
src/Microsoft.AspNetCore.NodeServices/baseline.netcore.json
Normal file
935
src/Microsoft.AspNetCore.NodeServices/baseline.netcore.json
Normal file
@@ -0,0 +1,935 @@
|
||||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.NodeServices, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.Extensions.DependencyInjection.NodeServicesServiceCollectionExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddNodeServices",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "serviceCollection",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddNodeServices",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "serviceCollection",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
},
|
||||
{
|
||||
"Name": "setupAction",
|
||||
"Type": "System.Action<Microsoft.AspNetCore.NodeServices.NodeServicesOptions>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.NodeServicesFactory",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CreateNodeServices",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.NodeServices.NodeServicesOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "Microsoft.AspNetCore.NodeServices.INodeServices",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.NodeServicesOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_NodeInstanceFactory",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Func<Microsoft.AspNetCore.NodeServices.HostingModels.INodeInstance>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_NodeInstanceFactory",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Func<Microsoft.AspNetCore.NodeServices.HostingModels.INodeInstance>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ProjectPath",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ProjectPath",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_WatchFileExtensions",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String[]",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_WatchFileExtensions",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String[]"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_NodeInstanceOutputLogger",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.Extensions.Logging.ILogger",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_NodeInstanceOutputLogger",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "Microsoft.Extensions.Logging.ILogger"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_LaunchWithDebugging",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_LaunchWithDebugging",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_DebuggingPort",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_DebuggingPort",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EnvironmentVariables",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_EnvironmentVariables",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_InvocationTimeoutMilliseconds",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_InvocationTimeoutMilliseconds",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ApplicationStoppingToken",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Threading.CancellationToken",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ApplicationStoppingToken",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "serviceProvider",
|
||||
"Type": "System.IServiceProvider"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.INodeServices",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [
|
||||
"System.IDisposable"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "args",
|
||||
"Type": "System.Object[]",
|
||||
"IsParams": true
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "cancellationToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
},
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "args",
|
||||
"Type": "System.Object[]",
|
||||
"IsParams": true
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeExportAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "exportedFunctionName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "args",
|
||||
"Type": "System.Object[]",
|
||||
"IsParams": true
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeExportAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "cancellationToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
},
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "exportedFunctionName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "args",
|
||||
"Type": "System.Object[]",
|
||||
"IsParams": true
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.EmbeddedResourceReader",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Read",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "assemblyContainingType",
|
||||
"Type": "System.Type"
|
||||
},
|
||||
{
|
||||
"Name": "path",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.String",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.StringAsTempFile",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [
|
||||
"System.IDisposable"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_FileName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Dispose",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Void",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "System.IDisposable",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Finalize",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Override": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "content",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "applicationStoppingToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.HostingModels.INodeInstance",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [
|
||||
"System.IDisposable"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeExportAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "cancellationToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
},
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "exportNameOrNull",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "args",
|
||||
"Type": "System.Object[]",
|
||||
"IsParams": true
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.HostingModels.NodeInvocationException",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"BaseType": "System.Exception",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_NodeInstanceUnavailable",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_AllowConnectionDraining",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "message",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "details",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "message",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "details",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "nodeInstanceUnavailable",
|
||||
"Type": "System.Boolean"
|
||||
},
|
||||
{
|
||||
"Name": "allowConnectionDraining",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.HostingModels.NodeInvocationInfo",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ModuleName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ModuleName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ExportedFunctionName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ExportedFunctionName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Args",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Object[]",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_Args",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Object[]"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.HostingModels.NodeServicesOptionsExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseHttpHosting",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.NodeServices.NodeServicesOptions"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.NodeServices.HostingModels.OutOfProcessNodeInstance",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [
|
||||
"Microsoft.AspNetCore.NodeServices.HostingModels.INodeInstance"
|
||||
],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeExportAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "cancellationToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
},
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "exportNameOrNull",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "args",
|
||||
"Type": "System.Object[]",
|
||||
"IsParams": true
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.NodeServices.HostingModels.INodeInstance",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Dispose",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Void",
|
||||
"Sealed": true,
|
||||
"Virtual": true,
|
||||
"ImplementedInterface": "System.IDisposable",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "InvokeExportAsync<T0>",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "invocationInfo",
|
||||
"Type": "Microsoft.AspNetCore.NodeServices.HostingModels.NodeInvocationInfo"
|
||||
},
|
||||
{
|
||||
"Name": "cancellationToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<T0>",
|
||||
"Virtual": true,
|
||||
"Abstract": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": [
|
||||
{
|
||||
"ParameterName": "T",
|
||||
"ParameterPosition": 0,
|
||||
"BaseTypeOrInterfaces": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "PrepareNodeProcessStartInfo",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "entryPointFilename",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "projectPath",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "commandLineArguments",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "environmentVars",
|
||||
"Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
|
||||
},
|
||||
{
|
||||
"Name": "launchWithDebugging",
|
||||
"Type": "System.Boolean"
|
||||
},
|
||||
{
|
||||
"Name": "debuggingPort",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Diagnostics.ProcessStartInfo",
|
||||
"Virtual": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "OnOutputDataReceived",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "outputData",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "OnErrorDataReceived",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "errorData",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Dispose",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "disposing",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "Finalize",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Void",
|
||||
"Virtual": true,
|
||||
"Override": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "entryPointScript",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "projectPath",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "watchFileExtensions",
|
||||
"Type": "System.String[]"
|
||||
},
|
||||
{
|
||||
"Name": "commandLineArguments",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "applicationStoppingToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
},
|
||||
{
|
||||
"Name": "nodeOutputLogger",
|
||||
"Type": "Microsoft.Extensions.Logging.ILogger"
|
||||
},
|
||||
{
|
||||
"Name": "environmentVars",
|
||||
"Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
|
||||
},
|
||||
{
|
||||
"Name": "invocationTimeoutMilliseconds",
|
||||
"Type": "System.Int32"
|
||||
},
|
||||
{
|
||||
"Name": "launchWithDebugging",
|
||||
"Type": "System.Boolean"
|
||||
},
|
||||
{
|
||||
"Name": "debuggingPort",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Field",
|
||||
"Name": "OutputLogger",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.Extensions.Logging.ILogger",
|
||||
"ReadOnly": true,
|
||||
"Visibility": "Protected",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="ISpaPrerendererBuilder"/> that can build
|
||||
/// an Angular application by invoking the Angular CLI.
|
||||
/// </summary>
|
||||
public class AngularCliBuilder : ISpaPrerendererBuilder
|
||||
{
|
||||
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
|
||||
|
||||
private readonly string _npmScriptName;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="AngularCliBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="npmScript">The name of the script in your package.json file that builds the server-side bundle for your Angular application.</param>
|
||||
public AngularCliBuilder(string npmScript)
|
||||
{
|
||||
if (string.IsNullOrEmpty(npmScript))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty.", nameof(npmScript));
|
||||
}
|
||||
|
||||
_npmScriptName = npmScript;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Build(ISpaBuilder spaBuilder)
|
||||
{
|
||||
var sourcePath = spaBuilder.Options.SourcePath;
|
||||
if (string.IsNullOrEmpty(sourcePath))
|
||||
{
|
||||
throw new InvalidOperationException($"To use {nameof(AngularCliBuilder)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
||||
}
|
||||
|
||||
var logger = LoggerFinder.GetOrCreateLogger(
|
||||
spaBuilder.ApplicationBuilder,
|
||||
nameof(AngularCliBuilder));
|
||||
var npmScriptRunner = new NpmScriptRunner(
|
||||
sourcePath,
|
||||
_npmScriptName,
|
||||
"--watch",
|
||||
null);
|
||||
npmScriptRunner.AttachToLogger(logger);
|
||||
|
||||
using (var stdOutReader = new EventedStreamStringReader(npmScriptRunner.StdOut))
|
||||
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
|
||||
{
|
||||
try
|
||||
{
|
||||
await npmScriptRunner.StdOut.WaitForMatch(
|
||||
new Regex("Date", RegexOptions.None, RegexMatchTimeout));
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The NPM script '{_npmScriptName}' exited without indicating success.\n" +
|
||||
$"Output was: {stdOutReader.ReadAsString()}\n" +
|
||||
$"Error output was: {stdErrReader.ReadAsString()}", ex);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The NPM script '{_npmScriptName}' timed out without indicating success. " +
|
||||
$"Output was: {stdOutReader.ReadAsString()}\n" +
|
||||
$"Error output was: {stdErrReader.ReadAsString()}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Net.Http;
|
||||
using Microsoft.AspNetCore.SpaServices.Extensions.Util;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
||||
{
|
||||
internal static class AngularCliMiddleware
|
||||
{
|
||||
private const string LogCategoryName = "Microsoft.AspNetCore.SpaServices";
|
||||
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
|
||||
|
||||
public static void Attach(
|
||||
ISpaBuilder spaBuilder,
|
||||
string npmScriptName)
|
||||
{
|
||||
var sourcePath = spaBuilder.Options.SourcePath;
|
||||
if (string.IsNullOrEmpty(sourcePath))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(npmScriptName))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
|
||||
}
|
||||
|
||||
// Start Angular CLI and attach to middleware pipeline
|
||||
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
|
||||
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger);
|
||||
|
||||
// Everything we proxy is hardcoded to target http://localhost because:
|
||||
// - the requests are always from the local machine (we're not accepting remote
|
||||
// requests that go directly to the Angular CLI middleware server)
|
||||
// - given that, there's no reason to use https, and we couldn't even if we
|
||||
// wanted to, because in general the Angular CLI server has no certificate
|
||||
var targetUriTask = angularCliServerInfoTask.ContinueWith(
|
||||
task => new UriBuilder("http", "localhost", task.Result.Port).Uri);
|
||||
|
||||
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
|
||||
{
|
||||
// On each request, we create a separate startup task with its own timeout. That way, even if
|
||||
// the first request times out, subsequent requests could still work.
|
||||
var timeout = spaBuilder.Options.StartupTimeout;
|
||||
return targetUriTask.WithTimeout(timeout,
|
||||
$"The Angular CLI process did not start listening for requests " +
|
||||
$"within the timeout period of {timeout.Seconds} seconds. " +
|
||||
$"Check the log output for error information.");
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<AngularCliServerInfo> StartAngularCliServerAsync(
|
||||
string sourcePath, string npmScriptName, ILogger logger)
|
||||
{
|
||||
var portNumber = TcpPortFinder.FindAvailablePort();
|
||||
logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
|
||||
|
||||
var npmScriptRunner = new NpmScriptRunner(
|
||||
sourcePath, npmScriptName, $"--port {portNumber}", null);
|
||||
npmScriptRunner.AttachToLogger(logger);
|
||||
|
||||
Match openBrowserLine;
|
||||
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
|
||||
{
|
||||
try
|
||||
{
|
||||
openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch(
|
||||
new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout));
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The NPM script '{npmScriptName}' exited without indicating that the " +
|
||||
$"Angular CLI was listening for requests. The error output was: " +
|
||||
$"{stdErrReader.ReadAsString()}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
var uri = new Uri(openBrowserLine.Groups[1].Value);
|
||||
var serverInfo = new AngularCliServerInfo { Port = uri.Port };
|
||||
|
||||
// Even after the Angular CLI claims to be listening for requests, there's a short
|
||||
// period where it will give an error if you make a request too quickly
|
||||
await WaitForAngularCliServerToAcceptRequests(uri);
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
private static async Task WaitForAngularCliServerToAcceptRequests(Uri cliServerUri)
|
||||
{
|
||||
// To determine when it's actually ready, try making HEAD requests to '/'. If it
|
||||
// produces any HTTP response (even if it's 404) then it's ready. If it rejects the
|
||||
// connection then it's not ready. We keep trying forever because this is dev-mode
|
||||
// only, and only a single startup attempt will be made, and there's a further level
|
||||
// of timeouts enforced on a per-request basis.
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If we get any HTTP response, the CLI server is ready
|
||||
await client.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Head, cliServerUri),
|
||||
new CancellationTokenSource(1000).Token);
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await Task.Delay(1000); // 1 second
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AngularCliServerInfo
|
||||
{
|
||||
public int Port { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for enabling Angular CLI middleware support.
|
||||
/// </summary>
|
||||
public static class AngularCliMiddlewareExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles requests by passing them through to an instance of the Angular CLI server.
|
||||
/// This means you can always serve up-to-date CLI-built resources without having
|
||||
/// to run the Angular CLI server manually.
|
||||
///
|
||||
/// This feature should only be used in development. For production deployments, be
|
||||
/// sure not to enable the Angular CLI server.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <param name="npmScript">The name of the script in your package.json file that launches the Angular CLI process.</param>
|
||||
public static void UseAngularCliServer(
|
||||
this ISpaBuilder spaBuilder,
|
||||
string npmScript)
|
||||
{
|
||||
if (spaBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(spaBuilder));
|
||||
}
|
||||
|
||||
var spaOptions = spaBuilder.Options;
|
||||
|
||||
if (string.IsNullOrEmpty(spaOptions.SourcePath))
|
||||
{
|
||||
throw new InvalidOperationException($"To use {nameof(UseAngularCliServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
||||
}
|
||||
|
||||
AngularCliMiddleware.Attach(spaBuilder, npmScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices
|
||||
{
|
||||
internal class DefaultSpaBuilder : ISpaBuilder
|
||||
{
|
||||
public IApplicationBuilder ApplicationBuilder { get; }
|
||||
|
||||
public SpaOptions Options { get; }
|
||||
|
||||
public DefaultSpaBuilder(IApplicationBuilder applicationBuilder, SpaOptions options)
|
||||
{
|
||||
ApplicationBuilder = applicationBuilder
|
||||
?? throw new ArgumentNullException(nameof(applicationBuilder));
|
||||
|
||||
Options = options
|
||||
?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a class that provides mechanisms for configuring the hosting
|
||||
/// of a Single Page Application (SPA) and attaching middleware.
|
||||
/// </summary>
|
||||
public interface ISpaBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IApplicationBuilder"/> representing the middleware pipeline
|
||||
/// in which the SPA is being hosted.
|
||||
/// </summary>
|
||||
IApplicationBuilder ApplicationBuilder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Describes configuration options for hosting a SPA.
|
||||
/// </summary>
|
||||
SpaOptions Options { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Helpers for building single-page applications on ASP.NET MVC Core.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.SpaServices\Microsoft.AspNetCore.SpaServices.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="$(MicrosoftAspNetCoreWebSocketsPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="$(MicrosoftExtensionsFileProvidersPhysicalPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// This is under the NodeServices namespace because post 2.1 it will be moved to that package
|
||||
namespace Microsoft.AspNetCore.NodeServices.Npm
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the <c>script</c> entries defined in a <c>package.json</c> file,
|
||||
/// capturing any output written to stdio.
|
||||
/// </summary>
|
||||
internal class NpmScriptRunner
|
||||
{
|
||||
public EventedStreamReader StdOut { get; }
|
||||
public EventedStreamReader StdErr { get; }
|
||||
|
||||
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
|
||||
|
||||
public NpmScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars)
|
||||
{
|
||||
if (string.IsNullOrEmpty(workingDirectory))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty.", nameof(workingDirectory));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(scriptName))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty.", nameof(scriptName));
|
||||
}
|
||||
|
||||
var npmExe = "npm";
|
||||
var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// On Windows, the NPM executable is a .cmd file, so it can't be executed
|
||||
// directly (except with UseShellExecute=true, but that's no good, because
|
||||
// it prevents capturing stdio). So we need to invoke it via "cmd /c".
|
||||
npmExe = "cmd";
|
||||
completeArguments = $"/c npm {completeArguments}";
|
||||
}
|
||||
|
||||
var processStartInfo = new ProcessStartInfo(npmExe)
|
||||
{
|
||||
Arguments = completeArguments,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
WorkingDirectory = workingDirectory
|
||||
};
|
||||
|
||||
if (envVars != null)
|
||||
{
|
||||
foreach (var keyValuePair in envVars)
|
||||
{
|
||||
processStartInfo.Environment[keyValuePair.Key] = keyValuePair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
var process = LaunchNodeProcess(processStartInfo);
|
||||
StdOut = new EventedStreamReader(process.StandardOutput);
|
||||
StdErr = new EventedStreamReader(process.StandardError);
|
||||
}
|
||||
|
||||
public void AttachToLogger(ILogger logger)
|
||||
{
|
||||
// When the NPM task emits complete lines, pass them through to the real logger
|
||||
StdOut.OnReceivedLine += line =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
// NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward
|
||||
// those to loggers (because a logger isn't necessarily any kind of terminal)
|
||||
logger.LogInformation(StripAnsiColors(line));
|
||||
}
|
||||
};
|
||||
|
||||
StdErr.OnReceivedLine += line =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
logger.LogError(StripAnsiColors(line));
|
||||
}
|
||||
};
|
||||
|
||||
// But when it emits incomplete lines, assume this is progress information and
|
||||
// hence just pass it through to StdOut regardless of logger config.
|
||||
StdErr.OnReceivedChunk += chunk =>
|
||||
{
|
||||
var containsNewline = Array.IndexOf(
|
||||
chunk.Array, '\n', chunk.Offset, chunk.Count) >= 0;
|
||||
if (!containsNewline)
|
||||
{
|
||||
Console.Write(chunk.Array, chunk.Offset, chunk.Count);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string StripAnsiColors(string line)
|
||||
=> AnsiColorRegex.Replace(line, string.Empty);
|
||||
|
||||
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = Process.Start(startInfo);
|
||||
|
||||
// See equivalent comment in OutOfProcessNodeInstance.cs for why
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
return process;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var message = $"Failed to start 'npm'. To resolve this:.\n\n"
|
||||
+ "[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.\n"
|
||||
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
|
||||
+ " Make sure the executable is in one of those directories, or update your PATH.\n\n"
|
||||
+ "[2] See the InnerException for further details of the cause.";
|
||||
throw new InvalidOperationException(message, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the ability to build a Single Page Application (SPA) on demand
|
||||
/// so that it can be prerendered. This is only intended to be used at development
|
||||
/// time. In production, a SPA should already have been built during publishing.
|
||||
/// </summary>
|
||||
public interface ISpaPrerendererBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the Single Page Application so that a JavaScript entrypoint file
|
||||
/// exists on disk. Prerendering middleware can then execute that file in
|
||||
/// a Node environment.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <returns>A <see cref="Task"/> representing completion of the build process.</returns>
|
||||
Task Build(ISpaBuilder spaBuilder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.SpaServices;
|
||||
using Microsoft.AspNetCore.SpaServices.Extensions.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for configuring prerendering of a Single Page Application.
|
||||
/// </summary>
|
||||
public static class SpaPrerenderingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables server-side prerendering middleware for a Single Page Application.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <param name="configuration">Supplies configuration for the prerendering middleware.</param>
|
||||
public static void UseSpaPrerendering(
|
||||
this ISpaBuilder spaBuilder,
|
||||
Action<SpaPrerenderingOptions> configuration)
|
||||
{
|
||||
if (spaBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(spaBuilder));
|
||||
}
|
||||
|
||||
if (configuration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
var options = new SpaPrerenderingOptions();
|
||||
configuration.Invoke(options);
|
||||
|
||||
var capturedBootModulePath = options.BootModulePath;
|
||||
if (string.IsNullOrEmpty(capturedBootModulePath))
|
||||
{
|
||||
throw new InvalidOperationException($"To use {nameof(UseSpaPrerendering)}, you " +
|
||||
$"must set a nonempty value on the ${nameof(SpaPrerenderingOptions.BootModulePath)} " +
|
||||
$"property on the ${nameof(SpaPrerenderingOptions)}.");
|
||||
}
|
||||
|
||||
// If we're building on demand, start that process in the background now
|
||||
var buildOnDemandTask = options.BootModuleBuilder?.Build(spaBuilder);
|
||||
|
||||
// Get all the necessary context info that will be used for each prerendering call
|
||||
var applicationBuilder = spaBuilder.ApplicationBuilder;
|
||||
var serviceProvider = applicationBuilder.ApplicationServices;
|
||||
var nodeServices = GetNodeServices(serviceProvider);
|
||||
var applicationStoppingToken = serviceProvider.GetRequiredService<IApplicationLifetime>()
|
||||
.ApplicationStopping;
|
||||
var applicationBasePath = serviceProvider.GetRequiredService<IHostingEnvironment>()
|
||||
.ContentRootPath;
|
||||
var moduleExport = new JavaScriptModuleExport(capturedBootModulePath);
|
||||
var excludePathStrings = (options.ExcludeUrls ?? Array.Empty<string>())
|
||||
.Select(url => new PathString(url))
|
||||
.ToArray();
|
||||
var buildTimeout = spaBuilder.Options.StartupTimeout;
|
||||
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
// If this URL is excluded, skip prerendering.
|
||||
// This is typically used to ensure that static client-side resources
|
||||
// (e.g., /dist/*.css) are served normally or through SPA development
|
||||
// middleware, and don't return the prerendered index.html page.
|
||||
foreach (var excludePathString in excludePathStrings)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments(excludePathString))
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're building on demand, wait for that to finish, or raise any build errors
|
||||
if (buildOnDemandTask != null && !buildOnDemandTask.IsCompleted)
|
||||
{
|
||||
// For better debuggability, create a per-request timeout that makes it clear if the
|
||||
// prerendering builder took too long for this request, but without aborting the
|
||||
// underlying build task so that subsequent requests could still work.
|
||||
await buildOnDemandTask.WithTimeout(buildTimeout,
|
||||
$"The prerendering build process did not complete within the " +
|
||||
$"timeout period of {buildTimeout.Seconds} seconds. " +
|
||||
$"Check the log output for error information.");
|
||||
}
|
||||
|
||||
// It's no good if we try to return a 304. We need to capture the actual
|
||||
// HTML content so it can be passed as a template to the prerenderer.
|
||||
RemoveConditionalRequestHeaders(context.Request);
|
||||
|
||||
// Make sure we're not capturing compressed content, because then we'd have
|
||||
// to decompress it. Since this sub-request isn't leaving the machine, there's
|
||||
// little to no benefit in having compression on it.
|
||||
var originalAcceptEncodingValue = GetAndRemoveAcceptEncodingHeader(context.Request);
|
||||
|
||||
// Capture the non-prerendered responses, which in production will typically only
|
||||
// be returning the default SPA index.html page (because other resources will be
|
||||
// served statically from disk). We will use this as a template in which to inject
|
||||
// the prerendered output.
|
||||
using (var outputBuffer = new MemoryStream())
|
||||
{
|
||||
var originalResponseStream = context.Response.Body;
|
||||
context.Response.Body = outputBuffer;
|
||||
|
||||
try
|
||||
{
|
||||
await next();
|
||||
outputBuffer.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Response.Body = originalResponseStream;
|
||||
|
||||
if (!string.IsNullOrEmpty(originalAcceptEncodingValue))
|
||||
{
|
||||
context.Request.Headers[HeaderNames.AcceptEncoding] = originalAcceptEncodingValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If it isn't an HTML page that we can use as the template for prerendering,
|
||||
// - ... because it's not text/html
|
||||
// - ... or because it's an error
|
||||
// then prerendering doesn't apply to this request, so just pass through the
|
||||
// response as-is. Note that the non-text/html case is not an error: this is
|
||||
// typically how the SPA dev server responses for static content are returned
|
||||
// in development mode.
|
||||
var canPrerender = IsSuccessStatusCode(context.Response.StatusCode)
|
||||
&& IsHtmlContentType(context.Response.ContentType);
|
||||
if (!canPrerender)
|
||||
{
|
||||
await outputBuffer.CopyToAsync(context.Response.Body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Most prerendering logic will want to know about the original, unprerendered
|
||||
// HTML that the client would be getting otherwise. Typically this is used as
|
||||
// a template from which the fully prerendered page can be generated.
|
||||
var customData = new Dictionary<string, object>
|
||||
{
|
||||
{ "originalHtml", Encoding.UTF8.GetString(outputBuffer.GetBuffer()) }
|
||||
};
|
||||
|
||||
// If the developer wants to use custom logic to pass arbitrary data to the
|
||||
// prerendering JS code (e.g., to pass through cookie data), now's their chance
|
||||
options.SupplyData?.Invoke(context, customData);
|
||||
|
||||
var (unencodedAbsoluteUrl, unencodedPathAndQuery)
|
||||
= GetUnencodedUrlAndPathQuery(context);
|
||||
var renderResult = await Prerenderer.RenderToString(
|
||||
applicationBasePath,
|
||||
nodeServices,
|
||||
applicationStoppingToken,
|
||||
moduleExport,
|
||||
unencodedAbsoluteUrl,
|
||||
unencodedPathAndQuery,
|
||||
customDataParameter: customData,
|
||||
timeoutMilliseconds: 0,
|
||||
requestPathBase: context.Request.PathBase.ToString());
|
||||
|
||||
await ServePrerenderResult(context, renderResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static bool IsHtmlContentType(string contentType)
|
||||
{
|
||||
if (string.Equals(contentType, "text/html", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return contentType != null
|
||||
&& contentType.StartsWith("text/html;", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsSuccessStatusCode(int statusCode)
|
||||
=> statusCode >= 200 && statusCode < 300;
|
||||
|
||||
private static void RemoveConditionalRequestHeaders(HttpRequest request)
|
||||
{
|
||||
request.Headers.Remove(HeaderNames.IfMatch);
|
||||
request.Headers.Remove(HeaderNames.IfModifiedSince);
|
||||
request.Headers.Remove(HeaderNames.IfNoneMatch);
|
||||
request.Headers.Remove(HeaderNames.IfUnmodifiedSince);
|
||||
request.Headers.Remove(HeaderNames.IfRange);
|
||||
}
|
||||
|
||||
private static string GetAndRemoveAcceptEncodingHeader(HttpRequest request)
|
||||
{
|
||||
var headers = request.Headers;
|
||||
var value = (string)null;
|
||||
|
||||
if (headers.ContainsKey(HeaderNames.AcceptEncoding))
|
||||
{
|
||||
value = headers[HeaderNames.AcceptEncoding];
|
||||
headers.Remove(HeaderNames.AcceptEncoding);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static (string, string) GetUnencodedUrlAndPathQuery(HttpContext httpContext)
|
||||
{
|
||||
// This is a duplicate of code from Prerenderer.cs in the SpaServices package.
|
||||
// Once the SpaServices.Extension package implementation gets merged back into
|
||||
// SpaServices, this duplicate can be removed. To remove this, change the code
|
||||
// above that calls Prerenderer.RenderToString to use the internal overload
|
||||
// that takes an HttpContext instead of a url/path+query pair.
|
||||
var requestFeature = httpContext.Features.Get<IHttpRequestFeature>();
|
||||
var unencodedPathAndQuery = requestFeature.RawTarget;
|
||||
var request = httpContext.Request;
|
||||
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
|
||||
return (unencodedAbsoluteUrl, unencodedPathAndQuery);
|
||||
}
|
||||
|
||||
private static async Task ServePrerenderResult(HttpContext context, RenderToStringResult renderResult)
|
||||
{
|
||||
context.Response.Clear();
|
||||
|
||||
if (!string.IsNullOrEmpty(renderResult.RedirectUrl))
|
||||
{
|
||||
var permanentRedirect = renderResult.StatusCode.GetValueOrDefault() == 301;
|
||||
context.Response.Redirect(renderResult.RedirectUrl, permanentRedirect);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The Globals property exists for back-compatibility but is meaningless
|
||||
// for prerendering that returns complete HTML pages
|
||||
if (renderResult.Globals != null)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(renderResult.Globals)} is not " +
|
||||
$"supported when prerendering via {nameof(UseSpaPrerendering)}(). Instead, " +
|
||||
$"your prerendering logic should return a complete HTML page, in which you " +
|
||||
$"embed any information you wish to return to the client.");
|
||||
}
|
||||
|
||||
if (renderResult.StatusCode.HasValue)
|
||||
{
|
||||
context.Response.StatusCode = renderResult.StatusCode.Value;
|
||||
}
|
||||
|
||||
context.Response.ContentType = "text/html";
|
||||
await context.Response.WriteAsync(renderResult.Html);
|
||||
}
|
||||
}
|
||||
|
||||
private static INodeServices GetNodeServices(IServiceProvider serviceProvider)
|
||||
{
|
||||
// Use the registered instance, or create a new private instance if none is registered
|
||||
var instance = (INodeServices)serviceProvider.GetService(typeof(INodeServices));
|
||||
return instance ?? NodeServicesFactory.CreateNodeServices(
|
||||
new NodeServicesOptions(serviceProvider));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents options for the SPA prerendering middleware.
|
||||
/// </summary>
|
||||
public class SpaPrerenderingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets an <see cref="ISpaPrerendererBuilder"/> that the prerenderer will invoke before
|
||||
/// looking for the boot module file.
|
||||
///
|
||||
/// This is only intended to be used during development as a way of generating the JavaScript boot
|
||||
/// file automatically when the application runs. This property should be left as <c>null</c> in
|
||||
/// production applications.
|
||||
/// </summary>
|
||||
public ISpaPrerendererBuilder BootModuleBuilder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path, relative to your application root, of the JavaScript file
|
||||
/// containing prerendering logic.
|
||||
/// </summary>
|
||||
public string BootModulePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an array of URL prefixes for which prerendering should not run.
|
||||
/// </summary>
|
||||
public string[] ExcludeUrls { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a callback that will be invoked during prerendering, allowing you to pass additional
|
||||
/// data to the prerendering entrypoint code.
|
||||
/// </summary>
|
||||
public Action<HttpContext, IDictionary<string, object>> SupplyData { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Extensions.Proxy
|
||||
{
|
||||
// This duplicates and updates the proxying logic in SpaServices so that we can update
|
||||
// the project templates without waiting for 2.1 to ship. When 2.1 is ready to ship,
|
||||
// merge the additional proxying features (e.g., proxying websocket connections) back
|
||||
// into the SpaServices proxying code. It's all internal.
|
||||
internal class ConditionalProxyMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly Task<Uri> _baseUriTask;
|
||||
private readonly string _pathPrefix;
|
||||
private readonly bool _pathPrefixIsRoot;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly CancellationToken _applicationStoppingToken;
|
||||
|
||||
public ConditionalProxyMiddleware(
|
||||
RequestDelegate next,
|
||||
string pathPrefix,
|
||||
TimeSpan requestTimeout,
|
||||
Task<Uri> baseUriTask,
|
||||
IApplicationLifetime applicationLifetime)
|
||||
{
|
||||
if (!pathPrefix.StartsWith("/"))
|
||||
{
|
||||
pathPrefix = "/" + pathPrefix;
|
||||
}
|
||||
|
||||
_next = next;
|
||||
_pathPrefix = pathPrefix;
|
||||
_pathPrefixIsRoot = string.Equals(_pathPrefix, "/", StringComparison.Ordinal);
|
||||
_baseUriTask = baseUriTask;
|
||||
_httpClient = SpaProxy.CreateHttpClientForProxy(requestTimeout);
|
||||
_applicationStoppingToken = applicationLifetime.ApplicationStopping;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments(_pathPrefix) || _pathPrefixIsRoot)
|
||||
{
|
||||
var didProxyRequest = await SpaProxy.PerformProxyRequest(
|
||||
context, _httpClient, _baseUriTask, _applicationStoppingToken, proxy404s: false);
|
||||
if (didProxyRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Not a request we can proxy
|
||||
await _next.Invoke(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Extensions.Proxy
|
||||
{
|
||||
// This duplicates and updates the proxying logic in SpaServices so that we can update
|
||||
// the project templates without waiting for 2.1 to ship. When 2.1 is ready to ship,
|
||||
// remove the old ConditionalProxy.cs from SpaServices and replace its usages with this.
|
||||
// Doesn't affect public API surface - it's all internal.
|
||||
internal static class SpaProxy
|
||||
{
|
||||
private const int DefaultWebSocketBufferSize = 4096;
|
||||
private const int StreamCopyBufferSize = 81920;
|
||||
|
||||
// Don't forward User-Agent/Accept because of https://github.com/aspnet/JavaScriptServices/issues/1469
|
||||
// Others just aren't applicable in proxy scenarios
|
||||
private static readonly string[] NotForwardedWebSocketHeaders = new[] { "Accept", "Connection", "Host", "User-Agent", "Upgrade", "Sec-WebSocket-Key", "Sec-WebSocket-Version" };
|
||||
|
||||
public static HttpClient CreateHttpClientForProxy(TimeSpan requestTimeout)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
UseCookies = false,
|
||||
|
||||
};
|
||||
|
||||
return new HttpClient(handler)
|
||||
{
|
||||
Timeout = requestTimeout
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<bool> PerformProxyRequest(
|
||||
HttpContext context,
|
||||
HttpClient httpClient,
|
||||
Task<Uri> baseUriTask,
|
||||
CancellationToken applicationStoppingToken,
|
||||
bool proxy404s)
|
||||
{
|
||||
// Stop proxying if either the server or client wants to disconnect
|
||||
var proxyCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
context.RequestAborted,
|
||||
applicationStoppingToken).Token;
|
||||
|
||||
// We allow for the case where the target isn't known ahead of time, and want to
|
||||
// delay proxied requests until the target becomes known. This is useful, for example,
|
||||
// when proxying to Angular CLI middleware: we won't know what port it's listening
|
||||
// on until it finishes starting up.
|
||||
var baseUri = await baseUriTask;
|
||||
var targetUri = new Uri(
|
||||
baseUri,
|
||||
context.Request.Path + context.Request.QueryString);
|
||||
|
||||
try
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
await AcceptProxyWebSocketRequest(context, ToWebSocketScheme(targetUri), proxyCancellationToken);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var requestMessage = CreateProxyHttpRequest(context, targetUri))
|
||||
using (var responseMessage = await httpClient.SendAsync(
|
||||
requestMessage,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
proxyCancellationToken))
|
||||
{
|
||||
if (!proxy404s)
|
||||
{
|
||||
if (responseMessage.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
// We're not proxying 404s, i.e., we want to resume the middleware pipeline
|
||||
// and let some other middleware handle this.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await CopyProxyHttpResponse(context, responseMessage, proxyCancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// If we're aborting because either the client disconnected, or the server
|
||||
// is shutting down, don't treat this as an error.
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// This kind of exception can also occur if a proxy read/write gets interrupted
|
||||
// due to the process shutting down.
|
||||
return true;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"Failed to proxy the request to {targetUri.ToString()}, because the request to " +
|
||||
$"the proxy target failed. Check that the proxy target server is running and " +
|
||||
$"accepting requests to {baseUri.ToString()}.\n\n" +
|
||||
$"The underlying exception message was '{ex.Message}'." +
|
||||
$"Check the InnerException for more details.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateProxyHttpRequest(HttpContext context, Uri uri)
|
||||
{
|
||||
var request = context.Request;
|
||||
|
||||
var requestMessage = new HttpRequestMessage();
|
||||
var requestMethod = request.Method;
|
||||
if (!HttpMethods.IsGet(requestMethod) &&
|
||||
!HttpMethods.IsHead(requestMethod) &&
|
||||
!HttpMethods.IsDelete(requestMethod) &&
|
||||
!HttpMethods.IsTrace(requestMethod))
|
||||
{
|
||||
var streamContent = new StreamContent(request.Body);
|
||||
requestMessage.Content = streamContent;
|
||||
}
|
||||
|
||||
// Copy the request headers
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
|
||||
{
|
||||
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
requestMessage.Headers.Host = uri.Authority;
|
||||
requestMessage.RequestUri = uri;
|
||||
requestMessage.Method = new HttpMethod(request.Method);
|
||||
|
||||
return requestMessage;
|
||||
}
|
||||
|
||||
private static async Task CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Response.StatusCode = (int)responseMessage.StatusCode;
|
||||
foreach (var header in responseMessage.Headers)
|
||||
{
|
||||
context.Response.Headers[header.Key] = header.Value.ToArray();
|
||||
}
|
||||
|
||||
foreach (var header in responseMessage.Content.Headers)
|
||||
{
|
||||
context.Response.Headers[header.Key] = header.Value.ToArray();
|
||||
}
|
||||
|
||||
// SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response.
|
||||
context.Response.Headers.Remove("transfer-encoding");
|
||||
|
||||
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
|
||||
{
|
||||
await responseStream.CopyToAsync(context.Response.Body, StreamCopyBufferSize, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static Uri ToWebSocketScheme(Uri uri)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
}
|
||||
|
||||
var uriBuilder = new UriBuilder(uri);
|
||||
if (string.Equals(uriBuilder.Scheme, "https", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
uriBuilder.Scheme = "wss";
|
||||
}
|
||||
else if (string.Equals(uriBuilder.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
uriBuilder.Scheme = "ws";
|
||||
}
|
||||
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
|
||||
private static async Task<bool> AcceptProxyWebSocketRequest(HttpContext context, Uri destinationUri, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (destinationUri == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destinationUri));
|
||||
}
|
||||
|
||||
using (var client = new ClientWebSocket())
|
||||
{
|
||||
foreach (var headerEntry in context.Request.Headers)
|
||||
{
|
||||
if (!NotForwardedWebSocketHeaders.Contains(headerEntry.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
client.Options.SetRequestHeader(headerEntry.Key, headerEntry.Value);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// On net461, certain header names are reserved and can't be set.
|
||||
// We filter out the known ones via the test above, but there could
|
||||
// be others arbitrarily set by the client. It's not helpful to
|
||||
// consider it an error, so just skip non-forwardable headers.
|
||||
// The perf implications of handling this via a catch aren't an
|
||||
// issue since this is a dev-time only feature.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Note that this is not really good enough to make Websockets work with
|
||||
// Angular CLI middleware. For some reason, ConnectAsync takes over 1 second,
|
||||
// on Windows, by which time the logic in SockJS has already timed out and made
|
||||
// it fall back on some other transport (xhr_streaming, usually). It's fine
|
||||
// on Linux though, completing almost instantly.
|
||||
//
|
||||
// The slowness on Windows does not cause a problem though, because the transport
|
||||
// fallback logic works correctly and doesn't surface any errors, but it would be
|
||||
// better if ConnectAsync was fast enough and the initial Websocket transport
|
||||
// could actually be used.
|
||||
await client.ConnectAsync(destinationUri, cancellationToken);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return false;
|
||||
}
|
||||
|
||||
using (var server = await context.WebSockets.AcceptWebSocketAsync(client.SubProtocol))
|
||||
{
|
||||
var bufferSize = DefaultWebSocketBufferSize;
|
||||
await Task.WhenAll(
|
||||
PumpWebSocket(client, server, bufferSize, cancellationToken),
|
||||
PumpWebSocket(server, client, bufferSize, cancellationToken));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task PumpWebSocket(WebSocket source, WebSocket destination, int bufferSize, CancellationToken cancellationToken)
|
||||
{
|
||||
if (bufferSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||
}
|
||||
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Because WebSocket.ReceiveAsync doesn't work well with CancellationToken (it doesn't
|
||||
// actually exit when the token notifies, at least not in the 'server' case), use
|
||||
// polling. The perf might not be ideal, but this is a dev-time feature only.
|
||||
var resultTask = source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
|
||||
while (true)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultTask.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
var result = resultTask.Result; // We know it's completed already
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
if (destination.State == WebSocketState.Open || destination.State == WebSocketState.CloseReceived)
|
||||
{
|
||||
await destination.CloseOutputAsync(source.CloseStatus.Value, source.CloseStatusDescription, cancellationToken);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.SpaServices;
|
||||
using Microsoft.AspNetCore.SpaServices.Extensions.Proxy;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for proxying requests to a local SPA development server during
|
||||
/// development. Not for use in production applications.
|
||||
/// </summary>
|
||||
public static class SpaProxyingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the application to forward incoming requests to a local Single Page
|
||||
/// Application (SPA) development server. This is only intended to be used during
|
||||
/// development. Do not enable this middleware in production applications.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <param name="baseUri">The target base URI to which requests should be proxied.</param>
|
||||
public static void UseProxyToSpaDevelopmentServer(
|
||||
this ISpaBuilder spaBuilder,
|
||||
string baseUri)
|
||||
{
|
||||
UseProxyToSpaDevelopmentServer(
|
||||
spaBuilder,
|
||||
new Uri(baseUri));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application to forward incoming requests to a local Single Page
|
||||
/// Application (SPA) development server. This is only intended to be used during
|
||||
/// development. Do not enable this middleware in production applications.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <param name="baseUri">The target base URI to which requests should be proxied.</param>
|
||||
public static void UseProxyToSpaDevelopmentServer(
|
||||
this ISpaBuilder spaBuilder,
|
||||
Uri baseUri)
|
||||
{
|
||||
UseProxyToSpaDevelopmentServer(
|
||||
spaBuilder,
|
||||
() => Task.FromResult(baseUri));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application to forward incoming requests to a local Single Page
|
||||
/// Application (SPA) development server. This is only intended to be used during
|
||||
/// development. Do not enable this middleware in production applications.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <param name="baseUriTaskFactory">A callback that will be invoked on each request to supply a <see cref="Task"/> that resolves with the target base URI to which requests should be proxied.</param>
|
||||
public static void UseProxyToSpaDevelopmentServer(
|
||||
this ISpaBuilder spaBuilder,
|
||||
Func<Task<Uri>> baseUriTaskFactory)
|
||||
{
|
||||
var applicationBuilder = spaBuilder.ApplicationBuilder;
|
||||
var applicationStoppingToken = GetStoppingToken(applicationBuilder);
|
||||
|
||||
// Since we might want to proxy WebSockets requests (e.g., by default, AngularCliMiddleware
|
||||
// requires it), enable it for the app
|
||||
applicationBuilder.UseWebSockets();
|
||||
|
||||
// It's important not to time out the requests, as some of them might be to
|
||||
// server-sent event endpoints or similar, where it's expected that the response
|
||||
// takes an unlimited time and never actually completes
|
||||
var neverTimeOutHttpClient =
|
||||
SpaProxy.CreateHttpClientForProxy(Timeout.InfiniteTimeSpan);
|
||||
|
||||
// Proxy all requests to the SPA development server
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
var didProxyRequest = await SpaProxy.PerformProxyRequest(
|
||||
context, neverTimeOutHttpClient, baseUriTaskFactory(), applicationStoppingToken,
|
||||
proxy404s: true);
|
||||
});
|
||||
}
|
||||
|
||||
private static CancellationToken GetStoppingToken(IApplicationBuilder appBuilder)
|
||||
{
|
||||
var applicationLifetime = appBuilder
|
||||
.ApplicationServices
|
||||
.GetService(typeof(IApplicationLifetime));
|
||||
return ((IApplicationLifetime)applicationLifetime).ApplicationStopping;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||
using Microsoft.AspNetCore.NodeServices.Util;
|
||||
using Microsoft.AspNetCore.SpaServices.Util;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SpaServices.Extensions.Util;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
||||
{
|
||||
internal static class ReactDevelopmentServerMiddleware
|
||||
{
|
||||
private const string LogCategoryName = "Microsoft.AspNetCore.SpaServices";
|
||||
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
|
||||
|
||||
public static void Attach(
|
||||
ISpaBuilder spaBuilder,
|
||||
string npmScriptName)
|
||||
{
|
||||
var sourcePath = spaBuilder.Options.SourcePath;
|
||||
if (string.IsNullOrEmpty(sourcePath))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(npmScriptName))
|
||||
{
|
||||
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
|
||||
}
|
||||
|
||||
// Start create-react-app and attach to middleware pipeline
|
||||
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
|
||||
var portTask = StartCreateReactAppServerAsync(sourcePath, npmScriptName, logger);
|
||||
|
||||
// Everything we proxy is hardcoded to target http://localhost because:
|
||||
// - the requests are always from the local machine (we're not accepting remote
|
||||
// requests that go directly to the create-react-app server)
|
||||
// - given that, there's no reason to use https, and we couldn't even if we
|
||||
// wanted to, because in general the create-react-app server has no certificate
|
||||
var targetUriTask = portTask.ContinueWith(
|
||||
task => new UriBuilder("http", "localhost", task.Result).Uri);
|
||||
|
||||
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
|
||||
{
|
||||
// On each request, we create a separate startup task with its own timeout. That way, even if
|
||||
// the first request times out, subsequent requests could still work.
|
||||
var timeout = spaBuilder.Options.StartupTimeout;
|
||||
return targetUriTask.WithTimeout(timeout,
|
||||
$"The create-react-app server did not start listening for requests " +
|
||||
$"within the timeout period of {timeout.Seconds} seconds. " +
|
||||
$"Check the log output for error information.");
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<int> StartCreateReactAppServerAsync(
|
||||
string sourcePath, string npmScriptName, ILogger logger)
|
||||
{
|
||||
var portNumber = TcpPortFinder.FindAvailablePort();
|
||||
logger.LogInformation($"Starting create-react-app server on port {portNumber}...");
|
||||
|
||||
var envVars = new Dictionary<string, string>
|
||||
{
|
||||
{ "PORT", portNumber.ToString() },
|
||||
{ "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port
|
||||
};
|
||||
var npmScriptRunner = new NpmScriptRunner(
|
||||
sourcePath, npmScriptName, null, envVars);
|
||||
npmScriptRunner.AttachToLogger(logger);
|
||||
|
||||
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Although the React dev server may eventually tell us the URL it's listening on,
|
||||
// it doesn't do so until it's finished compiling, and even then only if there were
|
||||
// no compiler warnings. So instead of waiting for that, consider it ready as soon
|
||||
// as it starts listening for requests.
|
||||
await npmScriptRunner.StdOut.WaitForMatch(
|
||||
new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout));
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The NPM script '{npmScriptName}' exited without indicating that the " +
|
||||
$"create-react-app server was listening for requests. The error output was: " +
|
||||
$"{stdErrReader.ReadAsString()}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return portNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for enabling React development server middleware support.
|
||||
/// </summary>
|
||||
public static class ReactDevelopmentServerMiddlewareExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles requests by passing them through to an instance of the create-react-app server.
|
||||
/// This means you can always serve up-to-date CLI-built resources without having
|
||||
/// to run the create-react-app server manually.
|
||||
///
|
||||
/// This feature should only be used in development. For production deployments, be
|
||||
/// sure not to enable the create-react-app server.
|
||||
/// </summary>
|
||||
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||
/// <param name="npmScript">The name of the script in your package.json file that launches the create-react-app server.</param>
|
||||
public static void UseReactDevelopmentServer(
|
||||
this ISpaBuilder spaBuilder,
|
||||
string npmScript)
|
||||
{
|
||||
if (spaBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(spaBuilder));
|
||||
}
|
||||
|
||||
var spaOptions = spaBuilder.Options;
|
||||
|
||||
if (string.IsNullOrEmpty(spaOptions.SourcePath))
|
||||
{
|
||||
throw new InvalidOperationException($"To use {nameof(UseReactDevelopmentServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
||||
}
|
||||
|
||||
ReactDevelopmentServerMiddleware.Attach(spaBuilder, npmScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.SpaServices;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods used for configuring an application to
|
||||
/// host a client-side Single Page Application (SPA).
|
||||
/// </summary>
|
||||
public static class SpaApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles all requests from this point in the middleware chain by returning
|
||||
/// the default page for the Single Page Application (SPA).
|
||||
///
|
||||
/// This middleware should be placed late in the chain, so that other middleware
|
||||
/// for serving static files, MVC actions, etc., takes precedence.
|
||||
/// </summary>
|
||||
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <param name="configuration">
|
||||
/// This callback will be invoked so that additional middleware can be registered within
|
||||
/// the context of this SPA.
|
||||
/// </param>
|
||||
public static void UseSpa(this IApplicationBuilder app, Action<ISpaBuilder> configuration)
|
||||
{
|
||||
if (configuration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
// Use the options configured in DI (or blank if none was configured). We have to clone it
|
||||
// otherwise if you have multiple UseSpa calls, their configurations would interfere with one another.
|
||||
var optionsProvider = app.ApplicationServices.GetService<IOptions<SpaOptions>>();
|
||||
var options = new SpaOptions(optionsProvider.Value);
|
||||
|
||||
var spaBuilder = new DefaultSpaBuilder(app, options);
|
||||
configuration.Invoke(spaBuilder);
|
||||
SpaDefaultPageMiddleware.Attach(spaBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices
|
||||
{
|
||||
internal class SpaDefaultPageMiddleware
|
||||
{
|
||||
public static void Attach(ISpaBuilder spaBuilder)
|
||||
{
|
||||
if (spaBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(spaBuilder));
|
||||
}
|
||||
|
||||
var app = spaBuilder.ApplicationBuilder;
|
||||
var options = spaBuilder.Options;
|
||||
|
||||
// Rewrite all requests to the default page
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
context.Request.Path = options.DefaultPage;
|
||||
return next();
|
||||
});
|
||||
|
||||
// Serve it as a static file
|
||||
// Developers who need to host more than one SPA with distinct default pages can
|
||||
// override the file provider
|
||||
app.UseSpaStaticFilesInternal(
|
||||
options.DefaultPageStaticFileOptions ?? new StaticFileOptions(),
|
||||
allowFallbackOnServingWebRootFiles: true);
|
||||
|
||||
// If the default file didn't get served as a static file (usually because it was not
|
||||
// present on disk), the SPA is definitely not going to work.
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
var message = "The SPA default page middleware could not return the default page " +
|
||||
$"'{options.DefaultPage}' because it was not found, and no other middleware " +
|
||||
"handled the request.\n";
|
||||
|
||||
// Try to clarify the common scenario where someone runs an application in
|
||||
// Production environment without first publishing the whole application
|
||||
// or at least building the SPA.
|
||||
var hostEnvironment = (IHostingEnvironment)context.RequestServices.GetService(typeof(IHostingEnvironment));
|
||||
if (hostEnvironment != null && hostEnvironment.IsProduction())
|
||||
{
|
||||
message += "Your application is running in Production mode, so make sure it has " +
|
||||
"been published, or that you have built your SPA manually. Alternatively you " +
|
||||
"may wish to switch to the Development environment.\n";
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes options for hosting a Single Page Application (SPA).
|
||||
/// </summary>
|
||||
public class SpaOptions
|
||||
{
|
||||
private PathString _defaultPage = "/index.html";
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="SpaOptions"/>.
|
||||
/// </summary>
|
||||
public SpaOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="SpaOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="copyFromOptions">An instance of <see cref="SpaOptions"/> from which values should be copied.</param>
|
||||
internal SpaOptions(SpaOptions copyFromOptions)
|
||||
{
|
||||
_defaultPage = copyFromOptions.DefaultPage;
|
||||
DefaultPageStaticFileOptions = copyFromOptions.DefaultPageStaticFileOptions;
|
||||
SourcePath = copyFromOptions.SourcePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the default page that hosts your SPA user interface.
|
||||
/// The default value is <c>"/index.html"</c>.
|
||||
/// </summary>
|
||||
public PathString DefaultPage
|
||||
{
|
||||
get => _defaultPage;
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value.Value))
|
||||
{
|
||||
throw new ArgumentException($"The value for {nameof(DefaultPage)} cannot be null or empty.");
|
||||
}
|
||||
|
||||
_defaultPage = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="StaticFileOptions"/> that supplies content
|
||||
/// for serving the SPA's default page.
|
||||
///
|
||||
/// If not set, a default file provider will read files from the
|
||||
/// <see cref="IHostingEnvironment.WebRootPath"/>, which by default is
|
||||
/// the <c>wwwroot</c> directory.
|
||||
/// </summary>
|
||||
public StaticFileOptions DefaultPageStaticFileOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path, relative to the application working directory,
|
||||
/// of the directory that contains the SPA source files during
|
||||
/// development. The directory may not exist in published applications.
|
||||
/// </summary>
|
||||
public string SourcePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum duration that a request will wait for the SPA
|
||||
/// to become ready to serve to the client.
|
||||
/// </summary>
|
||||
public TimeSpan StartupTimeout { get; set; } = TimeSpan.FromSeconds(50);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.StaticFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an implementation of <see cref="ISpaStaticFileProvider"/> that supplies
|
||||
/// physical files at a location configured using <see cref="SpaStaticFilesOptions"/>.
|
||||
/// </summary>
|
||||
internal class DefaultSpaStaticFileProvider : ISpaStaticFileProvider
|
||||
{
|
||||
private IFileProvider _fileProvider;
|
||||
|
||||
public DefaultSpaStaticFileProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
SpaStaticFilesOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(options.RootPath))
|
||||
{
|
||||
throw new ArgumentException($"The {nameof(options.RootPath)} property " +
|
||||
$"of {nameof(options)} cannot be null or empty.");
|
||||
}
|
||||
|
||||
var env = serviceProvider.GetRequiredService<IHostingEnvironment>();
|
||||
var absoluteRootPath = Path.Combine(
|
||||
env.ContentRootPath,
|
||||
options.RootPath);
|
||||
|
||||
// PhysicalFileProvider will throw if you pass a non-existent path,
|
||||
// but we don't want that scenario to be an error because for SPA
|
||||
// scenarios, it's better if non-existing directory just means we
|
||||
// don't serve any static files.
|
||||
if (Directory.Exists(absoluteRootPath))
|
||||
{
|
||||
_fileProvider = new PhysicalFileProvider(absoluteRootPath);
|
||||
}
|
||||
}
|
||||
|
||||
public IFileProvider FileProvider => _fileProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.StaticFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a service that can provide static files to be served for a Single Page
|
||||
/// Application (SPA).
|
||||
/// </summary>
|
||||
public interface ISpaStaticFileProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the file provider, if available, that supplies the static files for the SPA.
|
||||
/// The value is <c>null</c> if no file provider is available.
|
||||
/// </summary>
|
||||
IFileProvider FileProvider { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.SpaServices.StaticFiles;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for configuring an application to serve static files for a
|
||||
/// Single Page Application (SPA).
|
||||
/// </summary>
|
||||
public static class SpaStaticFilesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers an <see cref="ISpaStaticFileProvider"/> service that can provide static
|
||||
/// files to be served for a Single Page Application (SPA).
|
||||
/// </summary>
|
||||
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
|
||||
/// <param name="configuration">If specified, this callback will be invoked to set additional configuration options.</param>
|
||||
public static void AddSpaStaticFiles(
|
||||
this IServiceCollection services,
|
||||
Action<SpaStaticFilesOptions> configuration = null)
|
||||
{
|
||||
services.AddSingleton<ISpaStaticFileProvider>(serviceProvider =>
|
||||
{
|
||||
// Use the options configured in DI (or blank if none was configured)
|
||||
var optionsProvider = serviceProvider.GetService<IOptions<SpaStaticFilesOptions>>();
|
||||
var options = optionsProvider.Value;
|
||||
|
||||
// Allow the developer to perform further configuration
|
||||
configuration?.Invoke(options);
|
||||
|
||||
if (string.IsNullOrEmpty(options.RootPath))
|
||||
{
|
||||
throw new InvalidOperationException($"No {nameof(SpaStaticFilesOptions.RootPath)} " +
|
||||
$"was set on the {nameof(SpaStaticFilesOptions)}.");
|
||||
}
|
||||
|
||||
return new DefaultSpaStaticFileProvider(serviceProvider, options);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application to serve static files for a Single Page Application (SPA).
|
||||
/// The files will be located using the registered <see cref="ISpaStaticFileProvider"/> service.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/>.</param>
|
||||
public static void UseSpaStaticFiles(this IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
UseSpaStaticFiles(applicationBuilder, new StaticFileOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the application to serve static files for a Single Page Application (SPA).
|
||||
/// The files will be located using the registered <see cref="ISpaStaticFileProvider"/> service.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/>.</param>
|
||||
/// <param name="options">Specifies options for serving the static files.</param>
|
||||
public static void UseSpaStaticFiles(this IApplicationBuilder applicationBuilder, StaticFileOptions options)
|
||||
{
|
||||
if (applicationBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(applicationBuilder));
|
||||
}
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
UseSpaStaticFilesInternal(applicationBuilder,
|
||||
staticFileOptions: options,
|
||||
allowFallbackOnServingWebRootFiles: false);
|
||||
}
|
||||
|
||||
internal static void UseSpaStaticFilesInternal(
|
||||
this IApplicationBuilder app,
|
||||
StaticFileOptions staticFileOptions,
|
||||
bool allowFallbackOnServingWebRootFiles)
|
||||
{
|
||||
if (staticFileOptions == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(staticFileOptions));
|
||||
}
|
||||
|
||||
// If the file provider was explicitly supplied, that takes precedence over any other
|
||||
// configured file provider. This is most useful if the application hosts multiple SPAs
|
||||
// (via multiple calls to UseSpa()), so each needs to serve its own separate static files
|
||||
// instead of using AddSpaStaticFiles/UseSpaStaticFiles.
|
||||
// But if no file provider was specified, try to get one from the DI config.
|
||||
if (staticFileOptions.FileProvider == null)
|
||||
{
|
||||
var shouldServeStaticFiles = ShouldServeStaticFiles(
|
||||
app,
|
||||
allowFallbackOnServingWebRootFiles,
|
||||
out var fileProviderOrDefault);
|
||||
if (shouldServeStaticFiles)
|
||||
{
|
||||
staticFileOptions.FileProvider = fileProviderOrDefault;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The registered ISpaStaticFileProvider says we shouldn't
|
||||
// serve static files
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app.UseStaticFiles(staticFileOptions);
|
||||
}
|
||||
|
||||
private static bool ShouldServeStaticFiles(
|
||||
IApplicationBuilder app,
|
||||
bool allowFallbackOnServingWebRootFiles,
|
||||
out IFileProvider fileProviderOrDefault)
|
||||
{
|
||||
var spaStaticFilesService = app.ApplicationServices.GetService<ISpaStaticFileProvider>();
|
||||
if (spaStaticFilesService != null)
|
||||
{
|
||||
// If an ISpaStaticFileProvider was configured but it says no IFileProvider is available
|
||||
// (i.e., it supplies 'null'), this implies we should not serve any static files. This
|
||||
// is typically the case in development when SPA static files are being served from a
|
||||
// SPA development server (e.g., Angular CLI or create-react-app), in which case no
|
||||
// directory of prebuilt files will exist on disk.
|
||||
fileProviderOrDefault = spaStaticFilesService.FileProvider;
|
||||
return fileProviderOrDefault != null;
|
||||
}
|
||||
else if (!allowFallbackOnServingWebRootFiles)
|
||||
{
|
||||
throw new InvalidOperationException($"To use {nameof(UseSpaStaticFiles)}, you must " +
|
||||
$"first register an {nameof(ISpaStaticFileProvider)} in the service provider, typically " +
|
||||
$"by calling services.{nameof(AddSpaStaticFiles)}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back on serving wwwroot
|
||||
fileProviderOrDefault = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.StaticFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents options for serving static files for a Single Page Application (SPA).
|
||||
/// </summary>
|
||||
public class SpaStaticFilesOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path, relative to the application root, of the directory in which
|
||||
/// the physical files are located.
|
||||
///
|
||||
/// If the specified directory does not exist, then the
|
||||
/// <see cref="SpaStaticFilesExtensions.UseSpaStaticFiles(Builder.IApplicationBuilder)"/>
|
||||
/// middleware will not serve any static files.
|
||||
/// </summary>
|
||||
public string RootPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="StreamReader"/> to expose an evented API, issuing notifications
|
||||
/// when the stream emits partial lines, completed lines, or finally closes.
|
||||
/// </summary>
|
||||
internal class EventedStreamReader
|
||||
{
|
||||
public delegate void OnReceivedChunkHandler(ArraySegment<char> chunk);
|
||||
public delegate void OnReceivedLineHandler(string line);
|
||||
public delegate void OnStreamClosedHandler();
|
||||
|
||||
public event OnReceivedChunkHandler OnReceivedChunk;
|
||||
public event OnReceivedLineHandler OnReceivedLine;
|
||||
public event OnStreamClosedHandler OnStreamClosed;
|
||||
|
||||
private readonly StreamReader _streamReader;
|
||||
private readonly StringBuilder _linesBuffer;
|
||||
|
||||
public EventedStreamReader(StreamReader streamReader)
|
||||
{
|
||||
_streamReader = streamReader ?? throw new ArgumentNullException(nameof(streamReader));
|
||||
_linesBuffer = new StringBuilder();
|
||||
Task.Factory.StartNew(Run);
|
||||
}
|
||||
|
||||
public Task<Match> WaitForMatch(Regex regex)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<Match>();
|
||||
var completionLock = new object();
|
||||
|
||||
OnReceivedLineHandler onReceivedLineHandler = null;
|
||||
OnStreamClosedHandler onStreamClosedHandler = null;
|
||||
|
||||
void ResolveIfStillPending(Action applyResolution)
|
||||
{
|
||||
lock (completionLock)
|
||||
{
|
||||
if (!tcs.Task.IsCompleted)
|
||||
{
|
||||
OnReceivedLine -= onReceivedLineHandler;
|
||||
OnStreamClosed -= onStreamClosedHandler;
|
||||
applyResolution();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReceivedLineHandler = line =>
|
||||
{
|
||||
var match = regex.Match(line);
|
||||
if (match.Success)
|
||||
{
|
||||
ResolveIfStillPending(() => tcs.SetResult(match));
|
||||
}
|
||||
};
|
||||
|
||||
onStreamClosedHandler = () =>
|
||||
{
|
||||
ResolveIfStillPending(() => tcs.SetException(new EndOfStreamException()));
|
||||
};
|
||||
|
||||
OnReceivedLine += onReceivedLineHandler;
|
||||
OnStreamClosed += onStreamClosedHandler;
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private async Task Run()
|
||||
{
|
||||
var buf = new char[8 * 1024];
|
||||
while (true)
|
||||
{
|
||||
var chunkLength = await _streamReader.ReadAsync(buf, 0, buf.Length);
|
||||
if (chunkLength == 0)
|
||||
{
|
||||
OnClosed();
|
||||
break;
|
||||
}
|
||||
|
||||
OnChunk(new ArraySegment<char>(buf, 0, chunkLength));
|
||||
|
||||
var lineBreakPos = Array.IndexOf(buf, '\n', 0, chunkLength);
|
||||
if (lineBreakPos < 0)
|
||||
{
|
||||
_linesBuffer.Append(buf, 0, chunkLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
_linesBuffer.Append(buf, 0, lineBreakPos + 1);
|
||||
OnCompleteLine(_linesBuffer.ToString());
|
||||
_linesBuffer.Clear();
|
||||
_linesBuffer.Append(buf, lineBreakPos + 1, chunkLength - (lineBreakPos + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChunk(ArraySegment<char> chunk)
|
||||
{
|
||||
var dlg = OnReceivedChunk;
|
||||
dlg?.Invoke(chunk);
|
||||
}
|
||||
|
||||
private void OnCompleteLine(string line)
|
||||
{
|
||||
var dlg = OnReceivedLine;
|
||||
dlg?.Invoke(line);
|
||||
}
|
||||
|
||||
private void OnClosed()
|
||||
{
|
||||
var dlg = OnStreamClosed;
|
||||
dlg?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.NodeServices.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Captures the completed-line notifications from a <see cref="EventedStreamReader"/>,
|
||||
/// combining the data into a single <see cref="string"/>.
|
||||
/// </summary>
|
||||
internal class EventedStreamStringReader : IDisposable
|
||||
{
|
||||
private EventedStreamReader _eventedStreamReader;
|
||||
private bool _isDisposed;
|
||||
private StringBuilder _stringBuilder = new StringBuilder();
|
||||
|
||||
public EventedStreamStringReader(EventedStreamReader eventedStreamReader)
|
||||
{
|
||||
_eventedStreamReader = eventedStreamReader
|
||||
?? throw new ArgumentNullException(nameof(eventedStreamReader));
|
||||
_eventedStreamReader.OnReceivedLine += OnReceivedLine;
|
||||
}
|
||||
|
||||
public string ReadAsString() => _stringBuilder.ToString();
|
||||
|
||||
private void OnReceivedLine(string line) => _stringBuilder.AppendLine(line);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_eventedStreamReader.OnReceivedLine -= OnReceivedLine;
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Util
|
||||
{
|
||||
internal static class LoggerFinder
|
||||
{
|
||||
public static ILogger GetOrCreateLogger(
|
||||
IApplicationBuilder appBuilder,
|
||||
string logCategoryName)
|
||||
{
|
||||
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
|
||||
var loggerFactory = appBuilder.ApplicationServices.GetService<ILoggerFactory>();
|
||||
var logger = loggerFactory != null
|
||||
? loggerFactory.CreateLogger(logCategoryName)
|
||||
: new ConsoleLogger(logCategoryName, null, false);
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Extensions.Util
|
||||
{
|
||||
internal static class TaskTimeoutExtensions
|
||||
{
|
||||
public static async Task WithTimeout(this Task task, TimeSpan timeoutDelay, string message)
|
||||
{
|
||||
if (task == await Task.WhenAny(task, Task.Delay(timeoutDelay)))
|
||||
{
|
||||
task.Wait(); // Allow any errors to propagate
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeoutDelay, string message)
|
||||
{
|
||||
if (task == await Task.WhenAny(task, Task.Delay(timeoutDelay)))
|
||||
{
|
||||
return task.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Util
|
||||
{
|
||||
internal static class TcpPortFinder
|
||||
{
|
||||
public static int FindAvailablePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
try
|
||||
{
|
||||
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\build\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Helpers for building single-page applications on ASP.NET MVC Core.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="node_modules\**\*" />
|
||||
<EmbeddedResource Include="Content\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="$(AspNetCoreVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="$(MicrosoftAspNetCoreMvcTagHelpersPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="$(MicrosoftAspNetCoreMvcViewFeaturesPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of a DI service that provides convenient access to
|
||||
/// server-side prerendering APIs. This is an alternative to prerendering via
|
||||
/// the asp-prerender-module tag helper.
|
||||
/// </summary>
|
||||
internal class DefaultSpaPrerenderer : ISpaPrerenderer
|
||||
{
|
||||
private readonly string _applicationBasePath;
|
||||
private readonly CancellationToken _applicationStoppingToken;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly INodeServices _nodeServices;
|
||||
|
||||
public DefaultSpaPrerenderer(
|
||||
INodeServices nodeServices,
|
||||
IApplicationLifetime applicationLifetime,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_applicationBasePath = hostingEnvironment.ContentRootPath;
|
||||
_applicationStoppingToken = applicationLifetime.ApplicationStopping;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_nodeServices = nodeServices;
|
||||
}
|
||||
|
||||
public Task<RenderToStringResult> RenderToString(
|
||||
string moduleName,
|
||||
string exportName = null,
|
||||
object customDataParameter = null,
|
||||
int timeoutMilliseconds = default(int))
|
||||
{
|
||||
return Prerenderer.RenderToString(
|
||||
_applicationBasePath,
|
||||
_nodeServices,
|
||||
_applicationStoppingToken,
|
||||
new JavaScriptModuleExport(moduleName) { ExportName = exportName },
|
||||
_httpContextAccessor.HttpContext,
|
||||
customDataParameter,
|
||||
timeoutMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a service that can perform server-side prerendering for
|
||||
/// JavaScript-based Single Page Applications. This is an alternative
|
||||
/// to using the 'asp-prerender-module' tag helper.
|
||||
/// </summary>
|
||||
public interface ISpaPrerenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes JavaScript code to perform server-side prerendering for a
|
||||
/// Single-Page Application. This is an alternative to using the
|
||||
/// 'asp-prerender-module' tag helper.
|
||||
/// </summary>
|
||||
/// <param name="moduleName">The JavaScript module that exports a prerendering function.</param>
|
||||
/// <param name="exportName">The name of the export from the JavaScript module, if it is not the default export.</param>
|
||||
/// <param name="customDataParameter">An optional JSON-serializable object to pass to the JavaScript prerendering function.</param>
|
||||
/// <param name="timeoutMilliseconds">If specified, the prerendering task will time out after this duration if not already completed.</param>
|
||||
/// <returns></returns>
|
||||
Task<RenderToStringResult> RenderToString(
|
||||
string moduleName,
|
||||
string exportName = null,
|
||||
object customDataParameter = null,
|
||||
int timeoutMilliseconds = default(int));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
@@ -90,19 +87,6 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
// We want to pass the original, unencoded incoming URL data through to Node, so that
|
||||
// server-side code has the same view of the URL as client-side code (on the client,
|
||||
// location.pathname returns an unencoded string).
|
||||
// The following logic handles special characters in URL paths in the same way that
|
||||
// Node and client-side JS does. For example, the path "/a=b%20c" gets passed through
|
||||
// unchanged (whereas other .NET APIs do change it - Path.Value will return it as
|
||||
// "/a=b c" and Path.ToString() will return it as "/a%3db%20c")
|
||||
var requestFeature = ViewContext.HttpContext.Features.Get<IHttpRequestFeature>();
|
||||
var unencodedPathAndQuery = requestFeature.RawTarget;
|
||||
|
||||
var request = ViewContext.HttpContext.Request;
|
||||
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
|
||||
|
||||
var result = await Prerenderer.RenderToString(
|
||||
_applicationBasePath,
|
||||
_nodeServices,
|
||||
@@ -111,11 +95,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
ExportName = ExportName
|
||||
},
|
||||
unencodedAbsoluteUrl,
|
||||
unencodedPathAndQuery,
|
||||
ViewContext.HttpContext,
|
||||
CustomDataParameter,
|
||||
TimeoutMillisecondsParameter,
|
||||
request.PathBase.ToString());
|
||||
TimeoutMillisecondsParameter);
|
||||
|
||||
if (!string.IsNullOrEmpty(result.RedirectUrl))
|
||||
{
|
||||
@@ -134,19 +116,10 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
|
||||
// Also attach any specified globals to the 'window' object. This is useful for transferring
|
||||
// general state between server and client.
|
||||
if (result.Globals != null)
|
||||
var globalsScript = result.CreateGlobalsAssignmentScript();
|
||||
if (!string.IsNullOrEmpty(globalsScript))
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
foreach (var property in result.Globals.Properties())
|
||||
{
|
||||
stringBuilder.AppendFormat("window.{0} = {1};",
|
||||
property.Name,
|
||||
property.Value.ToString(Formatting.None));
|
||||
}
|
||||
if (stringBuilder.Length > 0)
|
||||
{
|
||||
output.PostElement.SetHtmlContent($"<script>{stringBuilder}</script>");
|
||||
}
|
||||
output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
@@ -14,6 +16,40 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
|
||||
private static StringAsTempFile NodeScript;
|
||||
|
||||
internal static Task<RenderToStringResult> RenderToString(
|
||||
string applicationBasePath,
|
||||
INodeServices nodeServices,
|
||||
CancellationToken applicationStoppingToken,
|
||||
JavaScriptModuleExport bootModule,
|
||||
HttpContext httpContext,
|
||||
object customDataParameter,
|
||||
int timeoutMilliseconds)
|
||||
{
|
||||
// We want to pass the original, unencoded incoming URL data through to Node, so that
|
||||
// server-side code has the same view of the URL as client-side code (on the client,
|
||||
// location.pathname returns an unencoded string).
|
||||
// The following logic handles special characters in URL paths in the same way that
|
||||
// Node and client-side JS does. For example, the path "/a=b%20c" gets passed through
|
||||
// unchanged (whereas other .NET APIs do change it - Path.Value will return it as
|
||||
// "/a=b c" and Path.ToString() will return it as "/a%3db%20c")
|
||||
var requestFeature = httpContext.Features.Get<IHttpRequestFeature>();
|
||||
var unencodedPathAndQuery = requestFeature.RawTarget;
|
||||
|
||||
var request = httpContext.Request;
|
||||
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
|
||||
|
||||
return RenderToString(
|
||||
applicationBasePath,
|
||||
nodeServices,
|
||||
applicationStoppingToken,
|
||||
bootModule,
|
||||
unencodedAbsoluteUrl,
|
||||
unencodedPathAndQuery,
|
||||
customDataParameter,
|
||||
timeoutMilliseconds,
|
||||
request.PathBase.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs server-side prerendering by invoking code in Node.js.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for setting up prerendering features in an <see cref="IServiceCollection" />.
|
||||
/// </summary>
|
||||
public static class PrerenderingServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures the dependency injection system to supply an implementation
|
||||
/// of <see cref="ISpaPrerenderer"/>.
|
||||
/// </summary>
|
||||
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
|
||||
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddHttpContextAccessor();
|
||||
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
{
|
||||
@@ -30,5 +32,29 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||
/// If set, specifies the HTTP status code that should be sent back with the server response.
|
||||
/// </summary>
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a block of JavaScript code that assigns data from the
|
||||
/// <see cref="Globals"/> property to the global namespace.
|
||||
/// </summary>
|
||||
/// <returns>A block of JavaScript code.</returns>
|
||||
public string CreateGlobalsAssignmentScript()
|
||||
{
|
||||
if (Globals == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
foreach (var property in Globals.Properties())
|
||||
{
|
||||
stringBuilder.AppendFormat("window.{0} = {1};",
|
||||
property.Name,
|
||||
property.Value.ToString(Formatting.None));
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Behind the scenes, it uses the [`Microsoft.AspNetCore.NodeServices`](https://git
|
||||
|
||||
### Installation into existing projects
|
||||
|
||||
* Add `Microsoft.AspNetCore.SpaServices` to the dependencies list in your `project.json` file
|
||||
* Install the `Microsoft.AspNetCore.SpaServices` NuGet package
|
||||
* Run `dotnet restore` (or if you use Visual Studio, just wait a moment - it will restore dependencies automatically)
|
||||
* Install supporting NPM packages for the features you'll be using:
|
||||
* For **server-side prerendering**, install `aspnet-prerendering`
|
||||
@@ -561,7 +561,7 @@ Typically, when you change a source file, the effects appear in your local brows
|
||||
First ensure you already have a working Webpack dev middleware setup. Then, install the `webpack-hot-middleware` NPM module:
|
||||
|
||||
```
|
||||
npm install --save webpack-hot-middleware
|
||||
npm install --save-dev webpack-hot-middleware
|
||||
```
|
||||
|
||||
At the top of your `Startup.cs` file, add the following namespace reference:
|
||||
@@ -620,7 +620,7 @@ app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
|
||||
Also, install the NPM module `aspnet-webpack-react`, e.g.:
|
||||
|
||||
```
|
||||
npm install --save aspnet-webpack-react
|
||||
npm install --save-dev aspnet-webpack-react
|
||||
```
|
||||
|
||||
Now if you edit any React component (e.g., in `.jsx` or `.tsx` files), the updated component will be injected into the running application, and will even preserve its in-memory state.
|
||||
|
||||
@@ -22,14 +22,21 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ConditionalProxyMiddlewareOptions _options;
|
||||
private readonly string _pathPrefix;
|
||||
private readonly bool _pathPrefixIsRoot;
|
||||
|
||||
public ConditionalProxyMiddleware(
|
||||
RequestDelegate next,
|
||||
string pathPrefix,
|
||||
ConditionalProxyMiddlewareOptions options)
|
||||
{
|
||||
if (!pathPrefix.StartsWith("/"))
|
||||
{
|
||||
pathPrefix = "/" + pathPrefix;
|
||||
}
|
||||
|
||||
_next = next;
|
||||
_pathPrefix = pathPrefix;
|
||||
_pathPrefixIsRoot = string.Equals(_pathPrefix, "/", StringComparison.Ordinal);
|
||||
_options = options;
|
||||
_httpClient = new HttpClient(new HttpClientHandler());
|
||||
_httpClient.Timeout = _options.RequestTimeout;
|
||||
@@ -37,7 +44,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments(_pathPrefix))
|
||||
if (context.Request.Path.StartsWithSegments(_pathPrefix) || _pathPrefixIsRoot)
|
||||
{
|
||||
var didProxyRequest = await PerformProxyRequest(context);
|
||||
if (didProxyRequest)
|
||||
@@ -65,7 +72,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
||||
|
||||
requestMessage.Headers.Host = _options.Host + ":" + _options.Port;
|
||||
var uriString =
|
||||
$"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}";
|
||||
$"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.Path}{context.Request.QueryString}";
|
||||
requestMessage.RequestUri = new Uri(uriString);
|
||||
requestMessage.Method = new HttpMethod(context.Request.Method);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using Microsoft.AspNetCore.NodeServices;
|
||||
using Microsoft.AspNetCore.SpaServices.Webpack;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
@@ -14,6 +15,15 @@ namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
private const string DefaultConfigFile = "webpack.config.js";
|
||||
|
||||
private static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
// Note that the aspnet-webpack JS code specifically expects options to be serialized with
|
||||
// PascalCase property names, so it's important to be explicit about this contract resolver
|
||||
ContractResolver = new DefaultContractResolver(),
|
||||
|
||||
TypeNameHandling = TypeNameHandling.None
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Enables Webpack dev middleware support. This hosts an instance of the Webpack compiler in memory
|
||||
/// in your application so that you can always serve up-to-date Webpack-built resources without having
|
||||
@@ -87,7 +97,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||
};
|
||||
var devServerInfo =
|
||||
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
||||
JsonConvert.SerializeObject(devServerOptions)).Result;
|
||||
JsonConvert.SerializeObject(devServerOptions, jsonSerializerSettings)).Result;
|
||||
|
||||
// If we're talking to an older version of aspnet-webpack, it will return only a single PublicPath,
|
||||
// not an array of PublicPaths. Handle that scenario.
|
||||
@@ -101,9 +111,9 @@ namespace Microsoft.AspNetCore.Builder
|
||||
// plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request).
|
||||
foreach (var publicPath in devServerInfo.PublicPaths)
|
||||
{
|
||||
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath + hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan);
|
||||
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100));
|
||||
}
|
||||
appBuilder.UseProxyToLocalWebpackDevMiddleware(hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout)
|
||||
|
||||
@@ -50,5 +50,12 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
||||
/// the webpack compiler.
|
||||
/// </summary>
|
||||
public IDictionary<string, string> EnvironmentVariables { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a value for the "env" parameter to be passed into the Webpack configuration
|
||||
/// function. The value must be JSON-serializable, and will only be used if the Webpack
|
||||
/// configuration is exported as a function.
|
||||
/// </summary>
|
||||
public object EnvParam { get; set; }
|
||||
}
|
||||
}
|
||||
730
src/Microsoft.AspNetCore.SpaServices/baseline.netcore.json
Normal file
730
src/Microsoft.AspNetCore.SpaServices/baseline.netcore.json
Normal file
@@ -0,0 +1,730 @@
|
||||
{
|
||||
"AssemblyIdentity": "Microsoft.AspNetCore.SpaServices, Version=2.0.3.0, Culture=neutral, PublicKeyToken=adb9793829ddae60",
|
||||
"Types": [
|
||||
{
|
||||
"Name": "Microsoft.Extensions.DependencyInjection.PrerenderingServiceCollectionExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "AddSpaPrerenderer",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "serviceCollection",
|
||||
"Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.SpaRouteExtensions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "MapSpaFallbackRoute",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "routeBuilder",
|
||||
"Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "name",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "defaults",
|
||||
"Type": "System.Object"
|
||||
},
|
||||
{
|
||||
"Name": "constraints",
|
||||
"Type": "System.Object",
|
||||
"DefaultValue": "null"
|
||||
},
|
||||
{
|
||||
"Name": "dataTokens",
|
||||
"Type": "System.Object",
|
||||
"DefaultValue": "null"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "MapSpaFallbackRoute",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "routeBuilder",
|
||||
"Type": "Microsoft.AspNetCore.Routing.IRouteBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "name",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "templatePrefix",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "defaults",
|
||||
"Type": "System.Object"
|
||||
},
|
||||
{
|
||||
"Name": "constraints",
|
||||
"Type": "System.Object",
|
||||
"DefaultValue": "null"
|
||||
},
|
||||
{
|
||||
"Name": "dataTokens",
|
||||
"Type": "System.Object",
|
||||
"DefaultValue": "null"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.Builder.WebpackDevMiddleware",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "UseWebpackDevMiddleware",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "appBuilder",
|
||||
"Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder"
|
||||
},
|
||||
{
|
||||
"Name": "options",
|
||||
"Type": "Microsoft.AspNetCore.SpaServices.Webpack.WebpackDevMiddlewareOptions",
|
||||
"DefaultValue": "null"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Static": true,
|
||||
"Extension": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.SpaServices.Webpack.WebpackDevMiddlewareOptions",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_HotModuleReplacement",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_HotModuleReplacement",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_HotModuleReplacementEndpoint",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_HotModuleReplacementEndpoint",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_HotModuleReplacementServerPort",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_HotModuleReplacementServerPort",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ReactHotModuleReplacement",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Boolean",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ReactHotModuleReplacement",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Boolean"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_HotModuleReplacementClientOptions",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_HotModuleReplacementClientOptions",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ConfigFile",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ConfigFile",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ProjectPath",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ProjectPath",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_EnvironmentVariables",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Collections.Generic.IDictionary<System.String, System.String>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_EnvironmentVariables",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Collections.Generic.IDictionary<System.String, System.String>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.SpaServices.Prerendering.ISpaPrerenderer",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Interface",
|
||||
"Abstract": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "RenderToString",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "exportName",
|
||||
"Type": "System.String",
|
||||
"DefaultValue": "null"
|
||||
},
|
||||
{
|
||||
"Name": "customDataParameter",
|
||||
"Type": "System.Object",
|
||||
"DefaultValue": "null"
|
||||
},
|
||||
{
|
||||
"Name": "timeoutMilliseconds",
|
||||
"Type": "System.Int32",
|
||||
"DefaultValue": "0"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.SpaServices.Prerendering.RenderToStringResult>",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.SpaServices.Prerendering.JavaScriptModuleExport",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ModuleName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ExportName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ExportName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "moduleName",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.SpaServices.Prerendering.Prerenderer",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"Abstract": true,
|
||||
"Static": true,
|
||||
"Sealed": true,
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "RenderToString",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "applicationBasePath",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "nodeServices",
|
||||
"Type": "Microsoft.AspNetCore.NodeServices.INodeServices"
|
||||
},
|
||||
{
|
||||
"Name": "applicationStoppingToken",
|
||||
"Type": "System.Threading.CancellationToken"
|
||||
},
|
||||
{
|
||||
"Name": "bootModule",
|
||||
"Type": "Microsoft.AspNetCore.SpaServices.Prerendering.JavaScriptModuleExport"
|
||||
},
|
||||
{
|
||||
"Name": "requestAbsoluteUrl",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "requestPathAndQuery",
|
||||
"Type": "System.String"
|
||||
},
|
||||
{
|
||||
"Name": "customDataParameter",
|
||||
"Type": "System.Object"
|
||||
},
|
||||
{
|
||||
"Name": "timeoutMilliseconds",
|
||||
"Type": "System.Int32"
|
||||
},
|
||||
{
|
||||
"Name": "requestPathBase",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task<Microsoft.AspNetCore.SpaServices.Prerendering.RenderToStringResult>",
|
||||
"Static": true,
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.SpaServices.Prerendering.PrerenderTagHelper",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"BaseType": "Microsoft.AspNetCore.Razor.TagHelpers.TagHelper",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ModuleName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ModuleName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ExportName",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ExportName",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_CustomDataParameter",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Object",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_CustomDataParameter",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Object"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_TimeoutMillisecondsParameter",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Int32",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_TimeoutMillisecondsParameter",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Int32"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_ViewContext",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Microsoft.AspNetCore.Mvc.Rendering.ViewContext",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_ViewContext",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "Microsoft.AspNetCore.Mvc.Rendering.ViewContext"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "ProcessAsync",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "context",
|
||||
"Type": "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext"
|
||||
},
|
||||
{
|
||||
"Name": "output",
|
||||
"Type": "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Threading.Tasks.Task",
|
||||
"Virtual": true,
|
||||
"Override": true,
|
||||
"ImplementedInterface": "Microsoft.AspNetCore.Razor.TagHelpers.ITagHelperComponent",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "serviceProvider",
|
||||
"Type": "System.IServiceProvider"
|
||||
}
|
||||
],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.AspNetCore.SpaServices.Prerendering.RenderToStringResult",
|
||||
"Visibility": "Public",
|
||||
"Kind": "Class",
|
||||
"ImplementedInterfaces": [],
|
||||
"Members": [
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Globals",
|
||||
"Parameters": [],
|
||||
"ReturnType": "Newtonsoft.Json.Linq.JObject",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_Globals",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "Newtonsoft.Json.Linq.JObject"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_Html",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_Html",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_RedirectUrl",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_RedirectUrl",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.String"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "get_StatusCode",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.Nullable<System.Int32>",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "set_StatusCode",
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "value",
|
||||
"Type": "System.Nullable<System.Int32>"
|
||||
}
|
||||
],
|
||||
"ReturnType": "System.Void",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Method",
|
||||
"Name": "CreateGlobalsAssignmentScript",
|
||||
"Parameters": [],
|
||||
"ReturnType": "System.String",
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
},
|
||||
{
|
||||
"Kind": "Constructor",
|
||||
"Name": ".ctor",
|
||||
"Parameters": [],
|
||||
"Visibility": "Public",
|
||||
"GenericParameter": []
|
||||
}
|
||||
],
|
||||
"GenericParameters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
5
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-angular/.gitignore
vendored
Normal file
5
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-angular/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/node_modules/
|
||||
**/*.js
|
||||
**/*.d.ts
|
||||
**/*.metadata.json
|
||||
/compiled
|
||||
@@ -0,0 +1,3 @@
|
||||
!/*.js
|
||||
!/*.d.ts
|
||||
/compiled
|
||||
@@ -0,0 +1,12 @@
|
||||
Copyright (c) .NET Foundation. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
these files except in compliance with the License. You may obtain a copy of the
|
||||
License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed
|
||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations under the License.
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "aspnet-angular",
|
||||
"version": "0.1.1",
|
||||
"description": "Helpers for using Angular in ASP.NET Core projects",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"prepublish": "rimraf *.d.ts && ngc && echo 'Finished building NPM package \"aspnet-angular\"'",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aspnet/JavaScriptServices.git"
|
||||
},
|
||||
"author": "Microsoft",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/aspnet/JavaScriptServices/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/common": "^4.3.2",
|
||||
"@angular/compiler": "^4.3.2",
|
||||
"@angular/compiler-cli": "^4.3.2",
|
||||
"@angular/core": "^4.3.2",
|
||||
"@angular/http": "^4.3.2",
|
||||
"@angular/platform-browser": "^4.3.2",
|
||||
"rimraf": "^2.6.1",
|
||||
"rxjs": "^5.4.2",
|
||||
"zone.js": "^0.8.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": "^4.2.5 || ^5.0.0-beta"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Provider, NgModule, Inject } from '@angular/core';
|
||||
import { Headers, Http, ResponseOptions, RequestOptionsArgs, Response } from '@angular/http';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
import 'rxjs/add/operator/map';
|
||||
const globalSerializedStateKey = 'HTTP_STATE_TRANSFER';
|
||||
const backingStoreDIToken = 'HTTP_STATE_BACKING_STORE';
|
||||
|
||||
export interface CacheOptions {
|
||||
permanent: boolean;
|
||||
}
|
||||
|
||||
export interface CachedHttpResponse {
|
||||
headers: { [name: string]: any } | null;
|
||||
status: number;
|
||||
statusText: string | null;
|
||||
text: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type BackingStore = { [key: string]: CachedHttpResponse };
|
||||
|
||||
export class HttpWithStateTransfer {
|
||||
private backingStore: BackingStore;
|
||||
private http: Http;
|
||||
|
||||
constructor(@Inject(Http) http: Http, @Inject(backingStoreDIToken) backingStore: BackingStore) {
|
||||
this.http = http;
|
||||
this.backingStore = backingStore;
|
||||
}
|
||||
|
||||
public stateForTransfer(): any {
|
||||
return { [globalSerializedStateKey]: this.backingStore };
|
||||
}
|
||||
|
||||
public get(url: string, options?: CacheOptions, requestOptions?: RequestOptionsArgs): Observable<Response> {
|
||||
return this.getCachedResponse(/* cacheKey */ url, () => this.http.get(url, requestOptions), options);
|
||||
}
|
||||
|
||||
private getCachedResponse(cacheKey: string, provider: () => Observable<Response>, options?: CacheOptions): Observable<Response> {
|
||||
// By default, the cache is only used for the *first* client-side read. So, we're only performing
|
||||
// a one-time transfer of server-side response to the client. If you want to keep and reuse cached
|
||||
// responses continually during server-side and client-side execution, set 'permanent' to 'true.
|
||||
const isClient = typeof window !== 'undefined';
|
||||
const isPermanent = options && options.permanent;
|
||||
|
||||
const allowReadFromCache = isClient || isPermanent;
|
||||
if (allowReadFromCache && this.backingStore.hasOwnProperty(cacheKey)) {
|
||||
const cachedValue = this.backingStore[cacheKey];
|
||||
if (!isPermanent) {
|
||||
delete this.backingStore[cacheKey];
|
||||
}
|
||||
return Observable.of(new Response(new ResponseOptions({
|
||||
body: cachedValue.text,
|
||||
headers: new Headers(cachedValue.headers),
|
||||
status: cachedValue.status,
|
||||
url: cachedValue.url
|
||||
})));
|
||||
}
|
||||
|
||||
return provider()
|
||||
.map(response => {
|
||||
const allowWriteToCache = !isClient || isPermanent;
|
||||
if (allowWriteToCache) {
|
||||
this.backingStore[cacheKey] = {
|
||||
headers: response.headers ? response.headers.toJSON() : null,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
text: response.text(),
|
||||
url: response.url
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function defaultBackingStoreFactory() {
|
||||
const transferredData = typeof window !== 'undefined' ? (window as any)[globalSerializedStateKey] : null;
|
||||
return transferredData || {};
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
// The backing store is a separate DI service so you could override exactly how it gets
|
||||
// transferred from server to client
|
||||
{ provide: backingStoreDIToken, useFactory: defaultBackingStoreFactory },
|
||||
|
||||
{ provide: HttpWithStateTransfer, useClass: HttpWithStateTransfer },
|
||||
]
|
||||
})
|
||||
export class HttpWithStateTransferModule {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './HttpWithStateTransfer';
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"declaration": true,
|
||||
"outDir": ".",
|
||||
"lib": ["es2015", "dom"]
|
||||
},
|
||||
"files": [
|
||||
"src/index.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"genDir": "compiled"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aspnet-prerendering",
|
||||
"version": "2.0.5",
|
||||
"version": "3.0.1",
|
||||
"description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"url": "https://github.com/aspnet/JavaScriptServices.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"domain-task": "^2.0.2"
|
||||
"domain-task": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^6.0.42",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/// <reference path="./PrerenderingInterfaces.d.ts" />
|
||||
import * as url from 'url';
|
||||
import * as path from 'path';
|
||||
import * as domain from 'domain';
|
||||
import { run as domainTaskRun, baseUrl as domainTaskBaseUrl } from 'domain-task/main';
|
||||
import { BootFunc, BootFuncParams, BootModuleInfo, RenderToStringCallback, RenderToStringFunc } from './PrerenderingInterfaces';
|
||||
|
||||
const defaultTimeoutMilliseconds = 30 * 1000;
|
||||
|
||||
@@ -26,6 +26,7 @@ export function createServerRenderer(bootFunc: BootFunc): RenderToStringFunc {
|
||||
domainTasks: domainTaskCompletionPromise,
|
||||
data: customDataParameter
|
||||
};
|
||||
const absoluteBaseUrl = params.origin + params.baseUrl; // Should be same value as page's <base href>
|
||||
|
||||
// Open a new domain that can track all the async tasks involved in the app's execution
|
||||
domainTaskRun(/* code to run */ () => {
|
||||
@@ -35,7 +36,7 @@ export function createServerRenderer(bootFunc: BootFunc): RenderToStringFunc {
|
||||
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']);
|
||||
|
||||
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
|
||||
domainTaskBaseUrl(absoluteRequestUrl);
|
||||
domainTaskBaseUrl(absoluteBaseUrl);
|
||||
|
||||
// Begin rendering, and apply a timeout
|
||||
const bootFuncPromise = bootFunc(params);
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
interface RenderToStringFunc {
|
||||
export interface RenderToStringFunc {
|
||||
(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number, requestPathBase: string): void;
|
||||
}
|
||||
|
||||
interface RenderToStringCallback {
|
||||
(error: any, result?: RenderToStringResult): void;
|
||||
export interface RenderToStringCallback {
|
||||
(error: any, result?: RenderResult): void;
|
||||
}
|
||||
|
||||
interface RenderToStringResult {
|
||||
export interface RenderToStringResult {
|
||||
html: string;
|
||||
statusCode?: number;
|
||||
globals?: { [key: string]: any };
|
||||
}
|
||||
|
||||
interface RedirectResult {
|
||||
export interface RedirectResult {
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
interface BootFunc {
|
||||
(params: BootFuncParams): Promise<RenderToStringResult>;
|
||||
export type RenderResult = RenderToStringResult | RedirectResult;
|
||||
|
||||
export interface BootFunc {
|
||||
(params: BootFuncParams): Promise<RenderResult>;
|
||||
}
|
||||
|
||||
interface BootFuncParams {
|
||||
export interface BootFuncParams {
|
||||
location: any; // e.g., Location object containing information '/some/path'
|
||||
origin: string; // e.g., 'https://example.com:1234'
|
||||
url: string; // e.g., '/some/path'
|
||||
@@ -30,7 +32,7 @@ interface BootFuncParams {
|
||||
data: any; // any custom object passed through from .NET
|
||||
}
|
||||
|
||||
interface BootModuleInfo {
|
||||
export interface BootModuleInfo {
|
||||
moduleName: string;
|
||||
exportName?: string;
|
||||
webpackConfig?: string;
|
||||
@@ -1,5 +1,2 @@
|
||||
/// <reference path="./PrerenderingInterfaces.d.ts" />
|
||||
|
||||
export * from './Prerendering';
|
||||
|
||||
export type RenderResult = RenderToStringResult | RedirectResult;
|
||||
export * from './PrerenderingInterfaces';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aspnet-webpack-react",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Helpers for using Webpack with React in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
1390
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package-lock.json
generated
Normal file
1390
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aspnet-webpack",
|
||||
"version": "1.0.29",
|
||||
"version": "2.0.3",
|
||||
"description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -33,6 +33,6 @@
|
||||
"webpack": "^1.13.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^1.13.2 || ^2.1.0-beta"
|
||||
"webpack": "^1.13.2 || ^2.1.0-beta || ^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user