mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 10:08:57 +00:00
Compare commits
1 Commits
2.1.0-prev
...
angular-an
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c47e3def |
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -24,8 +24,12 @@ nuget.exe
|
|||||||
*.ncrunchsolution
|
*.ncrunchsolution
|
||||||
*.*sdf
|
*.*sdf
|
||||||
*.ipch
|
*.ipch
|
||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
/.build/
|
/.build/
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
global.json
|
global.json
|
||||||
|
korebuild-lock.txt
|
||||||
|
|||||||
11
.travis.yml
11
.travis.yml
@@ -12,13 +12,8 @@ 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-.*$/
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
|
|
||||||
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
|
|
||||||
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.26730.16
|
VisualStudioVersion = 15.0.26430.4
|
||||||
MinimumVisualStudioVersion = 15.0.26730.03
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
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
|
||||||
@@ -31,13 +28,13 @@ 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}") = "Solution Items", "Solution Items", "{645F7363-1240-4FB6-9422-B32A327C979F}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{1598B415-73F1-4B37-B3B4-0A10677ABB2D}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
|
||||||
Directory.Build.props = Directory.Build.props
|
|
||||||
Directory.Build.targets = Directory.Build.targets
|
|
||||||
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}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E415FE14-13B0-469F-836D-95059E6BAA6E}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
src\build\common.props = src\build\common.props
|
||||||
|
src\build\Key.snk = src\build\Key.snk
|
||||||
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -69,10 +66,6 @@ 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
|
||||||
@@ -85,9 +78,5 @@ 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,6 +2,8 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<clear />
|
<clear />
|
||||||
<!-- Restore sources should be defined in build/sources.props. -->
|
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
|
||||||
|
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
|
||||||
|
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
48
README.md
48
README.md
@@ -24,37 +24,29 @@ 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 2.0 or later.
|
It's cross-platform (Windows, Linux, or macOS) and works with .NET Core 1.0.1 or later.
|
||||||
|
|
||||||
## Creating new applications
|
## Creating new applications
|
||||||
|
|
||||||
Prerequisites:
|
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!
|
||||||
|
|
||||||
* [.NET Core 2.0](https://www.microsoft.com/net/core) (or later) SDK
|
To do this, install Yeoman and these generator templates:
|
||||||
* [Node.js](https://nodejs.org/) version 6 (or later)
|
|
||||||
|
|
||||||
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.
|
npm install -g yo generator-aspnetcore-spa
|
||||||
|
|
||||||
### Option 1: Creating Angular/React/Redux applications from the command line (cross-platform)
|
Generate your new application starting point:
|
||||||
|
|
||||||
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`.
|
cd some-empty-directory
|
||||||
|
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
|
||||||
|
|
||||||
### Option 2: Creating Angular/React/Redux applications using Visual Studio 2017 Update 3 or later (Windows only)
|
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/).
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -66,13 +58,18 @@ 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 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.
|
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.
|
||||||
|
|
||||||
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
|
## Samples and templates
|
||||||
|
|
||||||
|
Inside this repo, [the `templates` directory](https://github.com/aspnet/JavaScriptServices/tree/dev/templates) contains the application starting points that the `aspnetcore-spa` generator emits. You can clone this repo and run those applications directly. But it's easier to [use the Yeoman tool to run the generator](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/).
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
@@ -91,6 +88,13 @@ 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!
|
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:
|
||||||
|
|
||||||
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.
|
* Clone the repo
|
||||||
|
* Run `dotnet restore` at the repo root dir
|
||||||
|
* Go to whatever sample or template you want to run (for example, `cd templates/AngularSpa`)
|
||||||
|
* Restore NPM dependencies (run `npm install`)
|
||||||
|
* If the sample/template you're trying to run has a file called `webpack.config.vendor.js` at its root, run `webpack --config webpack.config.vendor.js`. 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.
|
||||||
|
|||||||
42
appveyor.yml
Executable file
42
appveyor.yml
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
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\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 = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE"
|
PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE"
|
||||||
|
|||||||
@@ -3,13 +3,10 @@
|
|||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Executes KoreBuild commands.
|
Build this repository
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`.
|
Downloads korebuild if required. Then builds the repository.
|
||||||
|
|
||||||
.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.
|
||||||
@@ -27,35 +24,31 @@ 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 korebuild.json.
|
The path to the configuration file that stores values. Defaults to version.xml.
|
||||||
|
|
||||||
.PARAMETER ToolsSourceSuffix
|
.PARAMETER MSBuildArgs
|
||||||
The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores.
|
Arguments to be passed to MSBuild
|
||||||
|
|
||||||
.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 JSON file. It is optional, and the configuration values in it are optional as well. Any options set
|
The $ConfigFile is expected to be an XML file. It is optional, and the configuration values in it are optional as well.
|
||||||
in the file are overridden by command line parameters.
|
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
Example config file:
|
Example config file:
|
||||||
```json
|
```xml
|
||||||
{
|
<!-- version.xml -->
|
||||||
"$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json",
|
<Project>
|
||||||
"channel": "dev",
|
<PropertyGroup>
|
||||||
"toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools"
|
<KoreBuildChannel>dev</KoreBuildChannel>
|
||||||
}
|
<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,
|
||||||
@@ -65,10 +58,9 @@ param(
|
|||||||
[string]$ToolsSource,
|
[string]$ToolsSource,
|
||||||
[Alias('u')]
|
[Alias('u')]
|
||||||
[switch]$Update,
|
[switch]$Update,
|
||||||
[string]$ConfigFile,
|
[string]$ConfigFile = (Join-Path $PSScriptRoot 'version.xml'),
|
||||||
[string]$ToolsSourceSuffix,
|
|
||||||
[Parameter(ValueFromRemainingArguments = $true)]
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string[]]$Arguments
|
[string[]]$MSBuildArgs
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version 2
|
Set-StrictMode -Version 2
|
||||||
@@ -83,7 +75,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 $ToolsSourceSuffix
|
Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile
|
||||||
}
|
}
|
||||||
|
|
||||||
$version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
|
$version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1
|
||||||
@@ -100,7 +92,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 $ToolsSourceSuffix
|
Get-RemoteFile $remotePath $tmpfile
|
||||||
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
|
||||||
@@ -128,7 +120,7 @@ function Join-Paths([string]$path, [string[]]$childPaths) {
|
|||||||
return $path
|
return $path
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) {
|
function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) {
|
||||||
if ($RemotePath -notlike 'http*') {
|
if ($RemotePath -notlike 'http*') {
|
||||||
Copy-Item $RemotePath $LocalPath
|
Copy-Item $RemotePath $LocalPath
|
||||||
return
|
return
|
||||||
@@ -138,7 +130,7 @@ function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$Remote
|
|||||||
while ($retries -gt 0) {
|
while ($retries -gt 0) {
|
||||||
$retries -= 1
|
$retries -= 1
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath
|
Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -155,21 +147,10 @@ function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$Remote
|
|||||||
|
|
||||||
# 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) {
|
||||||
try {
|
[xml] $config = Get-Content $ConfigFile
|
||||||
$config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json
|
if (!($Channel)) { [string] $Channel = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildChannel' }
|
||||||
if ($config) {
|
if (!($ToolsSource)) { [string] $ToolsSource = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildToolsSource' }
|
||||||
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) {
|
||||||
@@ -188,8 +169,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 {
|
||||||
Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile
|
Install-Tools $ToolsSource $DotNetHome
|
||||||
Invoke-KoreBuildCommand $Command @Arguments
|
Invoke-RepositoryBuild $Path @MSBuildArgs
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
Remove-Module 'KoreBuild' -ErrorAction Ignore
|
Remove-Module 'KoreBuild' -ErrorAction Ignore
|
||||||
199
build.sh
199
build.sh
@@ -1,8 +1,199 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
|
|
||||||
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
#
|
||||||
chmod +x "$DIR/run.sh"; sync
|
# variables
|
||||||
"$DIR/run.sh" default-build "$@"
|
#
|
||||||
|
|
||||||
|
RESET="\033[0m"
|
||||||
|
RED="\033[0;31m"
|
||||||
|
MAGENTA="\033[0;95m"
|
||||||
|
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=''
|
||||||
|
|
||||||
|
#
|
||||||
|
# Functions
|
||||||
|
#
|
||||||
|
__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
|
||||||
|
|
||||||
|
local failed=false
|
||||||
|
if __machine_has wget; then
|
||||||
|
wget --tries 10 --quiet -O "$local_path" "$remote_path" || 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" || 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" "$@"
|
||||||
|
|||||||
23
build/common.props
Normal file
23
build/common.props
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<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,27 +1,11 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
<AspNetCoreVersion>2.1.0-*</AspNetCoreVersion>
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
version:2.1.0-preview1-1010
|
|
||||||
commithash:75ca924dfbd673c38841025b04c4dcd93b84f56d
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"$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
2
run.cmd
@@ -1,2 +0,0 @@
|
|||||||
@ECHO OFF
|
|
||||||
PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE"
|
|
||||||
231
run.sh
231
run.sh
@@ -1,231 +0,0 @@
|
|||||||
#!/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,7 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<Import Project="..\..\..\build\common.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<OutputType>exe</OutputType>
|
<OutputType>exe</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<Import Project="..\..\..\build\common.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -11,13 +13,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<Import Project="..\..\..\build\common.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -11,13 +13,13 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(MicrosoftAspNetCoreHostingPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(MicrosoftExtensionsLoggingDebugPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="$(AspNetCoreVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish">
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<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,8 +1,12 @@
|
|||||||
<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>
|
||||||
@@ -12,10 +16,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||||
</ItemGroup>
|
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="$(ThreadingDataflowVersion)" />
|
||||||
|
|
||||||
<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,7 +14,8 @@ 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 <see cref="Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections.VirtualConnectionClient" />
|
/// For details on the binary streaming protocol, see
|
||||||
|
/// 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.
|
||||||
///
|
///
|
||||||
@@ -237,4 +238,4 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets
|
|||||||
}
|
}
|
||||||
#pragma warning restore 649
|
#pragma warning restore 649
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<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>
|
||||||
@@ -11,9 +16,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(MicrosoftAspNetCoreHostingAbstractionsPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
// 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// 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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// 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,22 +1,21 @@
|
|||||||
<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" />
|
||||||
</ItemGroup>
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="$(AspNetCoreVersion)" />
|
||||||
|
<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,4 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
using System;
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@@ -14,7 +20,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.AddHttpContextAccessor();
|
serviceCollection.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
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-dev webpack-hot-middleware
|
npm install --save 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-dev aspnet-webpack-react
|
npm install --save 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,7 +22,6 @@ 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,
|
||||||
@@ -36,7 +35,6 @@ 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;
|
||||||
@@ -44,7 +42,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
if (context.Request.Path.StartsWithSegments(_pathPrefix) || _pathPrefixIsRoot)
|
if (context.Request.Path.StartsWithSegments(_pathPrefix))
|
||||||
{
|
{
|
||||||
var didProxyRequest = await PerformProxyRequest(context);
|
var didProxyRequest = await PerformProxyRequest(context);
|
||||||
if (didProxyRequest)
|
if (didProxyRequest)
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
|
|
||||||
private static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
private static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
// Note that the aspnet-webpack JS code specifically expects options to be serialized with
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||||
// PascalCase property names, so it's important to be explicit about this contract resolver
|
|
||||||
ContractResolver = new DefaultContractResolver(),
|
|
||||||
|
|
||||||
TypeNameHandling = TypeNameHandling.None
|
TypeNameHandling = TypeNameHandling.None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ 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
|
||||||
@@ -40,8 +39,7 @@ type WebpackConfigOrArray = webpack.Configuration | webpack.Configuration[];
|
|||||||
interface WebpackConfigFunc {
|
interface WebpackConfigFunc {
|
||||||
(env?: any): WebpackConfigOrArray;
|
(env?: any): WebpackConfigOrArray;
|
||||||
}
|
}
|
||||||
type WebpackConfigExport = WebpackConfigOrArray | WebpackConfigFunc;
|
type WebpackConfigFileExport = 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
|
||||||
@@ -237,11 +235,7 @@ 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
|
||||||
const webpackConfigModuleExports: WebpackConfigModuleExports = requireNewCopy(options.webpackConfigPath);
|
let webpackConfigExport: WebpackConfigFileExport = 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.
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||||
|
<metadata>
|
||||||
|
<id>Microsoft.AspNetCore.SpaTemplates</id>
|
||||||
|
<version>0.0.0</version>
|
||||||
|
<description>Single Page Application templates for ASP.NET Core</description>
|
||||||
|
<authors>Microsoft</authors>
|
||||||
|
<language>en-US</language>
|
||||||
|
<projectUrl>https://github.com/aspnet/javascriptservices</projectUrl>
|
||||||
|
<licenseUrl>https://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm</licenseUrl>
|
||||||
|
<copyright>Copyright © Microsoft Corporation</copyright>
|
||||||
|
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||||
|
<packageTypes>
|
||||||
|
<packageType name="Template" />
|
||||||
|
</packageTypes>
|
||||||
|
</metadata>
|
||||||
|
<files>
|
||||||
|
<file
|
||||||
|
src="**/*"
|
||||||
|
exclude="*/node_modules/**;*/bin/**;*/obj/**;*/ClientApp/dist/**;*/wwwroot/dist/**"
|
||||||
|
target="Content" />
|
||||||
|
</files>
|
||||||
|
</package>
|
||||||
237
templates/Microsoft.AspNetCore.SpaTemplates/aurelia/.gitignore
vendored
Normal file
237
templates/Microsoft.AspNetCore.SpaTemplates/aurelia/.gitignore
vendored
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/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/
|
||||||
|
|
||||||
|
/yarn.lock
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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/
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/dotnetcli.host",
|
||||||
|
"symbolInfo": {
|
||||||
|
"TargetFrameworkOverride": {
|
||||||
|
"isHidden": "true",
|
||||||
|
"longName": "target-framework-override",
|
||||||
|
"shortName": ""
|
||||||
|
},
|
||||||
|
"Framework": {
|
||||||
|
"longName": "framework"
|
||||||
|
},
|
||||||
|
"skipRestore": {
|
||||||
|
"longName": "no-restore",
|
||||||
|
"shortName": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"author": "Microsoft",
|
||||||
|
"classifications": [
|
||||||
|
"Web",
|
||||||
|
"MVC",
|
||||||
|
"SPA"
|
||||||
|
],
|
||||||
|
"groupIdentity": "Microsoft.AspNetCore.SpaTemplates.Aurelia",
|
||||||
|
"identity": "Microsoft.AspNetCore.SpaTemplates.Aurelia.CSharp",
|
||||||
|
"name": "ASP.NET Core with Aurelia",
|
||||||
|
"preferNameDirectory": true,
|
||||||
|
"primaryOutputs": [
|
||||||
|
{
|
||||||
|
"path": "AureliaSpa.csproj"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortName": "aurelia",
|
||||||
|
"sourceName": "AureliaSpa",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"source": "./",
|
||||||
|
"target": "./",
|
||||||
|
"exclude": [
|
||||||
|
".template.config/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"symbols": {
|
||||||
|
"TargetFrameworkOverride": {
|
||||||
|
"type": "parameter",
|
||||||
|
"description": "Overrides the target framework",
|
||||||
|
"replaces": "TargetFrameworkOverride",
|
||||||
|
"datatype": "string",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
"Framework": {
|
||||||
|
"type": "parameter",
|
||||||
|
"description": "The target framework for the project.",
|
||||||
|
"datatype": "choice",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"choice": "netcoreapp2.0",
|
||||||
|
"description": "Target netcoreapp2.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"replaces": "netcoreapp2.0",
|
||||||
|
"defaultValue": "netcoreapp2.0"
|
||||||
|
},
|
||||||
|
"HostIdentifier": {
|
||||||
|
"type": "bind",
|
||||||
|
"binding": "HostIdentifier"
|
||||||
|
},
|
||||||
|
"skipRestore": {
|
||||||
|
"type": "parameter",
|
||||||
|
"datatype": "bool",
|
||||||
|
"description": "If specified, skips the automatic restore of the project on create.",
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"language": "C#",
|
||||||
|
"type": "project"
|
||||||
|
},
|
||||||
|
"postActions": [
|
||||||
|
{
|
||||||
|
"condition": "(!skipRestore)",
|
||||||
|
"description": "Restore NuGet packages required by this project.",
|
||||||
|
"manualInstructions": [
|
||||||
|
{
|
||||||
|
"text": "Run 'dotnet restore'"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
|
||||||
|
"continueOnError": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
|
||||||
|
"actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C",
|
||||||
|
"description": "\n\n-------------------------------------------------------------------\nIMPORTANT: Before running this project on the command line,\n you must restore NPM packages by running \"npm install\"\n-------------------------------------------------------------------\n",
|
||||||
|
"manualInstructions": [
|
||||||
|
{
|
||||||
|
"text": "Run \"npm install\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/vs-2017.3.host",
|
||||||
|
"name": {
|
||||||
|
"text": "Aurelia",
|
||||||
|
"package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}",
|
||||||
|
"id": "1200"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"text": "A project template for creating an ASP.NET Core application with Aurelia",
|
||||||
|
"package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}",
|
||||||
|
"id": "1201"
|
||||||
|
},
|
||||||
|
"order": 301,
|
||||||
|
"icon": "icon.png",
|
||||||
|
"learnMoreLink": "https://github.com/aspnet/JavaScriptServices",
|
||||||
|
"uiFilters": [
|
||||||
|
"oneaspnet"
|
||||||
|
],
|
||||||
|
"minFullFrameworkVersion": "4.6.1"
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<!--/-: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\**" />
|
||||||
|
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||||
|
<RelativePath>%(DistFiles.Identity)</RelativePath>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
</ResolvedFileToPublish>
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<require from="../navmenu/navmenu.html"></require>
|
||||||
|
<require from="./app.css"></require>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<navmenu router.bind="router"></navmenu>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-9 body-content">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Aurelia, PLATFORM } from 'aurelia-framework';
|
||||||
|
import { Router, RouterConfiguration } from 'aurelia-router';
|
||||||
|
|
||||||
|
export class App {
|
||||||
|
router: Router;
|
||||||
|
|
||||||
|
configureRouter(config: RouterConfiguration, router: Router) {
|
||||||
|
config.title = 'Aurelia';
|
||||||
|
config.map([{
|
||||||
|
route: [ '', 'home' ],
|
||||||
|
name: 'home',
|
||||||
|
settings: { icon: 'home' },
|
||||||
|
moduleId: PLATFORM.moduleName('../home/home'),
|
||||||
|
nav: true,
|
||||||
|
title: 'Home'
|
||||||
|
}, {
|
||||||
|
route: 'counter',
|
||||||
|
name: 'counter',
|
||||||
|
settings: { icon: 'education' },
|
||||||
|
moduleId: PLATFORM.moduleName('../counter/counter'),
|
||||||
|
nav: true,
|
||||||
|
title: 'Counter'
|
||||||
|
}, {
|
||||||
|
route: 'fetch-data',
|
||||||
|
name: 'fetchdata',
|
||||||
|
settings: { icon: 'th-list' },
|
||||||
|
moduleId: PLATFORM.moduleName('../fetchdata/fetchdata'),
|
||||||
|
nav: true,
|
||||||
|
title: 'Fetch data'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
this.router = router;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<h1>Counter</h1>
|
||||||
|
|
||||||
|
<p>This is a simple example of an Aurelia component.</p>
|
||||||
|
|
||||||
|
<p>Current count: <strong>${currentCount}</strong></p>
|
||||||
|
|
||||||
|
<button click.delegate="incrementCounter()">Increment</button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class Counter {
|
||||||
|
public currentCount = 0;
|
||||||
|
|
||||||
|
public incrementCounter() {
|
||||||
|
this.currentCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<h1>Weather forecast</h1>
|
||||||
|
|
||||||
|
<p>This component demonstrates fetching data from the server.</p>
|
||||||
|
|
||||||
|
<p if.bind="!forecasts"><em>Loading...</em></p>
|
||||||
|
|
||||||
|
<table if.bind="forecasts" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Temp. (C)</th>
|
||||||
|
<th>Temp. (F)</th>
|
||||||
|
<th>Summary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr repeat.for="forecast of forecasts">
|
||||||
|
<td>${ forecast.dateFormatted }</td>
|
||||||
|
<td>${ forecast.temperatureC }</td>
|
||||||
|
<td>${ forecast.temperatureF }</td>
|
||||||
|
<td>${ forecast.summary }</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { HttpClient } from 'aurelia-fetch-client';
|
||||||
|
import { inject } from 'aurelia-framework';
|
||||||
|
|
||||||
|
@inject(HttpClient)
|
||||||
|
export class Fetchdata {
|
||||||
|
public forecasts: WeatherForecast[];
|
||||||
|
|
||||||
|
constructor(http: HttpClient) {
|
||||||
|
http.fetch('api/SampleData/WeatherForecasts')
|
||||||
|
.then(result => result.json() as Promise<WeatherForecast[]>)
|
||||||
|
.then(data => {
|
||||||
|
this.forecasts = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeatherForecast {
|
||||||
|
dateFormatted: string;
|
||||||
|
temperatureC: number;
|
||||||
|
temperatureF: number;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<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="http://aurelia.io/">Aurelia</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>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>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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export class Home {
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
li .glyphicon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlighting rules for nav menu items */
|
||||||
|
li.au-target.link-active a,
|
||||||
|
li.au-target.link-active a:hover,
|
||||||
|
li.au-target.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<template bindable="router">
|
||||||
|
<require from="./navmenu.css"></require>
|
||||||
|
<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" href="#/home">AureliaSpa</a>
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
<div class="navbar-collapse collapse">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li repeat.for = "row of router.navigation" class="${ row.isActive ? 'link-active' : '' }" >
|
||||||
|
<a href.bind = "row.href">
|
||||||
|
<span class="glyphicon glyphicon-${ row.settings.icon }"></span> ${ row.title }
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'isomorphic-fetch';
|
||||||
|
import { Aurelia, PLATFORM } from 'aurelia-framework';
|
||||||
|
import { HttpClient } from 'aurelia-fetch-client';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css';
|
||||||
|
import 'bootstrap';
|
||||||
|
declare const IS_DEV_BUILD: boolean; // The value is supplied by Webpack during the build
|
||||||
|
|
||||||
|
export function configure(aurelia: Aurelia) {
|
||||||
|
aurelia.use.standardConfiguration();
|
||||||
|
|
||||||
|
if (IS_DEV_BUILD) {
|
||||||
|
aurelia.use.developmentLogging();
|
||||||
|
}
|
||||||
|
|
||||||
|
new HttpClient().configure(config => {
|
||||||
|
const baseUrl = document.getElementsByTagName('base')[0].href;
|
||||||
|
config.withBaseUrl(baseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
aurelia.start().then(() => aurelia.setRoot(PLATFORM.moduleName('app/components/app/app')));
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace AureliaSpa.Controllers
|
||||||
|
{
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Error()
|
||||||
|
{
|
||||||
|
ViewData["RequestId"] = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace AureliaSpa.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)(this.TemperatureC / 0.5556);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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 AureliaSpa
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
BuildWebHost(args).Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IWebHost BuildWebHost(string[] args) =>
|
||||||
|
WebHost.CreateDefaultBuilder(args)
|
||||||
|
.UseStartup<Startup>()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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 AureliaSpa
|
||||||
|
{
|
||||||
|
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" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Home Page";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div aurelia-app="boot">Loading...</div>
|
||||||
|
|
||||||
|
@section scripts {
|
||||||
|
<script type="text/javascript" src="~/dist/vendor.js" asp-append-version="true"></script>
|
||||||
|
<script type="text/javascript" src="~/dist/app.js" asp-append-version="true"></script>
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@{
|
||||||
|
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>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@ViewData["Title"] - AureliaSpa</title>
|
||||||
|
<base href="~/" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="~/dist/vendor.css" asp-append-version="true" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
@RenderBody()
|
||||||
|
|
||||||
|
@RenderSection("scripts", required: false)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@using AureliaSpa
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.SpaServices
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"IncludeScopes": false,
|
||||||
|
"Debug": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"IncludeScopes": false,
|
||||||
|
"Debug": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Console": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2615
templates/Microsoft.AspNetCore.SpaTemplates/aurelia/npm-shrinkwrap.json
generated
Normal file
2615
templates/Microsoft.AspNetCore.SpaTemplates/aurelia/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "AureliaSpa",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/webpack-env": "^1.13.0",
|
||||||
|
"aspnet-webpack": "^2.0.1",
|
||||||
|
"aurelia-bootstrapper": "^2.0.1",
|
||||||
|
"aurelia-fetch-client": "^1.0.1",
|
||||||
|
"aurelia-framework": "^1.1.0",
|
||||||
|
"aurelia-loader-webpack": "^2.0.0",
|
||||||
|
"aurelia-pal": "^1.3.0",
|
||||||
|
"aurelia-router": "^1.2.1",
|
||||||
|
"aurelia-webpack-plugin": "^2.0.0-rc.2",
|
||||||
|
"bootstrap": "^3.3.7",
|
||||||
|
"css-loader": "^0.28.0",
|
||||||
|
"extract-text-webpack-plugin": "^2.1.0",
|
||||||
|
"file-loader": "^0.11.1",
|
||||||
|
"html-loader": "^0.4.5",
|
||||||
|
"isomorphic-fetch": "^2.2.1",
|
||||||
|
"jquery": "^3.2.1",
|
||||||
|
"json-loader": "^0.5.4",
|
||||||
|
"style-loader": "^0.16.1",
|
||||||
|
"ts-loader": "^2.0.3",
|
||||||
|
"typescript": "^2.2.2",
|
||||||
|
"url-loader": "^0.5.8",
|
||||||
|
"webpack": "^2.3.3",
|
||||||
|
"webpack-hot-middleware": "^2.18.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "es2015",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"target": "es5",
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"lib": [ "es2015", "dom" ],
|
||||||
|
"types": [ "webpack-env" ]
|
||||||
|
},
|
||||||
|
"exclude": [ "bin", "node_modules" ],
|
||||||
|
"atom": { "rewriteTsconfig": false }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const { AureliaPlugin } = require('aurelia-webpack-plugin');
|
||||||
|
const bundleOutputDir = './wwwroot/dist';
|
||||||
|
|
||||||
|
module.exports = (env) => {
|
||||||
|
const isDevBuild = !(env && env.prod);
|
||||||
|
return [{
|
||||||
|
stats: { modules: false },
|
||||||
|
entry: { 'app': 'aurelia-bootstrapper' },
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
modules: ['ClientApp', 'node_modules'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve(bundleOutputDir),
|
||||||
|
publicPath: 'dist/',
|
||||||
|
filename: '[name].js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{ test: /\.ts$/i, include: /ClientApp/, use: 'ts-loader?silent=true' },
|
||||||
|
{ test: /\.html$/i, use: 'html-loader' },
|
||||||
|
{ test: /\.css$/i, use: isDevBuild ? 'css-loader' : 'css-loader?minimize' },
|
||||||
|
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({ IS_DEV_BUILD: JSON.stringify(isDevBuild) }),
|
||||||
|
new webpack.DllReferencePlugin({
|
||||||
|
context: __dirname,
|
||||||
|
manifest: require('./wwwroot/dist/vendor-manifest.json')
|
||||||
|
}),
|
||||||
|
new AureliaPlugin({ aureliaApp: 'boot' })
|
||||||
|
].concat(isDevBuild ? [
|
||||||
|
new webpack.SourceMapDevToolPlugin({
|
||||||
|
filename: '[file].map', // Remove this line if you prefer inline source maps
|
||||||
|
moduleFilenameTemplate: path.relative(bundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
|
||||||
|
})
|
||||||
|
] : [
|
||||||
|
new webpack.optimize.UglifyJsPlugin()
|
||||||
|
])
|
||||||
|
}];
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
var path = require('path');
|
||||||
|
var webpack = require('webpack');
|
||||||
|
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
|
var extractCSS = new ExtractTextPlugin('vendor.css');
|
||||||
|
|
||||||
|
module.exports = ({ prod } = {}) => {
|
||||||
|
const isDevBuild = !prod;
|
||||||
|
|
||||||
|
return [{
|
||||||
|
stats: { modules: false },
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [
|
||||||
|
{ test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, loader: 'url-loader?limit=100000' },
|
||||||
|
{ test: /\.css(\?|$)/, loader: extractCSS.extract([isDevBuild ? 'css-loader' : 'css-loader?minimize']) }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
entry: {
|
||||||
|
vendor: [
|
||||||
|
'aurelia-event-aggregator',
|
||||||
|
'aurelia-fetch-client',
|
||||||
|
'aurelia-framework',
|
||||||
|
'aurelia-history-browser',
|
||||||
|
'aurelia-logging-console',
|
||||||
|
'aurelia-pal-browser',
|
||||||
|
'aurelia-polyfills',
|
||||||
|
'aurelia-route-recognizer',
|
||||||
|
'aurelia-router',
|
||||||
|
'aurelia-templating-binding',
|
||||||
|
'aurelia-templating-resources',
|
||||||
|
'aurelia-templating-router',
|
||||||
|
'bootstrap',
|
||||||
|
'bootstrap/dist/css/bootstrap.css',
|
||||||
|
'jquery'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'wwwroot', 'dist'),
|
||||||
|
publicPath: 'dist/',
|
||||||
|
filename: '[name].js',
|
||||||
|
library: '[name]_[hash]',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
extractCSS,
|
||||||
|
new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable)
|
||||||
|
new webpack.DllPlugin({
|
||||||
|
path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'),
|
||||||
|
name: '[name]_[hash]'
|
||||||
|
})
|
||||||
|
].concat(isDevBuild ? [] : [
|
||||||
|
new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })
|
||||||
|
])
|
||||||
|
}]
|
||||||
|
};
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
236
templates/Microsoft.AspNetCore.SpaTemplates/knockout/.gitignore
vendored
Normal file
236
templates/Microsoft.AspNetCore.SpaTemplates/knockout/.gitignore
vendored
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/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/
|
||||||
|
|
||||||
|
/yarn.lock
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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/
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/dotnetcli.host",
|
||||||
|
"symbolInfo": {
|
||||||
|
"TargetFrameworkOverride": {
|
||||||
|
"isHidden": "true",
|
||||||
|
"longName": "target-framework-override",
|
||||||
|
"shortName": ""
|
||||||
|
},
|
||||||
|
"Framework": {
|
||||||
|
"longName": "framework"
|
||||||
|
},
|
||||||
|
"skipRestore": {
|
||||||
|
"longName": "no-restore",
|
||||||
|
"shortName": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"author": "Microsoft",
|
||||||
|
"classifications": [
|
||||||
|
"Web",
|
||||||
|
"MVC",
|
||||||
|
"SPA"
|
||||||
|
],
|
||||||
|
"groupIdentity": "Microsoft.AspNetCore.SpaTemplates.Knockout",
|
||||||
|
"identity": "Microsoft.AspNetCore.SpaTemplates.Knockout.CSharp",
|
||||||
|
"name": "ASP.NET Core with Knockout.js",
|
||||||
|
"preferNameDirectory": true,
|
||||||
|
"primaryOutputs": [
|
||||||
|
{
|
||||||
|
"path": "KnockoutSpa.csproj"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortName": "knockout",
|
||||||
|
"sourceName": "KnockoutSpa",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"source": "./",
|
||||||
|
"target": "./",
|
||||||
|
"exclude": [
|
||||||
|
".template.config/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"symbols": {
|
||||||
|
"TargetFrameworkOverride": {
|
||||||
|
"type": "parameter",
|
||||||
|
"description": "Overrides the target framework",
|
||||||
|
"replaces": "TargetFrameworkOverride",
|
||||||
|
"datatype": "string",
|
||||||
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
"Framework": {
|
||||||
|
"type": "parameter",
|
||||||
|
"description": "The target framework for the project.",
|
||||||
|
"datatype": "choice",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"choice": "netcoreapp2.0",
|
||||||
|
"description": "Target netcoreapp2.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"replaces": "netcoreapp2.0",
|
||||||
|
"defaultValue": "netcoreapp2.0"
|
||||||
|
},
|
||||||
|
"HostIdentifier": {
|
||||||
|
"type": "bind",
|
||||||
|
"binding": "HostIdentifier"
|
||||||
|
},
|
||||||
|
"skipRestore": {
|
||||||
|
"type": "parameter",
|
||||||
|
"datatype": "bool",
|
||||||
|
"description": "If specified, skips the automatic restore of the project on create.",
|
||||||
|
"defaultValue": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"language": "C#",
|
||||||
|
"type": "project"
|
||||||
|
},
|
||||||
|
"postActions": [
|
||||||
|
{
|
||||||
|
"condition": "(!skipRestore)",
|
||||||
|
"description": "Restore NuGet packages required by this project.",
|
||||||
|
"manualInstructions": [
|
||||||
|
{
|
||||||
|
"text": "Run 'dotnet restore'"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025",
|
||||||
|
"continueOnError": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "(HostIdentifier == \"dotnetcli\" || HostIdentifier == \"dotnetcli-preview\")",
|
||||||
|
"actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C",
|
||||||
|
"description": "\n\n-------------------------------------------------------------------\nIMPORTANT: Before running this project on the command line,\n you must restore NPM packages by running \"npm install\"\n-------------------------------------------------------------------\n",
|
||||||
|
"manualInstructions": [
|
||||||
|
{
|
||||||
|
"text": "Run \"npm install\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user