diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index e22d9ec5a4..3b6abcf597 100644 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -58,6 +58,11 @@ "factory": "./service", "description": "Create an Angular service.", "schema": "./service/schema.json" + }, + "universal": { + "factory": "./universal", + "description": "Create an Angular universal app.", + "schema": "./universal/schema.json" } } } diff --git a/packages/schematics/angular/universal/files/__sourceDir__/__appDir__/__rootModuleFileName__ b/packages/schematics/angular/universal/files/__sourceDir__/__appDir__/__rootModuleFileName__ new file mode 100644 index 0000000000..af9a56e17c --- /dev/null +++ b/packages/schematics/angular/universal/files/__sourceDir__/__appDir__/__rootModuleFileName__ @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ], + bootstrap: [AppComponent], +}) +export class <%= rootModuleClassName %> {} diff --git a/packages/schematics/angular/universal/files/__sourceDir__/__main@stripTsExtension__.ts b/packages/schematics/angular/universal/files/__sourceDir__/__main@stripTsExtension__.ts new file mode 100644 index 0000000000..0d766835a2 --- /dev/null +++ b/packages/schematics/angular/universal/files/__sourceDir__/__main@stripTsExtension__.ts @@ -0,0 +1 @@ +export { <%= rootModuleClassName %> } from './<%= appDir %>/<%= stripTsExtension(rootModuleFileName) %>'; diff --git a/packages/schematics/angular/universal/files/__sourceDir__/__tsconfigFileName__.json b/packages/schematics/angular/universal/files/__sourceDir__/__tsconfigFileName__.json new file mode 100644 index 0000000000..afd34261c0 --- /dev/null +++ b/packages/schematics/angular/universal/files/__sourceDir__/__tsconfigFileName__.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../<%= outDir %>", + "baseUrl": "./", + "module": "commonjs", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ], + "angularCompilerOptions": { + "entryModule": "<%= appDir %>/<%= stripTsExtension(rootModuleFileName) %>#<%= rootModuleClassName %>" + } +} diff --git a/packages/schematics/angular/universal/index.ts b/packages/schematics/angular/universal/index.ts new file mode 100644 index 0000000000..8111658a79 --- /dev/null +++ b/packages/schematics/angular/universal/index.ts @@ -0,0 +1,203 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 { normalize } from '@angular-devkit/core'; +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + apply, + chain, + mergeWith, + template, + url, +} from '@angular-devkit/schematics'; +import 'rxjs/add/operator/merge'; +import * as ts from 'typescript'; +import * as stringUtils from '../strings'; +import { findNode, getDecoratorMetadata, getSourceNodes } from '../utility/ast-utils'; +import { InsertChange } from '../utility/change'; +import { AppConfig, getAppFromConfig, getConfig } from '../utility/config'; +import { findBootstrapModuleCall } from '../utility/ng-ast-utils'; +import { Schema as UniversalOptions } from './schema'; + + +function updateConfigFile(options: UniversalOptions): Rule { + return (host: Tree) => { + const config = getConfig(host); + const clientApp = getAppFromConfig(config, options.clientApp); + if (clientApp === null) { + throw new SchematicsException('Client app not found.'); + } + options.test = options.test || clientApp.test; + const serverApp: AppConfig = { + ...clientApp, + platform: 'server', + root: options.root, + outDir: options.outDir, + index: options.index, + main: options.main, + test: options.test, + tsconfig: options.tsconfigFileName, + testTsconfig: options.testTsconfigFileName, + }; + if (!config.apps) { + config.apps = []; + } + config.apps.push(serverApp); + + host.overwrite('/.angular-cli.json', JSON.stringify(config, null, 2)); + + return host; + }; +} + +function findBootstrapModulePath(host: Tree, mainPath: string): string { + const bootstrapCall = findBootstrapModuleCall(host, mainPath); + if (!bootstrapCall) { + throw new SchematicsException('Bootstrap call not found'); + } + + const bootstrapModule = bootstrapCall.arguments[0]; + + const mainBuffer = host.read(mainPath); + if (!mainBuffer) { + throw new SchematicsException(`Client app main file (${mainPath}) not found`); + } + const mainText = mainBuffer.toString('utf-8'); + const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); + const allNodes = getSourceNodes(source); + const bootstrapModuleRelativePath = allNodes + .filter(node => node.kind === ts.SyntaxKind.ImportDeclaration) + .filter(imp => { + return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText()); + }) + .map((imp: ts.ImportDeclaration) => { + const modulePathStringLiteral = imp.moduleSpecifier; + + return modulePathStringLiteral.text; + })[0]; + + return bootstrapModuleRelativePath; +} + +function findBrowserModuleImport(host: Tree, modulePath: string): ts.Node { + const moduleBuffer = host.read(modulePath); + if (!moduleBuffer) { + throw new SchematicsException(`Module file (${modulePath}) not found`); + } + const moduleFileText = moduleBuffer.toString('utf-8'); + + const source = ts.createSourceFile(modulePath, moduleFileText, ts.ScriptTarget.Latest, true); + + const decoratorMetadata = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; + const browserModuleNode = findNode(decoratorMetadata, ts.SyntaxKind.Identifier, 'BrowserModule'); + + if (browserModuleNode === null) { + throw new SchematicsException(`Cannot find BrowserModule import in ${modulePath}`); + } + + return browserModuleNode; +} + +function wrapBootstrapCall(options: UniversalOptions): Rule { + return (host: Tree) => { + const config = getConfig(host); + const clientApp = getAppFromConfig(config, options.clientApp); + if (clientApp === null) { + throw new SchematicsException('Client app not found.'); + } + const mainPath = normalize(`/${clientApp.root}/${clientApp.main}`); + let bootstrapCall: ts.Node | null = findBootstrapModuleCall(host, mainPath); + if (bootstrapCall === null) { + throw new SchematicsException('Bootstrap module not found.'); + } + + let bootstrapCallExpression: ts.Node | null = null; + let currentCall = bootstrapCall; + while (bootstrapCallExpression === null && currentCall.parent) { + currentCall = currentCall.parent; + if (currentCall.kind === ts.SyntaxKind.ExpressionStatement) { + bootstrapCallExpression = currentCall; + } + } + bootstrapCall = currentCall; + + const recorder = host.beginUpdate(mainPath); + const beforeText = `document.addEventListener('DOMContentLoaded', () => {\n `; + const afterText = `\n});`; + recorder.insertLeft(bootstrapCall.getStart(), beforeText); + recorder.insertRight(bootstrapCall.getEnd(), afterText); + host.commitUpdate(recorder); + }; +} + +function addServerTransition(options: UniversalOptions): Rule { + return (host: Tree) => { + const config = getConfig(host); + const clientApp = getAppFromConfig(config, options.clientApp); + if (clientApp === null) { + throw new SchematicsException('Client app not found.'); + } + const mainPath = normalize(`/${clientApp.root}/${clientApp.main}`); + + const bootstrapModuleRelativePath = findBootstrapModulePath(host, mainPath); + const bootstrapModulePath = normalize(`/${clientApp.root}/${bootstrapModuleRelativePath}.ts`); + + const browserModuleImport = findBrowserModuleImport(host, bootstrapModulePath); + const appId = options.appId; + const transitionCall = `.withServerTransition({ appId: '${appId}' })`; + const position = browserModuleImport.pos + browserModuleImport.getFullText().length; + const transitionCallChange = new InsertChange( + bootstrapModulePath, position, transitionCall); + + const transitionCallRecorder = host.beginUpdate(bootstrapModulePath); + transitionCallRecorder.insertLeft(transitionCallChange.pos, transitionCallChange.toAdd); + host.commitUpdate(transitionCallRecorder); + }; +} + +function addDependencies(): Rule { + return (host: Tree) => { + const pkgPath = '/package.json'; + const buffer = host.read(pkgPath); + if (buffer === null) { + throw new SchematicsException('Could not find package.json'); + } + + const pkg = JSON.parse(buffer.toString()); + + const ngCoreVersion = Object.keys(pkg.dependencies) + .filter((key: string) => key === '@angular/core')[0]; + pkg.dependencies['@angular/platform-server'] = ngCoreVersion; + + host.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); + + return host; + }; +} + +export default function (options: UniversalOptions): Rule { + return (host: Tree, context: SchematicContext) => { + const templateSource = apply(url('./files'), [ + template({ + ...stringUtils, + ...options as object, + stripTsExtension: (s: string) => { return s.replace(/\.ts$/, ''); }, + }), + ]); + + return chain([ + mergeWith(templateSource), + addDependencies(), + updateConfigFile(options), + wrapBootstrapCall(options), + addServerTransition(options), + ])(host, context); + }; +} diff --git a/packages/schematics/angular/universal/index_spec.ts b/packages/schematics/angular/universal/index_spec.ts new file mode 100644 index 0000000000..f1355a9a8b --- /dev/null +++ b/packages/schematics/angular/universal/index_spec.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import * as path from 'path'; +import { Schema as ApplicationOptions } from '../application/schema'; +import { Schema as UniversalOptions } from './schema'; + + +describe('Universal Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: UniversalOptions = { + name: 'foo', + }; + + let appTree: Tree; + + beforeEach(() => { + const appOptions: ApplicationOptions = { + directory: '', + name: 'app', + path: 'src', + prefix: '', + sourceDir: 'src', + inlineStyle: false, + inlineTemplate: false, + viewEncapsulation: 'None', + changeDetection: 'Default', + version: '1.2.3', + routing: false, + style: 'css', + skipTests: false, + minimal: false, + }; + appTree = schematicRunner.runSchematic('application', appOptions); + }); + + it('should create a root module file', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/src/app/app.server.module.ts'; + const file = tree.files.filter(f => f === filePath)[0]; + expect(file).toBeDefined(); + }); + + it('should create a main file', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/src/main.server.ts'; + const file = tree.files.filter(f => f === filePath)[0]; + expect(file).toBeDefined(); + const contents = tree.read(filePath); + expect(contents).toMatch(/export { AppServerModule } from '\.\/app\/app\.server\.module'/); + }); + + it('should create a tsconfig file', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/src/tsconfig.server.json'; + const file = tree.files.filter(f => f === filePath)[0]; + expect(file).toBeDefined(); + const contents = tree.read(filePath); + expect(contents).toMatch(/\"outDir\": \"\.\.\/dist-server\/\"/); + }); + + it('should add dependency: @angular/platform-server', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/package.json'; + const contents = tree.read(filePath); + expect(contents).toMatch(/\"@angular\/platform-server\": \"/); + }); + + it('should update .angular-cli.json with a server app', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/.angular-cli.json'; + const contents = tree.read(filePath) || new Buffer(''); + + const config = JSON.parse(contents.toString()); + expect(config.apps.length).toEqual(2); + const app = config.apps[1]; + expect(app.platform).toEqual('server'); + expect(app.root).toEqual('src'); + expect(app.outDir).toEqual('dist-server/'); + expect(app.index).toEqual('index.html'); + expect(app.main).toEqual('main.server.ts'); + expect(app.test).toEqual('test.ts'); + expect(app.tsconfig).toEqual('tsconfig.server'); + expect(app.testTsconfig).toEqual('tsconfig.spec'); + expect(app.environmentSource).toEqual('environments/environment.ts'); + }); + + it('should add a server transition to BrowerModule import', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/src/app/app.module.ts'; + const contents = tree.read(filePath); + expect(contents).toMatch(/BrowserModule\.withServerTransition\({ appId: 'serverApp' }\)/); + }); + + it('should wrap the bootstrap call in a DOMContentLoaded event handler', () => { + const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const filePath = '/src/main.ts'; + const contents = tree.read(filePath); + expect(contents).toMatch(/document.addEventListener\('DOMContentLoaded', \(\) => {/); + }); +}); diff --git a/packages/schematics/angular/universal/schema.d.ts b/packages/schematics/angular/universal/schema.d.ts new file mode 100644 index 0000000000..86fc582004 --- /dev/null +++ b/packages/schematics/angular/universal/schema.d.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 + */ + +export interface Schema { + /** + * Name of the universal app + */ + name?: string; + /** + * Name or index of related client app. + */ + clientApp?: string; + /** + * The appId to use withServerTransition. + */ + appId?: string; + /** + * The output directory for build results. + */ + outDir?: string; + /** + * The root directory of the app. + */ + root?: string; + /** + * Name of the index file + */ + index?: string; + /** + * The name of the main entry-point file. + */ + main?: string; + /** + * The name of the test entry-point file. + */ + test?: string; + /** + * The name of the TypeScript configuration file. + */ + tsconfigFileName?: string; + /** + * The name of the TypeScript configuration file for tests. + */ + testTsconfigFileName?: string; + /** + * The name of the applicatino directory. + */ + appDir?: string; + /** + * The name of the root module file + */ + rootModuleFileName?: string; + /** + * The name of the root module class. + */ + rootModuleClassName?: string; + sourceDir?: string; +} diff --git a/packages/schematics/angular/universal/schema.json b/packages/schematics/angular/universal/schema.json new file mode 100644 index 0000000000..fea4be93ed --- /dev/null +++ b/packages/schematics/angular/universal/schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularUniversalApp", + "title": "Angular Universal App Options Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the universal app" + }, + "clientApp": { + "type": "string", + "description": "Name or index of related client app.", + "default": "0" + }, + "appId": { + "type": "string", + "description": "The appId to use withServerTransition.", + "default": "serverApp" + }, + "outDir": { + "type": "string", + "description": "The output directory for build results.", + "default": "dist-server/" + }, + "root": { + "type": "string", + "description": "The root directory of the app.", + "default": "src" + }, + "index": { + "type": "string", + "description": "Name of the index file", + "default": "index.html" + }, + "main": { + "type": "string", + "description": "The name of the main entry-point file.", + "default": "main.server.ts" + }, + "test": { + "type": "string", + "description": "The name of the test entry-point file." + }, + "tsconfigFileName": { + "type": "string", + "default": "tsconfig.server", + "description": "The name of the TypeScript configuration file." + }, + "testTsconfigFileName": { + "type": "string", + "description": "The name of the TypeScript configuration file for tests.", + "default": "tsconfig.spec" + }, + "appDir": { + "type": "string", + "description": "The name of the applicatino directory.", + "default": "app" + }, + "rootModuleFileName": { + "type": "string", + "description": "The name of the root module file", + "default": "app.server.module.ts" + }, + "rootModuleClassName": { + "type": "string", + "description": "The name of the root module class.", + "default": "AppServerModule" + }, + "sourceDir": { + "type": "string", + "default": "src", + "alias": "sd" + } + }, + "required": [ + ] +} diff --git a/packages/schematics/angular/utility/ast-utils.ts b/packages/schematics/angular/utility/ast-utils.ts index 99f50af774..ec08ee6081 100644 --- a/packages/schematics/angular/utility/ast-utils.ts +++ b/packages/schematics/angular/utility/ast-utils.ts @@ -69,6 +69,20 @@ export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { return result; } +export function findNode(node: ts.Node, kind: ts.SyntaxKind, text: string): ts.Node | null { + if (node.kind === kind && node.getText() === text) { + // throw new Error(node.getText()); + return node; + } + + let foundNode: ts.Node | null = null; + ts.forEachChild(node, childNode => { + foundNode = foundNode || findNode(childNode, kind, text); + }); + + return foundNode; +} + /** * Helper for sorting nodes. diff --git a/packages/schematics/angular/utility/config.ts b/packages/schematics/angular/utility/config.ts new file mode 100644 index 0000000000..2c626bbfb2 --- /dev/null +++ b/packages/schematics/angular/utility/config.ts @@ -0,0 +1,461 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 { SchematicsException, Tree } from '@angular-devkit/schematics'; + + +// The interfaces below are generated from the Angular CLI configuration schema +// https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json +export interface AppConfig { + /** + * Name of the app. + */ + name?: string; + /** + * Directory where app files are placed. + */ + appRoot?: string; + /** + * The root directory of the app. + */ + root?: string; + /** + * The output directory for build results. + */ + outDir?: string; + /** + * List of application assets. + */ + assets?: (string | { + /** + * The pattern to match. + */ + glob?: string; + /** + * The dir to search within. + */ + input?: string; + /** + * The output path (relative to the outDir). + */ + output?: string; + })[]; + /** + * URL where files will be deployed. + */ + deployUrl?: string; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The runtime platform of the app. + */ + platform?: ('browser' | 'server'); + /** + * The name of the start HTML file. + */ + index?: string; + /** + * The name of the main entry-point file. + */ + main?: string; + /** + * The name of the polyfills file. + */ + polyfills?: string; + /** + * The name of the test entry-point file. + */ + test?: string; + /** + * The name of the TypeScript configuration file. + */ + tsconfig?: string; + /** + * The name of the TypeScript configuration file for unit tests. + */ + testTsconfig?: string; + /** + * The prefix to apply to generated selectors. + */ + prefix?: string; + /** + * Experimental support for a service worker from @angular/service-worker. + */ + serviceWorker?: boolean; + /** + * Global styles to be included in the build. + */ + styles?: (string | { + input?: string; + [name: string]: any; // tslint:disable-line:no-any + })[]; + /** + * Options to pass to style preprocessors + */ + stylePreprocessorOptions?: { + /** + * Paths to include. Paths will be resolved to project root. + */ + includePaths?: string[]; + }; + /** + * Global scripts to be included in the build. + */ + scripts?: (string | { + input: string; + [name: string]: any; // tslint:disable-line:no-any + })[]; + /** + * Source file for environment config. + */ + environmentSource?: string; + /** + * Name and corresponding file for environment config. + */ + environments?: { + [name: string]: any; // tslint:disable-line:no-any + }; +} + +export interface CliConfig { + $schema?: string; + /** + * The global configuration of the project. + */ + project?: { + /** + * The name of the project. + */ + name?: string; + /** + * Whether or not this project was ejected. + */ + ejected?: boolean; + }; + /** + * Properties of the different applications in this project. + */ + apps?: AppConfig[]; + /** + * Configuration for end-to-end tests. + */ + e2e?: { + protractor?: { + /** + * Path to the config file. + */ + config?: string; + }; + }; + /** + * Properties to be passed to TSLint. + */ + lint?: { + /** + * File glob(s) to lint. + */ + files?: (string | string[]); + /** + * Location of the tsconfig.json project file. + * Will also use as files to lint if 'files' property not present. + */ + project: string; + /** + * Location of the tslint.json configuration. + */ + tslintConfig?: string; + /** + * File glob(s) to ignore. + */ + exclude?: (string | string[]); + }[]; + /** + * Configuration for unit tests. + */ + test?: { + karma?: { + /** + * Path to the karma config file. + */ + config?: string; + }; + codeCoverage?: { + /** + * Globs to exclude from code coverage. + */ + exclude?: string[]; + }; + }; + /** + * Specify the default values for generating. + */ + defaults?: { + /** + * The file extension to be used for style files. + */ + styleExt?: string; + /** + * How often to check for file updates. + */ + poll?: number; + /** + * Use lint to fix files after generation + */ + lintFix?: boolean; + /** + * Options for generating a class. + */ + class?: { + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a component. + */ + component?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + /** + * Specifies if the style will be in the ts file. + */ + inlineStyle?: boolean; + /** + * Specifies if the template will be in the ts file. + */ + inlineTemplate?: boolean; + /** + * Specifies the view encapsulation strategy. + */ + viewEncapsulation?: ('Emulated' | 'Native' | 'None'); + /** + * Specifies the change detection strategy. + */ + changeDetection?: ('Default' | 'OnPush'); + }; + /** + * Options for generating a directive. + */ + directive?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a guard. + */ + guard?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating an interface. + */ + interface?: { + /** + * Prefix to apply to interface names. (i.e. I) + */ + prefix?: string; + }; + /** + * Options for generating a module. + */ + module?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a pipe. + */ + pipe?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Options for generating a service. + */ + service?: { + /** + * Flag to indicate if a dir is created. + */ + flat?: boolean; + /** + * Specifies if a spec file is generated. + */ + spec?: boolean; + }; + /** + * Properties to be passed to the build command. + */ + build?: { + /** + * Output sourcemaps. + */ + sourcemaps?: boolean; + /** + * Base url for the application being built. + */ + baseHref?: string; + /** + * The ssl key used by the server. + */ + progress?: boolean; + /** + * Enable and define the file watching poll time period (milliseconds). + */ + poll?: number; + /** + * Delete output path before build. + */ + deleteOutputPath?: boolean; + /** + * Do not use the real path when resolving modules. + */ + preserveSymlinks?: boolean; + /** + * Show circular dependency warnings on builds. + */ + showCircularDependencies?: boolean; + /** + * Use a separate bundle containing code used across multiple bundles. + */ + commonChunk?: boolean; + /** + * Use file name for lazy loaded chunks. + */ + namedChunks?: boolean; + }; + /** + * Properties to be passed to the serve command. + */ + serve?: { + /** + * The port the application will be served on. + */ + port?: number; + /** + * The host the application will be served on. + */ + host?: string; + /** + * Enables ssl for the application. + */ + ssl?: boolean; + /** + * The ssl key used by the server. + */ + sslKey?: string; + /** + * The ssl certificate used by the server. + */ + sslCert?: string; + /** + * Proxy configuration file. + */ + proxyConfig?: string; + }; + /** + * Properties about schematics. + */ + schematics?: { + /** + * The schematics collection to use. + */ + collection?: string; + /** + * The new app schematic. + */ + newApp?: string; + }; + }; + /** + * Specify which package manager tool to use. + */ + packageManager?: ('npm' | 'cnpm' | 'yarn' | 'default'); + /** + * Allow people to disable console warnings. + */ + warnings?: { + /** + * Show a warning when the user enabled the --hmr option. + */ + hmrWarning?: boolean; + /** + * Show a warning when the node version is incompatible. + */ + nodeDeprecation?: boolean; + /** + * Show a warning when the user installed angular-cli. + */ + packageDeprecation?: boolean; + /** + * Show a warning when the global version is newer than the local one. + */ + versionMismatch?: boolean; + /** + * Show a warning when the TypeScript version is incompatible + */ + typescriptMismatch?: boolean; + }; +} + +export const configPath = '/.angular-cli.json'; + +export function getConfig(host: Tree): CliConfig { + const configBuffer = host.read(configPath); + if (configBuffer === null) { + throw new SchematicsException('Could not find .angular-cli.json'); + } + + const config = JSON.parse(configBuffer.toString()); + + return config; +} + +export function getAppFromConfig(config: CliConfig, appIndexOrName = '0'): AppConfig | null { + if (!config.apps) { + return null; + } + + if (parseInt(appIndexOrName) >= 0) { + return config.apps[parseInt(appIndexOrName)]; + } + + return config.apps.filter((app) => app.name === appIndexOrName)[0]; +} diff --git a/packages/schematics/angular/utility/ng-ast-utils.ts b/packages/schematics/angular/utility/ng-ast-utils.ts new file mode 100644 index 0000000000..e3351cd39a --- /dev/null +++ b/packages/schematics/angular/utility/ng-ast-utils.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 { SchematicsException, Tree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { findNode, getSourceNodes } from '../utility/ast-utils'; + +export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { + const mainBuffer = host.read(mainPath); + if (!mainBuffer) { + throw new SchematicsException(`Main file (${mainPath}) not found`); + } + const mainText = mainBuffer.toString('utf-8'); + const source = ts.createSourceFile(mainPath, mainText, ts.ScriptTarget.Latest, true); + + const allNodes = getSourceNodes(source); + + let bootstrapCall: ts.CallExpression | null = null; + + for (const node of allNodes) { + + let bootstrapCallNode: ts.Node | null = null; + bootstrapCallNode = findNode(node, ts.SyntaxKind.Identifier, 'bootstrapModule'); + + // Walk up the parent until CallExpression is found. + while (bootstrapCallNode && bootstrapCallNode.parent + && bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression) { + + bootstrapCallNode = bootstrapCallNode.parent; + } + + if (bootstrapCallNode !== null && + bootstrapCallNode.parent !== undefined && + bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression) { + bootstrapCall = bootstrapCallNode.parent as ts.CallExpression; + break; + } + } + + return bootstrapCall; +}