feat(nx): support no framework apps

This commit is contained in:
Jason Jean 2019-02-13 13:22:30 -05:00 committed by Victor Savkin
parent 24f31d1495
commit 3bad40ea65
65 changed files with 3091 additions and 359 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
/.vscode
dist
/build
/coverage
test
.DS_Store
tmp

View File

@ -3,7 +3,8 @@ tmp
/build
node_modules
/package.json
packages/schematics/src/collection/**/files/*.json
packages/schematics/src/collection/**/files/**/*.json
/.vscode
/.idea
/.github
/coverage

View File

@ -9,6 +9,7 @@ Build a Node application
| `externalDependencies` | Dependencies to keep external to the bundle. ("all" (default), "none", or an array of module names) | string | `all` |
| `main` | The name of the main entry-point file. | string | `undefined` |
| `watch` | Run build when files change. | boolean | `false` |
| `poll` | Frequency of file watcher in ms. | number | `undefined` |
| `sourceMap` | Produce source maps. | boolean | `true` |
| `progress` | Log progress to the console while building. | boolean | `false` |
| `tsConfig` | The name of the Typescript configuration file. | string | `undefined` |
@ -17,3 +18,4 @@ Build a Node application
| `optimization` | Defines the optimization level of the build. | boolean | `false` |
| `showCircularDependencies` | Show circular dependency warnings on builds. | boolean | `true` |
| `maxWorkers` | Number of workers to use for type checking. (defaults to # of CPUS - 2) | number | `undefined` |
| `webpackConfig` | Path to a function which takes a webpack config, context and returns the resulting webpack config | string | `undefined` |

View File

@ -0,0 +1,32 @@
# web-build
Build a web application
### Properties
| Name | Description | Type | Default value |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------- |
| `namedChunks` | Names the produced bundles according to their entry file | boolean | `true` |
| `main` | The name of the main entry-point file. | string | `undefined` |
| `watch` | Enable re-building when files change. | boolean | `false` |
| `baseHref` | Base url for the application being built. | string | `/` |
| `deployUrl` | URL where the application will be deployed. | string | `undefined` |
| `vendorChunk` | Use a separate bundle containing only vendor libraries. | boolean | `true` |
| `commonChunk` | Use a separate bundle containing code used across multiple bundles. | boolean | `true` |
| `sourceMap` | Output sourcemaps. | boolean | `true` |
| `progress` | Log progress to the console while building. | boolean | `false` |
| `index` | HTML File which will be contain the application | string | `undefined` |
| `scripts` | External Scripts which will be included before the main application entry | array | `undefined` |
| `styles` | External Styles which will be included with the application | array | `undefined` |
| `tsConfig` | The name of the Typescript configuration file. | string | `undefined` |
| `outputHashing` | Define the output filename cache-busting hashing mode. | string | `none` |
| `optimization` | Enables optimization of the build output. | boolean | `undefined` |
| `extractCss` | Extract css into a .css file | boolean | `false` |
| `es2015Polyfills` | Conditional polyfills loaded in browsers which do not support ES2015. | string | `undefined` |
| `subresourceIntegrity` | Enables the use of subresource integrity validation. | boolean | `false` |
| `polyfills` | Polyfills to load before application | string | `undefined` |
| `statsJson` | Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https://webpack.github.io/analyse. | boolean | `false` |
| `extractLicenses` | Extract all licenses in a separate file, in the case of production builds only. | boolean | `false` |
| `showCircularDependencies` | Show circular dependency warnings on builds. | boolean | `true` |
| `maxWorkers` | Number of workers to use for type checking. (defaults to # of CPUS - 2) | number | `undefined` |
| `webpackConfig` | Path to a function which takes a webpack config, some context and returns the resulting webpack config | string | `undefined` |

View File

@ -0,0 +1,18 @@
# web-dev-server
Serve a web application
### Properties
| Name | Description | Type | Default value |
| ------------- | -------------------------------------------------------- | ------- | ------------- |
| `buildTarget` | Target which builds the application | string | `undefined` |
| `port` | Port to listen on. | number | `4200` |
| `host` | Host to listen on. | string | `localhost` |
| `ssl` | Serve using HTTPS. | boolean | `false` |
| `sslKey` | SSL key to use for serving HTTPS. | string | `undefined` |
| `sslCert` | SSL certificate to use for serving HTTPS. | string | `undefined` |
| `watch` | Watches for changes and rebuilds application | boolean | `true` |
| `liveReload` | Whether to reload the page on change, using live-reload. | boolean | `true` |
| `publicHost` | Public URL where the application will be served | string | `undefined` |
| `open` | Open the application in the browser. | boolean | `false` |

View File

@ -13,14 +13,15 @@ ng generate application ...
| Name | Alias | Description | Type | Default value |
| ------------------- | ----- | ------------------------------------------------- | ------- | ------------- |
| `style` | | The file extension to be used for style files. | string | `css` |
| `name` | | The name of the application. | string | `undefined` |
| `prefix` | p | The prefix to apply to generated selectors. | string | `undefined` |
| `framework` | | The Framework for the application. | string | `angular` |
| `directory` | | The directory of the new application. | string | `undefined` |
| `inlineStyle` | s | Specifies if the style will be in the ts file. | boolean | `false` |
| `inlineTemplate` | t | Specifies if the template will be in the ts file. | boolean | `false` |
| `viewEncapsulation` | | Specifies the view encapsulation strategy. | string | `undefined` |
| `routing` | | Generates a routing module. | boolean | `false` |
| `prefix` | p | The prefix to apply to generated selectors. | string | `undefined` |
| `directory` | | The directory of the new application. | string | `undefined` |
| `name` | | The name of the application. | string | `undefined` |
| `style` | | The file extension to be used for style files. | string | `css` |
| `skipTests` | S | Skip creating spec files. | boolean | `false` |
| `skipFormat` | | Skip formatting files | boolean | `false` |
| `skipPackageJson` | | Do not add dependencies to package.json. | boolean | `false` |

View File

@ -11,8 +11,10 @@ ng generate jest-project ...
### Options
| Name | Alias | Description | Type | Default value |
| ----------------- | ----- | ------------------------------------------------------------ | ------- | ------------- |
| `project` | | The name of the project. | string | `undefined` |
| `skipSetupFile` | | Skips the setup file required for angular | boolean | `false` |
| `skipSerializers` | | Skips the serializers required to snapshot angular templates | boolean | `false` |
| Name | Alias | Description | Type | Default value |
| ----------------- | ----- | --------------------------------------------------------------------------- | ------- | ------------- |
| `project` | | The name of the project. | string | `undefined` |
| `skipSetupFile` | | [Deprecated]: Skips the setup file required for angular. (Use --setup-file) | boolean | `false` |
| `setupFile` | | The setup file to be generated | string | `angular` |
| `skipSerializers` | | Skips the serializers required to snapshot angular templates | boolean | `false` |
| `supportTsx` | | Setup tsx support | boolean | `false` |

View File

@ -0,0 +1,88 @@
import {
ensureProject,
runCLI,
uniq,
newApp,
newLib,
updateFile,
readFile,
runCLIAsync,
checkFilesExist
} from '../utils';
describe('Web Applications', () => {
it('should be able to generate a react application', async () => {
ensureProject();
const appName = uniq('app');
const libName = uniq('lib');
newApp(`${appName} --framework react`);
newLib(`${libName} --framework none`);
const mainPath = `apps/${appName}/src/main.tsx`;
updateFile(mainPath, `import '@proj/${libName}';\n` + readFile(mainPath));
const lintResults = runCLI(`lint ${appName}`);
expect(lintResults).toContain('All files pass linting.');
runCLI(`build ${appName}`);
checkFilesExist(
`dist/apps/${appName}/index.html`,
`dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/vendor.js`,
`dist/apps/${appName}/main.js`,
`dist/apps/${appName}/styles.js`
);
runCLI(`build ${appName} --prod --output-hashing none`);
checkFilesExist(
`dist/apps/${appName}/index.html`,
`dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/main.js`,
`dist/apps/${appName}/styles.css`
);
const testResults = await runCLIAsync(`test ${appName}`);
expect(testResults.stderr).toContain('Test Suites: 1 passed, 1 total');
const lintE2eResults = runCLI(`lint ${appName}-e2e`);
expect(lintE2eResults).toContain('All files pass linting.');
const e2eResults = runCLI(`e2e ${appName}-e2e`);
expect(e2eResults).toContain('All specs passed!');
}, 30000);
it('should be able to generate a custom-elements application', async () => {
ensureProject();
const appName = uniq('app');
const libName = uniq('lib');
newApp(`${appName} --framework custom-elements`);
newLib(`${libName} --framework none`);
const mainPath = `apps/${appName}/src/main.ts`;
updateFile(mainPath, `import '@proj/${libName}';\n` + readFile(mainPath));
const lintResults = runCLI(`lint ${appName}`);
expect(lintResults).toContain('All files pass linting.');
runCLI(`build ${appName}`);
checkFilesExist(
`dist/apps/${appName}/index.html`,
`dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/main.js`,
`dist/apps/${appName}/styles.js`
);
runCLI(`build ${appName} --prod --output-hashing none`);
checkFilesExist(
`dist/apps/${appName}/index.html`,
`dist/apps/${appName}/polyfills.js`,
`dist/apps/${appName}/runtime.js`,
`dist/apps/${appName}/main.js`,
`dist/apps/${appName}/styles.css`
);
const testResults = await runCLIAsync(`test ${appName}`);
expect(testResults.stderr).toContain('Test Suites: 1 passed, 1 total');
const lintE2eResults = runCLI(`lint ${appName}-e2e`);
expect(lintE2eResults).toContain('All files pass linting.');
const e2eResults = runCLI(`e2e ${appName}-e2e`);
expect(e2eResults).toContain('All specs passed!');
}, 30000);
});

View File

@ -86,7 +86,15 @@ export function copyMissingPackages(): void {
'@types/jasminewd2',
'@nestjs',
'express',
'@types/express'
'@types/express',
'react',
'react-dom',
'@types/react',
'@types/react-dom',
'react-testing-library',
'document-register-element'
];
modulesToCopy.forEach(m => copyNodeModule(projectName, m));
updateFile(

View File

@ -36,17 +36,24 @@
"@angular/platform-browser-dynamic": "^7.2.1",
"@angular/router": "^7.2.1",
"@angular/upgrade": "^7.2.1",
"@nestjs/common": "5.5.0",
"@nestjs/core": "5.5.0",
"@nestjs/schematics": "5.11.2",
"@nestjs/testing": "5.5.0",
"@ngrx/effects": "7.1.0",
"@ngrx/router-store": "7.1.0",
"@ngrx/schematics": "7.1.0",
"@ngrx/store": "7.1.0",
"@ngrx/store-devtools": "7.1.0",
"@schematics/angular": "7.2.2",
"@types/express": "4.16.0",
"@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3",
"@types/jest": "^23.3.2",
"@types/node": "~8.9.4",
"@types/prettier": "^1.10.0",
"@types/react": "^16.8.4",
"@types/react-dom": "^16.8.2",
"@types/webpack": "^4.4.24",
"@types/yargs": "^11.0.0",
"angular": "1.6.6",
@ -57,9 +64,13 @@
"cosmiconfig": "^4.0.0",
"cypress": "3.1.0",
"cz-conventional-changelog": "^2.1.0",
"document-register-element": "^1.13.1",
"dotenv": "6.2.0",
"express": "4.16.3",
"fork-ts-checker-webpack-plugin": "^0.4.9",
"fs-extra": "5.0.0",
"graphviz": "^0.0.8",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.0.0-rc.13",
"ignore": "^5.0.4",
"jasmine-core": "~2.99.1",
@ -70,16 +81,19 @@
"jest-preset-angular": "^6.0.2",
"karma": "~2.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-jasmine": "~1.1.1",
"karma-webpack": "2.0.4",
"karma-jasmine-html-reporter": "^0.2.2",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2",
"karma-webpack": "2.0.4",
"license-webpack-plugin": "^1.4.0",
"ng-packagr": "4.3.1",
"npm-run-all": "^4.1.5",
"opn": "^5.3.0",
"precise-commits": "1.0.2",
"prettier": "1.15.3",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-testing-library": "6.0.0",
"release-it": "^7.4.0",
"rxjs": "6.3.3",
"semver": "5.4.1",
@ -96,14 +110,7 @@
"webpack-node-externals": "^1.7.2",
"yargs": "^11.0.0",
"yargs-parser": "10.0.0",
"zone.js": "^0.8.26",
"dotenv": "6.2.0",
"@nestjs/core": "5.5.0",
"@nestjs/common": "5.5.0",
"@nestjs/testing": "5.5.0",
"@nestjs/schematics": "5.11.2",
"express": "4.16.3",
"@types/express": "4.16.0"
"zone.js": "^0.8.26"
},
"author": "Victor Savkin",
"license": "MIT",
@ -111,7 +118,12 @@
"modulePathIgnorePatterns": [
"tmp",
"collection/.*/files"
]
],
"collectCoverage": true,
"coverageReporters": [
"html"
],
"coverageDirectory": "coverage"
},
"config": {
"commitizen": {

View File

@ -26,6 +26,7 @@
"builders": "./src/builders.json",
"dependencies": {
"@angular-devkit/architect": "~0.13.1",
"@angular-devkit/build-angular": "~0.13.1",
"@angular-devkit/build-webpack": "~0.13.1",
"copy-webpack-plugin": "4.6.0",
"fork-ts-checker-webpack-plugin": "0.4.9",

View File

@ -11,6 +11,16 @@
"schema": "./node/execute/schema.json",
"description": "Execute a Node application"
},
"web-build": {
"class": "./web/build/web-build.builder",
"schema": "./web/build/schema.json",
"description": "Build a web application"
},
"web-dev-server": {
"class": "./web/dev-server/web-dev-server.builder",
"schema": "./web/dev-server/schema.json",
"description": "Serve a web application"
},
"jest": {
"class": "./jest/jest.builder",
"schema": "./jest/schema.json",

View File

@ -1,4 +1,4 @@
import { normalize } from '@angular-devkit/core';
import { join, normalize } from '@angular-devkit/core';
import { TestLogger } from '@angular-devkit/architect/testing';
import BuildNodeBuilder from './node-build.builder';
import { BuildNodeBuilderOptions } from './node-build.builder';
@ -17,7 +17,11 @@ describe('NodeBuildBuilder', () => {
workspace: <any>{
root: '/root'
},
architect: <any>{}
architect: <any>{},
targetSpecifier: {
project: 'nodeapp',
target: 'build'
}
});
sourceRoot = '/root/apps/nodeapp/src';
testOptions = {
@ -35,7 +39,8 @@ describe('NodeBuildBuilder', () => {
with: 'module2.ts'
}
],
assets: []
assets: [],
statsJson: false
};
});
@ -125,75 +130,45 @@ describe('NodeBuildBuilder', () => {
);
});
});
});
describe('options normalization', () => {
it('should add the root', () => {
const result = (<any>builder).normalizeOptions(testOptions, sourceRoot);
expect(result.root).toEqual('/root');
});
it('should resolve main from root', () => {
const result = (<any>builder).normalizeOptions(testOptions, sourceRoot);
expect(result.main).toEqual('/root/apps/nodeapp/src/main.ts');
});
it('should resolve the output path', () => {
const result = (<any>builder).normalizeOptions(testOptions, sourceRoot);
expect(result.outputPath).toEqual('/root/dist/apps/nodeapp');
});
it('should resolve the tsConfig path', () => {
const result = (<any>builder).normalizeOptions(testOptions, sourceRoot);
expect(result.tsConfig).toEqual('/root/apps/nodeapp/tsconfig.app.json');
});
it('should normalize asset patterns', () => {
spyOn(fs, 'statSync').and.returnValue({
isDirectory: () => true
});
const result = (<any>builder).normalizeOptions(
{
...testOptions,
assets: [
'apps/nodeapp/src/assets',
{
input: '/outsideroot',
output: 'output',
glob: '**/*',
ignore: ['**/*.json']
describe('webpackConfig option', () => {
it('should require the specified function and use the return value', async () => {
const runWebpack = spyOn(
builder.webpackBuilder,
'runWebpack'
).and.returnValue(
of({
success: true
})
);
const mockFunction = jest.fn(config => ({
config: 'config'
}));
jest.mock(
join(normalize('/root'), 'apps/nodeapp/webpack.config.js'),
() => mockFunction,
{
virtual: true
}
);
await builder
.run({
root: normalize('/root'),
sourceRoot: join(normalize('/root'), 'apps/nodeapp'),
projectType: 'application',
builder: '@nrwl/builders:node-build',
options: {
...testOptions,
webpackConfig: 'apps/nodeapp/webpack.config.js'
}
]
},
sourceRoot
);
expect(result.assets).toEqual([
{
input: '/root/apps/nodeapp/src/assets',
output: 'assets',
glob: '**/*'
},
{
input: '/outsideroot',
output: 'output',
glob: '**/*',
ignore: ['**/*.json']
}
]);
});
})
.toPromise();
it('should resolve the file replacement paths', () => {
const result = (<any>builder).normalizeOptions(testOptions, sourceRoot);
expect(result.fileReplacements).toEqual([
{
replace: '/root/apps/environment/environment.ts',
with: '/root/apps/environment/environment.prod.ts'
},
{
replace: '/root/module1.ts',
with: '/root/module2.ts'
}
]);
expect(mockFunction).toHaveBeenCalled();
expect(runWebpack.calls.first().args[0]).toEqual({
config: 'config'
});
});
});
});
});

View File

@ -4,47 +4,26 @@ import {
BuilderConfiguration,
BuilderContext
} from '@angular-devkit/architect';
import { getSystemPath, normalize, Path } from '@angular-devkit/core';
import { getSystemPath } from '@angular-devkit/core';
import { WebpackBuilder } from '@angular-devkit/build-webpack';
import { Observable } from 'rxjs';
import { writeFileSync, statSync } from 'fs';
import { getWebpackConfig, OUT_FILENAME } from './webpack/config';
import { resolve, basename, dirname, relative } from 'path';
import { writeFileSync } from 'fs';
import { OUT_FILENAME } from '../../utils/webpack/config';
import { resolve } from 'path';
import { map } from 'rxjs/operators';
import {
AssetPattern,
AssetPatternObject
} from '@angular-devkit/build-angular';
import { getNodeWebpackConfig } from '../../utils/webpack/node.config';
import { normalizeBuildOptions } from '../../utils/normalize';
import { BuildBuilderOptions } from '../../utils/types';
try {
require('dotenv').config();
} catch (e) {}
export interface BuildNodeBuilderOptions {
main: string;
outputPath: string;
tsConfig: string;
watch?: boolean;
sourceMap?: boolean;
export interface BuildNodeBuilderOptions extends BuildBuilderOptions {
optimization?: boolean;
sourceMap?: boolean;
externalDependencies: 'all' | 'none' | string[];
showCircularDependencies?: boolean;
maxWorkers?: number;
fileReplacements: FileReplacement[];
assets?: AssetPattern[];
progress?: boolean;
statsJson?: boolean;
extractLicenses?: boolean;
root?: string;
}
export interface FileReplacement {
replace: string;
with: string;
}
export interface NodeBuildEvent extends BuildEvent {
@ -65,12 +44,19 @@ export default class BuildNodeBuilder
run(
builderConfig: BuilderConfiguration<BuildNodeBuilderOptions>
): Observable<NodeBuildEvent> {
const options = this.normalizeOptions(
const options = normalizeBuildOptions(
builderConfig.options,
this.root,
builderConfig.sourceRoot
);
let config = getWebpackConfig(options);
let config = getNodeWebpackConfig(options);
if (options.webpackConfig) {
config = require(options.webpackConfig)(config, {
options,
configuration: this.context.targetSpecifier.configuration
});
}
return this.webpackBuilder
.runWebpack(config, stats => {
if (options.statsJson) {
@ -89,69 +75,4 @@ export default class BuildNodeBuilder
}))
);
}
private normalizeOptions(options: BuildNodeBuilderOptions, sourceRoot: Path) {
return {
...options,
root: this.root,
main: resolve(this.root, options.main),
outputPath: resolve(this.root, options.outputPath),
tsConfig: resolve(this.root, options.tsConfig),
fileReplacements: this.normalizeFileReplacements(
options.fileReplacements
),
assets: this.normalizeAssets(options.assets, sourceRoot)
};
}
private normalizeAssets(
assets: AssetPattern[],
sourceRoot: Path
): AssetPatternObject[] {
return assets.map(asset => {
if (typeof asset === 'string') {
const assetPath = normalize(asset);
const resolvedAssetPath = resolve(this.root, assetPath);
const resolvedSourceRoot = resolve(this.root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(this.root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
return {
...asset,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, '')
};
}
});
}
private normalizeFileReplacements(
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements.map(fileReplacement => ({
replace: resolve(this.root, fileReplacement.replace),
with: resolve(this.root, fileReplacement.with)
}));
}
}

View File

@ -16,6 +16,10 @@
"description": "Run build when files change.",
"default": false
},
"poll": {
"type": "number",
"description": "Frequency of file watcher in ms."
},
"sourceMap": {
"type": "boolean",
"description": "Produce source maps.",
@ -91,6 +95,10 @@
"required": ["replace", "with"]
},
"default": []
},
"webpackConfig": {
"type": "string",
"description": "Path to a function which takes a webpack config, context and returns the resulting webpack config"
}
},
"required": ["tsConfig", "main"],

