Compare commits

..

53 Commits

Author SHA1 Message Date
SteveSandersonMS
9c9892ab18 Add example of isomorphic cookies for #581 2017-01-16 12:54:44 +00:00
SteveSandersonMS
f8714e9653 Fix react/redux TypeScript build errors caused by breaking changes in dependencies. Fixes #529, #582, #583 2017-01-16 10:46:28 +00:00
SteveSandersonMS
9d5006107f Bumping NodeServices/SpaServices versions to 1.1.0-rc1-* for RC1 release 2017-01-13 09:02:28 +00:00
SteveSandersonMS
c3316d0caa Hopefully fix BuildQuality setting in build 2017-01-10 19:43:15 +00:00
SteveSandersonMS
38441c6598 Override the build quality determined by KoreBuild 2017-01-10 19:13:15 +00:00
SteveSandersonMS
f8f42a53b2 Increase Mocha timeout. Trying to fix intermittent test failures on AppVeyor. 2017-01-10 10:37:00 +00:00
SteveSandersonMS
a93f59c660 Add Aurelia to generator's README.md. Fixes #561. 2017-01-09 14:43:28 +00:00
SteveSandersonMS
c377e6cd28 For project.json-based projects, fix Kudu deployment by adding a .deployment file at the root 2017-01-07 00:08:06 +00:00
SteveSandersonMS
2a1435d9c1 Bump generator-aspnetcore-spa version to 0.7.3 2017-01-07 00:04:50 +00:00
SteveSandersonMS
6789dbc102 csproj-based projects don't need the _placeholder.txt files, because the publishing code correctly includes dist files even if they were not on disk before 2017-01-07 00:04:20 +00:00
SteveSandersonMS
59526ff16a Update templates' csproj files to fix first-time publishing (previously, it didn't deploy dist files if they were not already on disk) 2017-01-07 00:00:52 +00:00
SteveSandersonMS
8c456aa0fc Make Karma serve .ts files with executable MIME type, otherwise newer versions of Chrome won't run it. Fixes #499 2017-01-03 17:35:30 +00:00
Niclas Emdelius
49bece54c2 Check if response is stream or object without using reference to reflection package. 2017-01-03 13:29:45 +00:00
nicemd
0907379da8 Streamed responses can be be Object or Stream
See issue https://github.com/aspnet/JavaScriptServices/issues/518
2017-01-03 13:29:45 +00:00
SteveSandersonMS
fbb8054c2e Make error message clearer if Node isn't installed (or not found on PATH). Fixes #527 2017-01-03 10:59:26 +00:00
SteveSandersonMS
70f59fc8ec Update core package versions to 1.1.0-beta2-* so that CI will produce non-timestamped packages 2017-01-03 10:29:22 +00:00
SteveSandersonMS
e6cfe93e13 Instruct AppVeyor to regard the generator-aspnetcore-spa.tar.gz file as a build artifact 2017-01-02 12:02:58 +00:00
SteveSandersonMS
dad095b749 Bump generator-aspnetcore-spa version to 0.7.2 2017-01-02 11:59:51 +00:00
SteveSandersonMS
f31b96fe1e Yeoman package builder emits .tar.gz artifact 2017-01-02 11:59:16 +00:00
SteveSandersonMS
312885acde Add AppVeyor status to root README.md 2017-01-02 11:31:40 +00:00
SteveSandersonMS
0763f1062b Add missing trailing slash on publicPath in docs. Fixes #516. 2017-01-02 10:58:03 +00:00
SteveSandersonMS
dcb819f644 Fix port testing logic in tests by being explicit about which interface we're checking 2016-12-15 22:53:12 +00:00
SteveSandersonMS
f722dcf98d Fix PrerenderResult in Webpack sample 2016-12-15 21:45:02 +00:00
SteveSandersonMS
153c9848ca Also build samples/misc 2016-12-15 21:42:40 +00:00
SteveSandersonMS
aefa578e04 Reuse TEST_PROJECT_GLOB to build templates, since the 'test' dir doesn't contain any .NET projects 2016-12-15 21:09:44 +00:00
SteveSandersonMS
953f370336 Wait for port to be freed before continuing 2016-12-15 20:34:55 +00:00
SteveSandersonMS
200d80627c Update Appveyor test environment to Node 6.9.2 to deal with Windows-specific NPM issues 2016-12-15 20:34:46 +00:00
SteveSandersonMS
994653a277 Stop including templates in regular build (they now contain .csproj, which is not supported by that build system, and in any case they are built and end-to-end tested on Appveyor) 2016-12-15 20:32:08 +00:00
SteveSandersonMS
17b107bb42 Fix how 'current' and 'preview' SDKs are merged in Appveyor builds 2016-12-15 19:31:09 +00:00
SteveSandersonMS
67cd5f6093 In csproj projects, reference 1.1.0-preview4-final version of Razor tools 2016-12-15 18:10:43 +00:00
SteveSandersonMS
06ccad4344 Automatically install both the 'current' and 'preview' SDKs when building in Appveyor 2016-12-15 18:07:34 +00:00
SteveSandersonMS
4fd19b4293 Run tests against both csproj and project.json-style projects. Assumes relevant dotnet SDKs are installed locally. 2016-12-15 17:17:04 +00:00
SteveSandersonMS
f34eb582ad Update tests to be compatible with newer Yeoman generator 2016-12-15 16:52:02 +00:00
SteveSandersonMS
4580412f73 Don't leave behind an unused package.json.tmp file 2016-12-15 16:42:25 +00:00
SteveSandersonMS
a76b6e25d4 Add .csproj files for the templates. Some are set up for prerendering; others not. 2016-12-15 16:42:25 +00:00
SteveSandersonMS
2135d77189 generator-aspnetcore-spa support for selecting between project.json and .csproj 2016-12-15 16:42:25 +00:00
SteveSandersonMS
62b676174e generator-aspnetcore-spa now displays its own version number on startup 2016-12-15 16:41:10 +00:00
SteveSandersonMS
67c2cfd84e Add Appveyor builds and webdriver.io tests (tests cover Angular2Spa template only at present) 2016-12-15 16:38:30 +00:00
SteveSandersonMS
6decb30681 Make 'counter' components use <h1> headers to be consistent with other pages 2016-12-15 16:37:29 +00:00
SteveSandersonMS
33ed333f92 Specify SDK at repo root 2016-12-15 16:35:48 +00:00
SteveSandersonMS
6c16ba3da7 Remove template_nodemodules_placeholder.txt files - they are redundant now that we no longer need to publish node_modules to production 2016-12-15 16:35:48 +00:00
SteveSandersonMS
495c95d227 If generator fails because NPM is too old, exit with nonzero status code 2016-12-15 16:35:48 +00:00
SteveSandersonMS
4dd91a9b78 Make template package build script automate running the prepublish steps needed for the Angular2Spa template 2016-12-14 10:21:04 +00:00
SteveSandersonMS
06335eeee1 Fix building generator-aspnetcore-spa (something in a newer TypeScript version broke it) 2016-12-14 09:56:23 +00:00
SteveSandersonMS
eb98067e86 Update docs about using socket transport. Fixes #500 2016-12-13 15:59:43 +00:00
SteveSandersonMS
a25d1f4731 Publish aspnet-webpack 1.0.26 2016-12-13 15:48:44 +00:00
SteveSandersonMS
4c3834361e Stop WebpackDevMiddleware littering the 'dist' directories with blah.host-update.js.map files (they never need to be written to disk) 2016-12-13 15:47:43 +00:00
SteveSandersonMS
4cc61d79e2 Add json-loader to add webpack.config.js files. Helps with #507. 2016-12-13 12:57:06 +00:00
SteveSandersonMS
1c4682e50d Fix all the ConditionalProxyMiddleware errors that happened if you ctrl+c on a "dotnet run" (not "dotnet watch run") since beta-000002. 2016-12-13 11:32:32 +00:00
Sébastien Ros
6545e11bf2 Fix filename in sample
Mismatch between code sample and described js filename.
2016-12-08 10:04:50 +00:00
SteveSandersonMS
8d1cb3a537 Remove angular2-aspnet NPM package source as none of its functionality is still in use (and hasn't been since beta versions of Angular 2) 2016-12-08 10:02:37 +00:00
SteveSandersonMS
8705a4b353 Remove redux-typed from source tree. It's no longer in use and doesn't need to be maintained. Source can be re-added in the future if needed. 2016-12-07 19:02:53 +00:00
SteveSandersonMS
7c685c1354 Add AssemblyInfo.cs to Microsoft.AspNetCore.NodeServices.Sockets package 2016-12-07 18:09:11 +00:00
97 changed files with 1385 additions and 580 deletions

View File

@@ -1,5 +1,7 @@
# JavaScriptServices
AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/gprilrckx116vc9m/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/javascriptservices/branch/dev)
This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
## What is this?

View File

@@ -1,7 +1,44 @@
init:
- git config --global core.autocrlf true
install:
- ps: Install-Product node 6.9.2 x64
# .NET Core SDK binaries
# Download .NET Core SDK and add to PATH
- ps: $urlCurrent = "https://go.microsoft.com/fwlink/?LinkID=834991"
- ps: $urlPreview = "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/rel-1.0.0/dotnet-dev-win-x64.latest.zip"
- ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk"
- ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null
- ps: $tempFileCurrent = [System.IO.Path]::GetTempFileName()
- ps: $tempFilePreview = [System.IO.Path]::GetTempFileName()
- ps: (New-Object System.Net.WebClient).DownloadFile($urlCurrent, $tempFileCurrent)
- ps: (New-Object System.Net.WebClient).DownloadFile($urlPreview, $tempFilePreview)
- 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($tempFilePreview, $env:DOTNET_INSTALL_DIR + "preview")
- ps: Copy-Item "${env:DOTNET_INSTALL_DIR}preview\*" "${env:DOTNET_INSTALL_DIR}" -Force -Recurse
- ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path"
build_script:
- build.cmd verify
- ps: Push-Location
- cd templates/package-builder
- npm install
- npm run build
- ps: Pop-Location
artifacts:
- path: templates\package-builder\dist\artifacts\generator-aspnetcore-spa.tar.gz
name: generator-aspnetcore-spa
# - build.cmd verify
clone_depth: 1
test: off
test_script:
- dotnet restore ./src
- npm install -g selenium-standalone
- selenium-standalone install
# The nosys flag is needed for selenium to work on Appveyor
- ps: Start-Process selenium-standalone 'start','--','-Djna.nosys=true'
- ps: Push-Location
- cd test
- npm install
- npm test
on_finish :
- ps: Pop-Location
# After running tests, upload results to Appveyor
- ps: (new-object net.webclient).UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test\tmp\junit\*.xml))
deploy: off

View File

@@ -1,3 +1,4 @@
{
"projects": ["src"]
"projects": ["src"],
"sdk": { "version": "1.0.0-preview2-1-003177" }
}

View File

@@ -2,7 +2,13 @@
var VERSION='0.1'
var FULL_VERSION='0.1'
var AUTHORS='Microsoft Open Technologies, Inc.'
var SAMPLES_PROJECT_GLOB='templates/*/project.json'
var TEST_PROJECT_GLOB='templates/*/project.json'
var SAMPLES_PROJECT_GLOB='samples/misc/*/project.json'
@{
// The build quality values are set in each of the project.json files
BuildQuality = "";
}
use-standard-lifecycle
k-standard-goals

View File

