mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 10:08:57 +00:00
Compare commits
87 Commits
httpwithst
...
2.1.0-prev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f35c814fc7 | ||
|
|
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 |
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,21 +24,8 @@ nuget.exe
|
|||||||
*.ncrunchsolution
|
*.ncrunchsolution
|
||||||
*.*sdf
|
*.*sdf
|
||||||
*.ipch
|
*.ipch
|
||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
/.build/
|
/.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/
|
.vscode/
|
||||||
|
|
||||||
/templates/*/Properties/launchSettings.json
|
|
||||||
global.json
|
global.json
|
||||||
korebuild-lock.txt
|
|
||||||
|
|||||||
11
.travis.yml
11
.travis.yml
@@ -12,8 +12,13 @@ addons:
|
|||||||
- zlib1g
|
- zlib1g
|
||||||
mono: none
|
mono: none
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
- osx
|
- osx
|
||||||
osx_image: xcode7.1
|
osx_image: xcode7.1
|
||||||
script:
|
script:
|
||||||
- ./build.sh
|
- ./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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.26430.4
|
VisualStudioVersion = 15.0.26730.16
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 15.0.26730.03
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{27304DDE-AFB2-4F8B-B765-E3E2F11E886C}"
|
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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.NodeServices", "src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj", "{66B77203-1469-41DF-92F2-2BE6900BD36F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.NodeServices", "src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj", "{66B77203-1469-41DF-92F2-2BE6900BD36F}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -28,14 +31,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Webpack", "samples\misc\Web
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodeServicesExamples", "samples\misc\NodeServicesExamples\NodeServicesExamples.csproj", "{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NodeServicesExamples", "samples\misc\NodeServicesExamples\NodeServicesExamples.csproj", "{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{1598B415-73F1-4B37-B3B4-0A10677ABB2D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{645F7363-1240-4FB6-9422-B32A327C979F}"
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E415FE14-13B0-469F-836D-95059E6BAA6E}"
|
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
src\build\common.props = src\build\common.props
|
Directory.Build.props = Directory.Build.props
|
||||||
src\build\Key.snk = src\build\Key.snk
|
Directory.Build.targets = Directory.Build.targets
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{93EFCC5F-C6EE-4623-894F-A42B22C0B6FE}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -78,5 +85,9 @@ Global
|
|||||||
{1931B19A-EC42-4D56-B2D0-FB06D17244DA} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
|
{1931B19A-EC42-4D56-B2D0-FB06D17244DA} = {E6A161EA-646C-4033-9090-95BE809AB8D9}
|
||||||
{DE479DC3-1461-4EAD-A188-4AF7AA4AE344} = {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}
|
{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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
|
<!-- Restore sources should be defined in build/sources.props. -->
|
||||||
<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" />
|
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
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 routing integration ([docs](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.SpaServices#routing-helper-mapspafallbackroute))
|
||||||
* Server-side and client-side validation integration
|
* Server-side and client-side validation integration
|
||||||
* "Lazy loading" for Knockout apps
|
* "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
|
* 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
|
## 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
|
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`.
|
||||||
yo aspnetcore-spa
|
|
||||||
|
|
||||||
Once the generator has run and restored all the dependencies, you can start up your new ASP.NET Core 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
|
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
|
## 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).
|
* Find [documentation and usage examples here](https://github.com/aspnet/JavaScriptServices/tree/dev/src/Microsoft.AspNetCore.NodeServices#microsoftaspnetcorenodeservices).
|
||||||
* `Microsoft.AspNetCore.SpaServices`
|
* `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.
|
* 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).
|
* 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).
|
|
||||||
|
|
||||||
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.
|
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
|
## Samples
|
||||||
|
|
||||||
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/).
|
|
||||||
|
|
||||||
The [`samples` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/samples) contains examples of:
|
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
|
## 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
|
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.
|
||||||
* 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`. If 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.
|
|
||||||
|
|||||||
44
appveyor.yml
44
appveyor.yml
@@ -1,44 +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 3 SDK and add to PATH
|
|
||||||
- ps: $urlCurrent = "https://dotnetcli.azureedge.net/dotnet/Sdk/2.0.0-preview3-006857/dotnet-sdk-2.0.0-preview3-006857-win-x64.zip"
|
|
||||||
- ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk"
|
|
||||||
- ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null
|
|
||||||
- ps: $tempFileCurrent = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
|
|
||||||
- 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
|
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 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
|
@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"
|
||||||
|
|||||||
195
build.sh
195
build.sh
@@ -1,197 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
#
|
|
||||||
# variables
|
|
||||||
#
|
|
||||||
|
|
||||||
RESET="\033[0m"
|
|
||||||
RED="\033[0;31m"
|
|
||||||
MAGENTA="\033[0;95m"
|
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet"
|
|
||||||
config_file="$DIR/version.xml"
|
|
||||||
verbose=false
|
|
||||||
update=false
|
|
||||||
repo_path="$DIR"
|
|
||||||
channel=''
|
|
||||||
tools_source=''
|
|
||||||
|
|
||||||
#
|
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
||||||
# Functions
|
chmod +x "$DIR/run.sh"; sync
|
||||||
#
|
"$DIR/run.sh" default-build "$@"
|
||||||
__usage() {
|
|
||||||
echo "Usage: $(basename "${BASH_SOURCE[0]}") [options] [[--] <MSBUILD_ARG>...]"
|
|
||||||
echo ""
|
|
||||||
echo "Arguments:"
|
|
||||||
echo " <MSBUILD_ARG>... Arguments passed to MSBuild. 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> TThe path to the configuration file that stores values. Defaults to version.xml."
|
|
||||||
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 <URL> The base url where build tools can be downloaded. Overrides the value from the config file."
|
|
||||||
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"
|
|
||||||
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"; 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}$*${RESET}" 1>&2
|
|
||||||
}
|
|
||||||
|
|
||||||
__machine_has() {
|
|
||||||
hash "$1" > /dev/null 2>&1
|
|
||||||
return $?
|
|
||||||
}
|
|
||||||
|
|
||||||
__get_remote_file() {
|
|
||||||
local remote_path=$1
|
|
||||||
local local_path=$2
|
|
||||||
|
|
||||||
if [[ "$remote_path" != 'http'* ]]; then
|
|
||||||
cp "$remote_path" "$local_path"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
failed=false
|
|
||||||
if __machine_has wget; then
|
|
||||||
wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$failed" = true ] && __machine_has curl; then
|
|
||||||
failed=false
|
|
||||||
curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$failed" = true ]; then
|
|
||||||
__error "Download failed: $remote_path" 1>&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
__read_dom () { local IFS=\> ; read -r -d \< ENTITY CONTENT ;}
|
|
||||||
|
|
||||||
#
|
|
||||||
# main
|
|
||||||
#
|
|
||||||
|
|
||||||
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
|
|
||||||
;;
|
|
||||||
-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
|
|
||||||
;;
|
|
||||||
-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
|
|
||||||
|
|
||||||
if [ -f "$config_file" ]; then
|
|
||||||
comment=false
|
|
||||||
while __read_dom; do
|
|
||||||
if [ "$comment" = true ]; then [[ $CONTENT == *'-->'* ]] && comment=false ; continue; fi
|
|
||||||
if [[ $ENTITY == '!--'* ]]; then comment=true; continue; fi
|
|
||||||
if [ -z "$channel" ] && [[ $ENTITY == "KoreBuildChannel" ]]; then channel=$CONTENT; fi
|
|
||||||
if [ -z "$tools_source" ] && [[ $ENTITY == "KoreBuildToolsSource" ]]; then tools_source=$CONTENT; fi
|
|
||||||
done < "$config_file"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -z "$channel" ] && channel='dev'
|
|
||||||
[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
|
|
||||||
|
|
||||||
get_korebuild
|
|
||||||
install_tools "$tools_source" "$DOTNET_HOME"
|
|
||||||
invoke_repository_build "$repo_path" "$@"
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<Import Project="dependencies.props" />
|
|
||||||
<Import Project="..\version.xml" />
|
|
||||||
|
|
||||||
<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>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Internal.AspNetCore.Sdk" Version="$(InternalAspNetCoreSdkVersion)" PrivateAssets="All" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETFramework'">
|
|
||||||
<PackageReference Include="NETStandard.Library" Version="$(NETStandardImplicitPackageVersion)" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,11 +1,27 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AspNetCoreVersion>2.1.0-*</AspNetCoreVersion>
|
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||||
<InternalAspNetCoreSdkVersion>2.1.1-*</InternalAspNetCoreSdkVersion>
|
|
||||||
<JsonNetVersion>10.0.1</JsonNetVersion>
|
|
||||||
<NETStandardImplicitPackageVersion>2.0.0-*</NETStandardImplicitPackageVersion>
|
|
||||||
<NETStandardLibraryNETFrameworkVersion>2.0.0-*</NETStandardLibraryNETFrameworkVersion>
|
|
||||||
<RuntimeFrameworkVersion Condition="'$(TargetFramework)'=='netcoreapp2.0'">2.0.0-*</RuntimeFrameworkVersion>
|
|
||||||
<ThreadingDataflowVersion>4.8.0-*</ThreadingDataflowVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Label="Package Versions">
|
||||||
|
<InternalAspNetCoreSdkPackageVersion>2.1.0-preview1-1010</InternalAspNetCoreSdkPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreDiagnosticsPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreDiagnosticsPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreHostingPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreHostingPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreMvcPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreMvcPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreMvcTagHelpersPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreMvcTagHelpersPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreMvcViewFeaturesPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreMvcViewFeaturesPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreStaticFilesPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreStaticFilesPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreWebSocketsPackageVersion>2.1.0-preview1-28193</MicrosoftAspNetCoreWebSocketsPackageVersion>
|
||||||
|
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.1.0-preview1-28193</MicrosoftExtensionsDependencyInjectionPackageVersion>
|
||||||
|
<MicrosoftExtensionsFileProvidersPhysicalPackageVersion>2.1.0-preview1-28193</MicrosoftExtensionsFileProvidersPhysicalPackageVersion>
|
||||||
|
<MicrosoftExtensionsLoggingConsolePackageVersion>2.1.0-preview1-28193</MicrosoftExtensionsLoggingConsolePackageVersion>
|
||||||
|
<MicrosoftExtensionsLoggingDebugPackageVersion>2.1.0-preview1-28193</MicrosoftExtensionsLoggingDebugPackageVersion>
|
||||||
|
<MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
|
||||||
|
<MicrosoftNETCoreApp21PackageVersion>2.1.0-preview1-26122-01</MicrosoftNETCoreApp21PackageVersion>
|
||||||
|
<NewtonsoftJsonPackageVersion>10.0.1</NewtonsoftJsonPackageVersion>
|
||||||
|
<SystemThreadingTasksDataflowPackageVersion>4.9.0-preview1-26119-06</SystemThreadingTasksDataflowPackageVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||||
</Project>
|
</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>
|
||||||
16
build/sources.props
Normal file
16
build/sources.props
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<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/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-preview1-1010
|
||||||
|
commithash:75ca924dfbd673c38841025b04c4dcd93b84f56d
|
||||||
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"
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Build this repository
|
Executes KoreBuild commands.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Downloads korebuild if required. Then builds the repository.
|
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
|
.PARAMETER Path
|
||||||
The folder to build. Defaults to the folder containing this script.
|
The folder to build. Defaults to the folder containing this script.
|
||||||
@@ -24,31 +27,35 @@ The base url where build tools can be downloaded. Overrides the value from the c
|
|||||||
Updates KoreBuild to the latest version even if a lock file is present.
|
Updates KoreBuild to the latest version even if a lock file is present.
|
||||||
|
|
||||||
.PARAMETER ConfigFile
|
.PARAMETER ConfigFile
|
||||||
The path to the configuration file that stores values. Defaults to version.xml.
|
The path to the configuration file that stores values. Defaults to korebuild.json.
|
||||||
|
|
||||||
.PARAMETER MSBuildArgs
|
.PARAMETER ToolsSourceSuffix
|
||||||
Arguments to be passed to MSBuild
|
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
|
.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.
|
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.
|
When the lockfile is not present, KoreBuild will create one using latest available version from $Channel.
|
||||||
|
|
||||||
The $ConfigFile is expected to be an XML file. It is optional, and the configuration values in it are optional as well.
|
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
|
||||||
Example config file:
|
Example config file:
|
||||||
```xml
|
```json
|
||||||
<!-- version.xml -->
|
{
|
||||||
<Project>
|
"$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json",
|
||||||
<PropertyGroup>
|
"channel": "dev",
|
||||||
<KoreBuildChannel>dev</KoreBuildChannel>
|
"toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools"
|
||||||
<KoreBuildToolsSource>https://aspnetcore.blob.core.windows.net/buildtools</KoreBuildToolsSource>
|
}
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
```
|
```
|
||||||
#>
|
#>
|
||||||
[CmdletBinding(PositionalBinding = $false)]
|
[CmdletBinding(PositionalBinding = $false)]
|
||||||
param(
|
param(
|
||||||
|
[Parameter(Mandatory = $true, Position = 0)]
|
||||||
|
[string]$Command,
|
||||||
[string]$Path = $PSScriptRoot,
|
[string]$Path = $PSScriptRoot,
|
||||||
[Alias('c')]
|
[Alias('c')]
|
||||||
[string]$Channel,
|
[string]$Channel,
|
||||||
@@ -58,9 +65,10 @@ param(
|
|||||||
[string]$ToolsSource,
|
[string]$ToolsSource,
|
||||||
[Alias('u')]
|
[Alias('u')]
|
||||||
[switch]$Update,
|
[switch]$Update,
|
||||||
[string]$ConfigFile = (Join-Path $PSScriptRoot 'version.xml'),
|
[string]$ConfigFile,
|
||||||
|
[string]$ToolsSourceSuffix,
|
||||||
[Parameter(ValueFromRemainingArguments = $true)]
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string[]]$MSBuildArgs
|
[string[]]$Arguments
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version 2
|
Set-StrictMode -Version 2
|
||||||
@@ -75,7 +83,7 @@ function Get-KoreBuild {
|
|||||||
$lockFile = Join-Path $Path 'korebuild-lock.txt'
|
$lockFile = Join-Path $Path 'korebuild-lock.txt'
|
||||||
|
|
||||||
if (!(Test-Path $lockFile) -or $Update) {
|
if (!(Test-Path $lockFile) -or $Update) {
|
||||||
Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile
|
Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
$version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
|
$version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
|
||||||
@@ -92,7 +100,7 @@ function Get-KoreBuild {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip"
|
$tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip"
|
||||||
Get-RemoteFile $remotePath $tmpfile
|
Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix
|
||||||
if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) {
|
if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) {
|
||||||
# Use built-in commands where possible as they are cross-plat compatible
|
# Use built-in commands where possible as they are cross-plat compatible
|
||||||
Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath
|
Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath
|
||||||
@@ -120,7 +128,7 @@ function Join-Paths([string]$path, [string[]]$childPaths) {
|
|||||||
return $path
|
return $path
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) {
|
function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) {
|
||||||
if ($RemotePath -notlike 'http*') {
|
if ($RemotePath -notlike 'http*') {
|
||||||
Copy-Item $RemotePath $LocalPath
|
Copy-Item $RemotePath $LocalPath
|
||||||
return
|
return
|
||||||
@@ -130,7 +138,7 @@ function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) {
|
|||||||
while ($retries -gt 0) {
|
while ($retries -gt 0) {
|
||||||
$retries -= 1
|
$retries -= 1
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath
|
Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -147,10 +155,21 @@ function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) {
|
|||||||
|
|
||||||
# Load configuration or set defaults
|
# Load configuration or set defaults
|
||||||
|
|
||||||
|
$Path = Resolve-Path $Path
|
||||||
|
if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' }
|
||||||
|
|
||||||
if (Test-Path $ConfigFile) {
|
if (Test-Path $ConfigFile) {
|
||||||
[xml] $config = Get-Content $ConfigFile
|
try {
|
||||||
if (!($Channel)) { [string] $Channel = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildChannel' }
|
$config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json
|
||||||
if (!($ToolsSource)) { [string] $ToolsSource = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildToolsSource' }
|
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) {
|
if (!$DotNetHome) {
|
||||||
@@ -169,8 +188,8 @@ $korebuildPath = Get-KoreBuild
|
|||||||
Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1')
|
Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Install-Tools $ToolsSource $DotNetHome
|
Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile
|
||||||
Invoke-RepositoryBuild $Path @MSBuildArgs
|
Invoke-KoreBuildCommand $Command @Arguments
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Remove-Module 'KoreBuild' -ErrorAction Ignore
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\common.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<OutputType>exe</OutputType>
|
<OutputType>exe</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -14,7 +12,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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
|
// Since .NET Core 1.1, the HTTP hosting model has become basically as fast as the Socket hosting model
|
||||||
//options.UseSocketHosting();
|
//options.UseSocketHosting();
|
||||||
|
|
||||||
options.ProjectPath = Directory.GetCurrentDirectory();
|
|
||||||
options.WatchFileExtensions = new string[] {}; // Don't watch anything
|
options.WatchFileExtensions = new string[] {}; // Don't watch anything
|
||||||
});
|
});
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\common.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -13,13 +11,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\common.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -13,13 +11,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
<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,12 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<Import Project="..\..\build\common.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Socket-based RPC for Microsoft.AspNetCore.NodeServices.</Description>
|
<Description>Socket-based RPC for Microsoft.AspNetCore.NodeServices.</Description>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -16,7 +12,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A specialisation of the OutOfProcessNodeInstance base class that uses a lightweight binary streaming protocol
|
/// 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.
|
/// 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
|
/// For details on the binary streaming protocol, see <see cref="Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections.VirtualConnectionClient" />
|
||||||
/// Microsoft.AspNetCore.NodeServices.HostingModels.VirtualConnections.VirtualConnectionClient.
|
|
||||||
/// The advantage versus using HTTP for RPC is that this is faster (not surprisingly - there's much less overhead
|
/// 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.
|
/// because we don't need most of the functionality of HTTP.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.NodeServices.HostingModels;
|
using Microsoft.AspNetCore.NodeServices.HostingModels;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -34,14 +35,18 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
InvocationTimeoutMilliseconds = DefaultInvocationTimeoutMilliseconds;
|
InvocationTimeoutMilliseconds = DefaultInvocationTimeoutMilliseconds;
|
||||||
WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone();
|
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>();
|
var hostEnv = serviceProvider.GetService<IHostingEnvironment>();
|
||||||
if (hostEnv != null)
|
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;
|
ProjectPath = hostEnv.ContentRootPath;
|
||||||
EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node
|
EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProjectPath = Directory.GetCurrentDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
var applicationLifetime = serviceProvider.GetService<IApplicationLifetime>();
|
var applicationLifetime = serviceProvider.GetService<IApplicationLifetime>();
|
||||||
if (applicationLifetime != null)
|
if (applicationLifetime != null)
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<Import Project="..\..\build\common.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Invoke Node.js modules at runtime in ASP.NET Core applications.</Description>
|
<Description>Invoke Node.js modules at runtime in ASP.NET Core applications.</Description>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -16,9 +11,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
|
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ module.exports = {
|
|||||||
|
|
||||||
## Hosting models
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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,288 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
private static readonly string[] NotForwardedWebSocketHeaders = new[] { "Connection", "Host", "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))
|
||||||
|
{
|
||||||
|
client.Options.SetRequestHeader(headerEntry.Key, headerEntry.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,21 +1,22 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<Import Project="..\..\build\common.props" />
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Helpers for building single-page applications on ASP.NET MVC Core.</Description>
|
<Description>Helpers for building single-page applications on ASP.NET MVC Core.</Description>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="node_modules\**\*" />
|
<None Remove="node_modules\**\*" />
|
||||||
<EmbeddedResource Include="Content\**\*" />
|
<EmbeddedResource Include="Content\**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="$(AspNetCoreVersion)" />
|
</ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="$(AspNetCoreVersion)" />
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="$(MicrosoftAspNetCoreMvcTagHelpersPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="$(MicrosoftAspNetCoreMvcViewFeaturesPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
using System;
|
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.NodeServices;
|
|
||||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
{
|
{
|
||||||
@@ -20,7 +14,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||||||
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
|
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
|
||||||
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
|
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
serviceCollection.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
serviceCollection.AddHttpContextAccessor();
|
||||||
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
|
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
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:
|
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.:
|
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.
|
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,6 +22,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly ConditionalProxyMiddlewareOptions _options;
|
private readonly ConditionalProxyMiddlewareOptions _options;
|
||||||
private readonly string _pathPrefix;
|
private readonly string _pathPrefix;
|
||||||
|
private readonly bool _pathPrefixIsRoot;
|
||||||
|
|
||||||
public ConditionalProxyMiddleware(
|
public ConditionalProxyMiddleware(
|
||||||
RequestDelegate next,
|
RequestDelegate next,
|
||||||
@@ -35,6 +36,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
|
|
||||||
_next = next;
|
_next = next;
|
||||||
_pathPrefix = pathPrefix;
|
_pathPrefix = pathPrefix;
|
||||||
|
_pathPrefixIsRoot = string.Equals(_pathPrefix, "/", StringComparison.Ordinal);
|
||||||
_options = options;
|
_options = options;
|
||||||
_httpClient = new HttpClient(new HttpClientHandler());
|
_httpClient = new HttpClient(new HttpClientHandler());
|
||||||
_httpClient.Timeout = _options.RequestTimeout;
|
_httpClient.Timeout = _options.RequestTimeout;
|
||||||
@@ -42,7 +44,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
if (context.Request.Path.StartsWithSegments(_pathPrefix))
|
if (context.Request.Path.StartsWithSegments(_pathPrefix) || _pathPrefixIsRoot)
|
||||||
{
|
{
|
||||||
var didProxyRequest = await PerformProxyRequest(context);
|
var didProxyRequest = await PerformProxyRequest(context);
|
||||||
if (didProxyRequest)
|
if (didProxyRequest)
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
{
|
{
|
||||||
private const string DefaultConfigFile = "webpack.config.js";
|
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>
|
/// <summary>
|
||||||
/// Enables Webpack dev middleware support. This hosts an instance of the Webpack compiler in memory
|
/// 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
|
/// in your application so that you can always serve up-to-date Webpack-built resources without having
|
||||||
@@ -88,7 +97,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
};
|
};
|
||||||
var devServerInfo =
|
var devServerInfo =
|
||||||
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
|
||||||
JsonConvert.SerializeObject(devServerOptions, new JsonSerializerSettings() { ContractResolver = new DefaultContractResolver() })).Result;
|
JsonConvert.SerializeObject(devServerOptions, jsonSerializerSettings)).Result;
|
||||||
|
|
||||||
// If we're talking to an older version of aspnet-webpack, it will return only a single PublicPath,
|
// 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.
|
// not an array of PublicPaths. Handle that scenario.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface CreateDevServerOptions {
|
|||||||
hotModuleReplacementEndpointUrl: string;
|
hotModuleReplacementEndpointUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EsModuleExports<T> = { __esModule: true, default: T };
|
||||||
type StringMap<T> = [(key: string) => T];
|
type StringMap<T> = [(key: string) => T];
|
||||||
|
|
||||||
// These are the options configured in C# and then JSON-serialized, hence the C#-style naming
|
// These are the options configured in C# and then JSON-serialized, hence the C#-style naming
|
||||||
@@ -39,7 +40,8 @@ type WebpackConfigOrArray = webpack.Configuration | webpack.Configuration[];
|
|||||||
interface WebpackConfigFunc {
|
interface WebpackConfigFunc {
|
||||||
(env?: any): WebpackConfigOrArray;
|
(env?: any): WebpackConfigOrArray;
|
||||||
}
|
}
|
||||||
type WebpackConfigFileExport = WebpackConfigOrArray | WebpackConfigFunc;
|
type WebpackConfigExport = WebpackConfigOrArray | WebpackConfigFunc;
|
||||||
|
type WebpackConfigModuleExports = WebpackConfigExport | EsModuleExports<WebpackConfigExport>;
|
||||||
|
|
||||||
function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrClientOptions: StringMap<string>, hmrServerEndpoint: string) {
|
function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrClientOptions: StringMap<string>, hmrServerEndpoint: string) {
|
||||||
// Build the final Webpack config based on supplied options
|
// Build the final Webpack config based on supplied options
|
||||||
@@ -235,7 +237,11 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the webpack config's export, and normalize it into the more general 'array of configs' format
|
// Read the webpack config's export, and normalize it into the more general 'array of configs' format
|
||||||
let webpackConfigExport: WebpackConfigFileExport = requireNewCopy(options.webpackConfigPath);
|
const webpackConfigModuleExports: WebpackConfigModuleExports = requireNewCopy(options.webpackConfigPath);
|
||||||
|
let webpackConfigExport = (webpackConfigModuleExports as EsModuleExports<{}>).__esModule === true
|
||||||
|
? (webpackConfigModuleExports as EsModuleExports<WebpackConfigExport>).default
|
||||||
|
: (webpackConfigModuleExports as WebpackConfigExport);
|
||||||
|
|
||||||
if (webpackConfigExport instanceof Function) {
|
if (webpackConfigExport instanceof Function) {
|
||||||
// If you export a function, we'll call it with an undefined 'env' arg, since we have nothing else
|
// If you export a function, we'll call it with an undefined 'env' arg, since we have nothing else
|
||||||
// to pass. This is the same as what the webpack CLI tool does if you specify no '--env.x' values.
|
// to pass. This is the same as what the webpack CLI tool does if you specify no '--env.x' values.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,62 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">netcoreapp2.0</TargetFramework>
|
|
||||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">TargetFrameworkOverride</TargetFramework>
|
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
|
||||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFrameworkOverride)' == ''">
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup Condition="'$(TargetFrameworkOverride)' != ''">
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<!-- Files not to publish (note that the 'dist' subfolders are re-added below) -->
|
|
||||||
<Content Remove="ClientApp\**" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!--/-:cnd:noEmit -->
|
|
||||||
<Target Name="DebugRunWebpack" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('wwwroot\dist') ">
|
|
||||||
<!-- Ensure Node.js is installed -->
|
|
||||||
<Exec Command="node --version" ContinueOnError="true">
|
|
||||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
|
||||||
</Exec>
|
|
||||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
|
||||||
|
|
||||||
<!-- In development, the dist files won't exist on the first run or when cloning to
|
|
||||||
a different machine, so rebuild them if not already present. -->
|
|
||||||
<Message Importance="high" Text="Performing first-run Webpack build..." />
|
|
||||||
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js" />
|
|
||||||
<Exec Command="node node_modules/webpack/bin/webpack.js" />
|
|
||||||
</Target>
|
|
||||||
<!--/+:cnd:noEmit -->
|
|
||||||
|
|
||||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
|
||||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
|
||||||
<Exec Command="npm install" />
|
|
||||||
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
|
|
||||||
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
|
|
||||||
|
|
||||||
<!-- Include the newly-built files in the publish output -->
|
|
||||||
<ItemGroup>
|
|
||||||
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
|
|
||||||
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
|
||||||
<RelativePath>%(DistFiles.Identity)</RelativePath>
|
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
|
||||||
</ResolvedFileToPublish>
|
|
||||||
</ItemGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { AppModuleShared } from './app.module.shared';
|
|
||||||
import { AppComponent } from './components/app/app.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
bootstrap: [ AppComponent ],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
AppModuleShared
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: 'BASE_URL', useFactory: getBaseUrl }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class AppModule {
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBaseUrl() {
|
|
||||||
return document.getElementsByTagName('base')[0].href;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { ServerModule } from '@angular/platform-server';
|
|
||||||
import { AppModuleShared } from './app.module.shared';
|
|
||||||
import { AppComponent } from './components/app/app.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
bootstrap: [ AppComponent ],
|
|
||||||
imports: [
|
|
||||||
ServerModule,
|
|
||||||
AppModuleShared
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class AppModule {
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { HttpModule } from '@angular/http';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { HttpWithStateTransferModule } from 'aspnet-angular';
|
|
||||||
|
|
||||||
import { AppComponent } from './components/app/app.component';
|
|
||||||
import { NavMenuComponent } from './components/navmenu/navmenu.component';
|
|
||||||
import { HomeComponent } from './components/home/home.component';
|
|
||||||
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
|
|
||||||
import { CounterComponent } from './components/counter/counter.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent,
|
|
||||||
NavMenuComponent,
|
|
||||||
CounterComponent,
|
|
||||||
FetchDataComponent,
|
|
||||||
HomeComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
HttpModule,
|
|
||||||
HttpWithStateTransferModule,
|
|
||||||
FormsModule,
|
|
||||||
RouterModule.forRoot([
|
|
||||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
|
||||||
{ path: 'home', component: HomeComponent },
|
|
||||||
{ path: 'counter', component: CounterComponent },
|
|
||||||
{ path: 'fetch-data', component: FetchDataComponent },
|
|
||||||
{ path: '**', redirectTo: 'home' }
|
|
||||||
])
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class AppModuleShared {
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
@media (max-width: 767px) {
|
|
||||||
/* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */
|
|
||||||
.body-content {
|
|
||||||
padding-top: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<div class='container-fluid'>
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-sm-3'>
|
|
||||||
<nav-menu></nav-menu>
|
|
||||||
</div>
|
|
||||||
<div class='col-sm-9 body-content'>
|
|
||||||
<router-outlet></router-outlet>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.css']
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<h1>Counter</h1>
|
|
||||||
|
|
||||||
<p>This is a simple example of an Angular component.</p>
|
|
||||||
|
|
||||||
<p>Current count: <strong>{{ currentCount }}</strong></p>
|
|
||||||
|
|
||||||
<button (click)="incrementCounter()">Increment</button>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/// <reference path="../../../../node_modules/@types/jasmine/index.d.ts" />
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import { CounterComponent } from './counter.component';
|
|
||||||
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
|
|
||||||
|
|
||||||
let fixture: ComponentFixture<CounterComponent>;
|
|
||||||
|
|
||||||
describe('Counter component', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({ declarations: [CounterComponent] });
|
|
||||||
fixture = TestBed.createComponent(CounterComponent);
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display a title', async(() => {
|
|
||||||
const titleText = fixture.nativeElement.querySelector('h1').textContent;
|
|
||||||
expect(titleText).toEqual('Counter');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should start with count 0, then increments by 1 when clicked', async(() => {
|
|
||||||
const countElement = fixture.nativeElement.querySelector('strong');
|
|
||||||
expect(countElement.textContent).toEqual('0');
|
|
||||||
|
|
||||||
const incrementButton = fixture.nativeElement.querySelector('button');
|
|
||||||
incrementButton.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
expect(countElement.textContent).toEqual('1');
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'counter',
|
|
||||||
templateUrl: './counter.component.html'
|
|
||||||
})
|
|
||||||
export class CounterComponent {
|
|
||||||
public currentCount = 0;
|
|
||||||
|
|
||||||
public incrementCounter() {
|
|
||||||
this.currentCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<h1>Weather forecast</h1>
|
|
||||||
|
|
||||||
<p>This component demonstrates fetching data from the server.</p>
|
|
||||||
|
|
||||||
<p *ngIf="!forecasts"><em>Loading...</em></p>
|
|
||||||
|
|
||||||
<table class='table' *ngIf="forecasts">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Temp. (C)</th>
|
|
||||||
<th>Temp. (F)</th>
|
|
||||||
<th>Summary</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let forecast of forecasts">
|
|
||||||
<td>{{ forecast.dateFormatted }}</td>
|
|
||||||
<td>{{ forecast.temperatureC }}</td>
|
|
||||||
<td>{{ forecast.temperatureF }}</td>
|
|
||||||
<td>{{ forecast.summary }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
|
||||||
import { HttpWithStateTransfer } from 'aspnet-angular';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'fetchdata',
|
|
||||||
templateUrl: './fetchdata.component.html'
|
|
||||||
})
|
|
||||||
export class FetchDataComponent {
|
|
||||||
public forecasts: WeatherForecast[];
|
|
||||||
|
|
||||||
constructor(http: HttpWithStateTransfer, @Inject('BASE_URL') baseUrl: string) {
|
|
||||||
http.get(baseUrl + 'api/SampleData/WeatherForecasts').subscribe(result => {
|
|
||||||
this.forecasts = result.json() as WeatherForecast[];
|
|
||||||
}, error => console.error(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WeatherForecast {
|
|
||||||
dateFormatted: string;
|
|
||||||
temperatureC: number;
|
|
||||||
temperatureF: number;
|
|
||||||
summary: string;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<h1>Hello, world!</h1>
|
|
||||||
<p>Welcome to your new single-page application, built with:</p>
|
|
||||||
<ul>
|
|
||||||
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
|
|
||||||
<li><a href='https://angular.io/'>Angular</a> and <a href='http://www.typescriptlang.org/'>TypeScript</a> for client-side code</li>
|
|
||||||
<li><a href='https://webpack.github.io/'>Webpack</a> for building and bundling client-side resources</li>
|
|
||||||
<li><a href='http://getbootstrap.com/'>Bootstrap</a> for layout and styling</li>
|
|
||||||
</ul>
|
|
||||||
<p>To help you get started, we've also set up:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
|
|
||||||
<li><strong>Server-side prerendering</strong>. For faster initial loading and improved SEO, your Angular app is prerendered on the server. The resulting HTML is then transferred to the browser where a client-side copy of the app takes over.</li>
|
|
||||||
<li><strong>Webpack dev middleware</strong>. In development mode, there's no need to run the <code>webpack</code> build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.</li>
|
|
||||||
<li><strong>Hot module replacement</strong>. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, your Angular app will be rebuilt and a new instance injected into the page.</li>
|
|
||||||
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and the <code>webpack</code> build tool produces minified static CSS and JavaScript files.</li>
|
|
||||||
</ul>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'home',
|
|
||||||
templateUrl: './home.component.html'
|
|
||||||
})
|
|
||||||
export class HomeComponent {
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
li .glyphicon {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlighting rules for nav menu items */
|
|
||||||
li.link-active a,
|
|
||||||
li.link-active a:hover,
|
|
||||||
li.link-active a:focus {
|
|
||||||
background-color: #4189C7;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keep the nav menu independent of scrolling and on top of other items */
|
|
||||||
.main-nav {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
/* On small screens, convert the nav menu to a vertical sidebar */
|
|
||||||
.main-nav {
|
|
||||||
height: 100%;
|
|
||||||
width: calc(25% - 20px);
|
|
||||||
}
|
|
||||||
.navbar {
|
|
||||||
border-radius: 0px;
|
|
||||||
border-width: 0px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.navbar-header {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
.navbar-collapse {
|
|
||||||
border-top: 1px solid #444;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
.navbar ul {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
.navbar li {
|
|
||||||
float: none;
|
|
||||||
font-size: 15px;
|
|
||||||
margin: 6px;
|
|
||||||
}
|
|
||||||
.navbar li a {
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.navbar a {
|
|
||||||
/* If a menu item's text is too long, truncate it */
|
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<div class='main-nav'>
|
|
||||||
<div class='navbar navbar-inverse'>
|
|
||||||
<div class='navbar-header'>
|
|
||||||
<button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'>
|
|
||||||
<span class='sr-only'>Toggle navigation</span>
|
|
||||||
<span class='icon-bar'></span>
|
|
||||||
<span class='icon-bar'></span>
|
|
||||||
<span class='icon-bar'></span>
|
|
||||||
</button>
|
|
||||||
<a class='navbar-brand' [routerLink]="['/home']">WebApplicationBasic</a>
|
|
||||||
</div>
|
|
||||||
<div class='clearfix'></div>
|
|
||||||
<div class='navbar-collapse collapse'>
|
|
||||||
<ul class='nav navbar-nav'>
|
|
||||||
<li [routerLinkActive]="['link-active']">
|
|
||||||
<a [routerLink]="['/home']">
|
|
||||||
<span class='glyphicon glyphicon-home'></span> Home
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li [routerLinkActive]="['link-active']">
|
|
||||||
<a [routerLink]="['/counter']">
|
|
||||||
<span class='glyphicon glyphicon-education'></span> Counter
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li [routerLinkActive]="['link-active']">
|
|
||||||
<a [routerLink]="['/fetch-data']">
|
|
||||||
<span class='glyphicon glyphicon-th-list'></span> Fetch data
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'nav-menu',
|
|
||||||
templateUrl: './navmenu.component.html',
|
|
||||||
styleUrls: ['./navmenu.component.css']
|
|
||||||
})
|
|
||||||
export class NavMenuComponent {
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'reflect-metadata';
|
|
||||||
import 'zone.js';
|
|
||||||
import 'bootstrap';
|
|
||||||
import { enableProdMode } from '@angular/core';
|
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
|
||||||
import { AppModule } from './app/app.module.browser';
|
|
||||||
|
|
||||||
if (module.hot) {
|
|
||||||
module.hot.accept();
|
|
||||||
module.hot.dispose(() => {
|
|
||||||
// Before restarting the app, we create a new root element and dispose the old one
|
|
||||||
const oldRootElem = document.querySelector('app');
|
|
||||||
const newRootElem = document.createElement('app');
|
|
||||||
oldRootElem!.parentNode!.insertBefore(newRootElem, oldRootElem);
|
|
||||||
modulePromise.then(appModule => appModule.destroy());
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
enableProdMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: @ng-tools/webpack looks for the following expression when performing production
|
|
||||||
// builds. Don't change how this line looks, otherwise you may break tree-shaking.
|
|
||||||
const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule);
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import 'reflect-metadata';
|
|
||||||
import 'zone.js';
|
|
||||||
import 'rxjs/add/operator/first';
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core';
|
|
||||||
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
|
|
||||||
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
|
|
||||||
import { AppModule } from './app/app.module.server';
|
|
||||||
import { HttpWithStateTransfer } from 'aspnet-angular';
|
|
||||||
|
|
||||||
enableProdMode();
|
|
||||||
|
|
||||||
export default createServerRenderer(params => {
|
|
||||||
const providers = [
|
|
||||||
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
|
|
||||||
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
|
|
||||||
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
|
|
||||||
];
|
|
||||||
|
|
||||||
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
|
|
||||||
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
|
|
||||||
const state = moduleRef.injector.get(PlatformState);
|
|
||||||
const zone = moduleRef.injector.get(NgZone);
|
|
||||||
const http: HttpWithStateTransfer = moduleRef.injector.get(HttpWithStateTransfer);
|
|
||||||
|
|
||||||
return new Promise<RenderResult>((resolve, reject) => {
|
|
||||||
zone.onError.subscribe((errorInfo: any) => reject(errorInfo));
|
|
||||||
appRef.isStable.first(isStable => isStable).subscribe(() => {
|
|
||||||
// Because 'onStable' fires before 'onError', we have to delay slightly before
|
|
||||||
// completing the request in case there's an error to report
|
|
||||||
setImmediate(() => {
|
|
||||||
resolve({
|
|
||||||
html: state.renderToString(),
|
|
||||||
globals: { ...http.stateForTransfer() }
|
|
||||||
});
|
|
||||||
moduleRef.destroy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Load required polyfills and testing libraries
|
|
||||||
import 'reflect-metadata';
|
|
||||||
import 'zone.js';
|
|
||||||
import 'zone.js/dist/long-stack-trace-zone';
|
|
||||||
import 'zone.js/dist/proxy.js';
|
|
||||||
import 'zone.js/dist/sync-test';
|
|
||||||
import 'zone.js/dist/jasmine-patch';
|
|
||||||
import 'zone.js/dist/async-test';
|
|
||||||
import 'zone.js/dist/fake-async-test';
|
|
||||||
import * as testing from '@angular/core/testing';
|
|
||||||
import * as testingBrowser from '@angular/platform-browser-dynamic/testing';
|
|
||||||
|
|
||||||
// There's no typing for the `__karma__` variable. Just declare it as any
|
|
||||||
declare var __karma__: any;
|
|
||||||
declare var require: any;
|
|
||||||
|
|
||||||
// Prevent Karma from running prematurely
|
|
||||||
__karma__.loaded = function () {};
|
|
||||||
|
|
||||||
// First, initialize the Angular testing environment
|
|
||||||
testing.getTestBed().initTestEnvironment(
|
|
||||||
testingBrowser.BrowserDynamicTestingModule,
|
|
||||||
testingBrowser.platformBrowserDynamicTesting()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Then we find all the tests
|
|
||||||
const context = require.context('../', true, /\.spec\.ts$/);
|
|
||||||
|
|
||||||
// And load the modules
|
|
||||||
context.keys().map(context);
|
|
||||||
|
|
||||||
// Finally, start Karma to run the tests
|
|
||||||
__karma__.start();
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Karma configuration file, see link for more information
|
|
||||||
// https://karma-runner.github.io/0.13/config/configuration-file.html
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
config.set({
|
|
||||||
basePath: '.',
|
|
||||||
frameworks: ['jasmine'],
|
|
||||||
files: [
|
|
||||||
'../../wwwroot/dist/vendor.js',
|
|
||||||
'./boot-tests.ts'
|
|
||||||
],
|
|
||||||
preprocessors: {
|
|
||||||
'./boot-tests.ts': ['webpack']
|
|
||||||
},
|
|
||||||
reporters: ['progress'],
|
|
||||||
port: 9876,
|
|
||||||
colors: true,
|
|
||||||
logLevel: config.LOG_INFO,
|
|
||||||
autoWatch: true,
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
mime: { 'application/javascript': ['ts','tsx'] },
|
|
||||||
singleRun: false,
|
|
||||||
webpack: require('../../webpack.config.js')().filter(config => config.target !== 'node'), // Test against client bundle, because tests run in a browser
|
|
||||||
webpackMiddleware: { stats: 'errors-only' }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace WebApplicationBasic.Controllers
|
|
||||||
{
|
|
||||||
public class HomeController : Controller
|
|
||||||
{
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IActionResult Error()
|
|
||||||
{
|
|
||||||
ViewData["RequestId"] = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace WebApplicationBasic.Controllers
|
|
||||||
{
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class SampleDataController : Controller
|
|
||||||
{
|
|
||||||
private static string[] Summaries = new[]
|
|
||||||
{
|
|
||||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
|
||||||
};
|
|
||||||
|
|
||||||
[HttpGet("[action]")]
|
|
||||||
public IEnumerable<WeatherForecast> WeatherForecasts()
|
|
||||||
{
|
|
||||||
var rng = new Random();
|
|
||||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
|
||||||
{
|
|
||||||
DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
|
|
||||||
TemperatureC = rng.Next(-20, 55),
|
|
||||||
Summary = Summaries[rng.Next(Summaries.Length)]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WeatherForecast
|
|
||||||
{
|
|
||||||
public string DateFormatted { get; set; }
|
|
||||||
public int TemperatureC { get; set; }
|
|
||||||
public string Summary { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureF
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace WebApplicationBasic
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
BuildWebHost(args).Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IWebHost BuildWebHost(string[] args) =>
|
|
||||||
WebHost.CreateDefaultBuilder(args)
|
|
||||||
.UseStartup<Startup>()
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.SpaServices.Webpack;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace WebApplicationBasic
|
|
||||||
{
|
|
||||||
public class Startup
|
|
||||||
{
|
|
||||||
public Startup(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IConfiguration Configuration { get; }
|
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddMvc();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
|
||||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
|
||||||
{
|
|
||||||
if (env.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseDeveloperExceptionPage();
|
|
||||||
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
|
|
||||||
{
|
|
||||||
HotModuleReplacement = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
app.UseExceptionHandler("/Home/Error");
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseStaticFiles();
|
|
||||||
|
|
||||||
app.UseMvc(routes =>
|
|
||||||
{
|
|
||||||
routes.MapRoute(
|
|
||||||
name: "default",
|
|
||||||
template: "{controller=Home}/{action=Index}/{id?}");
|
|
||||||
|
|
||||||
routes.MapSpaFallbackRoute(
|
|
||||||
name: "spa-fallback",
|
|
||||||
defaults: new { controller = "Home", action = "Index" });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
@{
|
|
||||||
ViewData["Title"] = "Home Page";
|
|
||||||
}
|
|
||||||
|
|
||||||
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
|
|
||||||
|
|
||||||
<script src="~/dist/vendor.js" asp-append-version="true"></script>
|
|
||||||
@section scripts {
|
|
||||||
<script src="~/dist/main-client.js" asp-append-version="true"></script>
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
@{
|
|
||||||
ViewData["Title"] = "Error";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1 class="text-danger">Error.</h1>
|
|
||||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty((string)ViewData["RequestId"]))
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
<strong>Request ID:</strong> <code>@ViewData["RequestId"]</code>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<h3>Development Mode</h3>
|
|
||||||
<p>
|
|
||||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
|
|
||||||
</p>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>@ViewData["Title"] - WebApplicationBasic</title>
|
|
||||||
<base href="~/" />
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="~/dist/vendor.css" asp-append-version="true" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
@RenderBody()
|
|
||||||
|
|
||||||
@RenderSection("scripts", required: false)
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@using WebApplicationBasic
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.SpaServices
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@{
|
|
||||||
Layout = "_Layout";
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"IncludeScopes": false,
|
|
||||||
"Debug": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Debug",
|
|
||||||
"System": "Information",
|
|
||||||
"Microsoft": "Information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Console": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Debug",
|
|
||||||
"System": "Information",
|
|
||||||
"Microsoft": "Information"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"IncludeScopes": false,
|
|
||||||
"Debug": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Console": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3735
templates/AngularSpa/npm-shrinkwrap.json
generated
3735
templates/AngularSpa/npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "WebApplicationBasic",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"scripts": {
|
|
||||||
"test": "karma start ClientApp/test/karma.conf.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@angular/animations": "4.2.5",
|
|
||||||
"@angular/common": "4.2.5",
|
|
||||||
"@angular/compiler": "4.2.5",
|
|
||||||
"@angular/compiler-cli": "4.2.5",
|
|
||||||
"@angular/core": "4.2.5",
|
|
||||||
"@angular/forms": "4.2.5",
|
|
||||||
"@angular/http": "4.2.5",
|
|
||||||
"@angular/platform-browser": "4.2.5",
|
|
||||||
"@angular/platform-browser-dynamic": "4.2.5",
|
|
||||||
"@angular/platform-server": "4.2.5",
|
|
||||||
"@angular/router": "4.2.5",
|
|
||||||
"@ngtools/webpack": "1.5.0",
|
|
||||||
"@types/webpack-env": "1.13.0",
|
|
||||||
"angular2-template-loader": "0.6.2",
|
|
||||||
"aspnet-angular": "^0.1.1",
|
|
||||||
"aspnet-prerendering": "^3.0.1",
|
|
||||||
"aspnet-webpack": "^2.0.1",
|
|
||||||
"awesome-typescript-loader": "3.2.1",
|
|
||||||
"bootstrap": "3.3.7",
|
|
||||||
"css": "2.2.1",
|
|
||||||
"css-loader": "0.28.4",
|
|
||||||
"es6-shim": "0.35.3",
|
|
||||||
"event-source-polyfill": "0.0.9",
|
|
||||||
"expose-loader": "0.7.3",
|
|
||||||
"extract-text-webpack-plugin": "2.1.2",
|
|
||||||
"file-loader": "0.11.2",
|
|
||||||
"html-loader": "0.4.5",
|
|
||||||
"isomorphic-fetch": "2.2.1",
|
|
||||||
"jquery": "3.2.1",
|
|
||||||
"json-loader": "0.5.4",
|
|
||||||
"preboot": "4.5.2",
|
|
||||||
"raw-loader": "0.5.1",
|
|
||||||
"reflect-metadata": "0.1.10",
|
|
||||||
"rxjs": "5.4.2",
|
|
||||||
"style-loader": "0.18.2",
|
|
||||||
"to-string-loader": "1.1.5",
|
|
||||||
"typescript": "2.4.1",
|
|
||||||
"url-loader": "0.5.9",
|
|
||||||
"webpack": "2.5.1",
|
|
||||||
"webpack-hot-middleware": "2.18.2",
|
|
||||||
"webpack-merge": "4.1.0",
|
|
||||||
"zone.js": "0.8.12"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/chai": "4.0.1",
|
|
||||||
"@types/jasmine": "2.5.53",
|
|
||||||
"chai": "4.0.2",
|
|
||||||
"jasmine-core": "2.6.4",
|
|
||||||
"karma": "1.7.0",
|
|
||||||
"karma-chai": "0.1.0",
|
|
||||||
"karma-chrome-launcher": "2.2.0",
|
|
||||||
"karma-cli": "1.0.1",
|
|
||||||
"karma-jasmine": "1.1.0",
|
|
||||||
"karma-webpack": "2.0.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
/Properties/launchSettings.json
|
|
||||||
|
|
||||||
## Ignore Visual Studio temporary files, build results, and
|
|
||||||
## files generated by popular Visual Studio add-ons.
|
|
||||||
|
|
||||||
# User-specific files
|
|
||||||
*.suo
|
|
||||||
*.user
|
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Build results
|
|
||||||
[Dd]ebug/
|
|
||||||
[Dd]ebugPublic/
|
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
build/
|
|
||||||
bld/
|
|
||||||
bin/
|
|
||||||
Bin/
|
|
||||||
obj/
|
|
||||||
Obj/
|
|
||||||
|
|
||||||
# Visual Studio 2015 cache/options directory
|
|
||||||
.vs/
|
|
||||||
/wwwroot/dist/
|
|
||||||
/ClientApp/dist/
|
|
||||||
|
|
||||||
# MSTest test Results
|
|
||||||
[Tt]est[Rr]esult*/
|
|
||||||
[Bb]uild[Ll]og.*
|
|
||||||
|
|
||||||
# NUNIT
|
|
||||||
*.VisualState.xml
|
|
||||||
TestResult.xml
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
|
||||||
[Dd]ebugPS/
|
|
||||||
[Rr]eleasePS/
|
|
||||||
dlldata.c
|
|
||||||
|
|
||||||
*_i.c
|
|
||||||
*_p.c
|
|
||||||
*_i.h
|
|
||||||
*.ilk
|
|
||||||
*.meta
|
|
||||||
*.obj
|
|
||||||
*.pch
|
|
||||||
*.pdb
|
|
||||||
*.pgc
|
|
||||||
*.pgd
|
|
||||||
*.rsp
|
|
||||||
*.sbr
|
|
||||||
*.tlb
|
|
||||||
*.tli
|
|
||||||
*.tlh
|
|
||||||
*.tmp
|
|
||||||
*.tmp_proj
|
|
||||||
*.log
|
|
||||||
*.vspscc
|
|
||||||
*.vssscc
|
|
||||||
.builds
|
|
||||||
*.pidb
|
|
||||||
*.svclog
|
|
||||||
*.scc
|
|
||||||
|
|
||||||
# Chutzpah Test files
|
|
||||||
_Chutzpah*
|
|
||||||
|
|
||||||
# Visual C++ cache files
|
|
||||||
ipch/
|
|
||||||
*.aps
|
|
||||||
*.ncb
|
|
||||||
*.opendb
|
|
||||||
*.opensdf
|
|
||||||
*.sdf
|
|
||||||
*.cachefile
|
|
||||||
|
|
||||||
# Visual Studio profiler
|
|
||||||
*.psess
|
|
||||||
*.vsp
|
|
||||||
*.vspx
|
|
||||||
*.sap
|
|
||||||
|
|
||||||
# TFS 2012 Local Workspace
|
|
||||||
$tf/
|
|
||||||
|
|
||||||
# Guidance Automation Toolkit
|
|
||||||
*.gpState
|
|
||||||
|
|
||||||
# ReSharper is a .NET coding add-in
|
|
||||||
_ReSharper*/
|
|
||||||
*.[Rr]e[Ss]harper
|
|
||||||
*.DotSettings.user
|
|
||||||
|
|
||||||
# JustCode is a .NET coding add-in
|
|
||||||
.JustCode
|
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
|
||||||
_TeamCity*
|
|
||||||
|
|
||||||
# DotCover is a Code Coverage Tool
|
|
||||||
*.dotCover
|
|
||||||
|
|
||||||
# NCrunch
|
|
||||||
_NCrunch_*
|
|
||||||
.*crunch*.local.xml
|
|
||||||
nCrunchTemp_*
|
|
||||||
|
|
||||||
# MightyMoose
|
|
||||||
*.mm.*
|
|
||||||
AutoTest.Net/
|
|
||||||
|
|
||||||
# Web workbench (sass)
|
|
||||||
.sass-cache/
|
|
||||||
|
|
||||||
# Installshield output folder
|
|
||||||
[Ee]xpress/
|
|
||||||
|
|
||||||
# DocProject is a documentation generator add-in
|
|
||||||
DocProject/buildhelp/
|
|
||||||
DocProject/Help/*.HxT
|
|
||||||
DocProject/Help/*.HxC
|
|
||||||
DocProject/Help/*.hhc
|
|
||||||
DocProject/Help/*.hhk
|
|
||||||
DocProject/Help/*.hhp
|
|
||||||
DocProject/Help/Html2
|
|
||||||
DocProject/Help/html
|
|
||||||
|
|
||||||
# Click-Once directory
|
|
||||||
publish/
|
|
||||||
|
|
||||||
# Publish Web Output
|
|
||||||
*.[Pp]ublish.xml
|
|
||||||
*.azurePubxml
|
|
||||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
|
||||||
*.pubxml
|
|
||||||
*.publishproj
|
|
||||||
|
|
||||||
# NuGet Packages
|
|
||||||
*.nupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
|
||||||
**/packages/*
|
|
||||||
# except build/, which is used as an MSBuild target.
|
|
||||||
!**/packages/build/
|
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
|
||||||
#!**/packages/repositories.config
|
|
||||||
|
|
||||||
# Microsoft Azure Build Output
|
|
||||||
csx/
|
|
||||||
*.build.csdef
|
|
||||||
|
|
||||||
# Microsoft Azure Emulator
|
|
||||||
ecf/
|
|
||||||
rcf/
|
|
||||||
|
|
||||||
# Microsoft Azure ApplicationInsights config file
|
|
||||||
ApplicationInsights.config
|
|
||||||
|
|
||||||
# Windows Store app package directory
|
|
||||||
AppPackages/
|
|
||||||
BundleArtifacts/
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
|
||||||
# files ending in .cache can be ignored
|
|
||||||
*.[Cc]ache
|
|
||||||
# but keep track of directories ending in .cache
|
|
||||||
!*.[Cc]ache/
|
|
||||||
|
|
||||||
# Others
|
|
||||||
ClientBin/
|
|
||||||
~$*
|
|
||||||
*~
|
|
||||||
*.dbmdl
|
|
||||||
*.dbproj.schemaview
|
|
||||||
*.pfx
|
|
||||||
*.publishsettings
|
|
||||||
orleans.codegen.cs
|
|
||||||
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
/yarn.lock
|
|
||||||
|
|
||||||
# RIA/Silverlight projects
|
|
||||||
Generated_Code/
|
|
||||||
|
|
||||||
# Backup & report files from converting an old project file
|
|
||||||
# to a newer Visual Studio version. Backup files are not needed,
|
|
||||||
# because we have git ;-)
|
|
||||||
_UpgradeReport_Files/
|
|
||||||
Backup*/
|
|
||||||
UpgradeLog*.XML
|
|
||||||
UpgradeLog*.htm
|
|
||||||
|
|
||||||
# SQL Server files
|
|
||||||
*.mdf
|
|
||||||
*.ldf
|
|
||||||
|
|
||||||
# Business Intelligence projects
|
|
||||||
*.rdl.data
|
|
||||||
*.bim.layout
|
|
||||||
*.bim_*.settings
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
|
||||||
FakesAssemblies/
|
|
||||||
|
|
||||||
# GhostDoc plugin setting file
|
|
||||||
*.GhostDoc.xml
|
|
||||||
|
|
||||||
# Node.js Tools for Visual Studio
|
|
||||||
.ntvs_analysis.dat
|
|
||||||
|
|
||||||
# Visual Studio 6 build log
|
|
||||||
*.plg
|
|
||||||
|
|
||||||
# Visual Studio 6 workspace options file
|
|
||||||
*.opt
|
|
||||||
|
|
||||||
# Visual Studio LightSwitch build output
|
|
||||||
**/*.HTMLClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/GeneratedArtifacts
|
|
||||||
**/*.DesktopClient/ModelManifest.xml
|
|
||||||
**/*.Server/GeneratedArtifacts
|
|
||||||
**/*.Server/ModelManifest.xml
|
|
||||||
_Pvt_Extensions
|
|
||||||
|
|
||||||
# Paket dependency manager
|
|
||||||
.paket/paket.exe
|
|
||||||
|
|
||||||
# FAKE - F# Make
|
|
||||||
.fake/
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "es2015",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"target": "es5",
|
|
||||||
"sourceMap": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"skipDefaultLibCheck": true,
|
|
||||||
"skipLibCheck": true, // Workaround for https://github.com/angular/angular/issues/17863. Remove this if you upgrade to a fixed version of Angular.
|
|
||||||
"strict": true,
|
|
||||||
"lib": [ "es6", "dom" ],
|
|
||||||
"types": [ "webpack-env" ]
|
|
||||||
},
|
|
||||||
"exclude": [ "bin", "node_modules" ],
|
|
||||||
"atom": { "rewriteTsconfig": false }
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user