View File

@ -0,0 +1,102 @@
import { normalizeBuildOptions } from './normalize';
import { BuildBuilderOptions } from './types';
import { Path, normalize } from '@angular-devkit/core';
import * as fs from 'fs';
describe('normalizeBuildOptions', () => {
let testOptions: BuildBuilderOptions;
let root: string;
let sourceRoot: Path;
beforeEach(() => {
testOptions = {
main: 'apps/nodeapp/src/main.ts',
tsConfig: 'apps/nodeapp/tsconfig.app.json',
outputPath: 'dist/apps/nodeapp',
fileReplacements: [
{
replace: 'apps/environment/environment.ts',
with: 'apps/environment/environment.prod.ts'
},
{
replace: 'module1.ts',
with: 'module2.ts'
}
],
assets: [],
statsJson: false
};
root = '/root';
sourceRoot = normalize('apps/nodeapp/src');
});
it('should add the root', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.root).toEqual('/root');
});
it('should resolve main from root', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.main).toEqual('/root/apps/nodeapp/src/main.ts');
});
it('should resolve the output path', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.outputPath).toEqual('/root/dist/apps/nodeapp');
});
it('should resolve the tsConfig path', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.tsConfig).toEqual('/root/apps/nodeapp/tsconfig.app.json');
});
it('should normalize asset patterns', () => {
spyOn(fs, 'statSync').and.returnValue({
isDirectory: () => true
});
const result = normalizeBuildOptions(
<BuildBuilderOptions>{
...testOptions,
root,
assets: [
'apps/nodeapp/src/assets',
{
input: '/outsideroot',
output: 'output',
glob: '**/*',
ignore: ['**/*.json']
}
]
},
root,
sourceRoot
);
expect(result.assets).toEqual([
{
input: '/root/apps/nodeapp/src/assets',
output: 'assets',
glob: '**/*'
},
{
input: '/outsideroot',
output: 'output',
glob: '**/*',
ignore: ['**/*.json']
}
]);
});
it('should resolve the file replacement paths', () => {
const result = normalizeBuildOptions(testOptions, root, sourceRoot);
expect(result.fileReplacements).toEqual([
{
replace: '/root/apps/environment/environment.ts',
with: '/root/apps/environment/environment.prod.ts'
},
{
replace: '/root/module1.ts',
with: '/root/module2.ts'
}
]);
});
});

View File

@ -0,0 +1,133 @@
import {
AssetPattern,
AssetPatternObject,
NormalizedBrowserBuilderSchema
} from '@angular-devkit/build-angular';
import { Path, normalize } from '@angular-devkit/core';
import { resolve, dirname, relative, basename } from 'path';
import { statSync } from 'fs';
import { BuildBuilderOptions } from './types';
import { WebBuildBuilderOptions } from '../web/build/web-build.builder';
import { BuildOptions } from '@angular-devkit/build-angular/src/angular-cli-files/models/build-options';
export interface FileReplacement {
replace: string;
with: string;
}
export function normalizeBuildOptions<T extends BuildBuilderOptions>(
options: T,
root: string,
sourceRoot: Path
): T {
return {
...options,
root: root,
sourceRoot: sourceRoot,
main: resolve(root, options.main),
outputPath: resolve(root, options.outputPath),
tsConfig: resolve(root, options.tsConfig),
fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
assets: normalizeAssets(options.assets, root, sourceRoot),
webpackConfig: options.webpackConfig
? resolve(root, options.webpackConfig)
: options.webpackConfig
};
}
export function normalizeWebBuildOptions(
options: WebBuildBuilderOptions,
root: string,
sourceRoot: Path
): WebBuildBuilderOptions {
return {
...normalizeBuildOptions(options, root, sourceRoot),
optimization:
typeof options.optimization !== 'object'
? {
scripts: options.optimization,
styles: options.optimization
}
: options.optimization,
sourceMap:
typeof options.sourceMap === 'object'
? options.sourceMap
: {
scripts: options.sourceMap,
styles: options.sourceMap,
hidden: false,
vendors: false
},
polyfills: options.polyfills ? resolve(root, options.polyfills) : undefined,
es2015Polyfills: options.es2015Polyfills
? resolve(root, options.es2015Polyfills)
: undefined
};
}
export function convertBuildOptions(
buildOptions: WebBuildBuilderOptions
): BuildOptions {
const options = buildOptions as any;
return <NormalizedBrowserBuilderSchema>{
...options,
buildOptimizer: options.optimization,
aot: false,
forkTypeChecker: false,
lazyModules: [] as string[],
assets: [] as string[]
};
}
function normalizeAssets(
assets: AssetPattern[],
root: string,
sourceRoot: Path
): AssetPatternObject[] {
return assets.map(asset => {
if (typeof asset === 'string') {
const assetPath = normalize(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
return {
...asset,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, '')
};
}
});
}
function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements.map(fileReplacement => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with)
}));
}

View File