@@ -38,7 +38,8 @@ namespace Webpack.ActionResults
request.GetEncodedUrl(),
request.Path + request.QueryString.Value,
_dataToSupply,
/* timeoutMilliseconds */ 30000
/* timeoutMilliseconds */ 30000,
/* requestPathBase */ "/"
);
response.ContentType = "text/html";

View File

@@ -1,4 +1,4 @@
import { createStore, applyMiddleware, compose, combineReducers, GenericStoreEnhancer } from 'redux';
import { createStore, applyMiddleware, compose, combineReducers, GenericStoreEnhancer, Store as ReduxStore } from 'redux';
import thunk from 'redux-thunk';
import { routerReducer } from 'react-router-redux';
import * as Store from './store';
@@ -16,7 +16,7 @@ export default function configureStore(initialState?: Store.ApplicationState) {
// Combine all reducers and instantiate the app-wide store instance
const allReducers = buildRootReducer(Store.reducers);
const store = createStoreWithMiddleware(allReducers, initialState) as Redux.Store<Store.ApplicationState>;
const store = createStoreWithMiddleware(allReducers, initialState) as ReduxStore<Store.ApplicationState>;
// Enable Webpack hot module replacement for reducers
if (module.hot) {

View File

@@ -2,6 +2,7 @@
"name": "music-store",
"version": "0.0.0",
"dependencies": {
"@types/history": "^2.0.0",
"@types/react": "^0.14.29",
"@types/react-bootstrap": "^0.0.35",
"@types/react-dom": "^0.14.14",
@@ -41,7 +42,7 @@
"redux-typed": "^2.0.0",
"style-loader": "^0.13.0",
"ts-loader": "^0.8.1",
"typescript": "2.0.3",
"typescript": "^2.0.3",
"url-loader": "^0.5.7",
"webpack": "^1.13.2",
"webpack-hot-middleware": "^2.12.2",

View File

@@ -1,11 +1,19 @@
{
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "node",
"target": "es6",
"jsx": "preserve",
"sourceMap": true,
"experimentalDecorators": true,
"types": [ "webpack-env", "whatwg-fetch" ]
"types": [ "webpack-env", "whatwg-fetch" ],
"paths": {
// Fix "Duplicate identifier" errors caused by multiple dependencies fetching their own copies of type definitions.
// We tell TypeScript which type definitions module to treat as the canonical one (instead of combining all of them).
"history": ["./node_modules/@types/history/index"],
"redux": ["./node_modules/@types/redux/index"],
"react": ["./node_modules/@types/react/index"]
}
},
"exclude": [
"node_modules"

View File

@@ -1,3 +0,0 @@
/node_modules/
/dist/
/bundles/

View File

@@ -1,4 +0,0 @@
/src/
/tsconfig.json
/build.js
/typings/

View File

@@ -1,33 +0,0 @@
// -------------
// No need to invoke this directly. To run a build, execute:
// npm run prepublish
// -------------
var Builder = require('systemjs-builder');
var builder = new Builder('./');
builder.config({
defaultJSExtensions: true,
paths: {
'angular2-aspnet': 'dist/Exports',
'angular2-aspnet/*': 'dist/*'
},
meta: {
'angular2/*': { build: false },
'rxjs/*': { build: false }
}
});
var entryPoint = 'dist/Exports';
var tasks = [
builder.bundle(entryPoint, './bundles/angular2-aspnet.js'),
builder.bundle(entryPoint, './bundles/angular2-aspnet.min.js', { minify: true })
];
Promise.all(tasks)
.then(function() {
console.log('Build complete');
})
.catch(function(err) {
console.error('Build error');
console.error(err);
});

View File

@@ -1,32 +0,0 @@
{
"name": "angular2-aspnet",
"version": "0.0.6",
"description": "Helpers for Angular 2 apps built on ASP.NET",
"main": "./dist/Exports",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "rimraf *.d.ts dist/*.d.ts && tsc && node build.js"
},
"typings": "dist/Exports",
"author": "Microsoft",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/aspnet/JavaScriptServices/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/aspnet/JavaScriptServices.git"
},
"dependencies": {
"angular2": "2.0.0-beta.13",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.2"
},
"devDependencies": {
"es6-shim": "^0.35.0",
"rimraf": "^2.5.4",
"systemjs-builder": "^0.14.11",
"typescript": "^2.0.0",
"zone.js": "^0.6.10"
}
}

View File

@@ -1,59 +0,0 @@
If you just want to use this package, then you *don't have to build it*. Instead, just grab the prebuilt package from NPM:
npm install angular2-aspnet
The rest of this file is notes for anyone contributing to this package itself.
## How to build
Run the following:
npm install
npm run prepublish
Requirements:
* Node, NPM
* `tsc` installed globally (via `npm install -g typescript`)
## Project structure
This package is intended to be consumable both on the server in Node.js, and on the client. Also, it's written in TypeScript,
which neither of those environments knows natively, but the TypeScript type definitions need to get delivered with the package
so that developers get a good IDE experience when consuming it.
The build process is therefore:
1. Compile the TypeScript to produce the development-time (.d.ts) and server-side (.js) artifacts
`tsc` reads `tsconfig.json` and is instructed to compile all the `.ts` files in `src/`. It produces a corresponding
structure of `.js` and `.d.ts` files in `dist/`.
When a developer consumes the resulting package (via `npm install angular2-aspnet`),
- No additional copy of `angular2` will be installed, because this package's dependency on it is declared as a
`peerDependency`. This means it will work with whatever (compatible) version of `angular2` is already installed.
- At runtime inside Node.js, the `main` configuration in `package.json` means the developer can use a standard
`import` statement to consume this package (i.e., `import * from 'angular2-aspnet';` in either JS or TS files).
- At development time inside an IDE such as Visual Studio Code, the `typings` configuration in `package.json` means
the IDE will use the corresponding `.d.ts` file as type metadata for the variable imported that way.
2. Use the SystemJS builder to produce the client-side artifacts
`build.js` uses the SystemJS Builder API to combine files in `dist/` into `.js` files ready for use in client-side
SystemJS environments, and puts them in `bundles/`. The bundle files contain `System.register` calls so that any
other part of your client-side code that tries to import `angular2-aspnet` via SystemJS will get that module at runtime.
To make it work in an application:
- Set up some build step that copies your chosen bundle file from `bundles/` to some location where it will
be served to the client
- Below your `<script>` tag that loads SystemJS itself, and above the `<script>` tag that makes the first call to
`System.import`, have a `<script>` tag that loads the desired `angular2-aspnet.js` bundle file
For an example, see https://github.com/aspnet/NodeServices/tree/master/samples/angular/MusicStore
Of course, you can also bundle the `angular2-aspnet.js` file into a larger SystemJS bundle if you want to combine
it with the rest of the code in your application.
Currently, this build system does *not* attempt to send sourcemaps of the original TypeScript to the client. This
could be added if a strong need emerges.

View File

@@ -1,59 +0,0 @@
import { provide, Injectable, Provider } from 'angular2/core';
import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethod, Response, ResponseOptions, ReadyState } from 'angular2/http';
@Injectable()
export class CachePrimedConnectionBackend extends ConnectionBackend {
private _preCachedResponses: PreCachedResponses;
constructor(private _underlyingBackend: ConnectionBackend, private _baseResponseOptions: ResponseOptions) {
super();
this._preCachedResponses = (<any>window).__preCachedResponses || {};
}
public createConnection(request: Request): Connection {
let cacheKey = request.url;
if (request.method === RequestMethod.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) {
return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions);
} else {
return this._underlyingBackend.createConnection(request);
}
}
}
class CacheHitConnection implements Connection {
readyState: ReadyState;
request: Request;
response: any;
constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) {
this.request = req;
this.readyState = ReadyState.Done;
// Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic
// 'require', and not an 'import' statement, because the module isn't available on the server.
let obsCtor: any = require('rxjs/Observable').Observable;
this.response = new obsCtor(responseObserver => {
let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode }));
responseObserver.next(response);
responseObserver.complete();
});
}
}
declare var require: any; // Part of the workaround mentioned below. Can remove this after updating Angular.
interface PreCachedResponses {
[url: string]: PreCachedResponse;
}
interface PreCachedResponse {
statusCode: number;
body: string;
}
export const CACHE_PRIMED_HTTP_PROVIDERS = [
provide(Http, {
useFactory: (xhrBackend, requestOptions, responseOptions) => new Http(new CachePrimedConnectionBackend(xhrBackend, responseOptions), requestOptions),
deps: [XHRBackend, RequestOptions, ResponseOptions]
}),
];

View File

@@ -1,2 +0,0 @@
export * from './CachePrimedHttp';
export * from './Validation';

View File

@@ -1,34 +0,0 @@
import { ControlGroup } from 'angular2/common';
import { Response } from 'angular2/http';
export class Validation {
public static showValidationErrors(response: ValidationErrorResult | Response, controlGroup: ControlGroup): void {
if (response instanceof Response) {
var httpResponse = <Response>response;
response = <ValidationErrorResult>(httpResponse.json());
}
// It's not yet clear whether this is a legitimate and supported use of the ng.ControlGroup API.
// Need feedback from the Angular 2 team on whether there's a better way.
var errors = <ValidationErrorResult>response;
Object.keys(errors || {}).forEach(key => {
errors[key].forEach(errorMessage => {
// If there's a specific control for this key, then use it. Otherwise associate the error
// with the whole control group.
var control = controlGroup.controls[key] || controlGroup;
// This is rough. Need to find out if there's a better way, or if this is even supported.
if (!control.errors) {
(<any>control)._errors = {};
}
control.errors[errorMessage] = true;
});
});
}
}
export interface ValidationErrorResult {
[propertyName: string]: string[];
}

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"sourceMap": false,
"declaration": true,
"experimentalDecorators": true,
"noLib": false,
"outDir": "./dist",
"lib": ["es2015", "dom"]
},
"exclude": [
"node_modules"
]
}

View File

