mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-23 18:19:40 +00:00
Compare commits
136 Commits
angular4-p
...
rel/2.0.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
287c10fd2e | ||
|
|
169ef12cd8 | ||
|
|
f5d58f3f2e | ||
|
|
c2e4d4f261 | ||
|
|
50481fe23f | ||
|
|
789ea5a320 | ||
|
|
c79db4e8e2 | ||
|
|
9528dd7432 | ||
|
|
b8c006a3e9 | ||
|
|
8b37dc8561 | ||
|
|
7b07fb66eb | ||
|
|
cdb04c74f9 | ||
|
|
a74941e3c8 | ||
|
|
576e3debf1 | ||
|
|
dab0faea66 | ||
|
|
e65ecebac6 | ||
|
|
bb0727c34c | ||
|
|
4903e12373 | ||
|
|
56c806b34e | ||
|
|
8acba88160 | ||
|
|
44f86eb8a1 | ||
|
|
22ff88ab79 | ||
|
|
70d89b9ad3 | ||
|
|
c94563c700 | ||
|
|
814df07034 | ||
|
|
4492d3eab7 | ||
|
|
efdfbf5dbb | ||
|
|
6f2e408083 | ||
|
|
70a4a68e7c | ||
|
|
a1942edbfe | ||
|
|
fd90301b1c | ||
|
|
58145d33e3 | ||
|
|
2135c30d66 | ||
|
|
5191531813 | ||
|
|
03018f32b7 | ||
|
|
e632d2bb69 | ||
|
|
7a56f643f7 | ||
|
|
056b955f52 | ||
|
|
0993f46c9e | ||
|
|
4ba0358987 | ||
|
|
721e3c45fd | ||
|
|
27f59c6971 | ||
|
|
943fb2f482 | ||
|
|
2c4bc302de | ||
|
|
5b634ee33c | ||
|
|
6e1fa2b2c3 | ||
|
|
f72ebbd66e | ||
|
|
391ceec559 | ||
|
|
076b5304e0 | ||
|
|
94fc84a9b4 | ||
|
|
9cce26ebd8 | ||
|
|
513d288a5d | ||
|
|
429a432dd3 | ||
|
|
76928bcbaa | ||
|
|
95c81729cd | ||
|
|
8352ec7626 | ||
|
|
bebb82eb69 | ||
|
|
3da87dd3d3 | ||
|
|
117c1a6cbd | ||
|
|
53f5a77490 | ||
|
|
5fd9b93324 | ||
|
|
55bd0c05cb | ||
|
|
f9068819be | ||
|
|
85bf621386 | ||
|
|
328eb0451b | ||
|
|
bc7742148e | ||
|
|
4ceed817a3 | ||
|
|
6d85e752e1 | ||
|
|
460f202832 | ||
|
|
73054559cc | ||
|
|
d007a3fbba | ||
|
|
fcd3cf9dd7 | ||
|
|
48bca777fe | ||
|
|
aadcfa1fb7 | ||
|
|
f683b39806 | ||
|
|
c128fa6e06 | ||
|
|
ed7e07a057 | ||
|
|
749d112b08 | ||
|
|
3ef23ece23 | ||
|
|
90922fb3c6 | ||
|
|
68e84bac3f | ||
|
|
bcb359e355 | ||
|
|
a7a3a359fa | ||
|
|
a38d3bdc4b | ||
|
|
627da65692 | ||
|
|
8e5f255346 | ||
|
|
67ba6271a0 | ||
|
|
a3bcc0d863 | ||
|
|
5e7bb0f5c5 | ||
|
|
8396be24c8 | ||
|
|
728b18431d | ||
|
|
32d9bdee6c | ||
|
|
9a59bccad9 | ||
|
|
e50d4825c3 | ||
|
|
bf6d2227db | ||
|
|
06c94d54eb | ||
|
|
bcaa3af92e | ||
|
|
3685592b01 | ||
|
|
f3b7103c83 | ||
|
|
2acdad38cd | ||
|
|
0dd24068ca | ||
|
|
0526852e05 | ||
|
|
e331355c1d | ||
|
|
60c8a3471d | ||
|
|
c7b7f40d2c | ||
|
|
d8c0bd8898 | ||
|
|
5127a74d3f | ||
|
|
e38399fbf4 | ||
|
|
045c05fc88 | ||
|
|
b444831c8d | ||
|
|
2aaceaa9f8 | ||
|
|
1c5bd98d8a | ||
|
|
0626bb1594 | ||
|
|
d9e75c89ae | ||
|
|
ebe9d8ae92 | ||
|
|
dbdc81a06a | ||
|
|
d42cd10975 | ||
|
|
8a1d64cc73 | ||
|
|
087183ea18 | ||
|
|
9b131ea7d3 | ||
|
|
75fd215b1a | ||
|
|
38997c3f3f | ||
|
|
bee4c6ff11 | ||
|
|
cc859306a3 | ||
|
|
44512226c6 | ||
|
|
a2bde750be | ||
|
|
c592282646 | ||
|
|
b86e9f9254 | ||
|
|
970dc6c457 | ||
|
|
6baa2faf09 | ||
|
|
918e7edbff | ||
|
|
e1c90b3601 | ||
|
|
b9e62cd4c9 | ||
|
|
ad9cfbb176 | ||
|
|
941ae9a34d | ||
|
|
3645ae6cf2 |
@@ -1,10 +1,12 @@
|
|||||||
Copyright (c) .NET Foundation. All rights reserved.
|
Copyright (c) .NET Foundation and Contributors
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||||
these files except in compliance with the License. You may obtain a copy of the
|
this file except in compliance with the License. You may obtain a copy of the
|
||||||
License at
|
License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software distributed
|
Unless required by applicable law or agreed to in writing, software distributed
|
||||||
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
|
<clear />
|
||||||
|
<add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-release/api/v3/index.json" />
|
||||||
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
|
<add key="AspNetCoreTools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
|
||||||
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
||||||
</packageSources>
|
</packageSources>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ If you're interested in contributing to the various packages, samples, and proje
|
|||||||
* Run `dotnet restore` at the repo root dir
|
* Run `dotnet restore` at the repo root dir
|
||||||
* Go to whatever sample or template you want to run (for example, `cd templates/AngularSpa`)
|
* Go to whatever sample or template you want to run (for example, `cd templates/AngularSpa`)
|
||||||
* Restore NPM dependencies (run `npm install`)
|
* Restore NPM dependencies (run `npm install`)
|
||||||
* If the sample/template you're trying to run has a file called `webpack.config.vendor.js` at its root, run `webpack --config webpack.config.vendor.js`. It it has a file called `webpack.config.js`, run `webpack` (no args). You might need to install webpack first, by running `npm install -g webpack`.
|
* 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`)
|
* 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.
|
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.
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ init:
|
|||||||
install:
|
install:
|
||||||
- ps: Install-Product node 6.9.2 x64
|
- ps: Install-Product node 6.9.2 x64
|
||||||
# .NET Core SDK binaries
|
# .NET Core SDK binaries
|
||||||
# Download .NET Core 2.0 Preview 1 SDK and add to PATH
|
# Download .NET Core 2.0 Preview 3 SDK and add to PATH
|
||||||
- ps: $urlCurrent = "https://download.microsoft.com/download/0/6/5/0656B047-5F2F-4281-A851-F30776F8616D/dotnet-dev-win-x64.2.0.0-preview1-005977.zip"
|
- ps: $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: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk"
|
||||||
- ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null
|
- ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null
|
||||||
- ps: $tempFileCurrent = [System.IO.Path]::GetTempFileName()
|
- ps: $tempFileCurrent = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName())
|
||||||
- ps: (New-Object System.Net.WebClient).DownloadFile($urlCurrent, $tempFileCurrent)
|
- 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: 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"
|
- ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path"
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ cd $PSScriptRoot
|
|||||||
$repoFolder = $PSScriptRoot
|
$repoFolder = $PSScriptRoot
|
||||||
$env:REPO_FOLDER = $repoFolder
|
$env:REPO_FOLDER = $repoFolder
|
||||||
|
|
||||||
$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
|
$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0.zip"
|
||||||
if ($env:KOREBUILD_ZIP)
|
if ($env:KOREBUILD_ZIP)
|
||||||
{
|
{
|
||||||
$koreBuildZip=$env:KOREBUILD_ZIP
|
$koreBuildZip=$env:KOREBUILD_ZIP
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -2,7 +2,7 @@
|
|||||||
repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
cd $repoFolder
|
cd $repoFolder
|
||||||
|
|
||||||
koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
|
koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0.zip"
|
||||||
if [ ! -z $KOREBUILD_ZIP ]; then
|
if [ ! -z $KOREBUILD_ZIP ]; then
|
||||||
koreBuildZip=$KOREBUILD_ZIP
|
koreBuildZip=$KOREBUILD_ZIP
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
<SignAssembly>true</SignAssembly>
|
<SignAssembly>true</SignAssembly>
|
||||||
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||||
<VersionSuffix Condition="'$(VersionSuffix)'!='' AND '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
|
<VersionSuffix Condition="'$(VersionSuffix)'!='' AND '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Internal.AspNetCore.Sdk" Version="$(InternalAspNetCoreSdkVersion)" PrivateAssets="All" />
|
<PackageReference Include="Internal.AspNetCore.Sdk" Version="$(InternalAspNetCoreSdkVersion)" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETFramework' AND '$(OutputType)'=='library'">
|
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETFramework'">
|
||||||
<PackageReference Include="NETStandard.Library" Version="$(BundledNETStandardPackageVersion)" />
|
<PackageReference Include="NETStandard.Library" Version="$(NETStandardImplicitPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AspNetCoreVersion>2.0.0-*</AspNetCoreVersion>
|
<AspNetCoreVersion>2.0.0-*</AspNetCoreVersion>
|
||||||
<AutoMapperVersion>5.0.2</AutoMapperVersion>
|
<AutoMapperVersion>5.0.2</AutoMapperVersion>
|
||||||
<CoreFxVersion>4.3.0</CoreFxVersion>
|
<InternalAspNetCoreSdkVersion>2.0.1-*</InternalAspNetCoreSdkVersion>
|
||||||
<InternalAspNetCoreSdkVersion>2.1.0-*</InternalAspNetCoreSdkVersion>
|
|
||||||
<JsonNetVersion>10.0.1</JsonNetVersion>
|
<JsonNetVersion>10.0.1</JsonNetVersion>
|
||||||
<NETStandardImplicitPackageVersion>$(BundledNETStandardPackageVersion)</NETStandardImplicitPackageVersion>
|
<NETStandardImplicitPackageVersion>2.0.0-*</NETStandardImplicitPackageVersion>
|
||||||
<ThreadingDataflowVersion>4.7.0</ThreadingDataflowVersion>
|
<NETStandardLibraryNETFrameworkVersion>2.0.0-*</NETStandardLibraryNETFrameworkVersion>
|
||||||
|
<RuntimeFrameworkVersion Condition="'$(TargetFramework)'=='netcoreapp2.0'">2.0.0-*</RuntimeFrameworkVersion>
|
||||||
|
<ThreadingDataflowVersion>4.8.0-*</ThreadingDataflowVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<Target Name="NpmRestore" AfterTargets="Restore" Condition="'$(PreflightRestore)' != 'true'">
|
|
||||||
<ItemGroup>
|
|
||||||
<NpmModules Include="$(RepositoryRoot)**\package.json"
|
|
||||||
Exclude="$(RepositoryRoot)**\node_modules\**\*" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Message Text="Restoring NPM modules for: %0A - @(NpmModules -> '%(FullPath)','%0A - ')" Importance="high" />
|
|
||||||
|
|
||||||
<Exec Command="npm install" WorkingDirectory="%(NpmModules.RootDir)%(Directory)" />
|
|
||||||
</Target>
|
|
||||||
</Project>
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\dependencies.props" />
|
<Import Project="..\..\..\build\common.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<OutputType>exe</OutputType>
|
<OutputType>exe</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.NodeServices;
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
|
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||||
|
|
||||||
namespace NodeServicesExamples.Controllers
|
namespace NodeServicesExamples.Controllers
|
||||||
{
|
{
|
||||||
@@ -34,6 +35,20 @@ namespace NodeServicesExamples.Controllers
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Prerendering([FromServices] ISpaPrerenderer prerenderer)
|
||||||
|
{
|
||||||
|
var result = await prerenderer.RenderToString("./Node/prerenderPage");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(result.RedirectUrl))
|
||||||
|
{
|
||||||
|
return Redirect(result.RedirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewData["PrerenderedHtml"] = result.Html;
|
||||||
|
ViewData["PrerenderedGlobals"] = result.CreateGlobalsAssignmentScript();
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
public IActionResult Error()
|
public IActionResult Error()
|
||||||
{
|
{
|
||||||
return View("~/Views/Shared/Error.cshtml");
|
return View("~/Views/Shared/Error.cshtml");
|
||||||
|
|||||||
14
samples/misc/NodeServicesExamples/Node/prerenderPage.js
Normal file
14
samples/misc/NodeServicesExamples/Node/prerenderPage.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
var createServerRenderer = require('aspnet-prerendering').createServerRenderer;
|
||||||
|
|
||||||
|
module.exports = createServerRenderer(function(params) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var message = 'The HTML was returned by the prerendering boot function. '
|
||||||
|
+ 'The boot function received the following params:'
|
||||||
|
+ '<pre>' + JSON.stringify(params, null, 4) + '</pre>';
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
html: '<h3>Hello, world!</h3>' + message,
|
||||||
|
globals: { sampleData: { nodeVersion: process.version } }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\dependencies.props" />
|
<Import Project="..\..\..\build\common.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.SpaServices\Microsoft.AspNetCore.SpaServices.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ namespace NodeServicesExamples
|
|||||||
|
|
||||||
// Enable Node Services
|
// Enable Node Services
|
||||||
services.AddNodeServices();
|
services.AddNodeServices();
|
||||||
|
services.AddSpaPrerenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -9,4 +9,5 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a asp-action="ES2015Transpilation">ES2015 transpilation</a></li>
|
<li><a asp-action="ES2015Transpilation">ES2015 transpilation</a></li>
|
||||||
<li><a asp-action="Chart">Server-side chart rendering</a></li>
|
<li><a asp-action="Chart">Server-side chart rendering</a></li>
|
||||||
|
<li><a asp-action="Prerendering">Server-side SPA prerendering</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<h1>Server-side prerendering</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This sample demonstrates how you can invoke a JavaScript module that contains
|
||||||
|
prerendering logic for a Single-Page Application framework.
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
Your prerendering boot function will receive parameters that describe the page
|
||||||
|
being rendered and any data supplied by the .NET code. The return value should be
|
||||||
|
a promise that resolves with data to be injected into the page, such as the
|
||||||
|
rendered HTML and any global data that should be made available to client-side code.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@Html.Raw(ViewData["PrerenderedHtml"])
|
||||||
|
|
||||||
|
<script>@Html.Raw(ViewData["PrerenderedGlobals"])</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Demonstrates how client-side code can receive data from the prerendering process
|
||||||
|
console.log('Received Node version from prerendering logic: ' + sampleData.nodeVersion);
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "nodeservicesexamples",
|
"name": "nodeservicesexamples",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"aspnet-prerendering": "^2.0.6",
|
||||||
"babel-core": "^6.7.4",
|
"babel-core": "^6.7.4",
|
||||||
"babel-preset-es2015": "^6.6.0",
|
"babel-preset-es2015": "^6.6.0",
|
||||||
"node-chartist": "^1.0.2"
|
"node-chartist": "^1.0.2"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ namespace Webpack.ActionResults
|
|||||||
{
|
{
|
||||||
var nodeServices = context.HttpContext.RequestServices.GetRequiredService<INodeServices>();
|
var nodeServices = context.HttpContext.RequestServices.GetRequiredService<INodeServices>();
|
||||||
var hostEnv = context.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
|
var hostEnv = context.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
|
||||||
|
var applicationLifetime = context.HttpContext.RequestServices.GetRequiredService<IApplicationLifetime>();
|
||||||
var applicationBasePath = hostEnv.ContentRootPath;
|
var applicationBasePath = hostEnv.ContentRootPath;
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
@@ -34,6 +35,7 @@ namespace Webpack.ActionResults
|
|||||||
var prerenderedHtml = await Prerenderer.RenderToString(
|
var prerenderedHtml = await Prerenderer.RenderToString(
|
||||||
applicationBasePath,
|
applicationBasePath,
|
||||||
nodeServices,
|
nodeServices,
|
||||||
|
applicationLifetime.ApplicationStopping,
|
||||||
_moduleExport,
|
_moduleExport,
|
||||||
request.GetEncodedUrl(),
|
request.GetEncodedUrl(),
|
||||||
request.Path + request.QueryString.Value,
|
request.Path + request.QueryString.Value,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<Import Project="..\..\..\build\dependencies.props" />
|
<Import Project="..\..\..\build\common.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -4,25 +4,18 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Description>Socket-based RPC for Microsoft.AspNetCore.NodeServices.</Description>
|
<Description>Socket-based RPC for Microsoft.AspNetCore.NodeServices.</Description>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="node_modules\**\*" />
|
||||||
<EmbeddedResource Include="Content\**\*" />
|
<EmbeddedResource Include="Content\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
|
|
||||||
<PackageReference Include="Microsoft.Tpl.Dataflow" Version="$(MicrosoftDataflowVersion)" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
|
|
||||||
<PackageReference Include="System.IO.Pipes" Version="$(CoreFxVersion)" />
|
|
||||||
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="$(ThreadingDataflowVersion)" />
|
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="$(ThreadingDataflowVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
|||||||
PipeDirection.InOut,
|
PipeDirection.InOut,
|
||||||
PipeOptions.Asynchronous);
|
PipeOptions.Asynchronous);
|
||||||
|
|
||||||
#if NET451
|
|
||||||
_namedPipeClientStream.Connect();
|
|
||||||
#else
|
|
||||||
await _namedPipeClientStream.ConnectAsync().ConfigureAwait(false);
|
await _namedPipeClientStream.ConnectAsync().ConfigureAwait(false);
|
||||||
#endif
|
|
||||||
|
|
||||||
return _namedPipeClientStream;
|
return _namedPipeClientStream;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
|||||||
|
|
||||||
public static StreamConnection Create()
|
public static StreamConnection Create()
|
||||||
{
|
{
|
||||||
#if NET451
|
|
||||||
return new NamedPipeConnection();
|
|
||||||
#else
|
|
||||||
var useNamedPipes = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
|
var useNamedPipes = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
|
||||||
System.Runtime.InteropServices.OSPlatform.Windows);
|
System.Runtime.InteropServices.OSPlatform.Windows);
|
||||||
if (useNamedPipes)
|
if (useNamedPipes)
|
||||||
@@ -24,7 +21,6 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
|||||||
{
|
{
|
||||||
return new UnixDomainSocketConnection();
|
return new UnixDomainSocketConnection();
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,16 +10,6 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
|||||||
private NetworkStream _networkStream;
|
private NetworkStream _networkStream;
|
||||||
private Socket _socket;
|
private Socket _socket;
|
||||||
|
|
||||||
#if NET451
|
|
||||||
public override Task<Stream> Open(string address)
|
|
||||||
{
|
|
||||||
// The 'null' assignments avoid the compiler warnings about unassigned fields.
|
|
||||||
// Note that this whole class isn't supported on .NET 4.5.1, since that's not cross-platform.
|
|
||||||
_networkStream = null;
|
|
||||||
_socket = null;
|
|
||||||
throw new System.PlatformNotSupportedException();
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
public override async Task<Stream> Open(string address)
|
public override async Task<Stream> Open(string address)
|
||||||
{
|
{
|
||||||
var endPoint = new UnixDomainSocketEndPoint("/tmp/" + address);
|
var endPoint = new UnixDomainSocketEndPoint("/tmp/" + address);
|
||||||
@@ -28,7 +18,6 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
|||||||
_networkStream = new NetworkStream(_socket);
|
_networkStream = new NetworkStream(_socket);
|
||||||
return _networkStream;
|
return _networkStream;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,11 +59,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.PhysicalConnections
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
#if NET451
|
|
||||||
_encodedPath = new byte[0];
|
|
||||||
#else
|
|
||||||
_encodedPath = Array.Empty<byte>();
|
_encodedPath = Array.Empty<byte>();
|
||||||
#endif
|
|
||||||
_path = string.Empty;
|
_path = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets
|
|||||||
options.ProjectPath,
|
options.ProjectPath,
|
||||||
options.WatchFileExtensions,
|
options.WatchFileExtensions,
|
||||||
MakeNewCommandLineOptions(socketAddress),
|
MakeNewCommandLineOptions(socketAddress),
|
||||||
|
options.ApplicationStoppingToken,
|
||||||
options.NodeInstanceOutputLogger,
|
options.NodeInstanceOutputLogger,
|
||||||
options.EnvironmentVariables,
|
options.EnvironmentVariables,
|
||||||
options.InvocationTimeoutMilliseconds,
|
options.InvocationTimeoutMilliseconds,
|
||||||
|
|||||||
@@ -12,11 +12,7 @@ namespace Microsoft.AspNetCore.NodeServices.Sockets.VirtualConnections
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class VirtualConnection : Stream
|
internal class VirtualConnection : Stream
|
||||||
{
|
{
|
||||||
#if NET451
|
|
||||||
private readonly static Task CompletedTask = Task.FromResult((object)null);
|
|
||||||
#else
|
|
||||||
private readonly static Task CompletedTask = Task.CompletedTask;
|
private readonly static Task CompletedTask = Task.CompletedTask;
|
||||||
#endif
|
|
||||||
private VirtualConnectionClient _host;
|
private VirtualConnectionClient _host;
|
||||||
private readonly BufferBlock<byte[]> _receivedDataQueue = new BufferBlock<byte[]>();
|
private readonly BufferBlock<byte[]> _receivedDataQueue = new BufferBlock<byte[]>();
|
||||||
private ArraySegment<byte> _receivedDataNotYetUsed;
|
private ArraySegment<byte> _receivedDataNotYetUsed;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.NodeServices.HostingModels;
|
using Microsoft.AspNetCore.NodeServices.HostingModels;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -42,6 +43,12 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node
|
EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var applicationLifetime = serviceProvider.GetService<IApplicationLifetime>();
|
||||||
|
if (applicationLifetime != null)
|
||||||
|
{
|
||||||
|
ApplicationStoppingToken = applicationLifetime.ApplicationStopping;
|
||||||
|
}
|
||||||
|
|
||||||
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
|
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
|
||||||
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
|
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
|
||||||
NodeInstanceOutputLogger = loggerFactory != null
|
NodeInstanceOutputLogger = loggerFactory != null
|
||||||
@@ -93,5 +100,10 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
/// Specifies the maximum duration, in milliseconds, that your .NET code should wait for Node.js RPC calls to return.
|
/// Specifies the maximum duration, in milliseconds, that your .NET code should wait for Node.js RPC calls to return.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int InvocationTimeoutMilliseconds { get; set; }
|
public int InvocationTimeoutMilliseconds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A token that indicates when the host application is stopping.
|
||||||
|
/// </summary>
|
||||||
|
public CancellationToken ApplicationStoppingToken { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,10 +55,11 @@
|
|||||||
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
||||||
// but simplifies things for the consumer of this module.
|
// but simplifies things for the consumer of this module.
|
||||||
__webpack_require__(2);
|
__webpack_require__(2);
|
||||||
var http = __webpack_require__(3);
|
__webpack_require__(4);
|
||||||
var path = __webpack_require__(4);
|
var http = __webpack_require__(5);
|
||||||
var ArgsUtil_1 = __webpack_require__(5);
|
var path = __webpack_require__(3);
|
||||||
var ExitWhenParentExits_1 = __webpack_require__(6);
|
var ArgsUtil_1 = __webpack_require__(6);
|
||||||
|
var ExitWhenParentExits_1 = __webpack_require__(7);
|
||||||
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
|
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
|
||||||
// reference to Node's runtime 'require' function.
|
// reference to Node's runtime 'require' function.
|
||||||
var dynamicRequire = eval('require');
|
var dynamicRequire = eval('require');
|
||||||
@@ -133,12 +134,73 @@
|
|||||||
}
|
}
|
||||||
function respondWithError(res, errorValue) {
|
function respondWithError(res, errorValue) {
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
res.end(errorValue.stack || errorValue.toString());
|
res.end(JSON.stringify({
|
||||||
|
errorMessage: errorValue.message || errorValue,
|
||||||
|
errorDetails: errorValue.stack || null
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/***/ },
|
/***/ },
|
||||||
/* 2 */
|
/* 2 */
|
||||||
|
/***/ function(module, exports, __webpack_require__) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
var path = __webpack_require__(3);
|
||||||
|
var startsWith = function (str, prefix) { return str.substring(0, prefix.length) === prefix; };
|
||||||
|
var appRootDir = process.cwd();
|
||||||
|
function patchedLStat(pathToStatLong, fsReqWrap) {
|
||||||
|
try {
|
||||||
|
// If the lstat completes without errors, we don't modify its behavior at all
|
||||||
|
return origLStat.apply(this, arguments);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
var shouldOverrideError = startsWith(ex.message, 'EPERM') // It's a permissions error
|
||||||
|
&& typeof appRootDirLong === 'string'
|
||||||
|
&& startsWith(appRootDirLong, pathToStatLong) // ... for an ancestor directory
|
||||||
|
&& ex.stack.indexOf('Object.realpathSync ') >= 0; // ... during symlink resolution
|
||||||
|
if (shouldOverrideError) {
|
||||||
|
// Fake the result to give the same result as an 'lstat' on the app root dir.
|
||||||
|
// This stops Node failing to load modules just because it doesn't know whether
|
||||||
|
// ancestor directories are symlinks or not. If there's a genuine file
|
||||||
|
// permissions issue, it will still surface later when Node actually
|
||||||
|
// tries to read the file.
|
||||||
|
return origLStat.call(this, appRootDir, fsReqWrap);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// In any other case, preserve the original error
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
;
|
||||||
|
// It's only necessary to apply this workaround on Windows
|
||||||
|
var appRootDirLong = null;
|
||||||
|
var origLStat = null;
|
||||||
|
if (/^win/.test(process.platform)) {
|
||||||
|
try {
|
||||||
|
// Get the app's root dir in Node's internal "long" format (e.g., \\?\C:\dir\subdir)
|
||||||
|
appRootDirLong = path._makeLong(appRootDir);
|
||||||
|
// Actually apply the patch, being as defensive as possible
|
||||||
|
var bindingFs = process.binding('fs');
|
||||||
|
origLStat = bindingFs.lstat;
|
||||||
|
if (typeof origLStat === 'function') {
|
||||||
|
bindingFs.lstat = patchedLStat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***/ },
|
||||||
|
/* 3 */
|
||||||
|
/***/ function(module, exports) {
|
||||||
|
|
||||||
|
module.exports = require("path");
|
||||||
|
|
||||||
|
/***/ },
|
||||||
|
/* 4 */
|
||||||
/***/ function(module, exports) {
|
/***/ function(module, exports) {
|
||||||
|
|
||||||
// When Node writes to stdout/strerr, we capture that and convert the lines into calls on the
|
// When Node writes to stdout/strerr, we capture that and convert the lines into calls on the
|
||||||
@@ -179,19 +241,13 @@
|
|||||||
|
|
||||||
|
|
||||||
/***/ },
|
/***/ },
|
||||||
/* 3 */
|
/* 5 */
|
||||||
/***/ function(module, exports) {
|
/***/ function(module, exports) {
|
||||||
|
|
||||||
module.exports = require("http");
|
module.exports = require("http");
|
||||||
|
|
||||||
/***/ },
|
/***/ },
|
||||||
/* 4 */
|
/* 6 */
|
||||||
/***/ function(module, exports) {
|
|
||||||
|
|
||||||
module.exports = require("path");
|
|
||||||
|
|
||||||
/***/ },
|
|
||||||
/* 5 */
|
|
||||||
/***/ function(module, exports) {
|
/***/ function(module, exports) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -217,7 +273,7 @@
|
|||||||
|
|
||||||
|
|
||||||
/***/ },
|
/***/ },
|
||||||
/* 6 */
|
/* 7 */
|
||||||
/***/ function(module, exports) {
|
/***/ function(module, exports) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
options.ProjectPath,
|
options.ProjectPath,
|
||||||
options.WatchFileExtensions,
|
options.WatchFileExtensions,
|
||||||
MakeCommandLineOptions(port),
|
MakeCommandLineOptions(port),
|
||||||
|
options.ApplicationStoppingToken,
|
||||||
options.NodeInstanceOutputLogger,
|
options.NodeInstanceOutputLogger,
|
||||||
options.EnvironmentVariables,
|
options.EnvironmentVariables,
|
||||||
options.InvocationTimeoutMilliseconds,
|
options.InvocationTimeoutMilliseconds,
|
||||||
@@ -66,8 +67,10 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// Unfortunately there's no true way to cancel ReadAsStringAsync calls, hence AbandonIfCancelled
|
// Unfortunately there's no true way to cancel ReadAsStringAsync calls, hence AbandonIfCancelled
|
||||||
var responseErrorString = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken);
|
var responseJson = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken);
|
||||||
throw new Exception("Call to Node module failed with error: " + responseErrorString);
|
var responseError = JsonConvert.DeserializeObject<RpcJsonResponse>(responseJson, jsonSerializerSettings);
|
||||||
|
|
||||||
|
throw new NodeInvocationException(responseError.ErrorMessage, responseError.ErrorDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseContentType = response.Content.Headers.ContentType;
|
var responseContentType = response.Content.Headers.ContentType;
|
||||||
@@ -135,5 +138,13 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pragma warning disable 649 // These properties are populated via JSON deserialization
|
||||||
|
private class RpcJsonResponse
|
||||||
|
{
|
||||||
|
public string ErrorMessage { get; set; }
|
||||||
|
public string ErrorDetails { get; set; }
|
||||||
|
}
|
||||||
|
#pragma warning restore 649
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
/// <param name="projectPath">The root path of the current project. This is used when resolving Node.js module paths relative to the project root.</param>
|
/// <param name="projectPath">The root path of the current project. This is used when resolving Node.js module paths relative to the project root.</param>
|
||||||
/// <param name="watchFileExtensions">The filename extensions that should be watched within the project root. The Node instance will automatically shut itself down if any matching file changes.</param>
|
/// <param name="watchFileExtensions">The filename extensions that should be watched within the project root. The Node instance will automatically shut itself down if any matching file changes.</param>
|
||||||
/// <param name="commandLineArguments">Additional command-line arguments to be passed to the Node.js instance.</param>
|
/// <param name="commandLineArguments">Additional command-line arguments to be passed to the Node.js instance.</param>
|
||||||
|
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
|
||||||
/// <param name="nodeOutputLogger">The <see cref="ILogger"/> to which the Node.js instance's stdout/stderr (and other log information) should be written.</param>
|
/// <param name="nodeOutputLogger">The <see cref="ILogger"/> to which the Node.js instance's stdout/stderr (and other log information) should be written.</param>
|
||||||
/// <param name="environmentVars">Environment variables to be set on the Node.js process.</param>
|
/// <param name="environmentVars">Environment variables to be set on the Node.js process.</param>
|
||||||
/// <param name="invocationTimeoutMilliseconds">The maximum duration, in milliseconds, to wait for RPC calls to complete.</param>
|
/// <param name="invocationTimeoutMilliseconds">The maximum duration, in milliseconds, to wait for RPC calls to complete.</param>
|
||||||
@@ -55,6 +56,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
string projectPath,
|
string projectPath,
|
||||||
string[] watchFileExtensions,
|
string[] watchFileExtensions,
|
||||||
string commandLineArguments,
|
string commandLineArguments,
|
||||||
|
CancellationToken applicationStoppingToken,
|
||||||
ILogger nodeOutputLogger,
|
ILogger nodeOutputLogger,
|
||||||
IDictionary<string, string> environmentVars,
|
IDictionary<string, string> environmentVars,
|
||||||
int invocationTimeoutMilliseconds,
|
int invocationTimeoutMilliseconds,
|
||||||
@@ -67,7 +69,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
OutputLogger = nodeOutputLogger;
|
OutputLogger = nodeOutputLogger;
|
||||||
_entryPointScript = new StringAsTempFile(entryPointScript);
|
_entryPointScript = new StringAsTempFile(entryPointScript, applicationStoppingToken);
|
||||||
_invocationTimeoutMilliseconds = invocationTimeoutMilliseconds;
|
_invocationTimeoutMilliseconds = invocationTimeoutMilliseconds;
|
||||||
_launchWithDebugging = launchWithDebugging;
|
_launchWithDebugging = launchWithDebugging;
|
||||||
|
|
||||||
@@ -295,7 +297,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
|
|
||||||
// Make sure the Node process is finished
|
// Make sure the Node process is finished
|
||||||
// TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
|
// TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
|
||||||
if (!_nodeProcess.HasExited)
|
if (_nodeProcess != null && !_nodeProcess.HasExited)
|
||||||
{
|
{
|
||||||
_nodeProcess.Kill();
|
_nodeProcess.Kill();
|
||||||
}
|
}
|
||||||
@@ -315,11 +317,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
|
|
||||||
private static void SetEnvironmentVariable(ProcessStartInfo startInfo, string name, string value)
|
private static void SetEnvironmentVariable(ProcessStartInfo startInfo, string name, string value)
|
||||||
{
|
{
|
||||||
#if NET451
|
|
||||||
startInfo.EnvironmentVariables[name] = value;
|
|
||||||
#else
|
|
||||||
startInfo.Environment[name] = value;
|
startInfo.Environment[name] = value;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
|
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
|
||||||
@@ -383,12 +381,6 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
{
|
{
|
||||||
OutputLogger.LogWarning(evt.Data);
|
OutputLogger.LogWarning(evt.Data);
|
||||||
}
|
}
|
||||||
else if (!initializationIsCompleted)
|
|
||||||
{
|
|
||||||
_connectionIsReadySource.SetException(
|
|
||||||
new InvalidOperationException("The Node.js process failed to initialize: " + evt.Data));
|
|
||||||
initializationIsCompleted = true;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
OnErrorDataReceived(UnencodeNewlines(evt.Data));
|
OnErrorDataReceived(UnencodeNewlines(evt.Data));
|
||||||
@@ -402,9 +394,11 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
|
|||||||
|
|
||||||
private static bool IsDebuggerMessage(string message)
|
private static bool IsDebuggerMessage(string message)
|
||||||
{
|
{
|
||||||
return message.StartsWith("Debugger attached", StringComparison.OrdinalIgnoreCase) ||
|
return message.StartsWith("Debugger attached", StringComparison.Ordinal) ||
|
||||||
message.StartsWith("Debugger listening ", StringComparison.OrdinalIgnoreCase) ||
|
message.StartsWith("Debugger listening ", StringComparison.Ordinal) ||
|
||||||
message.StartsWith("To start debugging", StringComparison.OrdinalIgnoreCase) ||
|
message.StartsWith("To start debugging", StringComparison.Ordinal) ||
|
||||||
|
message.Equals("Warning: This is an experimental feature and could change at any time.", StringComparison.Ordinal) ||
|
||||||
|
message.Equals("For help see https://nodejs.org/en/docs/inspector", StringComparison.Ordinal) ||
|
||||||
message.Contains("chrome-devtools:");
|
message.Contains("chrome-devtools:");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
<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>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="node_modules\**\*" />
|
||||||
<EmbeddedResource Include="Content\**\*" />
|
<EmbeddedResource Include="Content\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -19,12 +21,6 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
|
<PackageReference Include="Newtonsoft.Json" Version="$(JsonNetVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
|
|
||||||
<PackageReference Include="System.Diagnostics.Process" Version="$(CoreFxVersion)" />
|
|
||||||
<PackageReference Include="System.IO.FileSystem.Watcher" Version="$(CoreFxVersion)" />
|
|
||||||
<PackageReference Include="System.Runtime.Loader" Version="$(CoreFxVersion)" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||||
<Exec Command="npm install" />
|
<Exec Command="npm install" />
|
||||||
<Exec Command="node node_modules/webpack/bin/webpack.js" />
|
<Exec Command="node node_modules/webpack/bin/webpack.js" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
|
||||||
// but simplifies things for the consumer of this module.
|
// but simplifies things for the consumer of this module.
|
||||||
|
import './Util/PatchModuleResolutionLStat';
|
||||||
import './Util/OverrideStdOutputs';
|
import './Util/OverrideStdOutputs';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -86,5 +87,8 @@ function readRequestBodyAsJson(request, callback) {
|
|||||||
|
|
||||||
function respondWithError(res: http.ServerResponse, errorValue: any) {
|
function respondWithError(res: http.ServerResponse, errorValue: any) {
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
res.end(errorValue.stack || errorValue.toString());
|
res.end(JSON.stringify({
|
||||||
|
errorMessage: errorValue.message || errorValue,
|
||||||
|
errorDetails: errorValue.stack || null
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
const startsWith = (str: string, prefix: string) => str.substring(0, prefix.length) === prefix;
|
||||||
|
const appRootDir = process.cwd();
|
||||||
|
|
||||||
|
function patchedLStat(pathToStatLong: string, fsReqWrap?: any) {
|
||||||
|
try {
|
||||||
|
// If the lstat completes without errors, we don't modify its behavior at all
|
||||||
|
return origLStat.apply(this, arguments);
|
||||||
|
} catch(ex) {
|
||||||
|
const shouldOverrideError =
|
||||||
|
startsWith(ex.message, 'EPERM') // It's a permissions error
|
||||||
|
&& typeof appRootDirLong === 'string'
|
||||||
|
&& startsWith(appRootDirLong, pathToStatLong) // ... for an ancestor directory
|
||||||
|
&& ex.stack.indexOf('Object.realpathSync ') >= 0; // ... during symlink resolution
|
||||||
|
|
||||||
|
if (shouldOverrideError) {
|
||||||
|
// Fake the result to give the same result as an 'lstat' on the app root dir.
|
||||||
|
// This stops Node failing to load modules just because it doesn't know whether
|
||||||
|
// ancestor directories are symlinks or not. If there's a genuine file
|
||||||
|
// permissions issue, it will still surface later when Node actually
|
||||||
|
// tries to read the file.
|
||||||
|
return origLStat.call(this, appRootDir, fsReqWrap);
|
||||||
|
} else {
|
||||||
|
// In any other case, preserve the original error
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// It's only necessary to apply this workaround on Windows
|
||||||
|
let appRootDirLong: string = null;
|
||||||
|
let origLStat: Function = null;
|
||||||
|
if (/^win/.test(process.platform)) {
|
||||||
|
try {
|
||||||
|
// Get the app's root dir in Node's internal "long" format (e.g., \\?\C:\dir\subdir)
|
||||||
|
appRootDirLong = (path as any)._makeLong(appRootDir);
|
||||||
|
|
||||||
|
// Actually apply the patch, being as defensive as possible
|
||||||
|
const bindingFs = (process as any).binding('fs');
|
||||||
|
origLStat = bindingFs.lstat;
|
||||||
|
if (typeof origLStat === 'function') {
|
||||||
|
bindingFs.lstat = patchedLStat;
|
||||||
|
}
|
||||||
|
} catch(ex) {
|
||||||
|
// If some future version of Node throws (e.g., to prevent use of process.binding()),
|
||||||
|
// don't apply the patch, but still let the application run.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.NodeServices
|
namespace Microsoft.AspNetCore.NodeServices
|
||||||
{
|
{
|
||||||
@@ -11,27 +12,21 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
private bool _disposedValue;
|
private bool _disposedValue;
|
||||||
private bool _hasDeletedTempFile;
|
private bool _hasDeletedTempFile;
|
||||||
private object _fileDeletionLock = new object();
|
private object _fileDeletionLock = new object();
|
||||||
|
private IDisposable _applicationLifetimeRegistration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new instance of <see cref="StringAsTempFile"/>.
|
/// Create a new instance of <see cref="StringAsTempFile"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">The contents of the temporary file to be created.</param>
|
/// <param name="content">The contents of the temporary file to be created.</param>
|
||||||
public StringAsTempFile(string content)
|
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
|
||||||
|
public StringAsTempFile(string content, CancellationToken applicationStoppingToken)
|
||||||
{
|
{
|
||||||
FileName = Path.GetTempFileName();
|
FileName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||||
File.WriteAllText(FileName, content);
|
File.WriteAllText(FileName, content);
|
||||||
|
|
||||||
// Because .NET finalizers don't reliably run when the process is terminating, also
|
// Because .NET finalizers don't reliably run when the process is terminating, also
|
||||||
// add event handlers for other shutdown scenarios.
|
// add event handlers for other shutdown scenarios.
|
||||||
#if NET451
|
_applicationLifetimeRegistration = applicationStoppingToken.Register(EnsureTempFileDeleted);
|
||||||
AppDomain.CurrentDomain.ProcessExit += HandleProcessExit;
|
|
||||||
AppDomain.CurrentDomain.DomainUnload += HandleProcessExit;
|
|
||||||
#else
|
|
||||||
// Note that this still doesn't capture SIGKILL (at least on macOS) - there doesn't
|
|
||||||
// appear to be a way of doing that. So in that case, the temporary file will be
|
|
||||||
// left behind.
|
|
||||||
System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += HandleAssemblyUnloading;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,12 +50,7 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
// Dispose managed state
|
// Dispose managed state
|
||||||
#if NET451
|
_applicationLifetimeRegistration.Dispose();
|
||||||
AppDomain.CurrentDomain.ProcessExit -= HandleProcessExit;
|
|
||||||
AppDomain.CurrentDomain.DomainUnload -= HandleProcessExit;
|
|
||||||
#else
|
|
||||||
System.Runtime.Loader.AssemblyLoadContext.Default.Unloading -= HandleAssemblyUnloading;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureTempFileDeleted();
|
EnsureTempFileDeleted();
|
||||||
@@ -81,18 +71,6 @@ namespace Microsoft.AspNetCore.NodeServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET451
|
|
||||||
private void HandleProcessExit(object sender, EventArgs args)
|
|
||||||
{
|
|
||||||
EnsureTempFileDeleted();
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
private void HandleAssemblyUnloading(System.Runtime.Loader.AssemblyLoadContext context)
|
|
||||||
{
|
|
||||||
EnsureTempFileDeleted();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements the finalization part of the IDisposable pattern by calling Dispose(false).
|
/// Implements the finalization part of the IDisposable pattern by calling Dispose(false).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -4,15 +4,18 @@
|
|||||||
|
|
||||||
<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>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
<PackageTags>aspnetcore;aspnetcoremvc;nodeservices</PackageTags>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="node_modules\**\*" />
|
||||||
<EmbeddedResource Include="Content\**\*" />
|
<EmbeddedResource Include="Content\**\*" />
|
||||||
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
<ProjectReference Include="..\Microsoft.AspNetCore.NodeServices\Microsoft.AspNetCore.NodeServices.csproj" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.TagHelpers" Version="$(AspNetCoreVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="$(AspNetCoreVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
<Target Name="PrepublishScript" BeforeTargets="PrepareForPublish" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of a DI service that provides convenient access to
|
||||||
|
/// server-side prerendering APIs. This is an alternative to prerendering via
|
||||||
|
/// the asp-prerender-module tag helper.
|
||||||
|
/// </summary>
|
||||||
|
internal class DefaultSpaPrerenderer : ISpaPrerenderer
|
||||||
|
{
|
||||||
|
private readonly string _applicationBasePath;
|
||||||
|
private readonly CancellationToken _applicationStoppingToken;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly INodeServices _nodeServices;
|
||||||
|
|
||||||
|
public DefaultSpaPrerenderer(
|
||||||
|
INodeServices nodeServices,
|
||||||
|
IApplicationLifetime applicationLifetime,
|
||||||
|
IHostingEnvironment hostingEnvironment,
|
||||||
|
IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_applicationBasePath = hostingEnvironment.ContentRootPath;
|
||||||
|
_applicationStoppingToken = applicationLifetime.ApplicationStopping;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_nodeServices = nodeServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<RenderToStringResult> RenderToString(
|
||||||
|
string moduleName,
|
||||||
|
string exportName = null,
|
||||||
|
object customDataParameter = null,
|
||||||
|
int timeoutMilliseconds = default(int))
|
||||||
|
{
|
||||||
|
return Prerenderer.RenderToString(
|
||||||
|
_applicationBasePath,
|
||||||
|
_nodeServices,
|
||||||
|
_applicationStoppingToken,
|
||||||
|
new JavaScriptModuleExport(moduleName) { ExportName = exportName },
|
||||||
|
_httpContextAccessor.HttpContext,
|
||||||
|
customDataParameter,
|
||||||
|
timeoutMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service that can perform server-side prerendering for
|
||||||
|
/// JavaScript-based Single Page Applications. This is an alternative
|
||||||
|
/// to using the 'asp-prerender-module' tag helper.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISpaPrerenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes JavaScript code to perform server-side prerendering for a
|
||||||
|
/// Single-Page Application. This is an alternative to using the
|
||||||
|
/// 'asp-prerender-module' tag helper.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moduleName">The JavaScript module that exports a prerendering function.</param>
|
||||||
|
/// <param name="exportName">The name of the export from the JavaScript module, if it is not the default export.</param>
|
||||||
|
/// <param name="customDataParameter">An optional JSON-serializable object to pass to the JavaScript prerendering function.</param>
|
||||||
|
/// <param name="timeoutMilliseconds">If specified, the prerendering task will time out after this duration if not already completed.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<RenderToStringResult> RenderToString(
|
||||||
|
string moduleName,
|
||||||
|
string exportName = null,
|
||||||
|
object customDataParameter = null,
|
||||||
|
int timeoutMilliseconds = default(int));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.NodeServices;
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||||
{
|
{
|
||||||
@@ -24,6 +22,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI
|
private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI
|
||||||
|
|
||||||
private readonly string _applicationBasePath;
|
private readonly string _applicationBasePath;
|
||||||
|
private readonly CancellationToken _applicationStoppingToken;
|
||||||
private readonly INodeServices _nodeServices;
|
private readonly INodeServices _nodeServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -36,6 +35,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
_nodeServices = (INodeServices) serviceProvider.GetService(typeof(INodeServices)) ?? _fallbackNodeServices;
|
_nodeServices = (INodeServices) serviceProvider.GetService(typeof(INodeServices)) ?? _fallbackNodeServices;
|
||||||
_applicationBasePath = hostEnv.ContentRootPath;
|
_applicationBasePath = hostEnv.ContentRootPath;
|
||||||
|
|
||||||
|
var applicationLifetime = (IApplicationLifetime) serviceProvider.GetService(typeof(IApplicationLifetime));
|
||||||
|
_applicationStoppingToken = applicationLifetime.ApplicationStopping;
|
||||||
|
|
||||||
// Consider removing the following. Having it means you can get away with not putting app.AddNodeServices()
|
// Consider removing the following. Having it means you can get away with not putting app.AddNodeServices()
|
||||||
// in your startup file, but then again it might be confusing that you don't need to.
|
// in your startup file, but then again it might be confusing that you don't need to.
|
||||||
if (_nodeServices == null)
|
if (_nodeServices == null)
|
||||||
@@ -85,31 +87,17 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
/// <returns>A <see cref="Task"/> representing the operation.</returns>
|
||||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||||
{
|
{
|
||||||
// We want to pass the original, unencoded incoming URL data through to Node, so that
|
|
||||||
// server-side code has the same view of the URL as client-side code (on the client,
|
|
||||||
// location.pathname returns an unencoded string).
|
|
||||||
// The following logic handles special characters in URL paths in the same way that
|
|
||||||
// Node and client-side JS does. For example, the path "/a=b%20c" gets passed through
|
|
||||||
// unchanged (whereas other .NET APIs do change it - Path.Value will return it as
|
|
||||||
// "/a=b c" and Path.ToString() will return it as "/a%3db%20c")
|
|
||||||
var requestFeature = ViewContext.HttpContext.Features.Get<IHttpRequestFeature>();
|
|
||||||
var unencodedPathAndQuery = requestFeature.RawTarget;
|
|
||||||
|
|
||||||
var request = ViewContext.HttpContext.Request;
|
|
||||||
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
|
|
||||||
|
|
||||||
var result = await Prerenderer.RenderToString(
|
var result = await Prerenderer.RenderToString(
|
||||||
_applicationBasePath,
|
_applicationBasePath,
|
||||||
_nodeServices,
|
_nodeServices,
|
||||||
|
_applicationStoppingToken,
|
||||||
new JavaScriptModuleExport(ModuleName)
|
new JavaScriptModuleExport(ModuleName)
|
||||||
{
|
{
|
||||||
ExportName = ExportName
|
ExportName = ExportName
|
||||||
},
|
},
|
||||||
unencodedAbsoluteUrl,
|
ViewContext.HttpContext,
|
||||||
unencodedPathAndQuery,
|
|
||||||
CustomDataParameter,
|
CustomDataParameter,
|
||||||
TimeoutMillisecondsParameter,
|
TimeoutMillisecondsParameter);
|
||||||
request.PathBase.ToString());
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(result.RedirectUrl))
|
if (!string.IsNullOrEmpty(result.RedirectUrl))
|
||||||
{
|
{
|
||||||
@@ -128,19 +116,10 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
|
|
||||||
// Also attach any specified globals to the 'window' object. This is useful for transferring
|
// Also attach any specified globals to the 'window' object. This is useful for transferring
|
||||||
// general state between server and client.
|
// general state between server and client.
|
||||||
if (result.Globals != null)
|
var globalsScript = result.CreateGlobalsAssignmentScript();
|
||||||
|
if (!string.IsNullOrEmpty(globalsScript))
|
||||||
{
|
{
|
||||||
var stringBuilder = new StringBuilder();
|
output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
|
||||||
foreach (var property in result.Globals.Properties())
|
|
||||||
{
|
|
||||||
stringBuilder.AppendFormat("window.{0} = {1};",
|
|
||||||
property.Name,
|
|
||||||
property.Value.ToString(Formatting.None));
|
|
||||||
}
|
|
||||||
if (stringBuilder.Length > 0)
|
|
||||||
{
|
|
||||||
output.PostElement.SetHtmlContent($"<script>{stringBuilder}</script>");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.NodeServices;
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||||
{
|
{
|
||||||
@@ -9,15 +12,42 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Prerenderer
|
public static class Prerenderer
|
||||||
{
|
{
|
||||||
private static readonly Lazy<StringAsTempFile> NodeScript;
|
private static readonly object CreateNodeScriptLock = new object();
|
||||||
|
|
||||||
static Prerenderer()
|
private static StringAsTempFile NodeScript;
|
||||||
|
|
||||||
|
internal static Task<RenderToStringResult> RenderToString(
|
||||||
|
string applicationBasePath,
|
||||||
|
INodeServices nodeServices,
|
||||||
|
CancellationToken applicationStoppingToken,
|
||||||
|
JavaScriptModuleExport bootModule,
|
||||||
|
HttpContext httpContext,
|
||||||
|
object customDataParameter,
|
||||||
|
int timeoutMilliseconds)
|
||||||
{
|
{
|
||||||
NodeScript = new Lazy<StringAsTempFile>(() =>
|
// We want to pass the original, unencoded incoming URL data through to Node, so that
|
||||||
{
|
// server-side code has the same view of the URL as client-side code (on the client,
|
||||||
var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js");
|
// location.pathname returns an unencoded string).
|
||||||
return new StringAsTempFile(script); // Will be cleaned up on process exit
|
// The following logic handles special characters in URL paths in the same way that
|
||||||
});
|
// Node and client-side JS does. For example, the path "/a=b%20c" gets passed through
|
||||||
|
// unchanged (whereas other .NET APIs do change it - Path.Value will return it as
|
||||||
|
// "/a=b c" and Path.ToString() will return it as "/a%3db%20c")
|
||||||
|
var requestFeature = httpContext.Features.Get<IHttpRequestFeature>();
|
||||||
|
var unencodedPathAndQuery = requestFeature.RawTarget;
|
||||||
|
|
||||||
|
var request = httpContext.Request;
|
||||||
|
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
|
||||||
|
|
||||||
|
return RenderToString(
|
||||||
|
applicationBasePath,
|
||||||
|
nodeServices,
|
||||||
|
applicationStoppingToken,
|
||||||
|
bootModule,
|
||||||
|
unencodedAbsoluteUrl,
|
||||||
|
unencodedPathAndQuery,
|
||||||
|
customDataParameter,
|
||||||
|
timeoutMilliseconds,
|
||||||
|
request.PathBase.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -25,6 +55,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="applicationBasePath">The root path to your application. This is used when resolving project-relative paths.</param>
|
/// <param name="applicationBasePath">The root path to your application. This is used when resolving project-relative paths.</param>
|
||||||
/// <param name="nodeServices">The instance of <see cref="INodeServices"/> that will be used to invoke JavaScript code.</param>
|
/// <param name="nodeServices">The instance of <see cref="INodeServices"/> that will be used to invoke JavaScript code.</param>
|
||||||
|
/// <param name="applicationStoppingToken">A token that indicates when the host application is stopping.</param>
|
||||||
/// <param name="bootModule">The path to the JavaScript file containing the prerendering logic.</param>
|
/// <param name="bootModule">The path to the JavaScript file containing the prerendering logic.</param>
|
||||||
/// <param name="requestAbsoluteUrl">The URL of the currently-executing HTTP request. This is supplied to the prerendering code.</param>
|
/// <param name="requestAbsoluteUrl">The URL of the currently-executing HTTP request. This is supplied to the prerendering code.</param>
|
||||||
/// <param name="requestPathAndQuery">The path and query part of the URL of the currently-executing HTTP request. This is supplied to the prerendering code.</param>
|
/// <param name="requestPathAndQuery">The path and query part of the URL of the currently-executing HTTP request. This is supplied to the prerendering code.</param>
|
||||||
@@ -35,6 +66,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
public static Task<RenderToStringResult> RenderToString(
|
public static Task<RenderToStringResult> RenderToString(
|
||||||
string applicationBasePath,
|
string applicationBasePath,
|
||||||
INodeServices nodeServices,
|
INodeServices nodeServices,
|
||||||
|
CancellationToken applicationStoppingToken,
|
||||||
JavaScriptModuleExport bootModule,
|
JavaScriptModuleExport bootModule,
|
||||||
string requestAbsoluteUrl,
|
string requestAbsoluteUrl,
|
||||||
string requestPathAndQuery,
|
string requestPathAndQuery,
|
||||||
@@ -43,7 +75,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
string requestPathBase)
|
string requestPathBase)
|
||||||
{
|
{
|
||||||
return nodeServices.InvokeExportAsync<RenderToStringResult>(
|
return nodeServices.InvokeExportAsync<RenderToStringResult>(
|
||||||
NodeScript.Value.FileName,
|
GetNodeScriptFilename(applicationStoppingToken),
|
||||||
"renderToString",
|
"renderToString",
|
||||||
applicationBasePath,
|
applicationBasePath,
|
||||||
bootModule,
|
bootModule,
|
||||||
@@ -53,5 +85,19 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
timeoutMilliseconds,
|
timeoutMilliseconds,
|
||||||
requestPathBase);
|
requestPathBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetNodeScriptFilename(CancellationToken applicationStoppingToken)
|
||||||
|
{
|
||||||
|
lock(CreateNodeScriptLock)
|
||||||
|
{
|
||||||
|
if (NodeScript == null)
|
||||||
|
{
|
||||||
|
var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js");
|
||||||
|
NodeScript = new StringAsTempFile(script, applicationStoppingToken); // Will be cleaned up on process exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NodeScript.FileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for setting up prerendering features in an <see cref="IServiceCollection" />.
|
||||||
|
/// </summary>
|
||||||
|
public static class PrerenderingServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the dependency injection system to supply an implementation
|
||||||
|
/// of <see cref="ISpaPrerenderer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
|
||||||
|
public static void AddSpaPrerenderer(this IServiceCollection serviceCollection)
|
||||||
|
{
|
||||||
|
serviceCollection.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||||
|
serviceCollection.AddSingleton<ISpaPrerenderer, DefaultSpaPrerenderer>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
||||||
{
|
{
|
||||||
@@ -30,5 +32,29 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
|
|||||||
/// If set, specifies the HTTP status code that should be sent back with the server response.
|
/// If set, specifies the HTTP status code that should be sent back with the server response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? StatusCode { get; set; }
|
public int? StatusCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a block of JavaScript code that assigns data from the
|
||||||
|
/// <see cref="Globals"/> property to the global namespace.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A block of JavaScript code.</returns>
|
||||||
|
public string CreateGlobalsAssignmentScript()
|
||||||
|
{
|
||||||
|
if (Globals == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var property in Globals.Properties())
|
||||||
|
{
|
||||||
|
stringBuilder.AppendFormat("window.{0} = {1};",
|
||||||
|
property.Name,
|
||||||
|
property.Value.ToString(Formatting.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ Behind the scenes, it uses the [`Microsoft.AspNetCore.NodeServices`](https://git
|
|||||||
|
|
||||||
### Installation into existing projects
|
### Installation into existing projects
|
||||||
|
|
||||||
* Add `Microsoft.AspNetCore.SpaServices` to the dependencies list in your `project.json` file
|
* Install the `Microsoft.AspNetCore.SpaServices` NuGet package
|
||||||
* Run `dotnet restore` (or if you use Visual Studio, just wait a moment - it will restore dependencies automatically)
|
* Run `dotnet restore` (or if you use Visual Studio, just wait a moment - it will restore dependencies automatically)
|
||||||
* Install supporting NPM packages for the features you'll be using:
|
* Install supporting NPM packages for the features you'll be using:
|
||||||
* For **server-side prerendering**, install `aspnet-prerendering`
|
* For **server-side prerendering**, install `aspnet-prerendering`
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
string pathPrefix,
|
string pathPrefix,
|
||||||
ConditionalProxyMiddlewareOptions options)
|
ConditionalProxyMiddlewareOptions options)
|
||||||
{
|
{
|
||||||
|
if (!pathPrefix.StartsWith("/"))
|
||||||
|
{
|
||||||
|
pathPrefix = "/" + pathPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
_next = next;
|
_next = next;
|
||||||
_pathPrefix = pathPrefix;
|
_pathPrefix = pathPrefix;
|
||||||
_options = options;
|
_options = options;
|
||||||
@@ -65,7 +70,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
|
|
||||||
requestMessage.Headers.Host = _options.Host + ":" + _options.Port;
|
requestMessage.Headers.Host = _options.Host + ":" + _options.Port;
|
||||||
var uriString =
|
var uriString =
|
||||||
$"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}";
|
$"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.Path}{context.Request.QueryString}";
|
||||||
requestMessage.RequestUri = new Uri(uriString);
|
requestMessage.RequestUri = new Uri(uriString);
|
||||||
requestMessage.Method = new HttpMethod(context.Request.Method);
|
requestMessage.Method = new HttpMethod(context.Request.Method);
|
||||||
|
|
||||||
@@ -97,9 +102,17 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
|
|||||||
context.Response.Headers.Remove("transfer-encoding");
|
context.Response.Headers.Remove("transfer-encoding");
|
||||||
|
|
||||||
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
|
using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await responseStream.CopyToAsync(context.Response.Body, DefaultHttpBufferSize, context.RequestAborted);
|
await responseStream.CopyToAsync(context.Response.Body, DefaultHttpBufferSize, context.RequestAborted);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// The CopyToAsync task will be canceled if the client disconnects (e.g., user
|
||||||
|
// closes or refreshes the browser tab). Don't treat this as an error.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.NodeServices;
|
using Microsoft.AspNetCore.NodeServices;
|
||||||
using Microsoft.AspNetCore.SpaServices.Webpack;
|
using Microsoft.AspNetCore.SpaServices.Webpack;
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.PlatformAbstractions;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Builder
|
namespace Microsoft.AspNetCore.Builder
|
||||||
{
|
{
|
||||||
@@ -54,7 +49,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
// middleware). And since this is a dev-time-only feature, it doesn't matter if the default transport isn't
|
// middleware). And since this is a dev-time-only feature, it doesn't matter if the default transport isn't
|
||||||
// as fast as some theoretical future alternative.
|
// as fast as some theoretical future alternative.
|
||||||
var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices);
|
var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices);
|
||||||
nodeServicesOptions.WatchFileExtensions = new string[] {}; // Don't watch anything
|
nodeServicesOptions.WatchFileExtensions = new string[] { }; // Don't watch anything
|
||||||
if (!string.IsNullOrEmpty(options.ProjectPath))
|
if (!string.IsNullOrEmpty(options.ProjectPath))
|
||||||
{
|
{
|
||||||
nodeServicesOptions.ProjectPath = options.ProjectPath;
|
nodeServicesOptions.ProjectPath = options.ProjectPath;
|
||||||
@@ -73,7 +68,7 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
// Get a filename matching the middleware Node script
|
// Get a filename matching the middleware Node script
|
||||||
var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware),
|
var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware),
|
||||||
"/Content/Node/webpack-dev-middleware.js");
|
"/Content/Node/webpack-dev-middleware.js");
|
||||||
var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit
|
var nodeScript = new StringAsTempFile(script, nodeServicesOptions.ApplicationStoppingToken); // Will be cleaned up on process exit
|
||||||
|
|
||||||
// Ideally, this would be relative to the application's PathBase (so it could work in virtual directories)
|
// Ideally, this would be relative to the application's PathBase (so it could work in virtual directories)
|
||||||
// but it's not clear that such information exists during application startup, as opposed to within the context
|
// but it's not clear that such information exists during application startup, as opposed to within the context
|
||||||
@@ -106,9 +101,9 @@ namespace Microsoft.AspNetCore.Builder
|
|||||||
// plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request).
|
// plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request).
|
||||||
foreach (var publicPath in devServerInfo.PublicPaths)
|
foreach (var publicPath in devServerInfo.PublicPaths)
|
||||||
{
|
{
|
||||||
|
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath + hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan);
|
||||||
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100));
|
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100));
|
||||||
}
|
}
|
||||||
appBuilder.UseProxyToLocalWebpackDevMiddleware(hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout)
|
private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aspnet-prerendering",
|
"name": "aspnet-prerendering",
|
||||||
"version": "2.0.5",
|
"version": "3.0.1",
|
||||||
"description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
"description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"url": "https://github.com/aspnet/JavaScriptServices.git"
|
"url": "https://github.com/aspnet/JavaScriptServices.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domain-task": "^2.0.2"
|
"domain-task": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^6.0.42",
|
"@types/node": "^6.0.42",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/// <reference path="./PrerenderingInterfaces.d.ts" />
|
|
||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as domain from 'domain';
|
import * as domain from 'domain';
|
||||||
import { run as domainTaskRun, baseUrl as domainTaskBaseUrl } from 'domain-task/main';
|
import { run as domainTaskRun, baseUrl as domainTaskBaseUrl } from 'domain-task/main';
|
||||||
|
import { BootFunc, BootFuncParams, BootModuleInfo, RenderToStringCallback, RenderToStringFunc } from './PrerenderingInterfaces';
|
||||||
|
|
||||||
const defaultTimeoutMilliseconds = 30 * 1000;
|
const defaultTimeoutMilliseconds = 30 * 1000;
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ export function createServerRenderer(bootFunc: BootFunc): RenderToStringFunc {
|
|||||||
domainTasks: domainTaskCompletionPromise,
|
domainTasks: domainTaskCompletionPromise,
|
||||||
data: customDataParameter
|
data: customDataParameter
|
||||||
};
|
};
|
||||||
|
const absoluteBaseUrl = params.origin + params.baseUrl; // Should be same value as page's <base href>
|
||||||
|
|
||||||
// Open a new domain that can track all the async tasks involved in the app's execution
|
// Open a new domain that can track all the async tasks involved in the app's execution
|
||||||
domainTaskRun(/* code to run */ () => {
|
domainTaskRun(/* code to run */ () => {
|
||||||
@@ -35,7 +36,7 @@ export function createServerRenderer(bootFunc: BootFunc): RenderToStringFunc {
|
|||||||
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']);
|
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']);
|
||||||
|
|
||||||
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
|
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
|
||||||
domainTaskBaseUrl(absoluteRequestUrl);
|
domainTaskBaseUrl(absoluteBaseUrl);
|
||||||
|
|
||||||
// Begin rendering, and apply a timeout
|
// Begin rendering, and apply a timeout
|
||||||
const bootFuncPromise = bootFunc(params);
|
const bootFuncPromise = bootFunc(params);
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
interface RenderToStringFunc {
|
export interface RenderToStringFunc {
|
||||||
(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number, requestPathBase: string): void;
|
(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number, requestPathBase: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderToStringCallback {
|
export interface RenderToStringCallback {
|
||||||
(error: any, result?: RenderToStringResult): void;
|
(error: any, result?: RenderResult): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RenderToStringResult {
|
export interface RenderToStringResult {
|
||||||
html: string;
|
html: string;
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
globals?: { [key: string]: any };
|
globals?: { [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RedirectResult {
|
export interface RedirectResult {
|
||||||
redirectUrl: string;
|
redirectUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BootFunc {
|
export type RenderResult = RenderToStringResult | RedirectResult;
|
||||||
(params: BootFuncParams): Promise<RenderToStringResult>;
|
|
||||||
|
export interface BootFunc {
|
||||||
|
(params: BootFuncParams): Promise<RenderResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BootFuncParams {
|
export interface BootFuncParams {
|
||||||
location: any; // e.g., Location object containing information '/some/path'
|
location: any; // e.g., Location object containing information '/some/path'
|
||||||
origin: string; // e.g., 'https://example.com:1234'
|
origin: string; // e.g., 'https://example.com:1234'
|
||||||
url: string; // e.g., '/some/path'
|
url: string; // e.g., '/some/path'
|
||||||
@@ -30,7 +32,7 @@ interface BootFuncParams {
|
|||||||
data: any; // any custom object passed through from .NET
|
data: any; // any custom object passed through from .NET
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BootModuleInfo {
|
export interface BootModuleInfo {
|
||||||
moduleName: string;
|
moduleName: string;
|
||||||
exportName?: string;
|
exportName?: string;
|
||||||
webpackConfig?: string;
|
webpackConfig?: string;
|
||||||
@@ -1,5 +1,2 @@
|
|||||||
/// <reference path="./PrerenderingInterfaces.d.ts" />
|
|
||||||
|
|
||||||
export * from './Prerendering';
|
export * from './Prerendering';
|
||||||
|
export * from './PrerenderingInterfaces';
|
||||||
export type RenderResult = RenderToStringResult | RedirectResult;
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aspnet-webpack-react",
|
"name": "aspnet-webpack-react",
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"description": "Helpers for using Webpack with React in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
"description": "Helpers for using Webpack with React in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
1700
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package-lock.json
generated
Normal file
1700
src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aspnet-webpack",
|
"name": "aspnet-webpack",
|
||||||
"version": "1.0.29",
|
"version": "2.0.1",
|
||||||
"description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
"description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as querystring from 'querystring';
|
import * as querystring from 'querystring';
|
||||||
import { requireNewCopy } from './RequireNewCopy';
|
import { requireNewCopy } from './RequireNewCopy';
|
||||||
|
import { hasSufficientPermissions } from './WebpackTestPermissions';
|
||||||
|
|
||||||
export type CreateDevServerResult = {
|
export type CreateDevServerResult = {
|
||||||
Port: number,
|
Port: number,
|
||||||
PublicPaths: string[],
|
PublicPaths: string[]
|
||||||
PublicPath: string // For backward compatibility with older verions of Microsoft.AspNetCore.SpaServices. Will be removed soon.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CreateDevServerCallback {
|
export interface CreateDevServerCallback {
|
||||||
@@ -108,7 +108,7 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati
|
|||||||
const compiler = webpack(webpackConfig);
|
const compiler = webpack(webpackConfig);
|
||||||
app.use(require('webpack-dev-middleware')(compiler, {
|
app.use(require('webpack-dev-middleware')(compiler, {
|
||||||
noInfo: true,
|
noInfo: true,
|
||||||
publicPath: webpackConfig.output.publicPath,
|
publicPath: ensureLeadingSlash(webpackConfig.output.publicPath),
|
||||||
watchOptions: webpackConfig.watchOptions
|
watchOptions: webpackConfig.watchOptions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -195,6 +195,14 @@ function copyRecursiveToRealFsSync(from: typeof fs, rootDir: string, exclude: Re
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureLeadingSlash(value: string) {
|
||||||
|
if (value !== null && value.substring(0, 1) !== '/') {
|
||||||
|
value = '/' + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function pathJoinSafe(rootPath: string, filePath: string) {
|
function pathJoinSafe(rootPath: string, filePath: string) {
|
||||||
// On Windows, MemoryFileSystem's readdirSync output produces directory entries like 'C:'
|
// On Windows, MemoryFileSystem's readdirSync output produces directory entries like 'C:'
|
||||||
// which then trigger errors if you call statSync for them. Avoid this by detecting drive
|
// which then trigger errors if you call statSync for them. Avoid this by detecting drive
|
||||||
@@ -216,6 +224,16 @@ function beginWebpackWatcher(webpackConfig: webpack.Configuration) {
|
|||||||
export function createWebpackDevServer(callback: CreateDevServerCallback, optionsJson: string) {
|
export function createWebpackDevServer(callback: CreateDevServerCallback, optionsJson: string) {
|
||||||
const options: CreateDevServerOptions = JSON.parse(optionsJson);
|
const options: CreateDevServerOptions = JSON.parse(optionsJson);
|
||||||
|
|
||||||
|
// See the large comment in WebpackTestPermissions.ts for details about this
|
||||||
|
if (!hasSufficientPermissions()) {
|
||||||
|
console.log('WARNING: Webpack dev middleware is not enabled because the server process does not have sufficient permissions. You should either remove the UseWebpackDevMiddleware call from your code, or to make it work, give your server process user account permission to write to your application directory and to read all ancestor-level directories.');
|
||||||
|
callback(null, {
|
||||||
|
Port: 0,
|
||||||
|
PublicPaths: []
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Read the webpack config's export, and normalize it into the more general 'array of configs' format
|
// Read the webpack config's export, and normalize it into the more general 'array of configs' format
|
||||||
let webpackConfigExport: WebpackConfigFileExport = requireNewCopy(options.webpackConfigPath);
|
let webpackConfigExport: WebpackConfigFileExport = requireNewCopy(options.webpackConfigPath);
|
||||||
if (webpackConfigExport instanceof Function) {
|
if (webpackConfigExport instanceof Function) {
|
||||||
@@ -257,22 +275,32 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
|
|||||||
if (!publicPath) {
|
if (!publicPath) {
|
||||||
throw new Error('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack config (for any configuration that targets browsers)');
|
throw new Error('To use the Webpack dev server, you must specify a value for \'publicPath\' on the \'output\' section of your webpack config (for any configuration that targets browsers)');
|
||||||
}
|
}
|
||||||
normalizedPublicPaths.push(removeTrailingSlash(publicPath));
|
const publicPathNoTrailingSlash = removeTrailingSlash(publicPath);
|
||||||
|
normalizedPublicPaths.push(publicPathNoTrailingSlash);
|
||||||
|
|
||||||
// Newer versions of Microsoft.AspNetCore.SpaServices will explicitly pass an HMR endpoint URL
|
// This is the URL the client will connect to, except that since it's a relative URL
|
||||||
// (because it's relative to the app's URL space root, which the client doesn't otherwise know).
|
// (no leading slash), Webpack will resolve it against the runtime <base href> URL
|
||||||
// For back-compatibility, fall back on connecting directly to the underlying HMR server (though
|
// plus it also adds the publicPath
|
||||||
// that won't work if the app is hosted on HTTPS because of the mixed-content rule, and we can't
|
const hmrClientEndpoint = removeLeadingSlash(options.hotModuleReplacementEndpointUrl);
|
||||||
// run the HMR server itself on HTTPS because in general it has no valid cert).
|
|
||||||
const hmrClientEndpoint = options.hotModuleReplacementEndpointUrl // The URL that we'll proxy (e.g., /__asp_webpack_hmr)
|
// This is the URL inside the Webpack middleware Node server that we'll proxy to.
|
||||||
|| `http://localhost:${listener.address().port}/__webpack_hmr`; // Fall back on absolute URL to bypass proxying
|
// We have to prefix with the public path because Webpack will add the publicPath
|
||||||
const hmrServerEndpoint = options.hotModuleReplacementEndpointUrl
|
// when it resolves hmrClientEndpoint as a relative URL.
|
||||||
|| '/__webpack_hmr'; // URL is relative to webpack dev server root
|
const hmrServerEndpoint = ensureLeadingSlash(publicPathNoTrailingSlash + options.hotModuleReplacementEndpointUrl);
|
||||||
|
|
||||||
// We always overwrite the 'path' option as it needs to match what the .NET side is expecting
|
// We always overwrite the 'path' option as it needs to match what the .NET side is expecting
|
||||||
const hmrClientOptions = options.suppliedOptions.HotModuleReplacementClientOptions || <StringMap<string>>{};
|
const hmrClientOptions = options.suppliedOptions.HotModuleReplacementClientOptions || <StringMap<string>>{};
|
||||||
hmrClientOptions['path'] = hmrClientEndpoint;
|
hmrClientOptions['path'] = hmrClientEndpoint;
|
||||||
|
|
||||||
|
const dynamicPublicPathKey = 'dynamicPublicPath';
|
||||||
|
if (!(dynamicPublicPathKey in hmrClientOptions)) {
|
||||||
|
// dynamicPublicPath default to true, so we can work with nonempty pathbases (virtual directories)
|
||||||
|
hmrClientOptions[dynamicPublicPathKey] = true;
|
||||||
|
} else {
|
||||||
|
// ... but you can set it to any other value explicitly if you want (e.g., false)
|
||||||
|
hmrClientOptions[dynamicPublicPathKey] = JSON.parse(hmrClientOptions[dynamicPublicPathKey]);
|
||||||
|
}
|
||||||
|
|
||||||
attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientOptions, hmrServerEndpoint);
|
attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientOptions, hmrServerEndpoint);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -280,11 +308,7 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
|
|||||||
// Tell the ASP.NET app what addresses we're listening on, so that it can proxy requests here
|
// Tell the ASP.NET app what addresses we're listening on, so that it can proxy requests here
|
||||||
callback(null, {
|
callback(null, {
|
||||||
Port: listener.address().port,
|
Port: listener.address().port,
|
||||||
PublicPaths: normalizedPublicPaths,
|
PublicPaths: normalizedPublicPaths
|
||||||
|
|
||||||
// For back-compatibility with older versions of Microsoft.AspNetCore.SpaServices, in the case where
|
|
||||||
// you have exactly one webpackConfigArray entry. This will be removed soon.
|
|
||||||
PublicPath: normalizedPublicPaths[0]
|
|
||||||
});
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
callback(ex.stack, null);
|
callback(ex.stack, null);
|
||||||
@@ -292,6 +316,14 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeLeadingSlash(str: string) {
|
||||||
|
if (str.indexOf('/') === 0) {
|
||||||
|
str = str.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
function removeTrailingSlash(str: string) {
|
function removeTrailingSlash(str: string) {
|
||||||
if (str.lastIndexOf('/') === str.length - 1) {
|
if (str.lastIndexOf('/') === str.length - 1) {
|
||||||
str = str.substring(0, str.length - 1);
|
str = str.substring(0, str.length - 1);
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
const isWindows = /^win/.test(process.platform);
|
||||||
|
|
||||||
|
// On Windows, Node (still as of v8.1.3) has an issue whereby, when locating JavaScript modules
|
||||||
|
// on disk, it walks up the directory hierarchy to the disk root, testing whether each directory
|
||||||
|
// is a symlink or not. This fails with an exception if the process doesn't have permission to
|
||||||
|
// read those directories. This is a problem when hosting in full IIS, because in typical cases
|
||||||
|
// the process does not have read permission for higher-level directories.
|
||||||
|
//
|
||||||
|
// NodeServices itself works around this by injecting a patched version of Node's 'lstat' API that
|
||||||
|
// suppresses these irrelevant errors during module loads. This covers most scenarios, but isn't
|
||||||
|
// enough to make Webpack dev middleware work, because typical Webpack configs use loaders such as
|
||||||
|
// 'awesome-typescript-loader', which works by forking a child process to do some of its work. The
|
||||||
|
// child process does not get the patched 'lstat', and hence fails. It's an especially bad failure,
|
||||||
|
// because the Webpack compiler doesn't even surface the exception - it just never completes the
|
||||||
|
// compilation process, causing the application to hang indefinitely.
|
||||||
|
//
|
||||||
|
// Additionally, Webpack dev middleware will want to write its output to disk, which is also going
|
||||||
|
// to fail in a typical IIS process, because you won't have 'write' permission to the app dir by
|
||||||
|
// default. We have to actually write the build output to disk (and not purely keep it in the in-
|
||||||
|
// memory file system) because the server-side prerendering Node instance is a separate process
|
||||||
|
// that only knows about code changes when it sees the compiled files on disk change.
|
||||||
|
//
|
||||||
|
// In the future, we'll hopefully get Node to fix its underlying issue, and figure out whether VS
|
||||||
|
// could give 'write' access to the app dir when launching sites in IIS. But until then, disable
|
||||||
|
// Webpack dev middleware if we detect the server process doesn't have the necessary permissions.
|
||||||
|
|
||||||
|
export function hasSufficientPermissions() {
|
||||||
|
if (isWindows) {
|
||||||
|
return canReadDirectoryAndAllAncestors(process.cwd());
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canReadDirectoryAndAllAncestors(dir: string): boolean {
|
||||||
|
if (!canReadDirectory(dir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDir = path.resolve(dir, '..');
|
||||||
|
if (parentDir === dir) {
|
||||||
|
// There are no more parent directories - we've reached the disk root
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return canReadDirectoryAndAllAncestors(parentDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canReadDirectory(dir: string): boolean {
|
||||||
|
try {
|
||||||
|
fs.statSync(dir);
|
||||||
|
return true;
|
||||||
|
} catch(ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "domain-task",
|
"name": "domain-task",
|
||||||
"version": "3.0.0",
|
"version": "3.0.3",
|
||||||
"description": "Tracks outstanding operations for a logical thread of execution",
|
"description": "Tracks outstanding operations for a logical thread of execution",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domain-context": "^0.5.1",
|
"domain-context": "^0.5.1",
|
||||||
|
"is-absolute-url": "^2.1.0",
|
||||||
"isomorphic-fetch": "^2.2.1"
|
"isomorphic-fetch": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import * as url from 'url';
|
import * as url from 'url';
|
||||||
import * as domain from 'domain';
|
import * as domain from 'domain';
|
||||||
import * as domainContext from 'domain-context';
|
import * as domainContext from 'domain-context';
|
||||||
|
import * as isAbsoluteUrl from 'is-absolute-url';
|
||||||
import { baseUrl } from './main';
|
import { baseUrl } from './main';
|
||||||
const isomorphicFetch = require('isomorphic-fetch');
|
const isomorphicFetch = require('isomorphic-fetch');
|
||||||
const isNode = typeof process === 'object' && process.versions && !!process.versions.node;
|
const isNode = typeof process === 'object' && process.versions && !!process.versions.node;
|
||||||
|
const nodeHttps = isNode && require('https');
|
||||||
|
const isHttpsRegex = /^https\:/;
|
||||||
|
|
||||||
function issueRequest(baseUrl: string, req: string | Request, init?: RequestInit): Promise<any> {
|
function issueRequest(baseUrl: string, req: string | Request, init?: RequestInit): Promise<any> {
|
||||||
|
const reqUrl = (req instanceof Request) ? req.url : req;
|
||||||
|
const isRelativeUrl = reqUrl && !isAbsoluteUrl(reqUrl);
|
||||||
|
|
||||||
// Resolve relative URLs
|
// Resolve relative URLs
|
||||||
if (baseUrl) {
|
if (baseUrl) {
|
||||||
if (req instanceof Request) {
|
if (req instanceof Request) {
|
||||||
@@ -25,9 +31,45 @@ function issueRequest(baseUrl: string, req: string | Request, init?: RequestInit
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init = applyHttpsAgentPolicy(init, isRelativeUrl, baseUrl);
|
||||||
return isomorphicFetch(req, init);
|
return isomorphicFetch(req, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyHttpsAgentPolicy(init: RequestInit, isRelativeUrl: boolean, baseUrl: string): RequestInit {
|
||||||
|
// HTTPS is awkward in Node because it uses a built-in list of CAs, rather than recognizing
|
||||||
|
// the OS's system-level CA list. There are dozens of issues filed against Node about this,
|
||||||
|
// but still (as of v8.0.0) no resolution besides manually duplicating your CA config.
|
||||||
|
//
|
||||||
|
// The biggest problem for typical isomorphic-SPA development this causes is that if you're
|
||||||
|
// using a self-signed localhost cert in development, Node won't be able to make API calls
|
||||||
|
// to it (e.g., https://github.com/aspnet/JavaScriptServices/issues/1089). Developers could
|
||||||
|
// fix this by either manually configuring the cert in Node (which is extremely inconvenient,
|
||||||
|
// especially if multiple devs on a team have different self-signed localhost certs), or by
|
||||||
|
// disabling cert verification on their API requests.
|
||||||
|
//
|
||||||
|
// Fortunately, 'domain-task/fetch' knows when you're making a relative-URL request to your
|
||||||
|
// own web server (as opposed to an arbitrary request to anywhere else). In this specific case,
|
||||||
|
// there's no real point in cert verification, since the request never even leaves the machine
|
||||||
|
// so a MitM attack isn't meaningful. So by default, when your code is running in Node and
|
||||||
|
// is making a relative-URL request, *and* if you haven't explicitly configured any option
|
||||||
|
// for 'agent' (which would let you set up other HTTPS-handling policies), then we automatically
|
||||||
|
// disable cert verification for that request.
|
||||||
|
if (isNode && isRelativeUrl) {
|
||||||
|
const isHttps = baseUrl && isHttpsRegex.test(baseUrl);
|
||||||
|
if (isHttps) {
|
||||||
|
const hasAgentConfig = init && ('agent' in init);
|
||||||
|
if (!hasAgentConfig) {
|
||||||
|
const agentForRequest = new (nodeHttps.Agent)({ rejectUnauthorized: false });
|
||||||
|
|
||||||
|
init = init || {};
|
||||||
|
(init as any).agent = agentForRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return init;
|
||||||
|
}
|
||||||
|
|
||||||
export function fetch(url: string | Request, init?: RequestInit): Promise<any> {
|
export function fetch(url: string | Request, init?: RequestInit): Promise<any> {
|
||||||
// As of domain-task 2.0.0, we no longer auto-add the 'fetch' promise to the current domain task list.
|
// As of domain-task 2.0.0, we no longer auto-add the 'fetch' promise to the current domain task list.
|
||||||
// This is because it's misleading to do so, and can result in race-condition bugs, e.g.,
|
// This is because it's misleading to do so, and can result in race-condition bugs, e.g.,
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">netcoreapp2.0</TargetFramework>
|
||||||
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">TargetFrameworkOverride</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup Condition="'$(TargetFrameworkOverride)' == ''">
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0-*" />
|
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" 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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0-*" />
|
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -21,7 +27,23 @@
|
|||||||
<Content Remove="ClientApp\**" />
|
<Content Remove="ClientApp\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
|
<!--/-: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 -->
|
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||||
<Exec Command="npm install" />
|
<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 --config webpack.config.vendor.js --env.prod" />
|
||||||
|
|||||||
21
templates/AngularSpa/ClientApp/app/app.module.browser.ts
Normal file
21
templates/AngularSpa/ClientApp/app/app.module.browser.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { AppModuleShared } from './app.module.shared';
|
||||||
|
import { AppComponent } from './components/app/app.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
bootstrap: [ AppComponent ],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
AppModuleShared
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: 'BASE_URL', useFactory: getBaseUrl }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBaseUrl() {
|
||||||
|
return document.getElementsByTagName('base')[0].href;
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { HttpModule } from '@angular/http';
|
|
||||||
import { sharedConfig } from './app.module.shared';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
bootstrap: sharedConfig.bootstrap,
|
|
||||||
declarations: sharedConfig.declarations,
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
FormsModule,
|
|
||||||
HttpModule,
|
|
||||||
...sharedConfig.imports
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: 'ORIGIN_URL', useValue: location.origin },
|
|
||||||
{ provide: 'PRERENDERING_DATA', useValue: (window as any).PRERENDERING_DATA }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class AppModule {
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { ServerModule } from '@angular/platform-server';
|
import { ServerModule } from '@angular/platform-server';
|
||||||
import { sharedConfig } from './app.module.shared';
|
import { AppModuleShared } from './app.module.shared';
|
||||||
|
import { AppComponent } from './components/app/app.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
bootstrap: sharedConfig.bootstrap,
|
bootstrap: [ AppComponent ],
|
||||||
declarations: sharedConfig.declarations,
|
|
||||||
imports: [
|
imports: [
|
||||||
ServerModule,
|
ServerModule,
|
||||||
...sharedConfig.imports
|
AppModuleShared
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpModule } from '@angular/http';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { AppComponent } from './components/app/app.component'
|
import { AppComponent } from './components/app/app.component';
|
||||||
import { NavMenuComponent } from './components/navmenu/navmenu.component';
|
import { NavMenuComponent } from './components/navmenu/navmenu.component';
|
||||||
import { HomeComponent } from './components/home/home.component';
|
import { HomeComponent } from './components/home/home.component';
|
||||||
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
|
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
|
||||||
import { CounterComponent } from './components/counter/counter.component';
|
import { CounterComponent } from './components/counter/counter.component';
|
||||||
|
|
||||||
export const sharedConfig: NgModule = {
|
@NgModule({
|
||||||
bootstrap: [ AppComponent ],
|
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
NavMenuComponent,
|
NavMenuComponent,
|
||||||
@@ -17,6 +19,9 @@ export const sharedConfig: NgModule = {
|
|||||||
HomeComponent
|
HomeComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
HttpModule,
|
||||||
|
FormsModule,
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||||
{ path: 'home', component: HomeComponent },
|
{ path: 'home', component: HomeComponent },
|
||||||
@@ -25,4 +30,6 @@ export const sharedConfig: NgModule = {
|
|||||||
{ path: '**', redirectTo: 'home' }
|
{ path: '**', redirectTo: 'home' }
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
};
|
})
|
||||||
|
export class AppModuleShared {
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<h1>Weather forecast</h1>
|
<h1>Weather forecast</h1>
|
||||||
|
|
||||||
<p>Prerendering data: {{ prerenderingDataString }}</p>
|
|
||||||
|
|
||||||
<p>This component demonstrates fetching data from the server.</p>
|
<p>This component demonstrates fetching data from the server.</p>
|
||||||
|
|
||||||
<p *ngIf="!forecasts"><em>Loading...</em></p>
|
<p *ngIf="!forecasts"><em>Loading...</em></p>
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ import { Http } from '@angular/http';
|
|||||||
})
|
})
|
||||||
export class FetchDataComponent {
|
export class FetchDataComponent {
|
||||||
public forecasts: WeatherForecast[];
|
public forecasts: WeatherForecast[];
|
||||||
public prerenderingDataString: string;
|
|
||||||
|
|
||||||
constructor(http: Http, @Inject('ORIGIN_URL') originUrl: string, @Inject('PRERENDERING_DATA') prerenderingData: any) {
|
constructor(http: Http, @Inject('BASE_URL') baseUrl: string) {
|
||||||
this.prerenderingDataString = JSON.stringify(prerenderingData);
|
http.get(baseUrl + 'api/SampleData/WeatherForecasts').subscribe(result => {
|
||||||
|
|
||||||
http.get(originUrl + '/api/SampleData/WeatherForecasts').subscribe(result => {
|
|
||||||
this.forecasts = result.json() as WeatherForecast[];
|
this.forecasts = result.json() as WeatherForecast[];
|
||||||
});
|
}, error => console.error(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'zone.js';
|
import 'zone.js';
|
||||||
|
import 'bootstrap';
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
import { AppModule } from './app/app.module.client';
|
import { AppModule } from './app/app.module.browser';
|
||||||
|
|
||||||
if (module['hot']) {
|
if (module.hot) {
|
||||||
module['hot'].accept();
|
module.hot.accept();
|
||||||
module['hot'].dispose(() => {
|
module.hot.dispose(() => {
|
||||||
// Before restarting the app, we create a new root element and dispose the old one
|
// Before restarting the app, we create a new root element and dispose the old one
|
||||||
const oldRootElem = document.querySelector('app');
|
const oldRootElem = document.querySelector('app');
|
||||||
const newRootElem = document.createElement('app');
|
const newRootElem = document.createElement('app');
|
||||||
oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem);
|
oldRootElem!.parentNode!.insertBefore(newRootElem, oldRootElem);
|
||||||
modulePromise.then(appModule => appModule.destroy());
|
modulePromise.then(appModule => appModule.destroy());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'zone.js';
|
import 'zone.js';
|
||||||
import 'rxjs/add/operator/first';
|
import 'rxjs/add/operator/first';
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core';
|
import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core';
|
||||||
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
|
import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';
|
||||||
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
|
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
|
||||||
@@ -11,24 +12,23 @@ enableProdMode();
|
|||||||
export default createServerRenderer(params => {
|
export default createServerRenderer(params => {
|
||||||
const providers = [
|
const providers = [
|
||||||
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
|
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
|
||||||
{ provide: 'ORIGIN_URL', useValue: params.origin },
|
{ provide: APP_BASE_HREF, useValue: params.baseUrl },
|
||||||
{ provide: 'PRERENDERING_DATA', useValue: params.data }
|
{ provide: 'BASE_URL', useValue: params.origin + params.baseUrl },
|
||||||
];
|
];
|
||||||
|
|
||||||
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
|
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
|
||||||
const appRef = moduleRef.injector.get(ApplicationRef);
|
const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
|
||||||
const state = moduleRef.injector.get(PlatformState);
|
const state = moduleRef.injector.get(PlatformState);
|
||||||
const zone = moduleRef.injector.get(NgZone);
|
const zone = moduleRef.injector.get(NgZone);
|
||||||
|
|
||||||
return new Promise<RenderResult>((resolve, reject) => {
|
return new Promise<RenderResult>((resolve, reject) => {
|
||||||
zone.onError.subscribe(errorInfo => reject(errorInfo));
|
zone.onError.subscribe((errorInfo: any) => reject(errorInfo));
|
||||||
appRef.isStable.first(isStable => isStable).subscribe(() => {
|
appRef.isStable.first(isStable => isStable).subscribe(() => {
|
||||||
// Because 'onStable' fires before 'onError', we have to delay slightly before
|
// Because 'onStable' fires before 'onError', we have to delay slightly before
|
||||||
// completing the request in case there's an error to report
|
// completing the request in case there's an error to report
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
resolve({
|
resolve({
|
||||||
html: state.renderToString(),
|
html: state.renderToString()
|
||||||
globals: { PRERENDERING_DATA: params.data }
|
|
||||||
});
|
});
|
||||||
moduleRef.destroy();
|
moduleRef.destroy();
|
||||||
});
|
});
|
||||||
@@ -2,11 +2,7 @@
|
|||||||
ViewData["Title"] = "Home Page";
|
ViewData["Title"] = "Home Page";
|
||||||
}
|
}
|
||||||
|
|
||||||
<app asp-prerender-module="ClientApp/dist/main-server"
|
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
|
||||||
asp-prerender-data="new {
|
|
||||||
IsGoldUser = true,
|
|
||||||
Cookies = ViewContext.HttpContext.Request.Cookies
|
|
||||||
}">Loading...</app>
|
|
||||||
|
|
||||||
<script src="~/dist/vendor.js" asp-append-version="true"></script>
|
<script src="~/dist/vendor.js" asp-append-version="true"></script>
|
||||||
@section scripts {
|
@section scripts {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"sdk": { "version": "sdkVersionInjectedHere" }
|
|
||||||
}
|
|
||||||
3730
templates/AngularSpa/npm-shrinkwrap.json
generated
Normal file
3730
templates/AngularSpa/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "WebApplicationBasic",
|
"name": "WebApplicationBasic",
|
||||||
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "karma start ClientApp/test/karma.conf.js"
|
"test": "karma start ClientApp/test/karma.conf.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "4.1.2",
|
"@angular/animations": "4.2.5",
|
||||||
"@angular/common": "4.1.2",
|
"@angular/common": "4.2.5",
|
||||||
"@angular/compiler": "4.1.2",
|
"@angular/compiler": "4.2.5",
|
||||||
"@angular/core": "4.1.2",
|
"@angular/compiler-cli": "4.2.5",
|
||||||
"@angular/forms": "4.1.2",
|
"@angular/core": "4.2.5",
|
||||||
"@angular/http": "4.1.2",
|
"@angular/forms": "4.2.5",
|
||||||
"@angular/platform-browser": "4.1.2",
|
"@angular/http": "4.2.5",
|
||||||
"@angular/platform-browser-dynamic": "4.1.2",
|
"@angular/platform-browser": "4.2.5",
|
||||||
"@angular/platform-server": "4.1.2",
|
"@angular/platform-browser-dynamic": "4.2.5",
|
||||||
"@angular/router": "4.1.2",
|
"@angular/platform-server": "4.2.5",
|
||||||
"@types/node": "7.0.18",
|
"@angular/router": "4.2.5",
|
||||||
|
"@ngtools/webpack": "1.5.0",
|
||||||
|
"@types/webpack-env": "1.13.0",
|
||||||
"angular2-template-loader": "0.6.2",
|
"angular2-template-loader": "0.6.2",
|
||||||
"aspnet-prerendering": "^2.0.5",
|
"aspnet-prerendering": "^3.0.1",
|
||||||
"aspnet-webpack": "^1.0.29",
|
"aspnet-webpack": "^2.0.1",
|
||||||
"awesome-typescript-loader": "3.1.3",
|
"awesome-typescript-loader": "3.2.1",
|
||||||
"bootstrap": "3.3.7",
|
"bootstrap": "3.3.7",
|
||||||
"css": "2.2.1",
|
"css": "2.2.1",
|
||||||
"css-loader": "0.28.1",
|
"css-loader": "0.28.4",
|
||||||
"es6-shim": "0.35.3",
|
"es6-shim": "0.35.3",
|
||||||
"event-source-polyfill": "0.0.9",
|
"event-source-polyfill": "0.0.9",
|
||||||
"expose-loader": "0.7.3",
|
"expose-loader": "0.7.3",
|
||||||
"extract-text-webpack-plugin": "2.1.0",
|
"extract-text-webpack-plugin": "2.1.2",
|
||||||
"file-loader": "0.11.1",
|
"file-loader": "0.11.2",
|
||||||
"html-loader": "0.4.5",
|
"html-loader": "0.4.5",
|
||||||
"isomorphic-fetch": "2.2.1",
|
"isomorphic-fetch": "2.2.1",
|
||||||
"jquery": "3.2.1",
|
"jquery": "3.2.1",
|
||||||
@@ -35,24 +38,24 @@
|
|||||||
"preboot": "4.5.2",
|
"preboot": "4.5.2",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"reflect-metadata": "0.1.10",
|
"reflect-metadata": "0.1.10",
|
||||||
"rxjs": "5.4.0",
|
"rxjs": "5.4.2",
|
||||||
"style-loader": "0.17.0",
|
"style-loader": "0.18.2",
|
||||||
"to-string-loader": "1.1.5",
|
"to-string-loader": "1.1.5",
|
||||||
"typescript": "2.3.2",
|
"typescript": "2.4.1",
|
||||||
"url-loader": "0.5.8",
|
"url-loader": "0.5.9",
|
||||||
"webpack": "2.5.1",
|
"webpack": "2.5.1",
|
||||||
"webpack-hot-middleware": "2.18.0",
|
"webpack-hot-middleware": "2.18.2",
|
||||||
"webpack-merge": "4.1.0",
|
"webpack-merge": "4.1.0",
|
||||||
"zone.js": "0.8.10"
|
"zone.js": "0.8.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "3.5.2",
|
"@types/chai": "4.0.1",
|
||||||
"@types/jasmine": "2.5.47",
|
"@types/jasmine": "2.5.53",
|
||||||
"chai": "3.5.0",
|
"chai": "4.0.2",
|
||||||
"jasmine-core": "2.6.1",
|
"jasmine-core": "2.6.4",
|
||||||
"karma": "1.7.0",
|
"karma": "1.7.0",
|
||||||
"karma-chai": "0.1.0",
|
"karma-chai": "0.1.0",
|
||||||
"karma-chrome-launcher": "2.1.1",
|
"karma-chrome-launcher": "2.2.0",
|
||||||
"karma-cli": "1.0.1",
|
"karma-cli": "1.0.1",
|
||||||
"karma-jasmine": "1.1.0",
|
"karma-jasmine": "1.1.0",
|
||||||
"karma-webpack": "2.0.3"
|
"karma-webpack": "2.0.3"
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"module": "es2015",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
|
"skipLibCheck": true, // Workaround for https://github.com/angular/angular/issues/17863. Remove this if you upgrade to a fixed version of Angular.
|
||||||
|
"strict": true,
|
||||||
"lib": [ "es6", "dom" ],
|
"lib": [ "es6", "dom" ],
|
||||||
"types": [ "node" ]
|
"types": [ "webpack-env" ]
|
||||||
},
|
},
|
||||||
"exclude": [ "bin", "node_modules" ],
|
"exclude": [ "bin", "node_modules" ],
|
||||||
"atom": { "rewriteTsconfig": false }
|
"atom": { "rewriteTsconfig": false }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
|
const AotPlugin = require('@ngtools/webpack').AotPlugin;
|
||||||
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
|
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
|
||||||
|
|
||||||
module.exports = (env) => {
|
module.exports = (env) => {
|
||||||
@@ -12,11 +13,11 @@ module.exports = (env) => {
|
|||||||
resolve: { extensions: [ '.js', '.ts' ] },
|
resolve: { extensions: [ '.js', '.ts' ] },
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
|
publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{ test: /\.ts$/, include: /ClientApp/, use: ['awesome-typescript-loader?silent=true', 'angular2-template-loader'] },
|
{ test: /\.ts$/, include: /ClientApp/, use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader'] : '@ngtools/webpack' },
|
||||||
{ test: /\.html$/, use: 'html-loader?minimize=false' },
|
{ test: /\.html$/, use: 'html-loader?minimize=false' },
|
||||||
{ test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] },
|
{ test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] },
|
||||||
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
|
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
|
||||||
@@ -28,7 +29,7 @@ module.exports = (env) => {
|
|||||||
// Configuration for client-side bundle suitable for running in browsers
|
// Configuration for client-side bundle suitable for running in browsers
|
||||||
const clientBundleOutputDir = './wwwroot/dist';
|
const clientBundleOutputDir = './wwwroot/dist';
|
||||||
const clientBundleConfig = merge(sharedConfig, {
|
const clientBundleConfig = merge(sharedConfig, {
|
||||||
entry: { 'main-client': './ClientApp/boot-client.ts' },
|
entry: { 'main-client': './ClientApp/boot.browser.ts' },
|
||||||
output: { path: path.join(__dirname, clientBundleOutputDir) },
|
output: { path: path.join(__dirname, clientBundleOutputDir) },
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DllReferencePlugin({
|
new webpack.DllReferencePlugin({
|
||||||
@@ -43,14 +44,19 @@ module.exports = (env) => {
|
|||||||
})
|
})
|
||||||
] : [
|
] : [
|
||||||
// Plugins that apply in production builds only
|
// Plugins that apply in production builds only
|
||||||
new webpack.optimize.UglifyJsPlugin()
|
new webpack.optimize.UglifyJsPlugin(),
|
||||||
|
new AotPlugin({
|
||||||
|
tsConfigPath: './tsconfig.json',
|
||||||
|
entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'),
|
||||||
|
exclude: ['./**/*.server.ts']
|
||||||
|
})
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configuration for server-side (prerendering) bundle suitable for running in Node
|
// Configuration for server-side (prerendering) bundle suitable for running in Node
|
||||||
const serverBundleConfig = merge(sharedConfig, {
|
const serverBundleConfig = merge(sharedConfig, {
|
||||||
resolve: { mainFields: ['main'] },
|
resolve: { mainFields: ['main'] },
|
||||||
entry: { 'main-server': './ClientApp/boot-server.ts' },
|
entry: { 'main-server': './ClientApp/boot.server.ts' },
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DllReferencePlugin({
|
new webpack.DllReferencePlugin({
|
||||||
context: __dirname,
|
context: __dirname,
|
||||||
@@ -58,7 +64,14 @@ module.exports = (env) => {
|
|||||||
sourceType: 'commonjs2',
|
sourceType: 'commonjs2',
|
||||||
name: './vendor'
|
name: './vendor'
|
||||||
})
|
})
|
||||||
],
|
].concat(isDevBuild ? [] : [
|
||||||
|
// Plugins that apply in production builds only
|
||||||
|
new AotPlugin({
|
||||||
|
tsConfigPath: './tsconfig.json',
|
||||||
|
entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'),
|
||||||
|
exclude: ['./**/*.browser.ts']
|
||||||
|
})
|
||||||
|
]),
|
||||||
output: {
|
output: {
|
||||||
libraryTarget: 'commonjs',
|
libraryTarget: 'commonjs',
|
||||||
path: path.join(__dirname, './ClientApp/dist')
|
path: path.join(__dirname, './ClientApp/dist')
|
||||||
|
|||||||
@@ -2,6 +2,27 @@ const path = require('path');
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
|
const treeShakableModules = [
|
||||||
|
'@angular/animations',
|
||||||
|
'@angular/common',
|
||||||
|
'@angular/compiler',
|
||||||
|
'@angular/core',
|
||||||
|
'@angular/forms',
|
||||||
|
'@angular/http',
|
||||||
|
'@angular/platform-browser',
|
||||||
|
'@angular/platform-browser-dynamic',
|
||||||
|
'@angular/router',
|
||||||
|
'zone.js',
|
||||||
|
];
|
||||||
|
const nonTreeShakableModules = [
|
||||||
|
'bootstrap',
|
||||||
|
'bootstrap/dist/css/bootstrap.css',
|
||||||
|
'es6-promise',
|
||||||
|
'es6-shim',
|
||||||
|
'event-source-polyfill',
|
||||||
|
'jquery',
|
||||||
|
];
|
||||||
|
const allModules = treeShakableModules.concat(nonTreeShakableModules);
|
||||||
|
|
||||||
module.exports = (env) => {
|
module.exports = (env) => {
|
||||||
const extractCSS = new ExtractTextPlugin('vendor.css');
|
const extractCSS = new ExtractTextPlugin('vendor.css');
|
||||||
@@ -14,28 +35,8 @@ module.exports = (env) => {
|
|||||||
{ test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' }
|
{ test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
entry: {
|
|
||||||
vendor: [
|
|
||||||
'@angular/animations',
|
|
||||||
'@angular/common',
|
|
||||||
'@angular/compiler',
|
|
||||||
'@angular/core',
|
|
||||||
'@angular/forms',
|
|
||||||
'@angular/http',
|
|
||||||
'@angular/platform-browser',
|
|
||||||
'@angular/platform-browser-dynamic',
|
|
||||||
'@angular/router',
|
|
||||||
'bootstrap',
|
|
||||||
'bootstrap/dist/css/bootstrap.css',
|
|
||||||
'es6-shim',
|
|
||||||
'es6-promise',
|
|
||||||
'event-source-polyfill',
|
|
||||||
'jquery',
|
|
||||||
'zone.js',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
output: {
|
output: {
|
||||||
publicPath: '/dist/',
|
publicPath: 'dist/',
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
library: '[name]_[hash]'
|
library: '[name]_[hash]'
|
||||||
},
|
},
|
||||||
@@ -48,6 +49,11 @@ module.exports = (env) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clientBundleConfig = merge(sharedConfig, {
|
const clientBundleConfig = merge(sharedConfig, {
|
||||||
|
entry: {
|
||||||
|
// To keep development builds fast, include all vendor dependencies in the vendor bundle.
|
||||||
|
// But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle.
|
||||||
|
vendor: isDevBuild ? allModules : nonTreeShakableModules
|
||||||
|
},
|
||||||
output: { path: path.join(__dirname, 'wwwroot', 'dist') },
|
output: { path: path.join(__dirname, 'wwwroot', 'dist') },
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
@@ -68,6 +74,7 @@ module.exports = (env) => {
|
|||||||
const serverBundleConfig = merge(sharedConfig, {
|
const serverBundleConfig = merge(sharedConfig, {
|
||||||
target: 'node',
|
target: 'node',
|
||||||
resolve: { mainFields: ['main'] },
|
resolve: { mainFields: ['main'] },
|
||||||
|
entry: { vendor: allModules.concat(['aspnet-prerendering']) },
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'ClientApp', 'dist'),
|
path: path.join(__dirname, 'ClientApp', 'dist'),
|
||||||
libraryTarget: 'commonjs2',
|
libraryTarget: 'commonjs2',
|
||||||
@@ -75,7 +82,6 @@ module.exports = (env) => {
|
|||||||
module: {
|
module: {
|
||||||
rules: [ { test: /\.css(\?|$)/, use: ['to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] } ]
|
rules: [ { test: /\.css(\?|$)/, use: ['to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] } ]
|
||||||
},
|
},
|
||||||
entry: { vendor: ['aspnet-prerendering'] },
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DllPlugin({
|
new webpack.DllPlugin({
|
||||||
path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'),
|
path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'),
|
||||||
|
|||||||
@@ -1,22 +1,44 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">netcoreapp2.0</TargetFramework>
|
||||||
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">TargetFrameworkOverride</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup Condition="'$(TargetFrameworkOverride)' == ''">
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0-*" />
|
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" 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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0-*" />
|
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
|
<!--/-: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 -->
|
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||||
<Exec Command="npm install" />
|
<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 --config webpack.config.vendor.js --env.prod" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export class Fetchdata {
|
|||||||
public forecasts: WeatherForecast[];
|
public forecasts: WeatherForecast[];
|
||||||
|
|
||||||
constructor(http: HttpClient) {
|
constructor(http: HttpClient) {
|
||||||
http.fetch('/api/SampleData/WeatherForecasts')
|
http.fetch('api/SampleData/WeatherForecasts')
|
||||||
.then(result => result.json() as Promise<WeatherForecast[]>)
|
.then(result => result.json() as Promise<WeatherForecast[]>)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.forecasts = data;
|
this.forecasts = data;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'isomorphic-fetch';
|
import 'isomorphic-fetch';
|
||||||
import { Aurelia, PLATFORM } from 'aurelia-framework';
|
import { Aurelia, PLATFORM } from 'aurelia-framework';
|
||||||
|
import { HttpClient } from 'aurelia-fetch-client';
|
||||||
import 'bootstrap/dist/css/bootstrap.css';
|
import 'bootstrap/dist/css/bootstrap.css';
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
declare const IS_DEV_BUILD: boolean; // The value is supplied by Webpack during the build
|
declare const IS_DEV_BUILD: boolean; // The value is supplied by Webpack during the build
|
||||||
@@ -11,5 +12,10 @@ export function configure(aurelia: Aurelia) {
|
|||||||
aurelia.use.developmentLogging();
|
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')));
|
aurelia.start().then(() => aurelia.setRoot(PLATFORM.moduleName('app/components/app/app')));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"sdk": { "version": "sdkVersionInjectedHere" }
|
|
||||||
}
|
|
||||||
2615
templates/AureliaSpa/npm-shrinkwrap.json
generated
Normal file
2615
templates/AureliaSpa/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "WebApplicationBasic",
|
"name": "WebApplicationBasic",
|
||||||
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aurelia-bootstrapper": "^2.0.1",
|
"aurelia-bootstrapper": "^2.0.1",
|
||||||
@@ -13,8 +14,8 @@
|
|||||||
"jquery": "^3.2.1"
|
"jquery": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^7.0.12",
|
"@types/webpack-env": "^1.13.0",
|
||||||
"aspnet-webpack": "^1.0.28",
|
"aspnet-webpack": "^2.0.1",
|
||||||
"aurelia-webpack-plugin": "^2.0.0-rc.2",
|
"aurelia-webpack-plugin": "^2.0.0-rc.2",
|
||||||
"css-loader": "^0.28.0",
|
"css-loader": "^0.28.0",
|
||||||
"extract-text-webpack-plugin": "^2.1.0",
|
"extract-text-webpack-plugin": "^2.1.0",
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"module": "es2015",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
"lib": [ "es2015", "dom" ],
|
"lib": [ "es2015", "dom" ],
|
||||||
"types": [ "node" ]
|
"types": [ "webpack-env" ]
|
||||||
},
|
},
|
||||||
"exclude": [ "bin", "node_modules" ],
|
"exclude": [ "bin", "node_modules" ],
|
||||||
"atom": { "rewriteTsconfig": false }
|
"atom": { "rewriteTsconfig": false }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module.exports = (env) => {
|
|||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(bundleOutputDir),
|
path: path.resolve(bundleOutputDir),
|
||||||
publicPath: '/dist/',
|
publicPath: 'dist/',
|
||||||
filename: '[name].js'
|
filename: '[name].js'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ module.exports = ({ prod } = {}) => {
|
|||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'wwwroot', 'dist'),
|
path: path.join(__dirname, 'wwwroot', 'dist'),
|
||||||
publicPath: '/dist/',
|
publicPath: 'dist/',
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
library: '[name]_[hash]',
|
library: '[name]_[hash]',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import './css/site.css';
|
import './css/site.css';
|
||||||
import 'bootstrap';
|
import 'bootstrap';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
import './webpack-component-loader';
|
import './webpack-component-loader';
|
||||||
import AppRootComponent from './components/app-root/app-root';
|
import AppRootComponent from './components/app-root/app-root';
|
||||||
const createHistory = require('history').createBrowserHistory;
|
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href')!;
|
||||||
|
const basename = baseUrl.substring(0, baseUrl.length - 1); // History component needs no trailing slash
|
||||||
|
|
||||||
// Load and register the <app-root> component
|
// Load and register the <app-root> component
|
||||||
ko.components.register('app-root', AppRootComponent);
|
ko.components.register('app-root', AppRootComponent);
|
||||||
|
|
||||||
// Tell Knockout to start up an instance of your application
|
// Tell Knockout to start up an instance of your application
|
||||||
ko.applyBindings({ history: createHistory() });
|
ko.applyBindings({ history: createBrowserHistory({ basename }), basename });
|
||||||
|
|
||||||
// Basic hot reloading support. Automatically reloads and restarts the Knockout app each time
|
// Basic hot reloading support. Automatically reloads and restarts the Knockout app each time
|
||||||
// you modify source files. This will not preserve any application state other than the URL.
|
// you modify source files. This will not preserve any application state other than the URL.
|
||||||
declare var module: any;
|
|
||||||
if (module.hot) {
|
if (module.hot) {
|
||||||
module.hot.accept();
|
module.hot.accept();
|
||||||
module.hot.dispose(() => ko.cleanNode(document.body));
|
module.hot.dispose(() => ko.cleanNode(document.body));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
<div class='row'>
|
<div class='row'>
|
||||||
<div class='col-sm-3'>
|
<div class='col-sm-3'>
|
||||||
<nav-menu params='route: route'></nav-menu>
|
<nav-menu params='router: router'></nav-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-sm-9' data-bind='component: { name: route().page, params: route }'></div>
|
<div class='col-sm-9' data-bind='component: { name: route().page, params: route }'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ const routes: Route[] = [
|
|||||||
|
|
||||||
class AppRootViewModel {
|
class AppRootViewModel {
|
||||||
public route: KnockoutObservable<Route>;
|
public route: KnockoutObservable<Route>;
|
||||||
private _router: Router;
|
public router: Router;
|
||||||
|
|
||||||
constructor(params: { history: History.History }) {
|
constructor(params: { history: History.History, basename: string }) {
|
||||||
// Activate the client-side router
|
// Activate the client-side router
|
||||||
this._router = new Router(params.history, routes)
|
this.router = new Router(params.history, routes, params.basename);
|
||||||
this.route = this._router.currentRoute;
|
this.route = this.router.currentRoute;
|
||||||
|
|
||||||
// Load and register all the KO components needed to handle the routes
|
// Load and register all the KO components needed to handle the routes
|
||||||
// The optional 'bundle-loader?lazy!' prefix is a Webpack feature that causes the referenced modules
|
// The optional 'bundle-loader?lazy!' prefix is a Webpack feature that causes the referenced modules
|
||||||
@@ -32,7 +32,7 @@ class AppRootViewModel {
|
|||||||
// To support hot module replacement, this method unregisters the router and KO components.
|
// To support hot module replacement, this method unregisters the router and KO components.
|
||||||
// In production scenarios where hot module replacement is disabled, this would not be invoked.
|
// In production scenarios where hot module replacement is disabled, this would not be invoked.
|
||||||
public dispose() {
|
public dispose() {
|
||||||
this._router.dispose();
|
this.router.dispose();
|
||||||
|
|
||||||
// TODO: Need a better API for this
|
// TODO: Need a better API for this
|
||||||
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {
|
Object.getOwnPropertyNames((<any>ko).components._allRegisteredComponents).forEach(componentName => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class FetchDataViewModel {
|
|||||||
public forecasts = ko.observableArray<WeatherForecast>();
|
public forecasts = ko.observableArray<WeatherForecast>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
fetch('/api/SampleData/WeatherForecasts')
|
fetch('api/SampleData/WeatherForecasts')
|
||||||
.then(response => response.json() as Promise<WeatherForecast[]>)
|
.then(response => response.json() as Promise<WeatherForecast[]>)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.forecasts(data);
|
this.forecasts(data);
|
||||||
|
|||||||
@@ -13,17 +13,17 @@
|
|||||||
<div class='navbar-collapse collapse'>
|
<div class='navbar-collapse collapse'>
|
||||||
<ul class='nav navbar-nav'>
|
<ul class='nav navbar-nav'>
|
||||||
<li>
|
<li>
|
||||||
<a href='/' data-bind='css: { active: route().page === "home-page" }'>
|
<a data-bind='attr: { href: router.link("/") }, css: { active: route().page === "home-page" }'>
|
||||||
<span class='glyphicon glyphicon-home'></span> Home
|
<span class='glyphicon glyphicon-home'></span> Home
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href='/counter' data-bind='css: { active: route().page === "counter-example" }'>
|
<a data-bind='attr: { href: router.link("/counter") }, css: { active: route().page === "counter-example" }'>
|
||||||
<span class='glyphicon glyphicon-education'></span> Counter
|
<span class='glyphicon glyphicon-education'></span> Counter
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href='/fetch-data' data-bind='css: { active: route().page === "fetch-data" }'>
|
<a data-bind='attr: { href: router.link("/fetch-data") }, css: { active: route().page === "fetch-data" }'>
|
||||||
<span class='glyphicon glyphicon-th-list'></span> Fetch data
|
<span class='glyphicon glyphicon-th-list'></span> Fetch data
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import { Route } from '../../router';
|
import { Route, Router } from '../../router';
|
||||||
|
|
||||||
interface NavMenuParams {
|
interface NavMenuParams {
|
||||||
route: KnockoutObservable<Route>;
|
router: Router;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NavMenuViewModel {
|
class NavMenuViewModel {
|
||||||
|
public router: Router;
|
||||||
public route: KnockoutObservable<Route>;
|
public route: KnockoutObservable<Route>;
|
||||||
|
|
||||||
constructor(params: NavMenuParams) {
|
constructor(params: NavMenuParams) {
|
||||||
// This viewmodel doesn't do anything except pass through the 'route' parameter to the view.
|
// This viewmodel doesn't do anything except pass through the 'route' parameter to the view.
|
||||||
// You could remove this viewmodel entirely, and define 'nav-menu' as a template-only component.
|
// You could remove this viewmodel entirely, and define 'nav-menu' as a template-only component.
|
||||||
// But in most apps, you'll want some viewmodel logic to determine what navigation options appear.
|
// But in most apps, you'll want some viewmodel logic to determine what navigation options appear.
|
||||||
this.route = params.route;
|
this.router = params.router;
|
||||||
|
this.route = this.router.currentRoute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
import * as $ from 'jquery';
|
import * as $ from 'jquery';
|
||||||
import * as History from 'history';
|
import * as History from 'history';
|
||||||
import crossroads = require('crossroads');
|
import * as crossroads from 'crossroads';
|
||||||
|
|
||||||
// This module configures crossroads.js, a routing library. If you prefer, you
|
// This module configures crossroads.js, a routing library. If you prefer, you
|
||||||
// can use any other routing library (or none at all) as Knockout is designed to
|
// can use any other routing library (or none at all) as Knockout is designed to
|
||||||
@@ -16,13 +16,13 @@ export class Router {
|
|||||||
private disposeHistory: () => void;
|
private disposeHistory: () => void;
|
||||||
private clickEventListener: EventListener;
|
private clickEventListener: EventListener;
|
||||||
|
|
||||||
constructor(history: History.History, routes: Route[]) {
|
constructor(private history: History.History, routes: Route[], basename: string) {
|
||||||
// Reset and configure Crossroads so it matches routes and updates this.currentRoute
|
// Reset and configure Crossroads so it matches routes and updates this.currentRoute
|
||||||
crossroads.removeAllRoutes();
|
crossroads.removeAllRoutes();
|
||||||
crossroads.resetState();
|
crossroads.resetState();
|
||||||
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
|
(crossroads as any).normalizeFn = crossroads.NORM_AS_OBJECT;
|
||||||
routes.forEach(route => {
|
routes.forEach(route => {
|
||||||
crossroads.addRoute(route.url, (requestParams) => {
|
crossroads.addRoute(route.url, (requestParams: any) => {
|
||||||
this.currentRoute(ko.utils.extend(requestParams, route.params));
|
this.currentRoute(ko.utils.extend(requestParams, route.params));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -33,8 +33,9 @@ export class Router {
|
|||||||
let target: any = evt.currentTarget;
|
let target: any = evt.currentTarget;
|
||||||
if (target && target.tagName === 'A') {
|
if (target && target.tagName === 'A') {
|
||||||
let href = target.getAttribute('href');
|
let href = target.getAttribute('href');
|
||||||
if (href && href.charAt(0) == '/') {
|
if (href && href.indexOf(basename + '/') === 0) {
|
||||||
history.push(href);
|
const hrefAfterBasename = href.substring(basename.length);
|
||||||
|
history.push(hrefAfterBasename);
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +47,10 @@ export class Router {
|
|||||||
crossroads.parse((history as any).location.pathname);
|
crossroads.parse((history as any).location.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public link(url: string): string {
|
||||||
|
return this.history.createHref({ pathname: url });
|
||||||
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
this.disposeHistory();
|
this.disposeHistory();
|
||||||
$(document).off('click', 'a', this.clickEventListener);
|
$(document).off('click', 'a', this.clickEventListener);
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ ko.components.loaders.unshift({
|
|||||||
loadComponent: (name, componentConfig, callback) => {
|
loadComponent: (name, componentConfig, callback) => {
|
||||||
if (typeof componentConfig === 'function') {
|
if (typeof componentConfig === 'function') {
|
||||||
// It's a lazy-loaded Webpack bundle
|
// It's a lazy-loaded Webpack bundle
|
||||||
(componentConfig as any)(loadedModule => {
|
(componentConfig as any)((loadedModule: any) => {
|
||||||
// Handle TypeScript-style default exports
|
// Handle TypeScript-style default exports
|
||||||
if (loadedModule.__esModule && loadedModule.default) {
|
if (loadedModule.__esModule && loadedModule.default) {
|
||||||
loadedModule = loadedModule.default;
|
loadedModule = loadedModule.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the loaded module to KO's default loader
|
// Pass the loaded module to KO's default loader
|
||||||
ko.components.defaultLoader.loadComponent(name, loadedModule, callback);
|
ko.components.defaultLoader.loadComponent!(name, loadedModule as KnockoutComponentTypes.ComponentConfig, callback);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// It's something else - let another component loader handle it
|
// It's something else - let another component loader handle it
|
||||||
callback(null);
|
callback((null as any) as KnockoutComponentTypes.Definition); // workaround until https://github.com/DefinitelyTyped/DefinitelyTyped/pull/17999
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,44 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">netcoreapp2.0</TargetFramework>
|
||||||
|
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">TargetFrameworkOverride</TargetFramework>
|
||||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||||
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
|
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup Condition="'$(TargetFrameworkOverride)' == ''">
|
||||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0-*" />
|
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices" 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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0-*" />
|
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
|
<!--/-: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 -->
|
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||||
<Exec Command="npm install" />
|
<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 --config webpack.config.vendor.js --env.prod" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
ViewData["Title"] = "Home Page";
|
ViewData["Title"] = "Home Page";
|
||||||
}
|
}
|
||||||
|
|
||||||
<app-root params="history: history"></app-root>
|
<app-root params="history: history, basename: basename"></app-root>
|
||||||
|
|
||||||
@section scripts {
|
@section scripts {
|
||||||
<script src="~/dist/main.js" asp-append-version="true"></script>
|
<script src="~/dist/main.js" asp-append-version="true"></script>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"sdk": { "version": "sdkVersionInjectedHere" }
|
|
||||||
}
|
|
||||||
2491
templates/KnockoutSpa/npm-shrinkwrap.json
generated
Normal file
2491
templates/KnockoutSpa/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "WebApplicationBasic",
|
"name": "WebApplicationBasic",
|
||||||
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/core-js": "^0.9.34",
|
"@types/core-js": "^0.9.34",
|
||||||
"@types/crossroads": "0.0.29",
|
"@types/crossroads": "0.0.29",
|
||||||
"@types/es6-promise": "0.0.32",
|
"@types/es6-promise": "0.0.32",
|
||||||
"@types/history": "^2.0.38",
|
"@types/history": "^4.6.0",
|
||||||
"@types/jquery": "^2.0.32",
|
"@types/jquery": "^2.0.32",
|
||||||
"@types/knockout": "^3.4.35",
|
"@types/knockout": "^3.4.41",
|
||||||
"@types/react-router": "^2.0.37",
|
|
||||||
"@types/requirejs": "^2.1.26",
|
|
||||||
"@types/signals": "0.0.16",
|
"@types/signals": "0.0.16",
|
||||||
"aspnet-webpack": "^1.0.27",
|
"@types/webpack-env": "^1.13.0",
|
||||||
|
"aspnet-webpack": "^2.0.1",
|
||||||
"awesome-typescript-loader": "^3.0.0",
|
"awesome-typescript-loader": "^3.0.0",
|
||||||
"bootstrap": "^3.3.6",
|
"bootstrap": "^3.3.6",
|
||||||
"bundle-loader": "^0.5.4",
|
"bundle-loader": "^0.5.4",
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"event-source-polyfill": "^0.0.7",
|
"event-source-polyfill": "^0.0.7",
|
||||||
"extract-text-webpack-plugin": "^2.0.0-rc",
|
"extract-text-webpack-plugin": "^2.0.0-rc",
|
||||||
"file-loader": "^0.9.0",
|
"file-loader": "^0.9.0",
|
||||||
"history": "^4.3.0",
|
"history": "^4.6.3",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"jquery": "^2.2.1",
|
"jquery": "^2.2.1",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"module": "es2015",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"types": ["es6-promise", "history", "requirejs"]
|
"strict": true,
|
||||||
|
"types": ["es6-promise", "webpack-env"]
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"bin",
|
"bin",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, bundleOutputDir),
|
path: path.join(__dirname, bundleOutputDir),
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
publicPath: '/dist/'
|
publicPath: 'dist/'
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module.exports = (env) => {
|
|||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'wwwroot', 'dist'),
|
path: path.join(__dirname, 'wwwroot', 'dist'),
|
||||||
publicPath: '/dist/',
|
publicPath: 'dist/',
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
library: '[name]_[hash]',
|
library: '[name]_[hash]',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import * as RoutesModule from './routes';
|
|||||||
let routes = RoutesModule.routes;
|
let routes = RoutesModule.routes;
|
||||||
|
|
||||||
// Create browser history to use in the Redux store
|
// Create browser history to use in the Redux store
|
||||||
const history = createBrowserHistory();
|
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href')!;
|
||||||
|
const history = createBrowserHistory({ basename: baseUrl });
|
||||||
|
|
||||||
// Get the application-wide store instance, prepopulating with state from the server where available.
|
// Get the application-wide store instance, prepopulating with state from the server where available.
|
||||||
const initialState = (window as any).initialReduxState as ApplicationState;
|
const initialState = (window as any).initialReduxState as ApplicationState;
|
||||||
|
|||||||
@@ -12,15 +12,17 @@ export default createServerRenderer(params => {
|
|||||||
return new Promise<RenderResult>((resolve, reject) => {
|
return new Promise<RenderResult>((resolve, reject) => {
|
||||||
// Prepare Redux store with in-memory history, and dispatch a navigation event
|
// Prepare Redux store with in-memory history, and dispatch a navigation event
|
||||||
// corresponding to the incoming URL
|
// corresponding to the incoming URL
|
||||||
|
const basename = params.baseUrl.substring(0, params.baseUrl.length - 1); // Remove trailing slash
|
||||||
|
const urlAfterBasename = params.url.substring(basename.length);
|
||||||
const store = configureStore(createMemoryHistory());
|
const store = configureStore(createMemoryHistory());
|
||||||
store.dispatch(replace(params.location));
|
store.dispatch(replace(urlAfterBasename));
|
||||||
|
|
||||||
// Prepare an instance of the application and perform an inital render that will
|
// Prepare an instance of the application and perform an inital render that will
|
||||||
// cause any async tasks (e.g., data access) to begin
|
// cause any async tasks (e.g., data access) to begin
|
||||||
const routerContext: any = {};
|
const routerContext: any = {};
|
||||||
const app = (
|
const app = (
|
||||||
<Provider store={ store }>
|
<Provider store={ store }>
|
||||||
<StaticRouter context={ routerContext } location={ params.location.path } children={ routes } />
|
<StaticRouter basename={ basename } context={ routerContext } location={ params.location.path } children={ routes } />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
renderToString(app);
|
renderToString(app);
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ class FetchData extends React.Component<WeatherForecastProps, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderPagination() {
|
private renderPagination() {
|
||||||
let prevStartDateIndex = this.props.startDateIndex - 5;
|
let prevStartDateIndex = (this.props.startDateIndex || 0) - 5;
|
||||||
let nextStartDateIndex = this.props.startDateIndex + 5;
|
let nextStartDateIndex = (this.props.startDateIndex || 0) + 5;
|
||||||
|
|
||||||
return <p className='clearfix text-center'>
|
return <p className='clearfix text-center'>
|
||||||
<Link className='btn btn-default pull-left' to={ `/fetchdata/${ prevStartDateIndex }` }>Previous</Link>
|
<Link className='btn btn-default pull-left' to={ `/fetchdata/${ prevStartDateIndex }` }>Previous</Link>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createStore, applyMiddleware, compose, combineReducers, GenericStoreEnhancer, Store } from 'redux';
|
import { createStore, applyMiddleware, compose, combineReducers, GenericStoreEnhancer, Store, StoreEnhancerStoreCreator, ReducersMapObject } from 'redux';
|
||||||
import thunk from 'redux-thunk';
|
import thunk from 'redux-thunk';
|
||||||
import { routerReducer, routerMiddleware } from 'react-router-redux';
|
import { routerReducer, routerMiddleware } from 'react-router-redux';
|
||||||
import * as StoreModule from './store';
|
import * as StoreModule from './store';
|
||||||
@@ -12,7 +12,7 @@ export default function configureStore(history: History, initialState?: Applicat
|
|||||||
const devToolsExtension = windowIfDefined && windowIfDefined.devToolsExtension as () => GenericStoreEnhancer;
|
const devToolsExtension = windowIfDefined && windowIfDefined.devToolsExtension as () => GenericStoreEnhancer;
|
||||||
const createStoreWithMiddleware = compose(
|
const createStoreWithMiddleware = compose(
|
||||||
applyMiddleware(thunk, routerMiddleware(history)),
|
applyMiddleware(thunk, routerMiddleware(history)),
|
||||||
devToolsExtension ? devToolsExtension() : f => f
|
devToolsExtension ? devToolsExtension() : <S>(next: StoreEnhancerStoreCreator<S>) => next
|
||||||
)(createStore);
|
)(createStore);
|
||||||
|
|
||||||
// Combine all reducers and instantiate the app-wide store instance
|
// Combine all reducers and instantiate the app-wide store instance
|
||||||
@@ -30,6 +30,6 @@ export default function configureStore(history: History, initialState?: Applicat
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRootReducer(allReducers) {
|
function buildRootReducer(allReducers: ReducersMapObject) {
|
||||||
return combineReducers<ApplicationState>(Object.assign({}, allReducers, { routing: routerReducer }));
|
return combineReducers<ApplicationState>(Object.assign({}, allReducers, { routing: routerReducer }));
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user