@ -0,0 +1,54 @@
import { WebBuildBuilderOptions } from '../web/build/web-build.builder';
export function buildServePath(browserOptions: WebBuildBuilderOptions) {
let servePath =
_findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl) ||
'/';
if (servePath.endsWith('/')) {
servePath = servePath.substr(0, servePath.length - 1);
}
if (!servePath.startsWith('/')) {
servePath = `/${servePath}`;
}
return servePath;
}
export function _findDefaultServePath(
baseHref?: string,
deployUrl?: string
): string | null {
if (!baseHref && !deployUrl) {
return '';
}
if (
/^(\w+:)?\/\//.test(baseHref || '') ||
/^(\w+:)?\/\//.test(deployUrl || '')
) {
// If baseHref or deployUrl is absolute, unsupported by ng serve
return null;
}
// normalize baseHref
// for ng serve the starting base is always `/` so a relative
// and root relative value are identical
const baseHrefParts = (baseHref || '').split('/').filter(part => part !== '');
if (baseHref && !baseHref.endsWith('/')) {
baseHrefParts.pop();
}
const normalizedBaseHref =
baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`;
if (deployUrl && deployUrl[0] === '/') {
if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) {
// If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve
return null;
}
return deployUrl;
}
// Join together baseHref and deployUrl
return `${normalizedBaseHref}${deployUrl || ''}`;
}

View File

@ -0,0 +1,39 @@
import { FileReplacement } from './normalize';
import { AssetPattern } from '@angular-devkit/build-angular';
import { Path } from '@angular-devkit/core';
export interface OptimizationOptions {
scripts: boolean;
styles: boolean;
}
export interface SourceMapOptions {
scripts: boolean;
styles: boolean;
vendors: boolean;
hidden: boolean;
}
export interface BuildBuilderOptions {
main: string;
outputPath: string;
tsConfig: string;
watch?: boolean;
sourceMap?: boolean | SourceMapOptions;
optimization?: boolean | OptimizationOptions;
showCircularDependencies?: boolean;
maxWorkers?: number;
poll?: number;
fileReplacements: FileReplacement[];
assets?: AssetPattern[];
progress?: boolean;
statsJson?: boolean;
extractLicenses?: boolean;
webpackConfig?: string;
root?: string;
sourceRoot?: Path;
}

View File

@ -0,0 +1,11 @@
import * as ts from 'typescript';
import { dirname } from 'path';
export function readTsConfig(tsConfigPath: string) {
const readResult = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
return ts.parseJsonConfigFileContent(
readResult.config,
ts.sys,
dirname(tsConfigPath)
);
}

View File

@ -1,37 +1,41 @@
import { getWebpackConfig } from './config';
import { BuildNodeBuilderOptions } from '../node-build.builder';
import { getBaseWebpackPartial } from './config';
import { normalize, getSystemPath } from '@angular-devkit/core';
import * as ts from 'typescript';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import CircularDependencyPlugin = require('circular-dependency-plugin');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import { ProgressPlugin } from 'webpack';
import { BuildBuilderOptions } from '../types';
describe('getWebpackConfig', () => {
let input: BuildNodeBuilderOptions;
describe('getBaseWebpackPartial', () => {
let input: BuildBuilderOptions;
beforeEach(() => {
input = {
main: 'main.ts',
outputPath: 'dist',
tsConfig: 'tsconfig.json',
externalDependencies: 'all',
fileReplacements: [],
root: getSystemPath(normalize('/root'))
root: getSystemPath(normalize('/root')),
statsJson: false
};
});
describe('unconditional options', () => {
it('should have output options', () => {
const result = getWebpackConfig(input);
it('should have output filename', () => {
const result = getBaseWebpackPartial(input);
expect(result.output.filename).toEqual('main.js');
expect(result.output.libraryTarget).toEqual('commonjs');
});
it('should have output path', () => {
const result = getBaseWebpackPartial(input);
expect(result.output.path).toEqual('dist');
});
it('should have a rule for typescript', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
const typescriptRule = result.module.rules.find(rule =>
(rule.test as RegExp).test('app/main.ts')
@ -42,7 +46,7 @@ describe('getWebpackConfig', () => {
});
it('should split typescript type checking into a separate workers', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
const typeCheckerPlugin = result.plugins.find(
plugin => plugin instanceof ForkTsCheckerWebpackPlugin
@ -50,24 +54,24 @@ describe('getWebpackConfig', () => {
expect(typeCheckerPlugin).toBeTruthy();
});
it('should target node', () => {
const result = getWebpackConfig(input);
expect(result.target).toEqual('node');
});
it('should disable performance hints', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.performance).toEqual({
hints: false
});
});
it('should resolve typescript and javascript', () => {
const result = getWebpackConfig(input);
it('should resolve ts, tsx, mjs, js, and jsx', () => {
const result = getBaseWebpackPartial(input);
expect(result.resolve.extensions).toEqual(['.ts', '.mjs', '.js']);
expect(result.resolve.extensions).toEqual([
'.ts',
'.tsx',
'.mjs',
'.js',
'.jsx'
]);
});
it('should include module and main in mainFields', () => {
@ -77,29 +81,25 @@ describe('getWebpackConfig', () => {
}
});
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.resolve.mainFields).toContain('module');
expect(result.resolve.mainFields).toContain('main');
});
it('should not polyfill node apis', () => {
const result = getWebpackConfig(input);
expect(result.node).toEqual(false);
});
});
describe('the main option', () => {
it('should set the correct entry options', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.entry).toEqual(['main.ts']);
expect(result.entry).toEqual({
main: ['main.ts']
});
});
});
describe('the output option', () => {
it('should set the correct output options', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.output.path).toEqual('dist');
});
@ -107,7 +107,7 @@ describe('getWebpackConfig', () => {
describe('the tsConfig option', () => {
it('should set the correct typescript rule', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(
result.module.rules.find(rule => rule.loader === 'ts-loader').options
@ -119,7 +119,7 @@ describe('getWebpackConfig', () => {
});
it('should set the correct options for the type checker plugin', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
const typeCheckerPlugin = result.plugins.find(
plugin => plugin instanceof ForkTsCheckerWebpackPlugin
@ -136,7 +136,7 @@ describe('getWebpackConfig', () => {
}
});
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.resolve.alias).toEqual({
'@npmScope/libraryName': '/root/libs/libraryName/src/index.ts'
});
@ -149,7 +149,7 @@ describe('getWebpackConfig', () => {
}
});
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.resolve.mainFields).toContain('es2015');
});
});
@ -160,7 +160,7 @@ describe('getWebpackConfig', () => {
options: {}
});
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
fileReplacements: [
{
@ -176,39 +176,9 @@ describe('getWebpackConfig', () => {
});
});
describe('the externalDependencies option', () => {
it('should change all node_modules to commonjs imports', () => {
const result = getWebpackConfig(input);
const callback = jest.fn();
result.externals[0](null, '@angular/core', callback);
expect(callback).toHaveBeenCalledWith(null, 'commonjs @angular/core');
});
it('should change given module names to commonjs imports but not others', () => {
const result = getWebpackConfig({
...input,
externalDependencies: ['module1']
});
const callback = jest.fn();
result.externals[0](null, 'module1', callback);
expect(callback).toHaveBeenCalledWith(null, 'commonjs module1');
result.externals[0](null, '@angular/core', callback);
expect(callback).toHaveBeenCalledWith();
});
it('should not change any modules to commonjs imports', () => {
const result = getWebpackConfig({
...input,
externalDependencies: 'none'
});
expect(result.externals).not.toBeDefined();
});
});
describe('the watch option', () => {
it('should enable file watching', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
watch: true
});
@ -217,9 +187,20 @@ describe('getWebpackConfig', () => {
});
});
describe('the poll option', () => {
it('should determine the polling rate', () => {
const result = getBaseWebpackPartial({
...input,
poll: 1000
});
expect(result.watchOptions.poll).toEqual(1000);
});
});
describe('the source map option', () => {
it('should enable source-map devtool', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
sourceMap: true
});
@ -228,7 +209,7 @@ describe('getWebpackConfig', () => {
});
it('should enable source-map devtool', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
sourceMap: false
});
@ -240,7 +221,7 @@ describe('getWebpackConfig', () => {
describe('the optimization option', () => {
describe('by default', () => {
it('should set the mode to development', () => {
const result = getWebpackConfig(input);
const result = getBaseWebpackPartial(input);
expect(result.mode).toEqual('development');
});
@ -248,7 +229,7 @@ describe('getWebpackConfig', () => {
describe('when true', () => {
it('should set the mode to production', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
optimization: true
});
@ -257,7 +238,7 @@ describe('getWebpackConfig', () => {
});
it('should not minify', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
optimization: true
});
@ -266,7 +247,7 @@ describe('getWebpackConfig', () => {
});
it('should not concatenate modules', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
optimization: true
});
@ -278,7 +259,7 @@ describe('getWebpackConfig', () => {
describe('the max workers option', () => {
it('should set the maximum workers for the type checker', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
maxWorkers: 1
});
@ -292,7 +273,7 @@ describe('getWebpackConfig', () => {
describe('the assets option', () => {
it('should add a copy-webpack-plugin', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
assets: [
{
@ -319,7 +300,7 @@ describe('getWebpackConfig', () => {
describe('the circular dependencies option', () => {
it('should show warnings for circular dependencies', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
showCircularDependencies: true
});
@ -332,7 +313,7 @@ describe('getWebpackConfig', () => {
});
it('should exclude node modules', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
showCircularDependencies: true
});
@ -348,7 +329,7 @@ describe('getWebpackConfig', () => {
describe('the extract licenses option', () => {
it('should extract licenses to a separate file', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
extractLicenses: true
});
@ -368,7 +349,7 @@ describe('getWebpackConfig', () => {
describe('the progress option', () => {
it('should show build progress', () => {
const result = getWebpackConfig({
const result = getBaseWebpackPartial({
...input,
progress: true
});

View File

@ -2,39 +2,40 @@ import * as webpack from 'webpack';
import { Configuration, ProgressPlugin } from 'webpack';
import * as ts from 'typescript';
import { dirname, resolve } from 'path';
import { resolve } from 'path';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import CircularDependencyPlugin = require('circular-dependency-plugin');
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import { BuildNodeBuilderOptions } from '../node-build.builder';
import * as nodeExternals from 'webpack-node-externals';
import { AssetPatternObject } from '@angular-devkit/build-angular';
import { BuildBuilderOptions } from '../types';
import { readTsConfig } from '../typescript';
export const OUT_FILENAME = 'main.js';
export function getWebpackConfig(
options: BuildNodeBuilderOptions
export function getBaseWebpackPartial(
options: BuildBuilderOptions
): Configuration {
const compilerOptions = getCompilerOptions(options.tsConfig);
const { options: compilerOptions } = readTsConfig(options.tsConfig);
const supportsEs2015 =
compilerOptions.target !== ts.ScriptTarget.ES3 &&
compilerOptions.target !== ts.ScriptTarget.ES5;
const webpackConfig: Configuration = {
entry: [options.main],
entry: {
main: [options.main]
},
devtool: options.sourceMap ? 'source-map' : 'eval',
mode: options.optimization ? 'production' : 'development',
output: {
path: options.outputPath,
filename: OUT_FILENAME,
libraryTarget: 'commonjs'
filename: OUT_FILENAME
},
module: {
rules: [
{
test: /\.ts$/,
test: /\.tsx?$/,
loader: `ts-loader`,
options: {
configFile: options.tsConfig,
@ -46,12 +47,10 @@ export function getWebpackConfig(
]
},
resolve: {
extensions: ['.ts', '.mjs', '.js'],
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
alias: getAliases(options, compilerOptions),
mainFields: [...(supportsEs2015 ? ['es2015'] : []), 'module', 'main']
},
target: 'node',
node: false,
performance: {
hints: false
},
@ -61,7 +60,10 @@ export function getWebpackConfig(
workers: options.maxWorkers || ForkTsCheckerWebpackPlugin.TWO_CPUS_FREE
})
],
watch: options.watch
watch: options.watch,
watchOptions: {
poll: options.poll
}
};
const extraPlugins: webpack.Plugin[] = [];
@ -88,21 +90,6 @@ export function getWebpackConfig(
);
}
if (options.externalDependencies === 'all') {
webpackConfig.externals = [nodeExternals()];
} else if (Array.isArray(options.externalDependencies)) {
webpackConfig.externals = [
function(context, request, callback: Function) {
if (options.externalDependencies.includes(request)) {
// not bundled
return callback(null, 'commonjs ' + request);
}
// bundled
callback();
}
];
}
// process asset entries
if (options.assets) {
const copyWebpackPluginPatterns = options.assets.map(
@ -145,7 +132,7 @@ export function getWebpackConfig(
}
function getAliases(
options: BuildNodeBuilderOptions,
options: BuildBuilderOptions,
compilerOptions: ts.CompilerOptions
): { [key: string]: string } {
const replacements = [
@ -165,13 +152,3 @@ function getAliases(
{}
);
}
function getCompilerOptions(tsConfigPath: string) {
const readResult = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
const tsConfig = ts.parseJsonConfigFileContent(
readResult.config,
ts.sys,
dirname(tsConfigPath)
);
return tsConfig.options;
}

View File

@ -0,0 +1,352 @@
import { getSystemPath, normalize, join } from '@angular-devkit/core';
import { WebBuildBuilderOptions } from '../../web/build/web-build.builder';
import { WebDevServerOptions } from '../../web/dev-server/web-dev-server.builder';
import { getDevServerConfig } from './devserver.config';
import { Logger } from '@angular-devkit/core/src/logger';
import * as ts from 'typescript';
import * as fs from 'fs';
describe('getDevServerConfig', () => {
let buildInput: WebBuildBuilderOptions;
let serveInput: WebDevServerOptions;
let mockCompilerOptions: any;
let logger: Logger;
beforeEach(() => {
buildInput = {
main: 'main.ts',
index: 'index.html',
budgets: [],
baseHref: '/',
deployUrl: '/',
sourceMap: {
scripts: true,
styles: true,
hidden: false,
vendors: false
},
optimization: {
scripts: false,
styles: false
},
styles: [],
scripts: [],
outputPath: 'dist',
tsConfig: 'tsconfig.json',
fileReplacements: [],
root: getSystemPath(normalize(__dirname)),
sourceRoot: normalize('packages/builders')
};
serveInput = {
host: 'localhost',
port: 4200,
buildTarget: 'webapp:build',
ssl: false,
liveReload: true,
open: false,
watch: true
};
mockCompilerOptions = {
target: 'es2015',
paths: { path: ['mapped/path'] }
};
spyOn(ts, 'readConfigFile').and.callFake(() => ({
config: {
compilerOptions: mockCompilerOptions
}
}));
});
describe('unconditional settings', () => {
it('should allow requests from any domain', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.headers['Access-Control-Allow-Origin']).toEqual('*');
});
it('should not display warnings in the overlay', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.overlay.warnings).toEqual(false);
});
it('should not emit stats', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.stats).toEqual(false);
});
it('should not have a contentBase', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.contentBase).toEqual(false);
});
});
describe('host option', () => {
it('should set the host option', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.host).toEqual('localhost');
});
});
describe('port option', () => {
it('should set the port option', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.port).toEqual(4200);
});
});
describe('build options', () => {
it('should set the history api fallback options', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.historyApiFallback).toEqual({
index: '//index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
});
});
describe('optimization', () => {
it('should not compress assets by default', () => {
const { devServer: result } = getDevServerConfig(
buildInput,
serveInput,
logger
) as any;
expect(result.compress).toEqual(false);
});
it('should compress assets if scripts optimization is on', () => {
const { devServer: result } = getDevServerConfig(
{
...buildInput,
optimization: {
scripts: true,
styles: false
}
},
serveInput,
logger
) as any;
expect(result.compress).toEqual(true);
});
it('should compress assets if styles optimization is on', () => {
const { devServer: result } = getDevServerConfig(
{
...buildInput,
optimization: {
scripts: false,
styles: true
}
},
serveInput,
logger
) as any;
expect(result.compress).toEqual(true);
});
it('should compress assets if all optimization is on', () => {
const { devServer: result } = getDevServerConfig(
{
...buildInput,
optimization: {
scripts: true,
styles: true
}
},
serveInput,
logger
) as any;
expect(result.compress).toEqual(true);
});
it('should show an overlay when optimization is off', () => {
const { devServer: result } = getDevServerConfig(
{
...buildInput,
optimization: {
scripts: false,
styles: false
}
},
serveInput,
logger
) as any;
expect(result.overlay.errors).toEqual(true);
});
it('should not show an overlay when optimization is on', () => {
const { devServer: result } = getDevServerConfig(
{
...buildInput,
optimization: {
scripts: true,
styles: true
}
},
serveInput,
logger
) as any;
expect(result.overlay.errors).toEqual(false);
});
});
describe('liveReload option', () => {
it('should push the live reload entry to the main entry', () => {
const result = getDevServerConfig(buildInput, serveInput, logger);
expect(result.entry['main']).toContain(
`${require.resolve('webpack-dev-server/client')}?http://0.0.0.0:0`
);
});
it('should push the correct entry when publicHost option is used', () => {
const result = getDevServerConfig(
buildInput,
{
...serveInput,
publicHost: 'www.example.com'
},
logger
);
expect(result.entry['main']).toContain(
`${require.resolve(
'webpack-dev-server/client'
)}?http://www.example.com/`
);
});
it('should push the correct entry when publicHost and ssl options are used', () => {
const result = getDevServerConfig(
buildInput,
{
...serveInput,
ssl: true,
publicHost: 'www.example.com'
},
logger
);
expect(result.entry['main']).toContain(
`${require.resolve(
'webpack-dev-server/client'
)}?https://www.example.com/`
);
});
});
describe('ssl option', () => {
it('should set https to false if not on', () => {
const { devServer: result } = getDevServerConfig(
{
...buildInput,
optimization: {
scripts: true,
styles: true
}
},
serveInput,
logger
) as any;
expect(result.https).toEqual(false);
});
it('should configure it with the key and cert provided when on', () => {
spyOn(fs, 'readFileSync').and.callFake(path => {
if (path.endsWith('ssl.key')) {
return 'sslKeyContents';
} else if (path.endsWith('ssl.cert')) {
return 'sslCertContents';
}
});
const { devServer: result } = getDevServerConfig(
buildInput,
{
...serveInput,
ssl: true,
sslKey: 'ssl.key',
sslCert: 'ssl.cert'
},
logger
) as any;
expect(result.https).toEqual({
key: 'sslKeyContents',
cert: 'sslCertContents'
});
});
});
describe('proxyConfig option', () => {
it('should setProxyConfig', () => {
jest.mock(
join(normalize(__dirname), 'proxy.conf'),
() => ({
proxyConfig: 'proxyConfig'
}),
{
virtual: true
}
);
const { devServer: result } = getDevServerConfig(
buildInput,
{
...serveInput,
proxyConfig: 'proxy.conf'
},
logger
) as any;
expect(result.proxy).toEqual({
proxyConfig: 'proxyConfig'
});
});
});
});
});