@@ -1,6 +1,6 @@
{
"description": "Helpers for building Angular 2 applications on ASP.NET Core.",
"version": "1.1.0-*",
"version": "1.1.0-beta2-*",
"packOptions": {
"repository": {
"type": "git",

View File

@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
[assembly: AssemblyMetadata("Serviceable", "True")]
[assembly: NeutralResourcesLanguage("en-us")]
[assembly: AssemblyCompany("Microsoft Corporation.")]
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]

View File

@@ -1,6 +1,6 @@
{
"description": "Socket-based RPC for Microsoft.AspNetCore.NodeServices",
"version": "1.1.0-*",
"version": "1.1.0-beta2-*",
"packOptions": {
"repository": {
"type": "git",

View File

@@ -125,7 +125,7 @@
// Signal to the NodeServices base class that we're ready to accept invocations
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
});
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid), /* ignoreSigint */ true);
function readRequestBodyAsJson(request, callback) {
var requestBodyAsString = '';
request.on('data', function (chunk) { requestBodyAsString += chunk; });
@@ -255,7 +255,7 @@
*/
"use strict";
var pollIntervalMs = 1000;
function exitWhenParentExits(parentPid) {
function exitWhenParentExits(parentPid, ignoreSigint) {
setInterval(function () {
if (!processExists(parentPid)) {
// Can't log anything at this point, because out stdout was connected to the parent,
@@ -263,6 +263,24 @@
process.exit();
}
}, pollIntervalMs);
if (ignoreSigint) {
// Pressing ctrl+c in the terminal sends a SIGINT to all processes in the foreground process tree.
// By default, the Node process would then exit before the .NET process, because ASP.NET implements
// a delayed shutdown to allow ongoing requests to complete.
//
// This is problematic, because if Node exits first, the CopyToAsync code in ConditionalProxyMiddleware
// will experience a read fault, and logs a huge load of errors. Fortunately, since the Node process is
// already set up to shut itself down if it detects the .NET process is terminated, all we have to do is
// ignore the SIGINT. The Node process will then terminate automatically after the .NET process does.
//
// A better solution would be to have WebpackDevMiddleware listen for SIGINT and gracefully close any
// ongoing EventSource connections before letting the Node process exit, independently of the .NET
// process exiting. However, doing this well in general is very nontrivial (see all the discussion at
// https://github.com/nodejs/node/issues/2642).
process.on('SIGINT', function () {
console.log('Received SIGINT. Waiting for .NET process to exit...');
});
}
}
exports.exitWhenParentExits = exitWhenParentExits;
function processExists(pid) {

View File

@@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
case "application/octet-stream":
// Streamed responses have to be received as System.IO.Stream instances
if (typeof(T) != typeof(Stream))
if (typeof(T) != typeof(Stream) && typeof(T) != typeof(object))
{
throw new ArgumentException(
"Node module responded with binary stream. This cannot be converted to the requested generic type: " +
@@ -136,4 +136,4 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
}
}
}
}
}

View File

@@ -222,7 +222,7 @@ If you haven't yet installed node-inspector, you can do so as follows:
}
else
{
debuggingArgs = string.Empty;
debuggingArgs = string.Empty;
}
var thisProcessPid = Process.GetCurrentProcess().Id;
@@ -325,17 +325,26 @@ If you haven't yet installed node-inspector, you can do so as follows:
private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
{
var process = Process.Start(startInfo);
try {
var process = Process.Start(startInfo);
// On Mac at least, a killed child process is left open as a zombie until the parent
// captures its exit code. We don't need the exit code for this process, and don't want
// to use process.WaitForExit() explicitly (we'd have to block the thread until it really
// has exited), but we don't want to leave zombies lying around either. It's sufficient
// to use process.EnableRaisingEvents so that .NET will grab the exit code and let the
// zombie be cleaned away without having to block our thread.
process.EnableRaisingEvents = true;
// On Mac at least, a killed child process is left open as a zombie until the parent
// captures its exit code. We don't need the exit code for this process, and don't want
// to use process.WaitForExit() explicitly (we'd have to block the thread until it really
// has exited), but we don't want to leave zombies lying around either. It's sufficient
// to use process.EnableRaisingEvents so that .NET will grab the exit code and let the
// zombie be cleaned away without having to block our thread.
process.EnableRaisingEvents = true;
return process;
return process;
} catch (Exception ex) {
var message = "Failed to start Node process. To resolve this:.\n\n"
+ "[1] Ensure that Node.js is installed and can be found in one of the PATH directories.\n"
+ $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n"
+ " Make sure the Node executable is in one of those directories, or update your PATH.\n\n"
+ "[2] See the InnerException for further details of the cause.";
throw new InvalidOperationException(message, ex);
}
}
private static string UnencodeNewlines(string str)

View File