View File

@ -0,0 +1,108 @@
import {
Configuration as WebpackDevServerConfiguration,
HistoryApiFallbackConfig
} from 'webpack-dev-server';
import { readFileSync } from 'fs';
import * as path from 'path';
import * as url from 'url';
import { WebBuildBuilderOptions } from '../../web/build/web-build.builder';
import { WebDevServerOptions } from '../../web/dev-server/web-dev-server.builder';
import { getWebConfig } from './web.config';
import { Configuration } from 'webpack';
import { Logger } from '@angular-devkit/core/src/logger';
import { OptimizationOptions } from '../types';
import { buildServePath } from '../serve-path';
export function getDevServerConfig(
buildOptions: WebBuildBuilderOptions,
serveOptions: WebDevServerOptions,
logger: Logger
) {
const webpackConfig: Configuration = getWebConfig(buildOptions, logger);
(webpackConfig as any).devServer = getDevServerPartial(
serveOptions,
buildOptions
);
if (serveOptions.liveReload) {
webpackConfig.entry['main'].unshift(getLiveReloadEntry(serveOptions));
}
return webpackConfig;
}
function getLiveReloadEntry(serveOptions: WebDevServerOptions) {
let clientAddress = `${serveOptions.ssl ? 'https' : 'http'}://0.0.0.0:0`;
if (serveOptions.publicHost) {
let publicHost = serveOptions.publicHost;
if (!/^\w+:\/\//.test(publicHost)) {
publicHost = `${serveOptions.ssl ? 'https' : 'http'}://${publicHost}`;
}
const clientUrl = url.parse(publicHost);
serveOptions.publicHost = clientUrl.host;
clientAddress = url.format(clientUrl);
}
let webpackDevServerPath;
try {
webpackDevServerPath = require.resolve('webpack-dev-server/client');
} catch {
throw new Error('The "webpack-dev-server" package could not be found.');
}
return `${webpackDevServerPath}?${clientAddress}`;
}
function getDevServerPartial(
options: WebDevServerOptions,
buildOptions: WebBuildBuilderOptions
): WebpackDevServerConfiguration {
const servePath = buildServePath(buildOptions);
const {
scripts: scriptsOptimization,
styles: stylesOptimization
} = buildOptions.optimization as OptimizationOptions;
const config: WebpackDevServerConfiguration = {
host: options.host,
port: options.port,
headers: { 'Access-Control-Allow-Origin': '*' },
historyApiFallback: {
index: `${servePath}/${path.basename(buildOptions.index)}`,
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
} as HistoryApiFallbackConfig,
stats: false,
compress: scriptsOptimization || stylesOptimization,
https: options.ssl,
overlay: {
errors: !(scriptsOptimization || stylesOptimization),
warnings: false
},
watchOptions: {
poll: buildOptions.poll
},
public: options.publicHost,
publicPath: servePath,
contentBase: false
};
if (options.ssl && options.sslKey && options.sslCert) {
config.https = getSslConfig(buildOptions.root, options);
}
if (options.proxyConfig) {
config.proxy = getProxyConfig(buildOptions.root, options);
}
return config;
}
function getSslConfig(root: string, options: WebDevServerOptions) {
return {
key: readFileSync(path.resolve(root, options.sslKey), 'utf-8'),
cert: readFileSync(path.resolve(root, options.sslCert), 'utf-8')
};
}
function getProxyConfig(root: string, options: WebDevServerOptions) {
const proxyPath = path.resolve(root, options.proxyConfig as string);
return require(proxyPath);
}

View File

@ -0,0 +1,67 @@
import { getNodeWebpackConfig } from './node.config';
import { getSystemPath, normalize } from '@angular-devkit/core';
import { BuildNodeBuilderOptions } from '../../node/build/node-build.builder';
describe('getNodePartial', () => {
let input: BuildNodeBuilderOptions;
beforeEach(() => {
input = {
main: 'main.ts',
outputPath: 'dist',
tsConfig: 'tsconfig.json',
externalDependencies: 'all',
fileReplacements: [],
root: getSystemPath(normalize('/root')),
statsJson: false
};
});
describe('unconditionally', () => {
it('should target commonjs', () => {
const result = getNodeWebpackConfig(input);
expect(result.output.libraryTarget).toEqual('commonjs');
});
it('should target node', () => {
const result = getNodeWebpackConfig(input);
expect(result.target).toEqual('node');
});
it('should not polyfill node apis', () => {
const result = getNodeWebpackConfig(input);
expect(result.node).toEqual(false);
});
});
describe('the externalDependencies option', () => {
it('should change all node_modules to commonjs imports', () => {
const result = getNodeWebpackConfig(input);
const callback = jest.fn();
result.externals[0](null, '@angular/core', callback);
expect(callback).toHaveBeenCalledWith(null, 'commonjs @angular/core');
});
it('should change given module names to commonjs imports but not others', () => {
const result = getNodeWebpackConfig({
...input,
externalDependencies: ['module1']
});
const callback = jest.fn();
result.externals[0](null, 'module1', callback);
expect(callback).toHaveBeenCalledWith(null, 'commonjs module1');
result.externals[0](null, '@angular/core', callback);
expect(callback).toHaveBeenCalledWith();
});
it('should not change any modules to commonjs imports', () => {
const result = getNodeWebpackConfig({
...input,
externalDependencies: 'none'
});
expect(result.externals).not.toBeDefined();
});
});
});

View File

@ -0,0 +1,38 @@
import { Configuration } from 'webpack';
import * as mergeWebpack from 'webpack-merge';
import * as nodeExternals from 'webpack-node-externals';
import { BuildNodeBuilderOptions } from '../../node/build/node-build.builder';
import { getBaseWebpackPartial } from './config';
function getNodePartial(options: BuildNodeBuilderOptions) {
const webpackConfig: Configuration = {
output: {
libraryTarget: 'commonjs'
},
target: 'node',
node: false
};
if (options.externalDependencies === 'all') {
webpackConfig.externals = [nodeExternals()];
} else if (Array.isArray(options.externalDependencies)) {
webpackConfig.externals = [
function(context, request, callback: Function) {
if (options.externalDependencies.includes(request)) {
// not bundled
return callback(null, 'commonjs ' + request);
}
// bundled
callback();
}
];
}
return webpackConfig;
}
export function getNodeWebpackConfig(options: BuildNodeBuilderOptions) {
return mergeWebpack([
getBaseWebpackPartial(options),
getNodePartial(options)
]);
}

View File

@ -0,0 +1,95 @@
import { getSystemPath, normalize, Path } from '@angular-devkit/core';
import { getWebConfig as getWebPartial } from './web.config';
import { WebBuildBuilderOptions } from '../../web/build/web-build.builder';
import { createConsoleLogger } from '@angular-devkit/core/node';
import { Logger } from '@angular-devkit/core/src/logger';
import * as ts from 'typescript';
import { SourceMapDevToolPlugin } from 'webpack';
describe('getWebConfig', () => {
let input: WebBuildBuilderOptions;
let logger: Logger;
let mockCompilerOptions: any;
beforeEach(() => {
input = {
main: 'main.ts',
index: 'index.html',
budgets: [],
baseHref: '/',
deployUrl: '/',
sourceMap: {
scripts: true,
styles: true,
hidden: false,
vendors: false
},
optimization: {
scripts: false,
styles: false
},
styles: [],
scripts: [],
outputPath: 'dist',
tsConfig: 'tsconfig.json',
fileReplacements: [],
root: getSystemPath(normalize(__dirname)),
sourceRoot: normalize('packages/builders')
};
logger = createConsoleLogger();
mockCompilerOptions = {
target: 'es2015',
paths: { path: ['mapped/path'] }
};
spyOn(ts, 'readConfigFile').and.callFake(() => ({
config: {
compilerOptions: mockCompilerOptions
}
}));
});
it('should resolve the browser main field', () => {
const result = getWebPartial(input, logger);
expect(result.resolve.mainFields).toContain('browser');
});
it('should use the style-loader to load styles', () => {
const result = getWebPartial(input, logger);
expect(
result.module.rules.find(rule => rule.test.test('styles.css')).use[0]
.loader
).toEqual('style-loader');
expect(
result.module.rules.find(rule => rule.test.test('styles.scss')).use[0]
.loader
).toEqual('style-loader');
});
describe('polyfills', () => {
it('should set the polyfills entry', () => {
const result = getWebPartial(
{
...input,
polyfills: 'polyfills.ts'
},
logger
);
expect(result.entry.polyfills).toEqual(['polyfills.ts']);
});
});
describe('es2015 polyfills', () => {
it('should set the es2015-polyfills entry', () => {
const result = getWebPartial(
{
...input,
es2015Polyfills: 'polyfills.es2015.ts'
},
logger
);
expect(result.entry['es2015-polyfills']).toEqual(['polyfills.es2015.ts']);
});
});
});

View File

@ -0,0 +1,103 @@
import * as mergeWebpack from 'webpack-merge';
import { WebBuildBuilderOptions } from '../../web/build/web-build.builder';
import { getBaseWebpackPartial } from './config';
import {
getBrowserConfig,
getStylesConfig,
getCommonConfig
} from '@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs';
import { convertBuildOptions } from '../normalize';
import { Configuration } from 'webpack';
import { Logger } from '@angular-devkit/core/src/logger';
import { readTsConfig } from '../typescript';
import { resolve } from 'path';
import {
WebpackConfigOptions,
BuildOptions
} from '@angular-devkit/build-angular/src/angular-cli-files/models/build-options';
import typescript = require('typescript');
export function getWebConfig(options: WebBuildBuilderOptions, logger: Logger) {
const tsConfig = readTsConfig(options.tsConfig);
const supportES2015 =
tsConfig.options.target !== typescript.ScriptTarget.ES5 &&
tsConfig.options.target !== typescript.ScriptTarget.ES3;
const wco: WebpackConfigOptions<BuildOptions> = {
root: options.root,
projectRoot: resolve(options.root, options.sourceRoot),
buildOptions: convertBuildOptions(options),
supportES2015,
logger,
tsConfig,
tsConfigPath: options.tsConfig
};
return mergeWebpack([
_getBaseWebpackPartial(options),
options.polyfills ? getPolyfillsPartial(options) : {},
options.es2015Polyfills ? getEs2015PolyfillsPartial(options) : {},
getStylesPartial(wco),
getCommonPartial(wco),
getBrowserConfig(wco)
]);
}
function _getBaseWebpackPartial(options: WebBuildBuilderOptions) {
let partial = getBaseWebpackPartial(options);
delete partial.resolve.mainFields;
return partial;
}
function getCommonPartial(
wco: WebpackConfigOptions<BuildOptions>
): Configuration {
const commonConfig: Configuration = <Configuration>getCommonConfig(wco);
delete commonConfig.entry;
// delete commonConfig.devtool;
delete commonConfig.resolve.modules;
delete commonConfig.resolve.extensions;
delete commonConfig.output.path;
delete commonConfig.module;
return commonConfig;
}
function getStylesPartial(
wco: WebpackConfigOptions<BuildOptions>
): Configuration {
const partial = getStylesConfig(wco);
partial.module.rules = partial.module.rules.map(rule => {
if (!Array.isArray(rule.use)) {
return rule;
}
rule.use = rule.use.map(loaderConfig => {
if (
typeof loaderConfig === 'object' &&
loaderConfig.loader === 'raw-loader'
) {
return {
loader: 'style-loader'
};
}
return loaderConfig;
});
return rule;
});
return partial;
}
function getPolyfillsPartial(options: WebBuildBuilderOptions): Configuration {
return {
entry: {
polyfills: [options.polyfills]
}
};
}
function getEs2015PolyfillsPartial(
options: WebBuildBuilderOptions
): Configuration {
return {
entry: {
['es2015-polyfills']: [options.es2015Polyfills]
}
};
}

View File

@ -0,0 +1,303 @@
{
"title": "Node Application Build Target",
"description": "Node application build target options for Build Facade",
"type": "object",
"properties": {
"main": {
"type": "string",
"description": "The name of the main entry-point file."
},
"tsConfig": {
"type": "string",
"description": "The name of the Typescript configuration file."
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
},
"baseHref": {
"type": "string",
"description": "Base url for the application being built.",
"default": "/"
},
"deployUrl": {
"type": "string",
"description": "URL where the application will be deployed."
},
"vendorChunk": {
"type": "boolean",
"description": "Use a separate bundle containing only vendor libraries.",
"default": true
},
"commonChunk": {
"type": "boolean",
"description": "Use a separate bundle containing code used across multiple bundles.",
"default": true
},
"sourceMap": {
"description": "Output sourcemaps.",
"default": true,
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
"description": "Output sourcemaps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
"description": "Output sourcemaps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
"description": "Output sourcemaps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
"description": "Resolve vendor packages sourcemaps.",
"default": false
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building.",
"default": false
},
"assets": {
"type": "array",
"description": "List of static application assets.",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"index": {
"type": "string",
"description": "HTML File which will be contain the application"
},
"scripts": {
"type": "array",
"description": "External Scripts which will be included before the main application entry",
"items": {
"type": "string"
}
},
"styles": {
"type": "array",
"description": "External Styles which will be included with the application",
"items": {
"type": "string"
}
},
"budgets": {
"description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.",
"type": "array",
"items": {
"$ref": "#/definitions/budget"
},
"default": []
},
"namedChunks": {
"type": "boolean",
"description": "Names the produced bundles according to their entry file",
"default": true
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"default": "none",
"enum": ["none", "all", "media", "bundles"]
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors.",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to project root.",
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false
},
"optimization": {
"description": "Enables optimization of the build output.",
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
"description": "Enables optimization of the scripts output.",
"default": true
},
"styles": {
"type": "boolean",
"description": "Enables optimization of the styles output.",
"default": true
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
},
"extractCss": {
"type": "boolean",
"description": "Extract css into a .css file",
"default": false
},
"es2015Polyfills": {
"description": "Conditional polyfills loaded in browsers which do not support ES2015.",
"type": "string"
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",
"default": false
},
"polyfills": {
"type": "string",
"description": "Polyfills to load before application"
},
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https://webpack.github.io/analyse.",
"default": false
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file, in the case of production builds only.",
"default": false
},
"showCircularDependencies": {
"type": "boolean",
"description": "Show circular dependency warnings on builds.",
"default": true
},
"maxWorkers": {
"type": "number",
"description": "Number of workers to use for type checking. (defaults to # of CPUS - 2)"
},
"fileReplacements": {
"description": "Replace files with other files in the build.",
"type": "array",
"items": {
"type": "object",
"properties": {
"replace": {
"type": "string"
},
"with": {
"type": "string"
}
},
"additionalProperties": false,
"required": ["replace", "with"]
},
"default": []
},
"webpackConfig": {
"type": "string",
"description": "Path to a function which takes a webpack config, some context and returns the resulting webpack config"
}
},
"required": ["tsConfig", "main", "index"],
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
},
"ignore": {
"description": "An array of globs to ignore.",
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "string",
"description": "Absolute path within the output."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{
"type": "string"
}
]
},
"budget": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of budget.",
"enum": ["all", "allScript", "any", "anyScript", "bundle", "initial"]
},
"name": {
"type": "string",
"description": "The name of the bundle."
},
"baseline": {
"type": "string",
"description": "The baseline size for comparison."
},
"maximumWarning": {
"type": "string",
"description": "The maximum threshold for warning relative to the baseline."
},
"maximumError": {
"type": "string",
"description": "The maximum threshold for error relative to the baseline."
},
"minimumWarning": {
"type": "string",
"description": "The minimum threshold for warning relative to the baseline."
},
"minimumError": {
"type": "string",
"description": "The minimum threshold for error relative to the baseline."
},
"warning": {
"type": "string",
"description": "The threshold for warning relative to the baseline (min & max)."
},
"error": {
"type": "string",
"description": "The threshold for error relative to the baseline (min & max)."
}
},
"additionalProperties": false,
"required": ["type"]
}
}
}

View File

@ -0,0 +1,176 @@
import { normalize, join } from '@angular-devkit/core';
import { TestLogger } from '@angular-devkit/architect/testing';
import WebBuildBuilder from './web-build.builder';
import { WebBuildBuilderOptions } from './web-build.builder';
import { of } from 'rxjs';
import * as fs from 'fs';
describe('WebBuildBuilder', () => {
let builder: WebBuildBuilder;
let testOptions: WebBuildBuilderOptions;
beforeEach(() => {
builder = new WebBuildBuilder({
host: <any>{},
logger: new TestLogger('test'),
workspace: <any>{
root: __dirname
},
architect: <any>{},
targetSpecifier: {
project: 'webapp',
target: 'build'
}
});
testOptions = {
index: 'apps/webapp/src/index.html',
budgets: [],
baseHref: '/',
deployUrl: '/',
scripts: ['apps/webapp/src/scripts.js'],
styles: ['apps/webapp/src/styles.css'],
main: 'apps/webapp/src/main.ts',
tsConfig: 'apps/webapp/tsconfig.app.json',
outputPath: 'dist/apps/webapp',
fileReplacements: [
{
replace: 'apps/webapp/environment/environment.ts',
with: 'apps/webapp/environment/environment.prod.ts'
},
{
replace: 'module1.ts',
with: 'module2.ts'
}
],
assets: [],
statsJson: false
};
});
describe('run', () => {
it('should call runWebpack', () => {
const runWebpack = spyOn(
builder.webpackBuilder,
'runWebpack'
).and.returnValue(
of({
success: true
})
);
builder.run({
root: normalize(__dirname),
sourceRoot: join(normalize(__dirname), 'apps/webapp'),
projectType: 'application',
builder: '@nrwl/builders:node-build',
options: testOptions
});
expect(runWebpack).toHaveBeenCalled();
});
it('should emit success', async () => {
spyOn(builder.webpackBuilder, 'runWebpack').and.returnValue(
of({
success: true
})
);
const buildEvent = await builder
.run({
root: normalize(__dirname),
sourceRoot: join(normalize(__dirname), 'apps/webapp'),
projectType: 'application',
builder: '@nrwl/builders:node-build',
options: testOptions
})
.toPromise();
expect(buildEvent.success).toEqual(true);
});
describe('statsJson option', () => {
beforeEach(() => {
const stats = {
stats: 'stats'
};
spyOn(builder.webpackBuilder, 'runWebpack').and.callFake((opts, cb) => {
cb({
toJson: () => stats,
toString: () => JSON.stringify(stats)
});
return of({
success: true
});
});
spyOn(fs, 'writeFileSync');
});
it('should generate a stats json', async () => {
await builder
.run({
root: normalize(__dirname),
sourceRoot: join(normalize(__dirname), 'apps/webapp'),
projectType: 'application',
builder: '@nrwl/builders:web-build',
options: {
...testOptions,
statsJson: true
}
})
.toPromise();
expect(fs.writeFileSync).toHaveBeenCalledWith(
join(normalize(__dirname), 'dist/apps/webapp/stats.json'),
JSON.stringify(
{
stats: 'stats'
},
null,
2
)
);
});
});
describe('webpackConfig option', () => {
it('should require the specified function and use the return value', async () => {
const runWebpack = spyOn(
builder.webpackBuilder,
'runWebpack'
).and.returnValue(
of({
success: true
})
);
const mockFunction = jest.fn(config => ({
config: 'config'
}));
jest.mock(
join(normalize(__dirname), 'apps/webapp/webpack.config.js'),
() => mockFunction,
{
virtual: true
}
);
await builder
.run({
root: normalize(__dirname),
sourceRoot: join(normalize(__dirname), 'apps/webapp'),
projectType: 'application',
builder: '@nrwl/builders:web-build',
options: {
...testOptions,
webpackConfig: 'apps/webapp/webpack.config.js'
}
})
.toPromise();
expect(mockFunction).toHaveBeenCalled();
expect(runWebpack.calls.first().args[0]).toEqual({
config: 'config'
});
});
});
});
});

View File

@ -0,0 +1,79 @@
import {
Builder,
BuildEvent,
BuilderConfiguration,
BuilderContext
} from '@angular-devkit/architect';
import { getSystemPath } from '@angular-devkit/core';
import { WebpackBuilder } from '@angular-devkit/build-webpack';
import { Observable } from 'rxjs';
import { writeFileSync } from 'fs';
import { resolve } from 'path';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { BuildBuilderOptions } from '../../utils/types';
import { getWebConfig } from '../../utils/webpack/web.config';
import {
OutputHashing,
StylePreprocessorOptions,
Budget
} from '@angular-devkit/build-angular';
export interface WebBuildBuilderOptions extends BuildBuilderOptions {
index: string;
budgets: Budget[];
baseHref: string;
deployUrl: string;
polyfills?: string;
es2015Polyfills?: string;
scripts: string[];
styles: string[];
vendorChunk?: boolean;
commonChunk?: boolean;
outputHashing?: OutputHashing;
stylePreprocessingOptions?: StylePreprocessorOptions;
}
export default class BuildWebBuilder
implements Builder<WebBuildBuilderOptions> {
webpackBuilder: WebpackBuilder;
root: string;
constructor(private context: BuilderContext) {
this.webpackBuilder = new WebpackBuilder(this.context);
this.root = getSystemPath(this.context.workspace.root);
}
run(
builderConfig: BuilderConfiguration<WebBuildBuilderOptions>
): Observable<BuildEvent> {
const options = normalizeWebBuildOptions(
builderConfig.options,
this.root,
builderConfig.sourceRoot
);
let config = getWebConfig(options, this.context.logger);
if (options.webpackConfig) {
config = require(options.webpackConfig)(config, {
options,
configuration: this.context.targetSpecifier.configuration
});
}
return this.webpackBuilder.runWebpack(config, stats => {
if (options.statsJson) {
writeFileSync(
resolve(this.root, options.outputPath, 'stats.json'),
JSON.stringify(stats.toJson(), null, 2)
);
}
this.context.logger.info(stats.toString());
});
}
}

View File