@@ -67,7 +67,7 @@ public async Task<IActionResult> MyAction([FromServices] INodeServices nodeServi
}
```
Of course, you also need to supply the Node.js code you want to invoke. Create a file called `addNumber.js` at the root of your ASP.NET Core application, and add the following code:
Of course, you also need to supply the Node.js code you want to invoke. Create a file called `addNumbers.js` at the root of your ASP.NET Core application, and add the following code:
```javascript
module.exports = function (callback, first, second) {
@@ -333,25 +333,23 @@ People have asked about using [VroomJS](https://github.com/fogzot/vroomjs) as a
### Built-in hosting models
Normally, you can just use the default hosting model, and not worry about it. But if you have some special requirements, select a hosting model by setting the `HostingModel` property on the `options` object in `AddNodeServices`. Example:
Normally, you can just use the default hosting model, and not worry about it. But if you have some special requirements, you can write your own hosting model, or reference a package that supplies one.
For example, you could use the 'socket' hosting model. It performs RPC between .NET and Node.js using a fast, low-level binary channel rather than the default HTTP transport. To do this, first install the NuGet package `Microsoft.AspNetCore.NodeServices.Sockets`. Then, at the top of your `Startup.cs` file, add:
```csharp
services.AddNodeServices(options =>
{
options.HostingModel = NodeHostingModel.Socket;
using Microsoft.AspNetCore.NodeServices.Sockets;
```
...then in your `Startup.cs` file's `ConfigureServices` method, you can configure:
```csharp
services.AddNodeServices(options => {
options.UseSocketHosting();
});
```
**Available hosting models**
* `Socket`
* Launches Node as a separate process, and communicates with it using named pipes (on Windows) or domain sockets (on Linux / OS X).
* This is faster than `Http` because it uses a low-level binary protocol with very low overhead. It retains one continuous connection for the whole lifetime of the Node instance, so it doesn't have to keep waiting for new connections to open.
* `Http` (default)
* Launches Node as a separate process, and communicates with it by making HTTP requests.
* This primarily exists because it was implemented before `Socket`, but there's no particular reason to use it now that `Socket` is available. It could theoretically be useful if you wanted to run Node instances on separate servers (though there isn't currently any built-in API for configuring that).
The default transport may change from `Http` to `Socket` in the near future, because it's faster.
Now when you run your application, it will use the socket-based hosting and transport mechanism. In the past, the socket transport was faster than HTTP, but since .NET Core 1.1 improved the performance of `HttpClient` there isn't really any speed difference any more, so there's no longer any significant advantage to using `Microsoft.AspNetCore.NodeServices.Sockets`.
### Custom hosting models

View File

@@ -76,7 +76,7 @@ server.listen(requestedPortOrZero, 'localhost', function () {
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
});
exitWhenParentExits(parseInt(parsedArgs.parentPid));
exitWhenParentExits(parseInt(parsedArgs.parentPid), /* ignoreSigint */ true);
function readRequestBodyAsJson(request, callback) {
let requestBodyAsString = '';

View File

@@ -34,7 +34,7 @@ to check whether the parent PID is still running. So that's what we do here.
const pollIntervalMs = 1000;
export function exitWhenParentExits(parentPid: number) {
export function exitWhenParentExits(parentPid: number, ignoreSigint: boolean) {
setInterval(() => {
if (!processExists(parentPid)) {
// Can't log anything at this point, because out stdout was connected to the parent,
@@ -42,6 +42,25 @@ export function exitWhenParentExits(parentPid: number) {
process.exit();
}
}, pollIntervalMs);
if (ignoreSigint) {
// Pressing ctrl+c in the terminal sends a SIGINT to all processes in the foreground process tree.
// By default, the Node process would then exit before the .NET process, because ASP.NET implements
// a delayed shutdown to allow ongoing requests to complete.
//
// This is problematic, because if Node exits first, the CopyToAsync code in ConditionalProxyMiddleware
// will experience a read fault, and logs a huge load of errors. Fortunately, since the Node process is
// already set up to shut itself down if it detects the .NET process is terminated, all we have to do is
// ignore the SIGINT. The Node process will then terminate automatically after the .NET process does.
//
// A better solution would be to have WebpackDevMiddleware listen for SIGINT and gracefully close any
// ongoing EventSource connections before letting the Node process exit, independently of the .NET
// process exiting. However, doing this well in general is very nontrivial (see all the discussion at
// https://github.com/nodejs/node/issues/2642).
process.on('SIGINT', () => {
console.log('Received SIGINT. Waiting for .NET process to exit...');
});
}
}
function processExists(pid: number) {
@@ -56,7 +75,7 @@ function processExists(pid: number) {
if (ex.code === 'EPERM') {
throw new Error(`Attempted to check whether process ${pid} was running, but got a permissions error.`);
}
return false;
}
}

View File

@@ -1,6 +1,6 @@
{
"description": "Invoke Node.js modules at runtime in ASP.NET Core applications.",
"version": "1.1.0-*",
"version": "1.1.0-rc1-*",
"packOptions": {
"repository": {
"type": "git",

View File

@@ -1,3 +0,0 @@
/node_modules/
/*.js
/*.d.ts

View File

@@ -1,3 +0,0 @@
!/*.js
!/*.d.ts
/typings/

View File

@@ -1,12 +0,0 @@
Copyright (c) .NET Foundation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
these files except in compliance with the License. You may obtain a copy of the
License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

View File

@@ -1,27 +0,0 @@
{
"name": "redux-typed",
"version": "2.0.0",
"description": "Helpers for building React+Redux apps with strong TypeScript type checking everywhere",
"main": "main.js",
"typings": "main.d.ts",
"scripts": {
"prepublish": "rimraf *.d.ts && tsc && echo 'Finished building NPM package \"redux-typed\"'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Microsoft",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/aspnet/JavaScriptServices/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/aspnet/JavaScriptServices.git"
},
"devDependencies": {
"@types/react": "^0.14.38",
"@types/react-redux": "^4.4.32",
"@types/redux": "^3.5.30",
"rimraf": "^2.5.4",
"typescript": "^2.0.0"
}
}

View File

@@ -1,24 +0,0 @@
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
if (typeof (<any>Object).assign != 'function') {
(function () {
(<any>Object).assign = function (target) {
'use strict';
if (target === undefined || target === null) {
throw new TypeError('Cannot convert undefined or null to object');
}
var output = Object(target);
for (var index = 1; index < arguments.length; index++) {
var source = arguments[index];
if (source !== undefined && source !== null) {
for (var nextKey in source) {
if (source.hasOwnProperty(nextKey)) {
output[nextKey] = source[nextKey];
}
}
}
}
return output;
};
})();
}

View File

@@ -1,42 +0,0 @@
// Credits for the type detection technique: http://www.bluewire-technologies.com/2015/redux-actions-for-typescript/
import * as React from 'react';
import { Dispatch } from 'redux';
import './ObjectAssignPolyfill';
export interface ActionClass<T extends Action> {
prototype: T;
}
export function typeName(name: string) {
return function<T extends Action>(actionClass: ActionClass<T>) {
// Although we could determine the type name using actionClass.prototype.constructor.name,
// it's dangerous to do that because minifiers may interfere with it, and then serialized state
// might not have the expected meaning after a recompile. So we explicitly ask for a name string.
actionClass.prototype.type = name;
}
}
export function isActionType<T extends Action>(action: Action, actionClass: ActionClass<T>): action is T {
return action.type == actionClass.prototype.type;
}
// Middleware for transforming Typed Actions into plain actions
export const typedToPlain = (store: any) => (next: any) => (action: any) => {
next((<any>Object).assign({}, action));
};
export abstract class Action {
type: string;
constructor() {
// Make it an own-property (not a prototype property) so that it's included when JSON-serializing
this.type = this.type;
}
}
export interface Reducer<TState> extends Function {
(state: TState, action: Action): TState;
}
export interface ActionCreatorGeneric<TState> extends Function {
(dispatch: Dispatch<TState>, getState: () => TState): any;
}

View File

@@ -1,22 +0,0 @@
import * as React from 'react';
import { connect as nativeConnect } from 'react-redux';
export type ReactComponentClass<T, S> = new(props: T) => React.Component<T, S>;
export class ComponentBuilder<TOwnProps, TActions, TExternalProps> {
constructor(private stateToProps: (appState: any) => TOwnProps, private actionCreators: TActions) {
}
public withExternalProps<TAddExternalProps>() {
return this as any as ComponentBuilder<TOwnProps, TActions, TAddExternalProps>;
}
public get allProps(): TOwnProps & TActions & TExternalProps { return null; }
public connect<TState>(componentClass: ReactComponentClass<TOwnProps & TActions & TExternalProps, TState>): ReactComponentClass<TExternalProps, TState> {
return nativeConnect(this.stateToProps, this.actionCreators as any)(componentClass) as any;
}
}
export function provide<TOwnProps, TActions>(stateToProps: (appState: any) => TOwnProps, actionCreators: TActions) {
return new ComponentBuilder<TOwnProps, TActions, {}>(stateToProps, actionCreators);
}

View File

@@ -1,2 +0,0 @@
export * from './StrongActions';
export * from './StrongProvide';

View File

@@ -1,16 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"target": "es5",
"declaration": true,
"outDir": ".",
"lib": ["es2015", "dom"]
},
"files": [
"src/main.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -1,6 +1,6 @@
{
"description": "Helpers for building React applications on ASP.NET Core.",
"version": "1.1.0-*",
"version": "1.1.0-beta2-*",
"packOptions": {
"repository": {
"type": "git",

View File

@@ -538,7 +538,7 @@ module.exports = {
output: {
path: path.join(__dirname, 'wwwroot', 'dist'),
publicPath: '/dist',
publicPath: '/dist/',
filename: '[name].js'
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "aspnet-webpack",
"version": "1.0.25",
"version": "1.0.26",
"description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
"main": "index.js",
"scripts": {

View File

@@ -108,7 +108,7 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati
// file on disk wouldn't match the file served to the browser, and the source map line numbers wouldn't
// match up. Breakpoints would either not be hit, or would hit the wrong lines.
(compiler as any).plugin('done', stats => {
copyRecursiveToRealFsSync(compiler.outputFileSystem, '/', [/\.hot-update\.(js|json)$/]);
copyRecursiveToRealFsSync(compiler.outputFileSystem, '/', [/\.hot-update\.(js|json|js\.map)$/]);
});
if (enableHotModuleReplacement) {

View File

@@ -1,6 +1,6 @@
{
"description": "Helpers for building single-page applications on ASP.NET MVC Core",
"version": "1.1.0-*",
"version": "1.1.0-rc1-*",
"packOptions": {
"repository": {
"type": "git",

View File

@@ -0,0 +1,2 @@
[config]
SCM_SCRIPT_GENERATOR_ARGS=--aspNetCore project.json

View File

@@ -0,0 +1,90 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<GlobalExclude>$(GlobalExclude);bin\**;obj\**;node_modules\**;**\*.user;**\*.suo;**\*.*proj;.git\**</GlobalExclude>
</PropertyGroup>
<ItemGroup>
<None Include="**\*" Exclude="$(GlobalExclude)"/>
<Compile Include="**\*.cs" Exclude="$(GlobalExclude)" />
<EmbeddedResource Include="**\*.resx" Exclude="$(GlobalExclude)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk.Web">
<Version>1.0.0-alpha-20161104-2-112</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Razor.Tools">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SpaServices">
<Version>1.1.0-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions">
<Version>1.1.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<!-- Files not to show in IDE -->
<None Remove=".bowerrc" />
<None Remove="yarn.lock" />
<!-- Files not to publish (note that the 'dist' subfolders are re-added below) -->
<Content Remove="ClientApp\**" />
</ItemGroup>
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

View File

@@ -5,7 +5,7 @@ import { AppComponent } from './components/app/app.component'
import { NavMenuComponent } from './components/navmenu/navmenu.component';
import { HomeComponent } from './components/home/home.component';
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
import { CounterComponent } from 'ClientApp/app/components/counter/counter.component';
import { CounterComponent } from './components/counter/counter.component';
@NgModule({
bootstrap: [ AppComponent ],

View File

@@ -1,4 +1,4 @@
<h2>Counter</h2>
<h1>Counter</h1>
<p>This is a simple example of an Angular 2 component.</p>

View File

@@ -13,7 +13,7 @@ describe('Counter component', () => {
});
it('should display a title', async(() => {
const titleText = fixture.nativeElement.querySelector('h2').textContent;
const titleText = fixture.nativeElement.querySelector('h1').textContent;
expect(titleText).toEqual('Counter');
}));

View File

@@ -18,6 +18,7 @@ module.exports = function (config) {
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
mime: { 'application/javascript': ['ts','tsx'] },
singleRun: false,
webpack: require('../../webpack.config.js').filter(config => config.target !== 'node'), // Test against client bundle, because tests run in a browser
webpackMiddleware: { stats: 'errors-only' }

View File

@@ -27,7 +27,7 @@
"es6-shim": "^0.35.1",
"event-source-polyfill": "^0.0.7",
"expose-loader": "^0.7.1",
"extract-text-webpack-plugin": "^2.0.0-beta",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"html-loader": "^0.4.4",
"isomorphic-fetch": "^2.2.1",
@@ -39,10 +39,10 @@
"rxjs": "5.0.0-beta.12",
"style-loader": "^0.13.1",
"to-string-loader": "^1.1.5",
"ts-loader": "^1.3.0",
"typescript": "^2.1.0",
"ts-loader": "^0.8.2",
"typescript": "^2.0.3",
"url-loader": "^0.5.7",
"webpack": "^2.1.0-beta",
"webpack": "^1.13.2",
"webpack-hot-middleware": "^2.12.2",
"webpack-merge": "^0.14.1",
"zone.js": "^0.6.25"

View File

@@ -1,11 +0,0 @@
------------------------------------------------------------------
Don't delete this file. Do include it in your source control repo.
------------------------------------------------------------------
This file exists as a workaround for https://github.com/dotnet/cli/issues/1396
('dotnet publish' does not publish any directories that didn't exist or were
empty before the publish script started, which means it's not enough just to
run 'npm install' during publishing: you need to ensure node_modules already
existed at least).
Hopefully, this can be removed after the move to the new MSBuild.

View File

@@ -1,11 +1,5 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*"
]
},
"moduleResolution": "node",
"target": "es5",
"sourceMap": true,

View File

@@ -6,13 +6,7 @@ var merge = require('webpack-merge');
// Configuration in common to both client-side and server-side bundles
var sharedConfig = {
context: __dirname,
resolve: {
extensions: [ '.js', '.ts' ],
modules: [
'node_modules',
'.'
]
},
resolve: { extensions: [ '', '.js', '.ts' ] },
output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
@@ -22,7 +16,8 @@ var sharedConfig = {
{ test: /\.ts$/, include: /ClientApp/, loaders: ['ts-loader?silent=true', 'angular2-template-loader'] },
{ test: /\.html$/, loader: 'html-loader?minimize=false' },
{ test: /\.css$/, loader: 'to-string-loader!css-loader' },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader', query: { limit: 25000 } }
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader', query: { limit: 25000 } },
{ test: /\.json$/, loader: 'json-loader' }
]
}
};
@@ -52,7 +47,7 @@ var clientBundleConfig = merge(sharedConfig, {
// Configuration for server-side (prerendering) bundle suitable for running in Node
var serverBundleConfig = merge(sharedConfig, {
resolve: { mainFields: ['main'] },
resolve: { packageMains: ['main'] },
entry: { 'main-server': './ClientApp/boot-server.ts' },
plugins: [
new webpack.DllReferencePlugin({

View File

@@ -6,7 +6,7 @@ var merge = require('webpack-merge');
var extractCSS = new ExtractTextPlugin('vendor.css');
var sharedConfig = {
resolve: { extensions: [ '.js' ] },
resolve: { extensions: [ '', '.js' ] },
module: {
loaders: [
{ test: /\.json$/, loader: require.resolve('json-loader') },
@@ -68,7 +68,7 @@ var clientBundleConfig = merge(sharedConfig, {
var serverBundleConfig = merge(sharedConfig, {
target: 'node',
resolve: { mainFields: ['main'] },
resolve: { packageMains: ['main'] },
output: {
path: path.join(__dirname, 'ClientApp', 'dist'),
libraryTarget: 'commonjs2',

View File

@@ -0,0 +1,2 @@
[config]
SCM_SCRIPT_GENERATOR_ARGS=--aspNetCore project.json

View File

@@ -0,0 +1,87 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<GlobalExclude>$(GlobalExclude);bin\**;obj\**;node_modules\**;**\*.user;**\*.suo;**\*.*proj;.git\**</GlobalExclude>
</PropertyGroup>
<ItemGroup>
<None Include="**\*" Exclude="$(GlobalExclude)"/>
<Compile Include="**\*.cs" Exclude="$(GlobalExclude)" />
<EmbeddedResource Include="**\*.resx" Exclude="$(GlobalExclude)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk.Web">
<Version>1.0.0-alpha-20161104-2-112</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Razor.Tools">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SpaServices">
<Version>1.1.0-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions">
<Version>1.1.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<!-- Files not to show in IDE -->
<None Remove=".bowerrc" />
<None Remove="yarn.lock" />
</ItemGroup>
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

View File

@@ -1,5 +1,5 @@
<template>
<h2>Counter</h2>
<h1>Counter</h1>
<p>This is a simple example of an Aurelia component.</p>

View File

@@ -33,6 +33,7 @@
"file-loader": "^0.9.0",
"html-loader": "^0.4.4",
"html-webpack-plugin": "^2.22.0",
"json-loader": "^0.5.4",
"raw-loader": "^0.5.1",
"style-loader": "^0.13.1",
"to-string-loader": "^1.1.5",

View File

@@ -1,11 +0,0 @@
------------------------------------------------------------------
Don't delete this file. Do include it in your source control repo.
------------------------------------------------------------------
This file exists as a workaround for https://github.com/dotnet/cli/issues/1396
('dotnet publish' does not publish any directories that didn't exist or were
empty before the publish script started, which means it's not enough just to
run 'npm install' during publishing: you need to ensure node_modules already
existed at least).
Hopefully, this can be removed after the move to the new MSBuild.

View File

@@ -17,7 +17,8 @@ module.exports = {
{ test: /\.ts$/, include: /ClientApp/, loader: 'ts-loader', query: { silent: true } },
{ test: /\.html$/, loader: 'html-loader' },
{ test: /\.css$/, loaders: [ 'style-loader', 'css-loader' ] },
{ test: /\.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000' }
{ test: /\.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000' },
{ test: /\.json$/, loader: 'json-loader' }
]
},
plugins: [

View File

@@ -0,0 +1,2 @@
[config]
SCM_SCRIPT_GENERATOR_ARGS=--aspNetCore project.json

View File

@@ -1,4 +1,4 @@
<h2>Counter</h2>
<h1>Counter</h1>
<p>This is a simple example of a Knockout component.</p>

View File

@@ -0,0 +1,87 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<GlobalExclude>$(GlobalExclude);bin\**;obj\**;node_modules\**;**\*.user;**\*.suo;**\*.*proj;.git\**</GlobalExclude>
</PropertyGroup>
<ItemGroup>
<None Include="**\*" Exclude="$(GlobalExclude)"/>
<Compile Include="**\*.cs" Exclude="$(GlobalExclude)" />
<EmbeddedResource Include="**\*.resx" Exclude="$(GlobalExclude)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk.Web">
<Version>1.0.0-alpha-20161104-2-112</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Razor.Tools">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SpaServices">
<Version>1.1.0-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions">
<Version>1.1.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<!-- Files not to show in IDE -->
<None Remove=".bowerrc" />
<None Remove="yarn.lock" />
</ItemGroup>
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

View File

@@ -23,6 +23,7 @@
"history": "^4.3.0",
"isomorphic-fetch": "^2.2.1",
"jquery": "^2.2.1",
"json-loader": "^0.5.4",
"knockout": "^3.4.0",
"raw-loader": "^0.5.1",
"style-loader": "^0.13.1",

View File

@@ -1,7 +0,0 @@
This file exists as a workaround for https://github.com/dotnet/cli/issues/1396
('dotnet publish' does not publish any directories that didn't exist or were
empty before the publish script started, which means it's not enough just to
run 'npm install' during publishing: you need to ensure node_modules already
existed at least).
Hopefully, this can be removed after the move to the new MSBuild.

View File

@@ -17,7 +17,8 @@ module.exports = {
{ test: /\.ts$/, include: /ClientApp/, loader: 'ts-loader', query: { silent: true } },
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.css$/, loader: isDevBuild ? 'style-loader!css-loader' : ExtractTextPlugin.extract(['css-loader']) },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader', query: { limit: 25000 } }
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader', query: { limit: 25000 } },
{ test: /\.json$/, loader: 'json-loader' }
]
},
plugins: [

View File

@@ -0,0 +1,2 @@
[config]
SCM_SCRIPT_GENERATOR_ARGS=--aspNetCore project.json

View File

@@ -8,6 +8,15 @@ import { syncHistoryWithStore } from 'react-router-redux';
import routes from './routes';
import configureStore from './configureStore';
import { ApplicationState } from './store';
import cookie from 'react-cookie';
// If the server supplied any edits to cookies, apply them on the client
const cookieDataFromServer = window['cookieData'];
if (cookieDataFromServer) {
Object.getOwnPropertyNames(cookieDataFromServer).forEach(name => {
cookie.save(name, cookieDataFromServer[name]);
});
}
// Get the application-wide store instance, prepopulating with state from the server where available.
const initialState = (window as any).initialReduxState as ApplicationState;

View File

@@ -6,9 +6,28 @@ import createMemoryHistory from 'history/lib/createMemoryHistory';
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
import routes from './routes';
import configureStore from './configureStore';
import cookieUtil from 'cookie';
import cookie from 'react-cookie';
function plugInCookiesFromDotNet(cookieData: { key: string, value: string }[], res) {
const formattedData = {};
cookieData.forEach(keyValuePair => {
formattedData[keyValuePair.key] = keyValuePair.value;
});
cookie.plugToRequest({ cookies: formattedData }, res);
}
export default createServerRenderer(params => {
return new Promise<RenderResult>((resolve, reject) => {
const cookiesModifiedOnServer = {};
if (params.data.cookies) {
// If we received some cookie data, use that to prepopulate 'react-cookie'
plugInCookiesFromDotNet(params.data.cookies, {
// Also track any cookies written on the server
cookie: (name, val) => { cookiesModifiedOnServer[name] = val; }
})
}
// Match the incoming request against the list of client-side routes
match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => {
if (error) {
@@ -42,7 +61,17 @@ export default createServerRenderer(params => {
params.domainTasks.then(() => {
resolve({
html: renderToString(app),
globals: { initialReduxState: store.getState() }
globals: {
initialReduxState: store.getState(),
// Send any cookies written during server-side prerendering to the client.
// WARNING: Do not pass any security-sensitive cookies this way, because they will become
// readable in the HTML source. If your goal is to use this approach to manage authentication
// cookies, then be sure *not* to use 'globals' to send them to the client - instead, invoke
// Microsoft.AspNetCore.SpaServices.Prerendering.Prerender directly from your .NET code and
// only send the 'html' part back to the client (or at least, not all of the 'globals').
cookieData: cookiesModifiedOnServer
}
});
}, reject); // Also propagate any errors back into the host application
});

View File

@@ -1,4 +1,5 @@
import { Action, Reducer, ThunkAction } from 'redux';
import cookie from 'react-cookie';
// -----------------
// STATE - This defines the type of data maintained in the Redux store.
@@ -30,13 +31,20 @@ export const actionCreators = {
// ----------------
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
const cookieKey = 'counterValue';
function modifiedCount(state: CounterState, delta: number): CounterState {
const newCount = state.count + delta;
cookie.save(cookieKey, newCount); // Ideally, don't do this here: have something that watches the store instead of having a side-effect from a reducer
return { count: newCount };
}
export const reducer: Reducer<CounterState> = (state: CounterState, action: KnownAction) => {
switch (action.type) {
case 'INCREMENT_COUNT':
return { count: state.count + 1 };
return modifiedCount(state, +1);
case 'DECREMENT_COUNT':
return { count: state.count - 1 };
return modifiedCount(state, -1);
default:
// The following line guarantees that every action in the KnownAction union has been covered by a case above
const exhaustiveCheck: never = action;
@@ -44,5 +52,6 @@ export const reducer: Reducer<CounterState> = (state: CounterState, action: Know
// For unrecognized actions (or in cases where actions have no effect), must return the existing state
// (or default initial state if none was supplied)
return state || { count: 0 };
const prevCountFromCookie = parseInt(cookie.load(cookieKey) || '0');
return state || { count: prevCountFromCookie };
};

View File

@@ -0,0 +1,90 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<GlobalExclude>$(GlobalExclude);bin\**;obj\**;node_modules\**;**\*.user;**\*.suo;**\*.*proj;.git\**</GlobalExclude>
</PropertyGroup>
<ItemGroup>
<None Include="**\*" Exclude="$(GlobalExclude)"/>
<Compile Include="**\*.cs" Exclude="$(GlobalExclude)" />
<EmbeddedResource Include="**\*.resx" Exclude="$(GlobalExclude)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk.Web">
<Version>1.0.0-alpha-20161104-2-112</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Razor.Tools">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SpaServices">
<Version>1.1.0-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions">
<Version>1.1.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<!-- Files not to show in IDE -->
<None Remove=".bowerrc" />
<None Remove="yarn.lock" />
<!-- Files not to publish (note that the 'dist' subfolders are re-added below) -->
<Content Remove="ClientApp\**" />
</ItemGroup>
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

View File

@@ -2,7 +2,8 @@
ViewData["Title"] = "Home Page";
}
<div id="react-app" asp-prerender-module="ClientApp/dist/main-server">Loading...</div>
<div id="react-app" asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data="new { cookies = ViewContext.HttpContext.Request.Cookies }">Loading...</div>
@section scripts {
<script src="~/dist/main-client.js" asp-append-version="true"></script>

View File

@@ -2,6 +2,7 @@
"name": "WebApplicationBasic",
"version": "0.0.0",
"dependencies": {
"@types/history": "^2.0.0",
"@types/react": "^0.14.29",
"@types/react-dom": "^0.14.14",
"@types/react-redux": "^4.4.29",
@@ -31,6 +32,7 @@
"json-loader": "^0.5.4",
"node-noop": "^1.0.0",
"react": "^15.3.2",
"react-cookie": "^1.0.4",
"react-dom": "^15.3.2",
"react-redux": "^4.4.5",
"react-router": "^2.8.1",
@@ -39,7 +41,7 @@
"redux-thunk": "^2.1.0",
"style-loader": "^0.13.0",
"ts-loader": "^0.8.1",
"typescript": "2.0.3",
"typescript": "^2.0.3",
"url-loader": "^0.5.7",
"webpack": "^1.13.2",
"webpack-hot-middleware": "^2.12.2",

View File

@@ -1,7 +0,0 @@
This file exists as a workaround for https://github.com/dotnet/cli/issues/1396
('dotnet publish' does not publish any directories that didn't exist or were
empty before the publish script started, which means it's not enough just to
run 'npm install' during publishing: you need to ensure node_modules already
existed at least).
Hopefully, this can be removed after the move to the new MSBuild.

View File

@@ -10,8 +10,11 @@
"lib": ["es6", "dom"],
"types": [ "webpack-env", "whatwg-fetch" ],
"paths": {
// Fixes "Duplicate identifier 'Redux'" error when packages were installed via Yarn (by specifying which .d.ts file is authoritative)
"redux": ["./node_modules/@types/redux/index"]
// Fix "Duplicate identifier" errors caused by multiple dependencies fetching their own copies of type definitions.
// We tell TypeScript which type definitions module to treat as the canonical one (instead of combining all of them).
"history": ["./node_modules/@types/history/index"],
"redux": ["./node_modules/@types/redux/index"],
"react": ["./node_modules/@types/react/index"]
}
},
"exclude": [

View File

@@ -14,7 +14,8 @@ var sharedConfig = () => ({
module: {
loaders: [
{ test: /\.tsx?$/, include: /ClientApp/, loader: 'babel-loader' },
{ test: /\.tsx?$/, include: /ClientApp/, loader: 'ts-loader', query: { silent: true } }
{ test: /\.tsx?$/, include: /ClientApp/, loader: 'ts-loader', query: { silent: true } },
{ test: /\.json$/, loader: 'json-loader' }
]
}
});

View File

@@ -0,0 +1,2 @@
[config]
SCM_SCRIPT_GENERATOR_ARGS=--aspNetCore project.json

View File

@@ -0,0 +1,87 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
<PreserveCompilationContext>true</PreserveCompilationContext>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<GlobalExclude>$(GlobalExclude);bin\**;obj\**;node_modules\**;**\*.user;**\*.suo;**\*.*proj;.git\**</GlobalExclude>
</PropertyGroup>
<ItemGroup>
<None Include="**\*" Exclude="$(GlobalExclude)"/>
<Compile Include="**\*.cs" Exclude="$(GlobalExclude)" />
<EmbeddedResource Include="**\*.resx" Exclude="$(GlobalExclude)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.App">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.NET.Sdk.Web">
<Version>1.0.0-alpha-20161104-2-112</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Razor.Tools">
<Version>1.1.0-preview4-final</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SpaServices">
<Version>1.1.0-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.Json">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug">
<Version>1.1.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions">
<Version>1.1.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<!-- Files not to show in IDE -->
<None Remove=".bowerrc" />
<None Remove="yarn.lock" />
</ItemGroup>
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

View File

@@ -20,6 +20,7 @@
"file-loader": "^0.9.0",
"isomorphic-fetch": "^2.2.1",
"jquery": "^2.2.1",
"json-loader": "^0.5.4",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-router": "^2.8.1",

View File

@@ -1,7 +0,0 @@
This file exists as a workaround for https://github.com/dotnet/cli/issues/1396
('dotnet publish' does not publish any directories that didn't exist or were
empty before the publish script started, which means it's not enough just to
run 'npm install' during publishing: you need to ensure node_modules already
existed at least).
Hopefully, this can be removed after the move to the new MSBuild.

View File

@@ -1,10 +1,17 @@
{
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "node",
"target": "es6",
"jsx": "preserve",
"sourceMap": true,
"skipDefaultLibCheck": true
"skipDefaultLibCheck": true,
"paths": {
// Fix "Duplicate identifier" errors caused by multiple dependencies fetching their own copies of type definitions.
// We tell TypeScript which type definitions module to treat as the canonical one (instead of combining all of them).
"history": ["./node_modules/@types/history/index"],
"react": ["./node_modules/@types/react/index"]
}
},
"exclude": [
"bin",

View File

@@ -18,7 +18,8 @@ module.exports = {
{ test: /\.ts(x?)$/, include: /ClientApp/, loader: 'babel-loader' },
{ test: /\.tsx?$/, include: /ClientApp/, loader: 'ts-loader', query: { silent: true } },
{ test: /\.css$/, loader: isDevBuild ? 'style-loader!css-loader' : ExtractTextPlugin.extract(['css-loader']) },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader', query: { limit: 25000 } }
{ test: /\.(png|jpg|jpeg|gif|svg)$/, loader: 'url-loader', query: { limit: 25000 } },
{ test: /\.json$/, loader: 'json-loader' }
]
},
plugins: [

View File

@@ -1,7 +0,0 @@
This file exists as a workaround for https://github.com/dotnet/cli/issues/1396
('dotnet publish' does not publish any directories that didn't exist or were
empty before the publish script started, which means it's not enough just to
run 'npm install' during publishing: you need to ensure node_modules already
existed at least).
Hopefully, this can be removed after the move to the new MSBuild.

View File

@@ -10,6 +10,7 @@
"author": "Microsoft",
"license": "Apache-2.0",
"dependencies": {
"@types/chalk": "^0.4.31",
"@types/semver": "^5.3.30",
"diff": "^2.2.2",
"gitignore-parser": "0.0.2",
@@ -17,7 +18,8 @@
"lodash": "^4.11.1",
"mkdirp": "^0.5.1",
"rimraf": "^2.5.2",
"semver": "^5.3.0"
"semver": "^5.3.0",
"tar.gz": "^1.0.5"
},
"devDependencies": {
"@types/glob": "^5.0.30",

View File

@@ -6,13 +6,23 @@ import * as _ from 'lodash';
import * as mkdirp from 'mkdirp';
import * as rimraf from 'rimraf';
import * as childProcess from 'child_process';
import * as targz from 'tar.gz';
const isWindows = /^win/.test(process.platform);
const textFileExtensions = ['.gitignore', 'template_gitignore', '.config', '.cs', '.cshtml', 'Dockerfile', '.html', '.js', '.json', '.jsx', '.md', '.nuspec', '.ts', '.tsx', '.xproj'];
const textFileExtensions = ['.gitignore', 'template_gitignore', '.config', '.cs', '.cshtml', '.csproj', 'Dockerfile', '.html', '.js', '.json', '.jsx', '.md', '.nuspec', '.ts', '.tsx', '.xproj'];
const yeomanGeneratorSource = './src/yeoman';
const templates: { [key: string]: { dir: string, dotNetNewId: string, displayName: string, forceInclusion?: RegExp } } = {
'angular-2': { dir: '../../templates/Angular2Spa/', dotNetNewId: 'Angular', displayName: 'Angular 2', forceInclusion: /^(wwwroot|ClientApp)\/dist\// },
// For the Angular 2 template, we want to bundle prebuilt dist dev-mode files, because the VS template can't auto-run
// webpack on project creation. Note that these script entries are *not* the same as the project's usual prepublish
// scripts, because here we want dev-mode builds (e.g., to support HMR), not prod-mode builds.
const runWebpackInDevModeScripts = [
'npm install',
'node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js',
'node node_modules/webpack/bin/webpack.js'
];
const templates: { [key: string]: { dir: string, dotNetNewId: string, displayName: string, prepublish?: string[], forceInclusion?: RegExp } } = {
'angular-2': { dir: '../../templates/Angular2Spa/', dotNetNewId: 'Angular', displayName: 'Angular 2', prepublish: runWebpackInDevModeScripts, forceInclusion: /^(wwwroot|ClientApp)\/dist\// },
'aurelia': { dir: '../../templates/AureliaSpa/', dotNetNewId: 'Aurelia', displayName: 'Aurelia' },
'knockout': { dir: '../../templates/KnockoutSpa/', dotNetNewId: 'Knockout', displayName: 'Knockout.js' },
'react-redux': { dir: '../../templates/ReactReduxSpa/', dotNetNewId: 'ReactRedux', displayName: 'React.js and Redux' },
@@ -72,21 +82,25 @@ function copyRecursive(sourceRoot: string, destRoot: string, matchGlob: string)
});
}
function buildYeomanNpmPackage() {
const outputRoot = './dist/generator-aspnetcore-spa';
function buildYeomanNpmPackage(outputRoot: string) {
const outputTemplatesRoot = path.join(outputRoot, 'app/templates');
rimraf.sync(outputTemplatesRoot);
// Copy template files
const filenameReplacements = [
{ from: /.*\.xproj$/, to: 'tokenreplace-namePascalCase.xproj' }
{ from: /.*\.xproj$/, to: 'tokenreplace-namePascalCase.xproj' },
{ from: /.*\.csproj$/, to: 'tokenreplace-namePascalCase.csproj' }
];
const contentReplacements = [
// .xproj items
{ from: /\bWebApplicationBasic\b/g, to: '<%= namePascalCase %>' },
{ from: /<ProjectGuid>[0-9a-f\-]{36}<\/ProjectGuid>/g, to: '<ProjectGuid><%= projectGuid %></ProjectGuid>' },
{ from: /<RootNamespace>.*?<\/RootNamespace>/g, to: '<RootNamespace><%= namePascalCase %></RootNamespace>'},
{ from: /\s*<BaseIntermediateOutputPath.*?<\/BaseIntermediateOutputPath>/g, to: '' },
{ from: /\s*<OutputPath.*?<\/OutputPath>/g, to: '' },
// global.json items
{ from: /1\.0\.0-preview2-1-003177/, to: '<%= sdkVersion %>' }
];
_.forEach(templates, (templateConfig, templateName) => {
const outputDir = path.join(outputTemplatesRoot, templateName);
@@ -110,12 +124,9 @@ function buildDotNetNewNuGetPackage() {
const sourceProjectName = 'WebApplicationBasic';
const projectGuid = '00000000-0000-0000-0000-000000000000';
const filenameReplacements = [
// TODO: For dotnetnew templates, switch to csproj. No need for SDK choice as it can be Preview3+ only.
{ from: /.*\.xproj$/, to: `${sourceProjectName}.xproj` },
{ from: /\btemplate_gitignore$/, to: '.gitignore' },
// Workaround for https://github.com/aspnet/JavaScriptServices/issues/235
// For details, see the comment in ../yeoman/app/index.ts
{ from: /\btemplate_nodemodules_placeholder.txt$/, to: 'node_modules/_placeholder.txt' }
{ from: /\btemplate_gitignore$/, to: '.gitignore' }
];
const contentReplacements = [
{ from: /<ProjectGuid>[0-9a-f\-]{36}<\/ProjectGuid>/g, to: `<ProjectGuid>${projectGuid}</ProjectGuid>` },
@@ -161,21 +172,37 @@ function buildDotNetNewNuGetPackage() {
rimraf.sync('./tmp');
}
// TODO: Instead of just showing this warning, improve build script so it actually does build them
// in the correct format. Can do this once we've moved away from using ASPNETCORE_ENVIRONMENT to
// control the build output mode. The templates we warn about here are the ones where we ship some
// files that wouldn't normally be under source control (e.g., /wwwroot/dist/*).
const templatesWithForceIncludes = Object.getOwnPropertyNames(templates)
.filter(templateName => !!templates[templateName].forceInclusion);
if (templatesWithForceIncludes.length > 0) {
console.warn(`
---
WARNING: Ensure that the following templates are already built in the configuration desired for publishing.
For example, build the dist files in debug mode.
TEMPLATES: ${templatesWithForceIncludes.join(', ')}
---
`);
function runAllPrepublishScripts() {
Object.getOwnPropertyNames(templates).forEach(templateKey => {
const templateInfo = templates[templateKey];
if (templateInfo.prepublish) {
runScripts(templateInfo.dir, templateInfo.prepublish);
}
});
}
buildYeomanNpmPackage();
function runScripts(rootDir: string, scripts: string[]) {
console.log(`[Prepublish] In directory: ${ rootDir }`);
scripts.forEach(script => {
console.log(`[Prepublish] Running: ${ script }`);
childProcess.execSync(script, { cwd: rootDir, stdio: 'inherit' });
});
console.log(`[Prepublish] Done`)
}
const distDir = './dist';
const artifactsDir = path.join(distDir, 'artifacts');
const yeomanOutputRoot = path.join(distDir, 'generator-aspnetcore-spa');
rimraf.sync(distDir);
mkdirp.sync(artifactsDir);
runAllPrepublishScripts();
buildYeomanNpmPackage(yeomanOutputRoot);
buildDotNetNewNuGetPackage();
// Finally, create a .tar.gz file containing the built generator-aspnetcore-spa.
// The CI system can treat this as the final built artifact.
// Note that the targz APIs only come in async flavor.
targz().compress(yeomanOutputRoot, path.join(artifactsDir, 'generator-aspnetcore-spa.tar.gz'), err => {
if (err) { throw err; }
});

View File

@@ -3,6 +3,7 @@ Generates ASP.NET Core projects for single-page applications.
Can generate projects with:
* Angular 2
* Aurelia
* Knockout
* React
* React with Redux

View File

@@ -4,11 +4,13 @@ import * as yeoman from 'yeoman-generator';
import * as uuid from 'node-uuid';
import * as glob from 'glob';
import * as semver from 'semver';
import * as chalk from 'chalk';
import { execSync } from 'child_process';
import npmWhich = require('npm-which');
const yosay = require('yosay');
const toPascalCase = require('to-pascal-case');
const isWindows = /^win/.test(process.platform);
const generatorPackageJson = require(path.resolve(__dirname, '../package.json'));
// Paths matching these regexes will only be included if the user wants tests
const testSpecificPaths = [
@@ -41,6 +43,20 @@ const templates = [
{ value: 'react-redux', name: 'React with Redux', tests: false }
];
// Once everyone is on .csproj-compatible tooling, we might be able to remove the global.json files and eliminate
// this SDK choice altogether. That would be good because then it would work with whatever SDK version you have
// installed. For now, we need to specify an SDK version explicitly, because there's no support for wildcards, and
// preview3+ tooling doesn't support project.json at all.
const sdkChoices = [{
value: '1.0.0-preview2-1-003177', // Current released version
name: 'project.json' + chalk.gray(' (compatible with .NET Core tooling preview 2 and Visual Studio 2015)'),
includeFiles: [/^project.json$/, /\.xproj$/, /_placeholder.txt$/, /\.deployment$/]
}, {
value: '1.0.0-preview3-004056', // Version that ships with VS2017RC
name: '.csproj' + chalk.gray(' (compatible with .NET Core tooling preview 3 and Visual Studio 2017)'),
includeFiles: [/\.csproj$/]
}];
class MyGenerator extends yeoman.Base {
private _answers: any;
private _optionOrPrompt: YeomanPrompt;
@@ -48,7 +64,7 @@ class MyGenerator extends yeoman.Base {
constructor(args: string | string[], options: any) {
super(args, options);
this._optionOrPrompt = optionOrPrompt;
this.log(yosay('Welcome to the ASP.NET Core Single-Page App generator!'));
this.log(yosay('Welcome to the ASP.NET Core Single-Page App generator!\n\nVersion: ' + generatorPackageJson.version));
if (isWindows) {
assertNpmVersionIsAtLeast('3.0.0');
@@ -64,8 +80,13 @@ class MyGenerator extends yeoman.Base {
name: 'framework',
message: 'Framework',
choices: templates
}], frameworkAnswer => {
const frameworkChoice = templates.filter(t => t.value === frameworkAnswer.framework)[0];
}, {
type: 'list',
name: 'sdkVersion',
message: 'What type of project do you want to create?',
choices: sdkChoices
}], firstAnswers => {
const frameworkChoice = templates.filter(t => t.value === firstAnswers.framework)[0];
const furtherQuestions = [{
type: 'input',
name: 'name',
@@ -83,9 +104,10 @@ class MyGenerator extends yeoman.Base {
}
this._optionOrPrompt(furtherQuestions, answers => {
answers.framework = frameworkAnswer.framework;
answers.framework = firstAnswers.framework;
this._answers = answers;
this._answers.framework = frameworkAnswer.framework;
this._answers.framework = firstAnswers.framework;
this._answers.sdkVersion = firstAnswers.sdkVersion;
this._answers.namePascalCase = toPascalCase(answers.name);
this._answers.projectGuid = this.options['projectguid'] || uuid.v4();
done();
@@ -94,7 +116,8 @@ class MyGenerator extends yeoman.Base {
}
writing() {
var templateRoot = this.templatePath(this._answers.framework);
const templateRoot = this.templatePath(this._answers.framework);
const chosenSdk = sdkChoices.filter(sdk => sdk.value === this._answers.sdkVersion)[0];
glob.sync('**/*', { cwd: templateRoot, dot: true, nodir: true }).forEach(fn => {
// Token replacement in filenames
let outputFn = fn.replace(/tokenreplace\-([^\.\/]*)/g, (substr, token) => this._answers[token]);
@@ -104,23 +127,17 @@ class MyGenerator extends yeoman.Base {
outputFn = path.join(path.dirname(fn), '.gitignore');
}
// Likewise, output template_nodemodules_placeholder.txt as node_modules/_placeholder.txt
// This is a workaround for https://github.com/aspnet/JavaScriptServices/issues/235. We need the new project
// to have a nonempty node_modules dir as far as *source control* is concerned. So, there's a gitignore
// rule that explicitly causes node_modules/_placeholder.txt to be tracked in source control. But how
// does that file get there in the first place? It's not enough for such a file to exist when the
// generator-aspnetcore-spa NPM package is published, because NPM doesn't allow any directories called
// node_modules to exist in the package. So we have a file with at a different location, and move it
// to node_modules as part of executing the template.
if (path.basename(fn) === 'template_nodemodules_placeholder.txt') {
outputFn = path.join(path.dirname(fn), 'node_modules', '_placeholder.txt');
}
// Exclude test-specific files (unless the user has said they want tests)
// Decide whether to emit this file
const isTestSpecificFile = testSpecificPaths.some(regex => regex.test(outputFn));
if (this._answers.tests || !isTestSpecificFile) {
const isSdkSpecificFile = sdkChoices.some(sdk => sdk.includeFiles.some(regex => regex.test(outputFn)));
const matchesChosenSdk = chosenSdk.includeFiles.some(regex => regex.test(outputFn));
const emitFile = (matchesChosenSdk || !isSdkSpecificFile)
&& (this._answers.tests || !isTestSpecificFile);
if (emitFile) {
let inputFullPath = path.join(templateRoot, fn);
let destinationFullPath = this.destinationPath(outputFn);
let deleteInputFileAfter = false;
if (path.basename(fn) === 'package.json') {
// Special handling for package.json, because we rewrite it dynamically
const tempPath = destinationFullPath + '.tmp';
@@ -131,6 +148,7 @@ class MyGenerator extends yeoman.Base {
/* space */ 2
);
inputFullPath = tempPath;
deleteInputFileAfter = true;
}
const outputDirBasename = path.basename(path.dirname(destinationFullPath));
@@ -148,6 +166,10 @@ class MyGenerator extends yeoman.Base {
this._answers
);
}
if (deleteInputFileAfter) {
this.fs.delete(inputFullPath);
}
}
});
}
@@ -186,7 +208,7 @@ function assertNpmVersionIsAtLeast(minVersion: string) {
const runningVersion = execSync('npm -v').toString();
if (!semver.gte(runningVersion, minVersion, /* loose */ true)) {
console.error(`This generator requires NPM version ${minVersion} or later. You are running NPM version ${runningVersion}`);
process.exit(0);
process.exit(1);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "generator-aspnetcore-spa",
"version": "0.7.1",
"version": "0.7.3",
"description": "Single-Page App templates for ASP.NET Core",
"author": "Microsoft",
"license": "Apache-2.0",
@@ -18,6 +18,7 @@
"url": "https://github.com/aspnet/JavaScriptServices.git"
},
"dependencies": {
"chalk": "^1.1.3",
"glob": "^7.0.3",
"node-uuid": "^1.4.7",
"npm-which": "^3.0.1",

View File

@@ -7,6 +7,7 @@
"lib": ["es6"]
},
"exclude": [
"node_modules",
"**/node_modules",
"dist"
]

3
test/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/node_modules/
/tmp/
/yarn.lock

34
test/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "test",
"version": "1.0.0",
"description": "Integration tests for the templates in JavaScriptServices. This is not really an NPM package and will not be published.",
"main": "index.js",
"scripts": {
"test": "tsc && wdio"
},
"author": "Microsoft",
"license": "Apache-2.0",
"dependencies": {
"@types/chai": "^3.4.34",
"@types/mkdirp": "^0.3.29",
"@types/mocha": "^2.2.33",
"@types/node": "^6.0.52",
"@types/rimraf": "^0.0.28",
"@types/webdriverio": "^4.0.32",
"chai": "^3.5.0",
"cross-spawn": "^5.0.1",
"mkdirp": "^0.5.1",
"portastic": "^1.0.1",
"rimraf": "^2.5.4",
"selenium-standalone": "^5.9.0",
"tree-kill": "^1.1.0",
"typescript": "^2.1.4",
"webdriverio": "^4.5.0",
"yo": "^1.8.5"
},
"devDependencies": {
"wdio-junit-reporter": "^0.2.0",
"wdio-mocha-framework": "^0.5.7",
"wdio-selenium-standalone-service": "0.0.7"
}
}

View File

@@ -0,0 +1,106 @@
import * as fs from 'fs';
import * as path from 'path';
import { expect } from 'chai';
import { generateProjectSync } from './util/yeoman';
import { AspNetProcess, AspNetCoreEnviroment, defaultUrl, publishProjectSync } from './util/aspnet';
import { getValue, getCssPropertyValue } from './util/webdriverio';
// Currently we test both 'csproj' and 'project.json' project types. Eventually we'll only need csproj.
['csproj', 'projectjson'].forEach(toolingType => {
// First, generate a new project using the locally-built generator-aspnetcore-spa
// Do this outside the Mocha fixture, otherwise Mocha will time out
const appDir = path.resolve(__dirname, '../generated/angular', toolingType);
const publishedAppDir = path.resolve(appDir, './bin/Release/published');
if (!process.env.SKIP_PROJECT_GENERATION) {
generateProjectSync(appDir, {
framework: 'angular-2',
name: 'Test App',
sdkVersion: toolingType === 'projectjson' ? '1.0.0-preview2-1-003177' : '1.0.0-preview3-004056',
tests: false
});
publishProjectSync(appDir, publishedAppDir);
}
function testBasicNavigation() {
describe('Basic navigation', () => {
beforeEach(() => browser.url(defaultUrl));
it('should initially display the home page', () => {
expect(browser.getText('h1')).to.eq('Hello, world!');
expect(browser.getText('li a[href="https://angular.io/"]')).to.eq('Angular 2');
});
it('should be able to show the counter page', () => {
browser.click('a[href="/counter"]');
expect(browser.getText('h1')).to.eq('Counter');
// Test clicking the 'increment' button
expect(browser.getText('counter strong')).to.eq('0');
browser.click('counter button');
expect(browser.getText('counter strong')).to.eq('1');
});
it('should be able to show the fetchdata page', () => {
browser.click('a[href="/fetch-data"]');
expect(browser.getText('h1')).to.eq('Weather forecast');
browser.waitForExist('fetchdata table');
expect(getValue(browser.elements('fetchdata table tbody tr')).length).to.eq(5);
});
});
}
function testHotModuleReplacement() {
describe('Hot module replacement', () => {
beforeEach(() => browser.url(defaultUrl));
it('should update when HTML is changed', () => {
expect(browser.getText('h1')).to.eq('Hello, world!');
const filePath = path.resolve(appDir, './ClientApp/app/components/home/home.component.html');
const origFileContents = fs.readFileSync(filePath, 'utf8');
try {
const newFileContents = origFileContents.replace('<h1>Hello, world!</h1>', '<h1>HMR is working</h1>');
fs.writeFileSync(filePath, newFileContents, { encoding: 'utf8' });
browser.waitUntil(() => browser.getText('h1').toString() === 'HMR is working');
} finally {
// Restore old contents so that other tests don't have to account for this
fs.writeFileSync(filePath, origFileContents, { encoding: 'utf8' });
}
});
it('should update when CSS is changed', () => {
expect(getCssPropertyValue(browser, 'li.link-active a', 'color')).to.eq('rgba(255,255,255,1)');
const filePath = path.resolve(appDir, './ClientApp/app/components/navmenu/navmenu.component.css');
const origFileContents = fs.readFileSync(filePath, 'utf8');
try {
const newFileContents = origFileContents.replace('color: white;', 'color: purple;');
fs.writeFileSync(filePath, newFileContents, { encoding: 'utf8' });
browser.waitUntil(() => getCssPropertyValue(browser, 'li.link-active a', 'color') === 'rgba(128,0,128,1)');
} finally {
// Restore old contents so that other tests don't have to account for this
fs.writeFileSync(filePath, origFileContents, { encoding: 'utf8' });
}
});
});
}
// Now launch dotnet and use selenium to perform tests
describe('Angular template: dev mode', () => {
AspNetProcess.RunInMochaContext(appDir, AspNetCoreEnviroment.development);
testBasicNavigation();
testHotModuleReplacement();
});
describe('Angular template: production mode', () => {
// csproj tooling takes the assembly name from <name>.csproj, whereas project.json takes it from the directory name
const assemblyName = toolingType === 'csproj' ? 'TestApp.dll' : 'projectjson.dll';
AspNetProcess.RunInMochaContext(publishedAppDir, AspNetCoreEnviroment.production, assemblyName);
testBasicNavigation();
});
});

View File

@@ -0,0 +1,123 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import * as readline from 'readline';
import { waitUntilPortState } from './ports';
const treeKill = require('tree-kill');
const crossSpawn: typeof childProcess.spawn = require('cross-spawn');
const defaultPort = 5000;
const defaultInterface = 'localhost';
export const defaultUrl = `http://localhost:${ defaultPort }`;
export enum AspNetCoreEnviroment {
development,
production
}
export class AspNetProcess {
public static RunInMochaContext(cwd: string, mode: AspNetCoreEnviroment, dllToRun?: string) {
// Set up mocha before/after callbacks so that a 'dotnet run' process exists
// for the same duration as the context this is called inside
let aspNetProcess: AspNetProcess;
before(() => {
aspNetProcess = new AspNetProcess(cwd, mode, dllToRun);
return aspNetProcess.waitUntilListening();
});
after(() => aspNetProcess.dispose());
}
private _process: childProcess.ChildProcess;
private _processHasExited: boolean;
private _stdoutReader: readline.ReadLine;
constructor(cwd: string, mode: AspNetCoreEnviroment, dllToRun?: string) {
try {
// Prepare env for child process. Note that it doesn't inherit parent's env vars automatically,
// hence cloning process.env.
const childProcessEnv = Object.assign({}, process.env);
childProcessEnv.ASPNETCORE_ENVIRONMENT = mode === AspNetCoreEnviroment.development ? 'Development' : 'Production';
const verbOrAssembly = dllToRun || 'run';
console.log(`Running 'dotnet ${ verbOrAssembly }' in ${ cwd }`);
this._process = crossSpawn('dotnet', [verbOrAssembly], { cwd: cwd, stdio: 'pipe', env: childProcessEnv });
this._stdoutReader = readline.createInterface(this._process.stdout, null);
// Echo stdout to the test process's own stdout
this._stdoutReader.on('line', line => {
console.log(`[dotnet] ${ line.toString() }`);
});
// Also echo stderr
this._process.stderr.on('data', chunk => {
console.log(`[dotnet ERROR] ${ chunk.toString() }`);
});
// Ensure the process isn't orphaned even if Node crashes before we're disposed
process.on('exit', () => this._killAspNetProcess());
// Also track whether it exited on its own already
this._process.on('exit', () => {
this._processHasExited = true;
});
} catch(ex) {
console.log('ERROR: ' + ex.toString());
throw ex;
}
}
public waitUntilListening(): Promise<any> {
return new Promise((resolve, reject) => {
this._stdoutReader.on('line', (line: string) => {
if (line.startsWith('Now listening on:')) {
resolve();
}
});
});
}
public dispose(): Promise<any> {
return new Promise((resolve, reject) => {
this._killAspNetProcess(err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
private _killAspNetProcess(callback?: (err: any) => void) {
callback = callback || (() => {});
if (!this._processHasExited) {
// It's important to kill the whole tree, because 'dotnet run' launches a separate 'dotnet exec'
// child process that would otherwise be left running
treeKill(this._process.pid, 'SIGINT', err => {
if (err) {
callback(err);
} else {
// It's not enough just to send a SIGINT to ASP.NET. It will stay open for a moment, completing
// any outstanding requests. We have to wait for it really to be gone before continuing, otherwise
// the next test might be unable to start because of the port still being in use.
console.log(`Waiting until port ${ defaultPort } is closed...`);
waitUntilPortState(defaultPort, defaultInterface, /* isListening */ false, /* timeoutMs */ 15000, err => {
if (err) {
callback(err);
} else {
console.log(`Port ${ defaultPort } is now closed`);
callback(null);
}
});
}
});
}
}
}
export function publishProjectSync(sourceDir: string, outputDir: string) {
childProcess.execSync(`dotnet publish -c Release -o ${ outputDir }`, {
cwd: sourceDir,
stdio: 'inherit',
encoding: 'utf8'
});
}

View File

@@ -0,0 +1,24 @@
import * as portastic from 'portastic';
const pollInterval = 500;
export function waitUntilPortState(port: number, iface: string, isListening: boolean, timeoutMs: number, callback: (err: any) => void) {
if (!(timeoutMs > 0)) {
throw new Error(`Timed out waiting for port ${ port } to become ${ isListening ? 'in use' : 'free' }`);
}
portastic.test(port, iface).then(
actuallyIsAvailable => {
const actuallyIsListening = !actuallyIsAvailable;
if (actuallyIsListening === isListening) {
// Desired state is reached
callback(null);
} else {
// Wait longer
setTimeout(() => {
waitUntilPortState(port, iface, isListening, timeoutMs - pollInterval, callback);
}, pollInterval);
}
},
callback
)
}

View File

@@ -0,0 +1,12 @@
// Workaround for missing '.value' property on WebdriverIO.Client<RawResult<T>> that should be of type T
// Can't notify TypeScript that the property exists directly, because the interface merging feature doesn't
// appear to support pattern matching in such a way that WebdriverIO.Client<T> is extended only when T
// itself extends RawResult<U> for some U.
export function getValue<T>(client: WebdriverIO.Client<WebdriverIO.RawResult<T>>): T {
return (client as any).value;
}
// The official type declarations for getCssProperty are completely wrong. This function matches runtime behaviour.
export function getCssPropertyValue<T>(client: WebdriverIO.Client<T>, selector: string, cssProperty: string): string {
return (client.getCssProperty(selector, cssProperty) as any).value;
}

View File

@@ -0,0 +1,53 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import * as rimraf from 'rimraf';
import * as mkdirp from 'mkdirp';
const generatorDirRelative = '../templates/package-builder/dist/generator-aspnetcore-spa';
const yoPackageDirAbsolute = path.resolve('./node_modules/yo');
export interface GeneratorOptions {
framework: string;
name: string;
sdkVersion?: string;
tests?: boolean;
}
export function generateProjectSync(targetDir: string, generatorOptions: GeneratorOptions) {
const generatorDirAbsolute = path.resolve(generatorDirRelative);
console.log(`Running NPM install to prepare Yeoman generator at ${ generatorDirAbsolute }`);
childProcess.execSync(`npm install`, { stdio: 'inherit', cwd: generatorDirAbsolute });
console.log(`Ensuring empty output directory at ${ targetDir }`);
rimraf.sync(targetDir);
mkdirp.sync(targetDir);
const yoExecutableAbsolute = findYeomanCliScript();
console.log(`Will invoke Yeoman at ${ yoExecutableAbsolute } to generate application in ${ targetDir } with options:`);
console.log(JSON.stringify(generatorOptions, null, 2));
const command = `node "${ yoExecutableAbsolute }" "${ path.resolve(generatorDirAbsolute, './app/index.js') }"`;
const args = makeYeomanCommandLineArgs(generatorOptions);
childProcess.execSync(`${ command } ${ args }`, {
stdio: 'inherit',
cwd: targetDir
});
}
function findYeomanCliScript() {
// On Windows, you can't invoke ./node_modules/.bin/yo from the shell for some reason.
// So instead, we'll locate the CLI entrypoint that yeoman would expose if it was installed globally.
const yeomanPackageJsonPath = path.join(yoPackageDirAbsolute, './package.json');
const yeomanPackageJson = require(yeomanPackageJsonPath);
const yeomanCliScriptRelative = yeomanPackageJson.bin.yo;
if (!yeomanCliScriptRelative) {
throw new Error(`Could not find Yeoman CLI script. Looked for a bin/yo entry in ${ yeomanPackageJsonPath }`);
}
return path.join(yoPackageDirAbsolute, yeomanCliScriptRelative);
}
function makeYeomanCommandLineArgs(generatorOptions: GeneratorOptions) {
return Object.getOwnPropertyNames(generatorOptions)
.map(key => `--${ key }="${ generatorOptions[key] }"`)
.join(' ');
}

15
test/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "es5",
"rootDir": ".",
"outDir": "tmp",
"sourceMap": false,
"lib": ["es6", "dom"]
},
"exclude": [
"node_modules",
"**/node_modules",
"tmp"
]
}

204
test/wdio.conf.js Normal file
View File

@@ -0,0 +1,204 @@
exports.config = {
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
// directory is where your package.json resides, so `wdio` will be called from there.
//
specs: [
'./tmp/templates/**/*.spec.js'
],
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
],
//
// ============
// Capabilities
// ============
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
// time. Depending on the number of capabilities, WebdriverIO launches several test
// sessions. Within your capabilities you can overwrite the spec and exclude options in
// order to group specific specs to a specific capability.
//
// First, you can define how many instances should be started at the same time. Let's
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
// files and you set maxInstances to 10, all spec files will get tested at the same time
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://docs.saucelabs.com/reference/platforms-configurator
//
capabilities: [{
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
// grid with only 5 firefox instances available you can make sure that not more than
// 5 instances get started at a time.
maxInstances: 5,
//
browserName: 'chrome'
}],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// By default WebdriverIO commands are executed in a synchronous way using
// the wdio-sync package. If you still want to run your tests in an async way
// e.g. using promises you can set the sync option to false.
sync: true,
//
// Level of logging verbosity: silent | verbose | command | data | result | error
logLevel: 'silent',
//
// Enables colors for log output.
coloredLogs: true,
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Saves a screenshot to a given path if a command fails.
screenshotPath: './tmp/errorShots/',
//
// Set a base URL in order to shorten url command calls. If your url parameter starts
// with "/", then the base url gets prepended.
baseUrl: 'http://localhost:5000',
//
// Default timeout for all waitFor* commands.
waitforTimeout: 10000,
//
// Default timeout in milliseconds for request
// if Selenium Grid doesn't send response
connectionRetryTimeout: 90000,
//
// Default request retries count
connectionRetryCount: 3,
//
// Initialize the browser instance with a WebdriverIO plugin. The object should have the
// plugin name as key and the desired plugin options as properties. Make sure you have
// the plugin installed before running any tests. The following plugins are currently
// available:
// WebdriverCSS: https://github.com/webdriverio/webdrivercss
// WebdriverRTC: https://github.com/webdriverio/webdriverrtc
// Browserevent: https://github.com/webdriverio/browserevent
// plugins: {
// webdrivercss: {
// screenshotRoot: 'my-shots',
// failedComparisonsRoot: 'diffs',
// misMatchTolerance: 0.05,
// screenWidth: [320,480,640,1024]
// },
// webdriverrtc: {},
// browserevent: {}
// },
//
// Test runner services
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
// services: ['selenium-standalone'],
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: http://webdriver.io/guide/testrunner/frameworks.html
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: 'mocha',
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: http://webdriver.io/guide/testrunner/reporters.html
reporters: ['junit'],
reporterOptions: {
outputDir: './tmp/junit'
},
//
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: 'bdd',
timeout: 60000
},
//
// =====
// Hooks
// =====
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
// it and to build services around it. You can either apply a single function or an array of
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
// resolved to continue.
//
// Gets executed once before all workers get launched.
// onPrepare: function (config, capabilities) {
// },
//
// Gets executed just before initialising the webdriver session and test framework. It allows you
// to manipulate configurations depending on the capability or spec.
// beforeSession: function (config, capabilities, specs) {
// },
//
// Gets executed before test execution begins. At this point you can access all global
// variables, such as `browser`. It is the perfect place to define custom commands.
// before: function (capabilities, specs) {
// },
//
// Hook that gets executed before the suite starts
// beforeSuite: function (suite) {
// },
//
// Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
// beforeEach in Mocha)
// beforeHook: function () {
// },
//
// Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
// afterEach in Mocha)
// afterHook: function () {
// },
//
// Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
// beforeTest: function (test) {
// },
//
// Runs before a WebdriverIO command gets executed.
// beforeCommand: function (commandName, args) {
// },
//
// Runs after a WebdriverIO command gets executed
// afterCommand: function (commandName, args, result, error) {
// },
//
// Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
// afterTest: function (test) {
// },
//
// Hook that gets executed after the suite has ended
// afterSuite: function (suite) {
// },
//
// Gets executed after all tests are done. You still have access to all global variables from
// the test.
// after: function (result, capabilities, specs) {
// },
//
// Gets executed right after terminating the webdriver session.
// afterSession: function (config, capabilities, specs) {
// },
//
// Gets executed after all workers got shut down and the process is about to exit. It is not
// possible to defer the end of the process using a promise.
// onComplete: function(exitCode) {
// }
}