@ -0,0 +1,53 @@
{
"title": "Web Dev Server",
"description": "Web Dev Server",
"type": "object",
"properties": {
"buildTarget": {
"type": "string",
"description": "Target which builds the application"
},
"port": {
"type": "number",
"description": "Port to listen on.",
"default": 4200
},
"host": {
"type": "string",
"description": "Host to listen on.",
"default": "localhost"
},
"ssl": {
"type": "boolean",
"description": "Serve using HTTPS.",
"default": false
},
"sslKey": {
"type": "string",
"description": "SSL key to use for serving HTTPS."
},
"sslCert": {
"type": "string",
"description": "SSL certificate to use for serving HTTPS."
},
"watch": {
"type": "boolean",
"description": "Watches for changes and rebuilds application",
"default": true
},
"liveReload": {
"type": "boolean",
"description": "Whether to reload the page on change, using live-reload.",
"default": true
},
"publicHost": {
"type": "string",
"description": "Public URL where the application will be served"
},
"open": {
"type": "boolean",
"description": "Open the application in the browser.",
"default": false
}
}
}

View File

@ -0,0 +1,142 @@
import {
Builder,
BuildEvent,
BuilderConfiguration,
BuilderContext
} from '@angular-devkit/architect';
import { getSystemPath } from '@angular-devkit/core';
import { WebpackDevServerBuilder } from '@angular-devkit/build-webpack';
import { Observable } from 'rxjs';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { getDevServerConfig } from '../../utils/webpack/devserver.config';
import { concatMap, map, switchMap, tap } from 'rxjs/operators';
import { WebBuildBuilderOptions } from '../build/web-build.builder';
import { Configuration } from 'webpack';
import { writeFileSync } from 'fs';
import * as opn from 'opn';
import * as url from 'url';
import { resolve } from 'path';
import { buildServePath } from '../../utils/serve-path';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
export interface WebDevServerOptions {
host: string;
port: number;
publicHost?: string;
ssl: boolean;
sslKey?: string;
sslCert?: string;
proxyConfig?: string;
buildTarget: string;
open: boolean;
liveReload: boolean;
watch: boolean;
}
export default class WebDevServerBuilder
implements Builder<WebDevServerOptions> {
webpackDevServerBuilder = new WebpackDevServerBuilder(this.context);
root: string;
constructor(private context: BuilderContext) {
this.root = getSystemPath(this.context.workspace.root);
}
run(
builderConfig: BuilderConfiguration<WebDevServerOptions>
): Observable<BuildEvent> {
const serveOptions = builderConfig.options;
return this.getBuildOptions(serveOptions).pipe(
map(buildOptions => {
buildOptions = normalizeWebBuildOptions(
buildOptions,
this.root,
builderConfig.sourceRoot
);
let webpackConfig: Configuration = getDevServerConfig(
buildOptions,
serveOptions,
this.context.logger
);
if (buildOptions.webpackConfig) {
webpackConfig = require(buildOptions.webpackConfig)(webpackConfig, {
buildOptions,
configuration: serveOptions.buildTarget.split(':')[2]
});
}
return [webpackConfig, buildOptions] as [
Configuration,
WebBuildBuilderOptions
];
}),
tap(([_, options]) => {
const path = buildServePath(options);
const serverUrl = url.format({
protocol: serveOptions.ssl ? 'https' : 'http',
hostname:
serveOptions.host === '0.0.0.0' ? 'localhost' : serveOptions.host,
port: serveOptions.port.toString(),
path: path
});
this.context.logger.info(stripIndents`
**
Web Development Server is listening at ${serverUrl}
**
`);
if (serveOptions.open) {
opn(serverUrl, {
wait: false
});
}
}),
switchMap(([config, options]) => {
return this.webpackDevServerBuilder.runWebpackDevServer(
config,
undefined,
stats => {
if (options.statsJson) {
writeFileSync(
resolve(this.root, options.outputPath, 'stats.json'),
JSON.stringify(stats.toJson(), null, 2)
);
}
this.context.logger.info(stats.toString());
}
);
})
);
}
private getBuildOptions(options: WebDevServerOptions) {
const builderConfig = this.getBuildBuilderConfig(options);
return this.context.architect.getBuilderDescription(builderConfig).pipe(
concatMap(buildDescription =>
this.context.architect.validateBuilderOptions(
builderConfig,
buildDescription
)
),
map(builderConfig => builderConfig.options)
);
}
private getBuildBuilderConfig(options: WebDevServerOptions) {
const [project, target, configuration] = options.buildTarget.split(':');
return this.context.architect.getBuilderConfiguration<
WebBuildBuilderOptions
>({
project,
target,
configuration,
overrides: {
watch: options.watch
}
});
}
}

View File

@ -10,6 +10,7 @@ import { getFileContent } from '@schematics/angular/utility/test';
import * as stripJsonComments from 'strip-json-comments';
import { readJsonInTree, updateJsonInTree } from '../../utils/ast-utils';
import { NxJson } from '../../command-line/shared';
import { Framework } from '../../utils/frameworks';
describe('app', () => {
let appTree: Tree;
@ -349,6 +350,144 @@ describe('app', () => {
});
});
describe('--framework', () => {
describe('custom-elements', () => {
it('should replace app files', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', framework: Framework.CustomElements },
appTree
);
expect(tree.exists('apps/my-app/src/main.ts')).toBeTruthy();
expect(tree.exists('apps/my-app/src/app/app.component.ts')).toBeFalsy();
expect(
tree.exists('apps/my-app/src/app/app.component.css')
).toBeFalsy();
expect(
tree.exists('apps/my-app/src/app/app.component.html')
).toBeFalsy();
expect(
tree.exists('apps/my-app/src/app/app.component.spec.ts')
).toBeFalsy();
});
});
describe('react', () => {
it('should replace app files', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-App',
framework: Framework.React
},
appTree
);
expect(tree.exists('apps/my-app/src/main.ts')).toBeFalsy();
expect(tree.exists('apps/my-app/src/app/app.component.ts')).toBeFalsy();
expect(
tree.exists('apps/my-app/src/app/app.component.css')
).toBeFalsy();
expect(
tree.exists('apps/my-app/src/app/app.component.html')
).toBeFalsy();
expect(
tree.exists('apps/my-app/src/app/app.component.spec.ts')
).toBeFalsy();
expect(tree.exists('apps/my-app/src/main.tsx')).toBeTruthy();
expect(tree.exists('apps/my-app/src/app/app.tsx')).toBeTruthy();
expect(tree.exists('apps/my-app/src/app/app.spec.tsx')).toBeTruthy();
expect(tree.exists('apps/my-app/src/app/app.css')).toBeTruthy();
});
it('should setup jest with tsx support', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-App',
framework: Framework.React
},
appTree
);
expect(tree.readContent('apps/my-app/jest.config.js')).toContain(
`moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],`
);
});
it('should setup the nrwl web build builder', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-App',
framework: Framework.React
},
appTree
);
const angularJson = readJsonInTree(tree, 'angular.json');
const architectConfig = angularJson.projects['my-app'].architect;
expect(architectConfig.build.builder).toEqual(
'@nrwl/builders:web-build'
);
expect(architectConfig.build.options).toEqual({
assets: ['apps/my-app/src/favicon.ico', 'apps/my-app/src/assets'],
index: 'apps/my-app/src/index.html',
main: 'apps/my-app/src/main.tsx',
outputPath: 'dist/apps/my-app',
polyfills: 'apps/my-app/src/polyfills.ts',
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json'
});
expect(architectConfig.build.configurations.production).toEqual({
optimization: true,
budgets: [
{
maximumError: '5mb',
maximumWarning: '2mb',
type: 'initial'
}
],
extractCss: true,
extractLicenses: true,
fileReplacements: [
{
replace: 'apps/my-app/src/environments/environment.ts',
with: 'apps/my-app/src/environments/environment.prod.ts'
}
],
namedChunks: false,
outputHashing: 'all',
sourceMap: false,
vendorChunk: false
});
});
it('should setup the nrwl web dev server builder', async () => {
const tree = await runSchematic(
'app',
{
name: 'my-App',
framework: Framework.React
},
appTree
);
const angularJson = readJsonInTree(tree, 'angular.json');
const architectConfig = angularJson.projects['my-app'].architect;
expect(architectConfig.serve.builder).toEqual(
'@nrwl/builders:web-dev-server'
);
expect(architectConfig.serve.options).toEqual({
buildTarget: 'my-app:build'
});
expect(architectConfig.serve.configurations.production).toEqual({
buildTarget: 'my-app:build:production'
});
});
});
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
const tree = await runSchematic(

View File

@ -0,0 +1,9 @@
{
"extends": "<%= offsetFromRoot %>tsconfig.json",
"compilerOptions": {<% if (framework === 'react') { %>
"jsx": "react",<% } %><% if (framework === 'custom-elements') { %>
"target": "es2015",<% } %>
"types": []
},
"include": ["**/*.ts"<% if (framework === 'react') { %>, "**/*.tsx"<% } %>]
}

View File

@ -0,0 +1,21 @@
import { AppElement } from './app.element';
describe('AppElement', () => {
let app: AppElement;
beforeEach(() => {
app = new AppElement();
});
it('should create successfully', () => {
expect(app).toBeTruthy();
});
it('should have a greeting', () => {
app.connectedCallback();
expect(app.querySelector('h1').innerHTML).toEqual(
'Welcome to <%= name %>!'
);
});
});

View File

@ -0,0 +1,30 @@
export class AppElement extends HTMLElement {
public static observedAttributes = [
];
connectedCallback() {
const title = '<%= name %>';
this.innerHTML = `
<div style="text-align: center">
<h1>Welcome to ${title}!</h1>
<img
width="300"
src="https://raw.githubusercontent.com/nrwl/nx/master/nx-logo.png"
/>
</div>
<p>This is a Custom Elements app built with <a href="https://nx.dev">Nx</a>.</p>
<p>🔎 **Nx is a set of Angular CLI power-ups for modern development.**</p>
<h2>Quick Start & Documentation</h2>
<ul>
<li>
<a href="https://nx.dev/getting-started/what-is-nx">30-minute video showing all Nx features</a>
</li>
<li>
<a href="https://nx.dev/tutorial/01-create-application">Interactive tutorial</a>
</li>
</ul>
`;
}
}
customElements.define('<%= prefix %>-root', AppElement);

View File

@ -0,0 +1 @@
import './app/app.element.ts';

View File

@ -0,0 +1,3 @@
/**
* This file contains polyfills loaded on all browsers
**/

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import { render, cleanup } from 'react-testing-library';
import { App } from './app';
describe('App', () => {
afterEach(cleanup);
it('should render successfully', () => {
const { baseElement } = render(<App />);
expect(baseElement).toBeTruthy();
});
it('should have a greeting as the title', () => {
const { getByText } = render(<App />);
expect(getByText('Welcome to <%= name %>!')).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import { Component } from 'react';
import './app.<%= style %>';
export class App extends Component {
render() {
const title = '<%= name %>';
return (
<div>
<div style={{ textAlign: 'center' }}>
<h1>Welcome to {title}!</h1>
<img
width="300"
src="https://raw.githubusercontent.com/nrwl/nx/master/nx-logo.png"
/>
</div>
<p>This is a React app built with <a href="https://nx.dev">Nx</a>.</p>
<p>🔎 **Nx is a set of Angular CLI power-ups for modern development.**</p>
<h2>Quick Start & Documentation</h2>
<ul>
<li>
<a href="https://nx.dev/getting-started/what-is-nx">30-minute video showing all Nx features</a>
</li>
<li>
<a href="https://nx.dev/tutorial/01-create-application">Interactive tutorial</a>
</li>
</ul>
</div>
);
}
}

View File

@ -0,0 +1,6 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { App } from './app/app';
ReactDOM.render(<App />, document.querySelector('<%= prefix %>-root'));

View File

@ -0,0 +1,3 @@
/**
* This file contains polyfills loaded on all browsers
**/

View File

@ -1,7 +0,0 @@
{
"extends": "<%= offsetFromRoot %>tsconfig.json",
"compilerOptions": {
"types": []
},
"include": ["**/*.ts"]
}

View File

@ -34,8 +34,12 @@ import {
} from '../../utils/cli-config-utils';
import { formatFiles } from '../../utils/rules/format-files';
import { join, normalize } from '@angular-devkit/core';
import { readJson } from '../../../../../e2e/utils';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { Framework } from '../../utils/frameworks';
import {
reactVersions,
documentRegisterElementVersion
} from '../../lib-versions';
interface NormalizedSchema extends Schema {
appProjectRoot: string;
@ -141,19 +145,125 @@ function updateComponentTemplate(options: NormalizedSchema): Rule {
};
}
function updateBuilders(options: NormalizedSchema): Rule {
return (host: Tree) => {
return updateJsonInTree(getWorkspacePath(host), json => {
const project = json.projects[options.name];
const buildOptions = project.architect.build;
const serveOptions = project.architect.serve;
buildOptions.builder = '@nrwl/builders:web-build';
delete buildOptions.configurations.production.aot;
delete buildOptions.configurations.production.buildOptimizer;
serveOptions.builder = '@nrwl/builders:web-dev-server';
serveOptions.options.buildTarget = serveOptions.options.browserTarget;
delete serveOptions.options.browserTarget;
serveOptions.configurations.production.buildTarget =
serveOptions.configurations.production.browserTarget;
delete serveOptions.configurations.production.browserTarget;
if (options.framework === Framework.React) {
buildOptions.options.main = buildOptions.options.main.replace(
'.ts',
'.tsx'
);
}
return json;
});
};
}
function updateApplicationFiles(options: NormalizedSchema): Rule {
return chain([
deleteAngularApplicationFiles(options),
addApplicationFiles(options),
updateDependencies(options)
]);
}
function addApplicationFiles(options: NormalizedSchema): Rule {
return mergeWith(
apply(url(`./files/${options.framework}`), [
template({
...options,
tmpl: ''
}),
move(options.appProjectRoot)
])
);
}
function deleteAngularApplicationFiles(options: NormalizedSchema): Rule {
return (host: Tree) => {
const projectRoot = normalize(options.appProjectRoot);
[
'src/main.ts',
'src/polyfills.ts',
'src/app/app.module.ts',
'src/app/app.component.ts',
'src/app/app.component.spec.ts',
`src/app/app.component.${options.style}`,
'src/app/app.component.html'
].forEach(path => {
path = join(projectRoot, path);
if (host.exists(path)) {
host.delete(path);
}
});
};
}
function updateDependencies(options: NormalizedSchema): Rule {
return updateJsonInTree('package.json', (json, context) => {
json.dependencies = json.dependencies || {};
json.devDependencies = json.devDependencies || {};
switch (options.framework) {
case Framework.React:
json.dependencies = {
...json.dependencies,
react: reactVersions.framework,
'react-dom': reactVersions.framework
};
json.devDependencies = {
...json.devDependencies,
'@types/react': reactVersions.reactTypes,
'@types/react-dom': reactVersions.reactDomTypes,
'react-testing-library': reactVersions.testingLibrary
};
context.addTask(new NodePackageInstallTask());
return json;
case Framework.CustomElements:
json.dependencies = {
...json.dependencies,
'document-register-element': documentRegisterElementVersion
};
context.addTask(new NodePackageInstallTask());
return json;
default:
return json;
}
});
}
function addTsconfigs(options: NormalizedSchema): Rule {
return chain([
mergeWith(
apply(url('./files'), [
apply(url('./files/app'), [
template({
...options,
offsetFromRoot: offsetFromRoot(options.appProjectRoot)
}),
move(options.appProjectRoot)
])
),
mergeWith(
apply(url('./files'), [
apply(url('./files/app'), [
template({
...options,
offsetFromRoot: offsetFromRoot(options.e2eProjectRoot)
}),
move(options.e2eProjectRoot)
@ -246,17 +356,15 @@ function updateProject(options: NormalizedSchema): Rule {
host.delete(`${options.e2eProjectRoot}/protractor.conf.js`);
}
},
(host, context) => {
if (options.e2eTestRunner === 'protractor') {
updateJsonInTree('/package.json', json => {
options.e2eTestRunner === 'protractor'
? updateJsonInTree('/package.json', (json, context) => {
if (!json.devDependencies.protractor) {
json.devDependencies.protractor = '~5.4.0';
context.addTask(new NodePackageInstallTask());
}
return json;
})(host, context);
}
}
})
: noop()
]);
};
}
@ -361,11 +469,25 @@ export default function(schema: Schema): Rule {
move(appProjectRoot, options.appProjectRoot),
updateProject(options),
updateComponentTemplate(options),
options.routing ? addRouterRootConfiguration(options) : noop(),
options.framework !== Framework.Angular
? updateBuilders(options)
: noop(),
options.framework === Framework.Angular
? updateComponentTemplate(options)
: updateApplicationFiles(options),
options.framework === Framework.Angular && options.routing
? addRouterRootConfiguration(options)
: noop(),
options.unitTestRunner === 'jest'
? schematic('jest-project', {
project: options.name
project: options.name,
supportTsx: options.framework === Framework.React,
setupFile:
options.framework === Framework.Angular
? 'angular'
: options.framework === Framework.CustomElements
? 'custom-elements'
: 'none'
})
: noop(),
options.unitTestRunner === 'karma'
@ -379,6 +501,12 @@ export default function(schema: Schema): Rule {
}
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
if (options.framework !== Framework.Angular && options.routing) {
throw new Error(
`Routing is not supported yet with frameworks other than Angular`
);
}
const appDirectory = options.directory
? `${toFileName(options.directory)}/${toFileName(options.name)}`
: toFileName(options.name);

View File

@ -1,6 +1,8 @@
import { Framework } from './.../utils/frameworks';
import { E2eTestRunner, UnitTestRunner } from '../../utils/test-runners';
export interface Schema {
framework: Framework;
name: string;
skipFormat: boolean;
inlineStyle?: boolean;

View File

@ -4,6 +4,30 @@
"title": "Nx Application Options Schema",
"type": "object",
"properties": {
"framework": {
"description": "The Framework for the application.",
"type": "string",
"enum": ["angular", "react", "custom-elements"],
"default": "angular",
"x-prompt": {
"message": "What framework would you like to use for the application?",
"type": "list",
"items": [
{
"value": "angular",
"label": "Angular [ https://angular.io ]"
},
{
"value": "react",
"label": "React [ https://reactjs.org/ ]"
},
{
"value": "custom-elements",
"label": "Custom Elements"
}
]
}
},
"name": {
"description": "The name of the application.",
"type": "string",

View File

@ -1,6 +1,10 @@
module.exports = {
name: '<%= project %>',
preset: '<%= offsetFromRoot %>jest.config.js',
preset: '<%= offsetFromRoot %>jest.config.js',<% if (supportTsx) { %>
transform: {
'^.+\\.[tj]sx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],<% } %>
coverageDirectory: '<%= offsetFromRoot %>coverage/<%= projectRoot %>'<% if(!skipSerializers) { %>,
snapshotSerializers: [
'jest-preset-angular/AngularSnapshotSerializer.js',

View File

@ -1 +1,2 @@
import 'jest-preset-angular';
<% if (setupFile === 'angular') { %>import 'jest-preset-angular';
<% } else if (setupFile === 'custom-elements') { %>import 'document-register-element';<% } %>

View File

@ -4,7 +4,7 @@
"outDir": "<%= offsetFromRoot %>dist/out-tsc/<%= projectRoot %>",
"module": "commonjs",
"types": ["jest", "node"]
},
<% if(!skipSetupFile) { %>"files": ["src/test-setup.ts"],<% } %>
"include": ["**/*.spec.ts", "**/*.d.ts"]
},<% if(setupFile !== 'none') { %>
"files": ["src/test-setup.ts"],<% } %>
"include": ["**/*.spec.ts", "**/*.d.ts"<% if (supportTsx) { %>, "**/*.spec.tsx", "**/*.d.tsx"<% } %>]
}

View File

@ -22,7 +22,9 @@ import { join, normalize } from '@angular-devkit/core';
export interface JestProjectSchema {
project: string;
supportTsx: boolean;
skipSetupFile: boolean;
setupFile: 'angular' | 'custom-elements' | 'none';
skipSerializers: boolean;
}
@ -37,7 +39,7 @@ function generateFiles(options: JestProjectSchema): Rule {
projectRoot: projectConfig.root,
offsetFromRoot: offsetFromRoot(projectConfig.root)
}),
options.skipSetupFile
options.setupFile === 'none'
? filter(file => file !== '/src/test-setup.ts')
: noop(),
move(projectConfig.root)
@ -73,7 +75,7 @@ function updateAngularJson(options: JestProjectSchema): Rule {
tsConfig: join(normalize(projectConfig.root), 'tsconfig.spec.json')
}
};
if (!options.skipSetupFile) {
if (options.setupFile !== 'none') {
projectConfig.architect.test.options.setupFile = join(
normalize(projectConfig.root),
'src/test-setup.ts'
@ -104,7 +106,18 @@ function check(options: JestProjectSchema): Rule {
};
}
function normalizeOptions(options: JestProjectSchema): JestProjectSchema {
if (!options.skipSetupFile) {
return options;
}
return {
...options,
setupFile: 'none'
};
}
export default function(options: JestProjectSchema): Rule {
options = normalizeOptions(options);
return chain([
check(options),
generateFiles(options),

View File

@ -108,6 +108,51 @@ describe('jestProject', () => {
});
});
describe('--setup-file', () => {
it('should generate src/test-setup.ts', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
setupFile: 'none'
},
appTree
);
expect(resultTree.exists('src/test-setup.ts')).toBeFalsy();
});
it('should not list the setup file in angular.json', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
setupFile: 'none'
},
appTree
);
const angularJson = readJsonInTree(resultTree, 'angular.json');
expect(
angularJson.projects.lib1.architect.test.options.setupFile
).toBeUndefined();
});
it('should not list the setup file in tsconfig.spec.json', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
setupFile: 'none'
},
appTree
);
const tsConfig = readJsonInTree(
resultTree,
'libs/lib1/tsconfig.spec.json'
);
expect(tsConfig.files).toBeUndefined();
});
});
describe('--skip-setup-file', () => {
it('should generate src/test-setup.ts', async () => {
const resultTree = await runSchematic(
@ -172,4 +217,21 @@ describe('jestProject', () => {
`);
});
});
describe('--support-tsx', () => {
it('should add tsx to moduleExtensions', async () => {
const resultTree = await runSchematic(
'jest-project',
{
project: 'lib1',
supportTsx: true
},
appTree
);
const jestConfig = resultTree.readContent('libs/lib1/jest.config.js');
expect(jestConfig).toContain(
`moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],`
);
});
});
});

View File

@ -13,13 +13,25 @@
},
"skipSetupFile": {
"type": "boolean",
"description": "Skips the setup file required for angular",
"default": false
"description": "[Deprecated]: Skips the setup file required for angular. (Use --setup-file)",
"default": false,
"x-deprecated": true
},
"setupFile": {
"type": "string",
"enum": ["none", "angular", "custom-elements"],
"description": "The setup file to be generated",
"default": "angular"
},
"skipSerializers": {
"type": "boolean",
"description": "Skips the serializers required to snapshot angular templates",
"default": false
},
"supportTsx": {
"type": "boolean",
"description": "Setup tsx support",
"default": false
}
},
"required": []

View File

@ -24,6 +24,8 @@ export interface KarmaProjectSchema {
project: string;
}
// TODO: @jjean implement skipSetupFile
function generateFiles(options: KarmaProjectSchema): Rule {
return (host, context) => {
const projectConfig = getProjectConfig(host, options.project);

View File

@ -446,7 +446,8 @@ export default function(schema: Schema): Rule {
options.unitTestRunner === 'jest'
? schematic('jest-project', {
project: options.name,
skipSetupFile: options.framework !== Framework.Angular,
setupFile:
options.framework === Framework.Angular ? 'angular' : 'none',
skipSerializers: options.framework !== Framework.Angular
})
: noop(),

View File

@ -233,7 +233,7 @@ export default function(schema: Schema): Rule {
options.unitTestRunner === 'jest'
? schematic('jest-project', {
project: options.name,
skipSetupFile: true,
setupFile: 'none',
skipSerializers: true
})
: noop(),

View File

@ -178,7 +178,8 @@ export class DepsCalculator {
* Process a file and update it's dependencies
*/
processFile(filePath: string): void {
if (path.extname(filePath) !== '.ts') {
const extension = path.extname(filePath);
if (extension !== '.ts' && extension !== '.tsx') {
return;
}
const tsFile = ts.createSourceFile(

View File

@ -21,6 +21,15 @@ export const expressTypingsVersion = '4.16.0';
export const nestJsVersion = '5.5.0';
export const nestJsSchematicsVersion = '5.11.2';
export const reactVersions = {
framework: '16.8.3',
reactTypes: '16.8.4',
reactDomTypes: '16.8.2',
testingLibrary: '6.0.0'
};
export const documentRegisterElementVersion = '1.13.1';
export const libVersions = {
angularVersion,
angularDevkitVersion,

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT- style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Rule, Tree } from '@angular-devkit/schematics';
import { Rule, Tree, SchematicContext } from '@angular-devkit/schematics';
import {
findNodes,
getDecoratorMetadata,
@ -593,14 +593,17 @@ export function readJsonInTree<T = any>(host: Tree, path: string): T {
*/
export function updateJsonInTree<T = any, O = T>(
path: string,
callback: (json: T) => O
callback: (json: T, context: SchematicContext) => O
): Rule {
return (host: Tree): Tree => {
return (host: Tree, context: SchematicContext): Tree => {
if (!host.exists(path)) {
host.create(path, serializeJson(callback({} as T)));
host.create(path, serializeJson(callback({} as T, context)));
return host;
}
host.overwrite(path, serializeJson(callback(readJsonInTree(host, path))));
host.overwrite(
path,
serializeJson(callback(readJsonInTree(host, path), context))
);
return host;
};
}

View File

@ -1,4 +1,6 @@
export const enum Framework {
Angular = 'angular',
React = 'react',
CustomElements = 'custom-elements',
None = 'none'
}

View File

@ -18,13 +18,14 @@ import {
*/
const buildersSourceDirectory = path.join(
__dirname,
'../../build/packages/builders/src'
'../../packages/builders/src'
);
const buildersOutputDirectory = path.join(__dirname, '../../docs/api-builders');
const builderCollectionFile = path.join(
buildersSourceDirectory,
'builders.json'
);
fs.removeSync(buildersOutputDirectory);
const builderCollection = fs.readJsonSync(builderCollectionFile).builders;
const registry = new CoreSchemaRegistry();
registry.addFormat(pathFormat);

View File

@ -19,7 +19,7 @@ const path = require('path');
*/
const schematicsSourceDirectory = path.join(
__dirname,
'../../build/packages/schematics/src'
'../../packages/schematics/src'
);
const schematicsOutputDirectory = path.join(
__dirname,
@ -29,6 +29,7 @@ const schematicCollectionFile = path.join(
schematicsSourceDirectory,
'collection.json'
);
fs.removeSync(schematicsOutputDirectory);
const schematicCollection = fs.readJsonSync(schematicCollectionFile).schematics;
const registry = new CoreSchemaRegistry();
registry.addFormat(pathFormat);

342
yarn.lock
View File

@ -288,6 +288,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.2.tgz#95cdeddfc3992a6ca2a1315191c1679ca32c55cd"
integrity sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==
"@babel/runtime@^7.1.5", "@babel/runtime@^7.3.1":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
dependencies:
regenerator-runtime "^0.12.0"
"@babel/template@^7.0.0", "@babel/template@^7.1.0":
version "7.2.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
@ -493,6 +500,11 @@
semver "5.6.0"
semver-intersect "1.4.0"
"@sheerun/mutationobserver-shim@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==
"@sindresorhus/is@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
@ -637,11 +649,31 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.13.2.tgz#ffe96278e712a8d4e467e367a338b05e22872646"
integrity sha512-k6MCN8WuDiCj6O+UJsVMbrreZxkbrhQbO02oDj6yuRu8UAkp0MDdEcDKif8/gBKuJbT84kkO+VHQAqXkumEklg==
"@types/prop-types@*":
version "15.5.9"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.9.tgz#f2d14df87b0739041bc53a7d75e3d77d726a3ec0"
integrity sha512-Nha5b+jmBI271jdTMwrHiNXM+DvThjHOfyZtMX9kj/c/LUj2xiLHsG/1L3tJ8DjAoQN48cHwUwtqBotjyXaSdQ==
"@types/range-parser@*":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/react-dom@^16.8.2":
version "16.8.2"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.2.tgz#9bd7d33f908b243ff0692846ef36c81d4941ad12"
integrity sha512-MX7n1wq3G/De15RGAAqnmidzhr2Y9O/ClxPxyqaNg96pGyeXUYPSvujgzEVpLo9oIP4Wn1UETl+rxTN02KEpBw==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^16.8.4":
version "16.8.4"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.4.tgz#134307f5266e866d5e7c25e47f31f9abd5b2ea34"
integrity sha512-Mpz1NNMJvrjf0GcDqiK8+YeOydXfD8Mgag3UtqQ5lXYTsMnOiHcKmO48LiSWMb1rSHB9MV/jlgyNzeAVxWMZRQ==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/serve-static@*":
version "1.13.2"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"
@ -2146,6 +2178,11 @@ bonjour@^3.5.0:
multicast-dns "^6.0.1"
multicast-dns-service-types "^1.1.0"
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
boom@2.x.x:
version "2.10.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
@ -2555,6 +2592,14 @@ callsites@^2.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
camel-case@3.0.x:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
dependencies:
no-case "^2.2.0"
upper-case "^1.1.1"
camelcase-keys@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
@ -2759,7 +2804,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
clean-css@4.2.1, clean-css@^4.1.11:
clean-css@4.2.1, clean-css@4.2.x, clean-css@^4.1.11:
version "4.2.1"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==
@ -2936,16 +2981,16 @@ commander@2.11.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==
commander@2.17.x, commander@~2.17.1:
version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commander@^2.12.0, commander@^2.12.1, commander@^2.9.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
commander@~2.17.1:
version "2.17.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commitizen@^2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-2.10.1.tgz#8c395def34a895f4e94952c2efc3c9eb4c3683bd"
@ -3616,6 +3661,21 @@ css-parse@1.7.x:
resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.7.0.tgz#321f6cf73782a6ff751111390fc05e2c657d8c9b"
integrity sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs=
css-select@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
dependencies:
boolbase "~1.0.0"
css-what "2.1"
domutils "1.5.1"
nth-check "~1.0.1"
css-what@2.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
version "0.3.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
@ -3628,6 +3688,11 @@ cssstyle@^1.0.0:
dependencies:
cssom "0.3.x"
csstype@^2.2.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.2.tgz#3043d5e065454579afc7478a18de41909c8a2f01"
integrity sha512-Rl7PvTae0pflc1YtxtKbiSqq20Ts6vpIYOD5WBafl4y123DyHUeLrRdQP66sQW8/6gmX8jrYJLXwNeMqYVJcow==
cuint@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
@ -4077,6 +4142,20 @@ dns-txt@^2.0.2:
dependencies:
buffer-indexof "^1.0.0"
document-register-element@^1.13.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.13.1.tgz#dad8cb7be38e04ee3f56842e6cf81af46c1249ba"
integrity sha512-92ZyLDKg9j4rOll//NNXj25f+8rAzOkYsGJonhugKwXfeqH7bzs8Ucpvey0WzZ2ZzKdrvW9RnUw3UyOZ/uhBFw==
dependencies:
lightercollective "^0.1.0"
dom-converter@~0.2:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
dependencies:
utila "~0.4"
dom-serialize@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@ -4087,11 +4166,34 @@ dom-serialize@^2.2.0:
extend "^3.0.0"
void-elements "^2.0.0"
dom-serializer@0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
dependencies:
domelementtype "^1.3.0"
entities "^1.1.1"
dom-testing-library@^3.13.1:
version "3.16.8"
resolved "https://registry.yarnpkg.com/dom-testing-library/-/dom-testing-library-3.16.8.tgz#26549b249f131a25e4339ebec9fcaa2e7642527f"
integrity sha512-VGn2piehGoN9lmZDYd+xoTZwwcS+FoXebvZMw631UhS5LshiLTFNJs9bxRa9W7fVb1cAn9AYKAKZXh67rCDaqw==
dependencies:
"@babel/runtime" "^7.1.5"
"@sheerun/mutationobserver-shim" "^0.3.2"
pretty-format "^24.0.0"
wait-for-expect "^1.1.0"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
domelementtype@1, domelementtype@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
@ -4099,6 +4201,28 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"
domhandler@2.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594"
integrity sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=
dependencies:
domelementtype "1"
domutils@1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"
integrity sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=
dependencies:
domelementtype "1"
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
dependencies:
dom-serializer "0"
domelementtype "1"
dot-prop@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177"
@ -4269,6 +4393,11 @@ ent@~2.2.0:
resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
entities@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
err-code@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
@ -5767,6 +5896,11 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
he@1.2.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hipchat-notifier@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/hipchat-notifier/-/hipchat-notifier-1.1.0.tgz#b6d249755437c191082367799d3ba9a0f23b231e"
@ -5831,6 +5965,42 @@ html-entities@^1.2.0:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
html-minifier@^3.2.3:
version "3.5.21"
resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c"
integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==
dependencies:
camel-case "3.0.x"
clean-css "4.2.x"
commander "2.17.x"
he "1.2.x"
param-case "2.1.x"
relateurl "0.2.x"
uglify-js "3.4.x"
html-webpack-plugin@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s=
dependencies:
html-minifier "^3.2.3"
loader-utils "^0.2.16"
lodash "^4.17.3"
pretty-error "^2.0.2"
tapable "^1.0.0"
toposort "^1.0.0"
util.promisify "1.0.0"
htmlparser2@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe"
integrity sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=
dependencies:
domelementtype "1"
domhandler "2.1"
domutils "1.1"
readable-stream "1.0"
http-cache-semantics@3.8.1, http-cache-semantics@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
@ -7602,6 +7772,11 @@ license-webpack-plugin@^1.4.0:
dependencies:
ejs "^2.5.7"
lightercollective@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.1.0.tgz#70df102c530dcb8d0ccabfe6175a8d00d5f61300"
integrity sha512-J9tg5uraYoQKaWbmrzDDexbG6hHnMcWS1qLYgJSWE+mpA3U5OCSeMUhb+K55otgZJ34oFdR0ECvdIb3xuO5JOQ==
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
@ -7698,7 +7873,7 @@ loader-utils@1.2.3:
emojis-list "^2.0.0"
json5 "^1.0.1"
loader-utils@^0.2.5:
loader-utils@^0.2.16, loader-utils@^0.2.5:
version "0.2.17"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=
@ -7818,7 +7993,7 @@ lodash@4.17.10:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==
lodash@4.17.11, lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.10:
lodash@4.17.11, lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@ -7894,7 +8069,7 @@ longest@^1.0.1:
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
loose-envify@^1.0.0:
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -7909,6 +8084,11 @@ loud-rejection@^1.0.0:
currently-unhandled "^0.4.1"
signal-exit "^3.0.0"
lower-case@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
lowercase-keys@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
@ -8562,6 +8742,13 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
no-case@^2.2.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
dependencies:
lower-case "^1.1.1"
node-fetch-npm@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7"
@ -8926,6 +9113,13 @@ npm-run-path@^2.0.0:
gauge "~2.7.3"
set-blocking "~2.0.0"
nth-check@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
dependencies:
boolbase "~1.0.0"
null-check@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
@ -8961,7 +9155,7 @@ object-assign@^3.0.0:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0:
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@ -9367,6 +9561,13 @@ parallel-transform@^1.1.0:
inherits "^2.0.3"
readable-stream "^2.1.5"
param-case@2.1.x:
version "2.1.1"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
dependencies:
no-case "^2.2.0"
parse-asn1@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
@ -9726,6 +9927,14 @@ prettier@1.15.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a"
integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg==
pretty-error@^2.0.2:
version "2.1.1"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=
dependencies:
renderkid "^2.0.1"
utila "~0.4"
pretty-format@^23.6.0:
version "23.6.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760"
@ -9734,6 +9943,14 @@ pretty-format@^23.6.0:
ansi-regex "^3.0.0"
ansi-styles "^3.2.0"
pretty-format@^24.0.0:
version "24.0.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.0.0.tgz#cb6599fd73ac088e37ed682f61291e4678f48591"
integrity sha512-LszZaKG665djUcqg5ZQq+XzezHLKrxsA86ZABTozp+oNhkdqa+tG2dX4qa6ERl5c/sRDrAa3lHmwnvKoP+OG/g==
dependencies:
ansi-regex "^4.0.0"
ansi-styles "^3.2.0"
private@^0.1.6, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@ -9801,6 +10018,15 @@ prompts@^0.1.9:
kleur "^2.0.1"
sisteransi "^0.1.1"
prop-types@^15.6.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.8.1"
protoduck@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f"
@ -10028,6 +10254,39 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-dom@^16.8.3:
version "16.8.3"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.3.tgz#ae236029e66210783ac81999d3015dfc475b9c32"
integrity sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.3"
react-is@^16.8.1:
version "16.8.3"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.3.tgz#4ad8b029c2a718fc0cfc746c8d4e1b7221e5387d"
integrity sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==
react-testing-library@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/react-testing-library/-/react-testing-library-6.0.0.tgz#81edfcfae8a795525f48685be9bf561df45bb35d"
integrity sha512-h0h+YLe4KWptK6HxOMnoNN4ngu3W8isrwDmHjPC5gxc+nOZOCurOvbKVYCvvuAw91jdO7VZSm/5KR7TxKnz0qA==
dependencies:
"@babel/runtime" "^7.3.1"
dom-testing-library "^3.13.1"
react@^16.8.3:
version "16.8.3"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.3.tgz#c6f988a2ce895375de216edcfaedd6b9a76451d9"
integrity sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.13.3"
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@ -10116,6 +10375,16 @@ read-pkg@^4.0.1:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@1.0:
version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
version "1.1.14"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@ -10225,6 +10494,11 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-transform@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
@ -10285,6 +10559,11 @@ regjsparser@^0.1.4:
dependencies:
jsesc "~0.5.0"
relateurl@0.2.x:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
release-it@^7.4.0:
version "7.6.2"
resolved "https://registry.yarnpkg.com/release-it/-/release-it-7.6.2.tgz#9cdfdcdedc1bfe1889e111f862f0af8499931bb5"
@ -10323,6 +10602,17 @@ remove-trailing-separator@^1.0.1:
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
renderkid@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.2.tgz#12d310f255360c07ad8fde253f6c9e9de372d2aa"
integrity sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg==
dependencies:
css-select "^1.1.0"
dom-converter "~0.2"
htmlparser2 "~3.3.0"
strip-ansi "^3.0.0"
utila "^0.4.0"
repeat-element@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
@ -10728,6 +11018,14 @@ sax@^1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.13.3:
version "0.13.3"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.3.tgz#bed3c5850f62ea9c716a4d781f9daeb9b2a58896"
integrity sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
@ -11918,6 +12216,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
toposort@^1.0.0:
version "1.0.7"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
@ -12100,7 +12403,7 @@ typescript@3.2.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d"
integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==
uglify-js@^3.0.7, uglify-js@^3.1.4:
uglify-js@3.4.x, uglify-js@^3.0.7, uglify-js@^3.1.4:
version "3.4.9"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==
@ -12200,6 +12503,11 @@ update-notifier@2.5.0, update-notifier@^2.3.0:
semver-diff "^2.0.0"
xdg-basedir "^3.0.0"
upper-case@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@ -12270,7 +12578,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
util.promisify@^1.0.0:
util.promisify@1.0.0, util.promisify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
@ -12292,6 +12600,11 @@ util@^0.10.3:
dependencies:
inherits "2.0.3"
utila@^0.4.0, utila@~0.4:
version "0.4.0"
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
@ -12365,6 +12678,11 @@ w3c-hr-time@^1.0.1:
dependencies:
browser-process-hrtime "^0.1.2"
wait-for-expect@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-1.1.0.tgz#6607375c3f79d32add35cd2c87ce13f351a3d453"
integrity sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==
walker@~1.0.5:
version "1.0.7"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"