From b384a93667939c157d672ac0d666d2f059d53bed Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 10 Apr 2019 12:56:25 -0400 Subject: [PATCH 01/14] fix(@angular-devkit/core): add project type as a workspace special extension --- packages/angular_devkit/core/src/workspace/json/reader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular_devkit/core/src/workspace/json/reader.ts b/packages/angular_devkit/core/src/workspace/json/reader.ts index 53027a065b28..7f3e954b5e29 100644 --- a/packages/angular_devkit/core/src/workspace/json/reader.ts +++ b/packages/angular_devkit/core/src/workspace/json/reader.ts @@ -78,7 +78,7 @@ export async function readJsonWorkspace( const specialWorkspaceExtensions = ['cli', 'defaultProject', 'newProjectRoot', 'schematics']; -const specialProjectExtensions = ['cli', 'schematics']; +const specialProjectExtensions = ['cli', 'schematics', 'projectType']; function parseWorkspace(workspaceNode: JsonAstObject, context: ParserContext): WorkspaceDefinition { const jsonMetadata = context.metadata; From 4947f9747648df1c91c4dd93771a72051af2bd52 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 12 Apr 2019 14:58:16 -0400 Subject: [PATCH 02/14] refactor(@angular-devkit/core): rename workspace namespace to workspaces This avoids massive amounts of name conflicts between a commonly named variable of 'workspace' and the namespace --- packages/angular_devkit/core/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/core/src/index.ts b/packages/angular_devkit/core/src/index.ts index 388c3e7645ea..4e372e55f445 100644 --- a/packages/angular_devkit/core/src/index.ts +++ b/packages/angular_devkit/core/src/index.ts @@ -10,7 +10,7 @@ import * as experimental from './experimental'; import * as json from './json/index'; import * as logging from './logger/index'; import * as terminal from './terminal/index'; -import * as workspace from './workspace'; +import * as workspaces from './workspace'; export * from './exception/exception'; export * from './json/index'; @@ -23,5 +23,5 @@ export { json, logging, terminal, - workspace, + workspaces, }; From fd597af0292ebbed3ad49b4e5c8302c7c2584ee6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 10 Apr 2019 12:57:19 -0400 Subject: [PATCH 03/14] refactor(@schematics/angular): add initial workspace helper rules --- .../schematics/angular/utility/workspace.ts | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/schematics/angular/utility/workspace.ts diff --git a/packages/schematics/angular/utility/workspace.ts b/packages/schematics/angular/utility/workspace.ts new file mode 100644 index 000000000000..5b393daf7c78 --- /dev/null +++ b/packages/schematics/angular/utility/workspace.ts @@ -0,0 +1,90 @@ +/** + * @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 { virtualFs, workspaces } from '@angular-devkit/core'; +import { Rule, Tree } from '@angular-devkit/schematics'; + +function createHost(tree: Tree): workspaces.WorkspaceHost { + return { + async readFile(path: string): Promise { + const data = tree.read(path); + if (!data) { + throw new Error('File not found.'); + } + + return virtualFs.fileBufferToString(data); + }, + async writeFile(path: string, data: string): Promise { + return tree.overwrite(path, data); + }, + async isDirectory(path: string): Promise { + // approximate a directory check + return !tree.exists(path) && tree.getDir(path).subfiles.length > 0; + }, + async isFile(path: string): Promise { + return tree.exists(path); + }, + }; +} + +export function updateWorkspace( + updater: (workspace: workspaces.WorkspaceDefinition) => void | PromiseLike, +): Rule; +export function updateWorkspace( + workspace: workspaces.WorkspaceDefinition, +): Rule; +export function updateWorkspace( + updaterOrWorkspace: workspaces.WorkspaceDefinition + | ((workspace: workspaces.WorkspaceDefinition) => void | PromiseLike), +): Rule { + return async (tree: Tree) => { + const host = createHost(tree); + + if (typeof updaterOrWorkspace === 'function') { + + const { workspace } = await workspaces.readWorkspace('/', host); + + const result = updaterOrWorkspace(workspace); + if (result !== undefined) { + await result; + } + + await workspaces.writeWorkspace(workspace, host); + } else { + await workspaces.writeWorkspace(updaterOrWorkspace, host); + } + }; +} + +export async function getWorkspace(tree: Tree, path = '/') { + const host = createHost(tree); + + const { workspace } = await workspaces.readWorkspace(path, host); + + return workspace; +} + +/** + * Build a default project path for generating. + * @param project The project which will have its default path generated. + */ +export function buildDefaultPath(project: workspaces.ProjectDefinition): string { + const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`; + const projectDirName = project.extensions['projectType'] === 'application' ? 'app' : 'lib'; + + return `${root}${projectDirName}`; +} + +export async function createDefaultPath(tree: Tree, projectName: string): Promise { + const workspace = await getWorkspace(tree); + const project = workspace.projects.get(projectName); + if (!project) { + throw new Error('Specified project does not exist.'); + } + + return buildDefaultPath(project); +} From 74bf5b8c71f4a26565106d980efcae9a466e3c78 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 10 Apr 2019 13:26:21 -0400 Subject: [PATCH 04/14] fix(@angular-devkit/schematics): fully support async rules --- .../angular_devkit/schematics/src/_golden-api.d.ts | 2 +- .../angular_devkit/schematics/src/engine/interface.ts | 2 +- packages/angular_devkit/schematics/src/rules/call.ts | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/etc/api/angular_devkit/schematics/src/_golden-api.d.ts b/etc/api/angular_devkit/schematics/src/_golden-api.d.ts index f2b35fb764bc..4edbf47bee79 100644 --- a/etc/api/angular_devkit/schematics/src/_golden-api.d.ts +++ b/etc/api/angular_devkit/schematics/src/_golden-api.d.ts @@ -426,7 +426,7 @@ export interface RequiredWorkflowExecutionContext { schematic: string; } -export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable | Rule | Promise | void; +export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable | Rule | Promise | Promise | void; export declare type RuleFactory = (options: T) => Rule; diff --git a/packages/angular_devkit/schematics/src/engine/interface.ts b/packages/angular_devkit/schematics/src/engine/interface.ts index 2ca66dbdffca..5664780e19f1 100644 --- a/packages/angular_devkit/schematics/src/engine/interface.ts +++ b/packages/angular_devkit/schematics/src/engine/interface.ts @@ -231,4 +231,4 @@ export type AsyncFileOperator = (tree: FileEntry) => Observable Tree | Observable; export type Rule = (tree: Tree, context: SchematicContext) => - Tree | Observable | Rule | Promise | void; + Tree | Observable | Rule | Promise | Promise | void; diff --git a/packages/angular_devkit/schematics/src/rules/call.ts b/packages/angular_devkit/schematics/src/rules/call.ts index 4668d3df952c..369d81cd9eca 100644 --- a/packages/angular_devkit/schematics/src/rules/call.ts +++ b/packages/angular_devkit/schematics/src/rules/call.ts @@ -97,7 +97,16 @@ export function callRule( }), ); } else if (isPromise(result)) { - return from(result).pipe(map(() => inputTree)); + return from(result).pipe( + mergeMap(inner => { + if (typeof inner === 'function') { + // This is considered a Rule, chain the rule and return its output. + return callRule(inner, observableOf(inputTree), context); + } else { + return observableOf(inputTree); + } + }), + ); } else if (TreeSymbol in result) { return observableOf(result); } else { From 08e2826215d5cd5a450f8872e0b1e66bb3c94d80 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 10 Apr 2019 13:30:04 -0400 Subject: [PATCH 05/14] refactor(@schematics/angular): update webworker to use new workspace rules --- .../schematics/angular/web-worker/index.ts | 70 +++++++------------ .../angular/web-worker/index_spec.ts | 30 ++++---- 2 files changed, 45 insertions(+), 55 deletions(-) diff --git a/packages/schematics/angular/web-worker/index.ts b/packages/schematics/angular/web-worker/index.ts index ba7f32727b88..bbc83fcbca5d 100644 --- a/packages/schematics/angular/web-worker/index.ts +++ b/packages/schematics/angular/web-worker/index.ts @@ -10,46 +10,16 @@ import { Rule, SchematicContext, SchematicsException, Tree, apply, applyTemplates, chain, mergeWith, move, noop, url, } from '@angular-devkit/schematics'; -import { getWorkspace, updateWorkspace } from '../utility/config'; import { appendValueInAstArray, findPropertyInAstObject } from '../utility/json-utils'; import { parseName } from '../utility/parse-name'; -import { buildDefaultPath, getProject } from '../utility/project'; -import { getProjectTargets } from '../utility/project-targets'; -import { - BrowserBuilderOptions, - BrowserBuilderTarget, - WorkspaceSchema, -} from '../utility/workspace-models'; +import { buildDefaultPath, getWorkspace, updateWorkspace } from '../utility/workspace'; +import { BrowserBuilderOptions } from '../utility/workspace-models'; import { Schema as WebWorkerOptions } from './schema'; -function getProjectConfiguration( - workspace: WorkspaceSchema, - options: WebWorkerOptions, -): BrowserBuilderOptions { - if (!options.target) { - throw new SchematicsException('Option (target) is required.'); - } - - const projectTargets = getProjectTargets(workspace, options.project); - if (!projectTargets[options.target]) { - throw new Error(`Target is not defined for this project.`); - } - - const target = projectTargets[options.target] as BrowserBuilderTarget; - - return target.options; -} -function addConfig(options: WebWorkerOptions, root: string): Rule { +function addConfig(options: WebWorkerOptions, root: string, tsConfigPath: string): Rule { return (host: Tree, context: SchematicContext) => { context.logger.debug('updating project configuration.'); - const workspace = getWorkspace(host); - const config = getProjectConfiguration(workspace, options); - - if (config.webWorkerTsConfig) { - // Don't do anything if the configuration is already there. - return; - } const tsConfigRules = []; @@ -60,9 +30,6 @@ function addConfig(options: WebWorkerOptions, root: string): Rule { move(root), ]))); - // Add build-angular config flag. - config.webWorkerTsConfig = `${root.endsWith('/') ? root : root + '/'}tsconfig.worker.json`; - // Add project tsconfig.json. // The project level tsconfig.json with webworker lib is for editor support since // the dom and webworker libs are mutually exclusive. @@ -102,7 +69,6 @@ function addConfig(options: WebWorkerOptions, root: string): Rule { // Add worker glob exclusion to tsconfig.app.json. const workerGlob = '**/*.worker.ts'; - const tsConfigPath = config.tsConfig; const buffer = host.read(tsConfigPath); if (buffer) { const tsCfgAst = parseJsonAst(buffer.toString(), JsonParseMode.Loose); @@ -124,8 +90,6 @@ function addConfig(options: WebWorkerOptions, root: string): Rule { return chain([ // Add tsconfigs. ...tsConfigRules, - // Add workspace configuration. - updateWorkspace(workspace), ]); }; } @@ -174,18 +138,31 @@ function addSnippet(options: WebWorkerOptions): Rule { } export default function (options: WebWorkerOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const project = getProject(host, options.project); + return async (host: Tree) => { + const workspace = await getWorkspace(host); + if (!options.project) { throw new SchematicsException('Option "project" is required.'); } + if (!options.target) { + throw new SchematicsException('Option (target) is required.'); + } + + const project = workspace.projects.get(options.project); if (!project) { throw new SchematicsException(`Invalid project name (${options.project})`); } - if (project.projectType !== 'application') { + const projectType = project.extensions['projectType']; + if (projectType !== 'application') { throw new SchematicsException(`Web Worker requires a project type of "application".`); } + const projectTarget = project.targets.get(options.target); + if (!projectTarget) { + throw new Error(`Target is not defined for this project.`); + } + const projectTargetOptions = (projectTarget.options || {}) as unknown as BrowserBuilderOptions; + if (options.path === undefined) { options.path = buildDefaultPath(project); } @@ -194,6 +171,12 @@ export default function (options: WebWorkerOptions): Rule { options.path = parsedPath.path; const root = project.root || project.sourceRoot || ''; + const needWebWorkerConfig = !projectTargetOptions.webWorkerTsConfig; + if (needWebWorkerConfig) { + projectTargetOptions.webWorkerTsConfig = + `${root.endsWith('/') ? root : root + '/'}tsconfig.worker.json`; + } + const templateSource = apply(url('./files/worker'), [ applyTemplates({ ...options, ...strings }), move(parsedPath.path), @@ -201,7 +184,8 @@ export default function (options: WebWorkerOptions): Rule { return chain([ // Add project configuration. - addConfig(options, root), + needWebWorkerConfig ? addConfig(options, root, projectTargetOptions.tsConfig) : noop(), + needWebWorkerConfig ? updateWorkspace(workspace) : noop(), // Create the worker in a sibling module. options.snippet ? addSnippet(options) : noop(), // Add the worker. diff --git a/packages/schematics/angular/web-worker/index_spec.ts b/packages/schematics/angular/web-worker/index_spec.ts index dbe82d773374..6a41658f77ce 100644 --- a/packages/schematics/angular/web-worker/index_spec.ts +++ b/packages/schematics/angular/web-worker/index_spec.ts @@ -46,39 +46,45 @@ describe('Service Worker Schematic', () => { appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should put the worker file in the project root', () => { - const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree); + it('should put the worker file in the project root', async () => { + const tree = await schematicRunner.runSchematicAsync('web-worker', defaultOptions, appTree) + .toPromise(); const path = '/projects/bar/src/app/app.worker.ts'; expect(tree.exists(path)).toEqual(true); }); - it('should put a new tsconfig.json file in the project root', () => { - const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree); + it('should put a new tsconfig.json file in the project root', async () => { + const tree = await schematicRunner.runSchematicAsync('web-worker', defaultOptions, appTree) + .toPromise(); const path = '/projects/bar/tsconfig.json'; expect(tree.exists(path)).toEqual(true); }); - it('should put the tsconfig.worker.json file in the project root', () => { - const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree); + it('should put the tsconfig.worker.json file in the project root', async () => { + const tree = await schematicRunner.runSchematicAsync('web-worker', defaultOptions, appTree) + .toPromise(); const path = '/projects/bar/tsconfig.worker.json'; expect(tree.exists(path)).toEqual(true); }); - it('should add the webWorkerTsConfig option to workspace', () => { - const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree); + it('should add the webWorkerTsConfig option to workspace', async () => { + const tree = await schematicRunner.runSchematicAsync('web-worker', defaultOptions, appTree) + .toPromise(); const { projects } = JSON.parse(tree.readContent('/angular.json')); expect(projects.bar.architect.build.options.webWorkerTsConfig) .toBe('projects/bar/tsconfig.worker.json'); }); - it('should add exclusions to tsconfig.app.json', () => { - const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree); + it('should add exclusions to tsconfig.app.json', async () => { + const tree = await schematicRunner.runSchematicAsync('web-worker', defaultOptions, appTree) + .toPromise(); const { exclude } = JSON.parse(tree.readContent('/projects/bar/tsconfig.app.json')); expect(exclude).toContain('**/*.worker.ts'); }); - it('should add snippet to sibling file', () => { - const tree = schematicRunner.runSchematic('web-worker', defaultOptions, appTree); + it('should add snippet to sibling file', async () => { + const tree = await schematicRunner.runSchematicAsync('web-worker', defaultOptions, appTree) + .toPromise(); const appComponent = tree.readContent('/projects/bar/src/app/app.component.ts'); expect(appComponent).toContain(`new Worker('./${defaultOptions.name}.worker`); expect(appComponent).toContain('console.log(`page got message: ${data}`)'); From fcce9823cb83ab3737e19ad245d4c9aa5aa0ca27 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 10 Apr 2019 21:00:04 -0400 Subject: [PATCH 06/14] refactor(@schematics/angular): update universal to use new workspace rules --- .../schematics/angular/universal/index.ts | 121 ++++++++---------- .../angular/universal/index_spec.ts | 51 +++++--- 2 files changed, 87 insertions(+), 85 deletions(-) diff --git a/packages/schematics/angular/universal/index.ts b/packages/schematics/angular/universal/index.ts index 8b015b968332..2dcd9f197697 100644 --- a/packages/schematics/angular/universal/index.ts +++ b/packages/schematics/angular/universal/index.ts @@ -8,7 +8,6 @@ import { Path, basename, - experimental, join, normalize, parseJson, @@ -32,52 +31,44 @@ import { import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { findNode, getDecoratorMetadata } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; -import { getWorkspace, updateWorkspace } from '../utility/config'; import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies'; import { findBootstrapModuleCall, findBootstrapModulePath } from '../utility/ng-ast-utils'; -import { getProject } from '../utility/project'; -import { getProjectTargets, targetBuildNotFoundError } from '../utility/project-targets'; -import { Builders, WorkspaceTargets } from '../utility/workspace-models'; +import { targetBuildNotFoundError } from '../utility/project-targets'; +import { getWorkspace, updateWorkspace } from '../utility/workspace'; +import { BrowserBuilderOptions, Builders } from '../utility/workspace-models'; import { Schema as UniversalOptions } from './schema'; - -function getFileReplacements(target: WorkspaceTargets) { - const fileReplacements = - target.build && - target.build.configurations && - target.build.configurations.production && - target.build.configurations.production.fileReplacements; - - return fileReplacements || []; -} - function updateConfigFile(options: UniversalOptions, tsConfigDirectory: Path): Rule { - return (host: Tree) => { - const workspace = getWorkspace(host); - const clientProject = getProject(workspace, options.clientProject); - const projectTargets = getProjectTargets(clientProject); - - projectTargets.server = { - builder: Builders.Server, - options: { - outputPath: `dist/${options.clientProject}-server`, - main: `${clientProject.root}src/main.server.ts`, - tsConfig: join(tsConfigDirectory, `${options.tsconfigFileName}.json`), - }, - configurations: { - production: { - fileReplacements: getFileReplacements(projectTargets), - sourceMap: false, - optimization: { - scripts: false, - styles: true, + return updateWorkspace(workspace => { + const clientProject = workspace.projects.get(options.clientProject); + if (clientProject) { + const buildTarget = clientProject.targets.get('build'); + let fileReplacements; + if (buildTarget && buildTarget.configurations && buildTarget.configurations.production) { + fileReplacements = buildTarget.configurations.production.fileReplacements; + } + + clientProject.targets.add({ + name: 'server', + builder: Builders.Server, + options: { + outputPath: `dist/${options.clientProject}-server`, + main: `${clientProject.root}src/main.server.ts`, + tsConfig: join(tsConfigDirectory, `${options.tsconfigFileName}.json`), + }, + configurations: { + production: { + fileReplacements, + sourceMap: false, + optimization: { + scripts: false, + styles: true, + }, }, }, - }, - }; - - return updateWorkspace(workspace); - }; + }); + } + }); } function findBrowserModuleImport(host: Tree, modulePath: string): ts.Node { @@ -99,13 +90,9 @@ function findBrowserModuleImport(host: Tree, modulePath: string): ts.Node { return browserModuleNode; } -function wrapBootstrapCall(options: UniversalOptions): Rule { +function wrapBootstrapCall(mainFile: string): Rule { return (host: Tree) => { - const clientTargets = getProjectTargets(host, options.clientProject); - if (!clientTargets.build) { - throw targetBuildNotFoundError(); - } - const mainPath = normalize('/' + clientTargets.build.options.main); + const mainPath = normalize('/' + mainFile); let bootstrapCall: ts.Node | null = findBootstrapModuleCall(host, mainPath); if (bootstrapCall === null) { throw new SchematicsException('Bootstrap module not found.'); @@ -172,18 +159,17 @@ function findCallExpressionNode(node: ts.Node, text: string): ts.Node | null { return foundNode; } -function addServerTransition(options: UniversalOptions): Rule { +function addServerTransition( + options: UniversalOptions, + mainFile: string, + clientProjectRoot: string, +): Rule { return (host: Tree) => { - const clientProject = getProject(host, options.clientProject); - const clientTargets = getProjectTargets(clientProject); - if (!clientTargets.build) { - throw targetBuildNotFoundError(); - } - const mainPath = normalize('/' + clientTargets.build.options.main); + const mainPath = normalize('/' + mainFile); const bootstrapModuleRelativePath = findBootstrapModulePath(host, mainPath); const bootstrapModulePath = normalize( - `/${clientProject.root}/src/${bootstrapModuleRelativePath}.ts`); + `/${clientProjectRoot}/src/${bootstrapModuleRelativePath}.ts`); const browserModuleImport = findBrowserModuleImport(host, bootstrapModulePath); const appId = options.appId; @@ -214,8 +200,7 @@ function addDependencies(): Rule { }; } -function getTsConfigOutDir(host: Tree, targets: experimental.workspace.WorkspaceTool): string { - const tsConfigPath = targets.build.options.tsConfig; +function getTsConfigOutDir(host: Tree, tsConfigPath: string): string { const tsConfigBuffer = host.read(tsConfigPath); if (!tsConfigBuffer) { throw new SchematicsException(`Could not read ${tsConfigPath}`); @@ -233,18 +218,24 @@ function getTsConfigOutDir(host: Tree, targets: experimental.workspace.Workspace } export default function (options: UniversalOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const clientProject = getProject(host, options.clientProject); - if (clientProject.projectType !== 'application') { + return async (host: Tree, context: SchematicContext) => { + const workspace = await getWorkspace(host); + + const clientProject = workspace.projects.get(options.clientProject); + if (!clientProject || clientProject.extensions.projectType !== 'application') { throw new SchematicsException(`Universal requires a project type of "application".`); } - const clientTargets = getProjectTargets(clientProject); - const outDir = getTsConfigOutDir(host, clientTargets); - if (!clientTargets.build) { + + const clientBuildTarget = clientProject.targets.get('build'); + if (!clientBuildTarget) { throw targetBuildNotFoundError(); } + const clientBuildOptions = + (clientBuildTarget.options || {}) as unknown as BrowserBuilderOptions; + + const outDir = getTsConfigOutDir(host, clientBuildOptions.tsConfig); - const clientTsConfig = normalize(clientTargets.build.options.tsConfig); + const clientTsConfig = normalize(clientBuildOptions.tsConfig); const tsConfigExtends = basename(clientTsConfig); // this is needed because prior to version 8, tsconfig might have been in 'src' // and we don't want to break the 'ng add @nguniversal/express-engine schematics' @@ -281,8 +272,8 @@ export default function (options: UniversalOptions): Rule { mergeWith(rootSource), addDependencies(), updateConfigFile(options, tsConfigDirectory), - wrapBootstrapCall(options), - addServerTransition(options), + wrapBootstrapCall(clientBuildOptions.main), + addServerTransition(options, clientBuildOptions.main, clientProject.root), ]); }; } diff --git a/packages/schematics/angular/universal/index_spec.ts b/packages/schematics/angular/universal/index_spec.ts index d08d3bb3f8cf..59011d001d14 100644 --- a/packages/schematics/angular/universal/index_spec.ts +++ b/packages/schematics/angular/universal/index_spec.ts @@ -57,22 +57,27 @@ describe('Universal Schematic', () => { appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should create a root module file', () => { - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should create a root module file', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; expect(tree.exists(filePath)).toEqual(true); }); - it('should create a main file', () => { - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should create a main file', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/main.server.ts'; expect(tree.exists(filePath)).toEqual(true); const contents = tree.readContent(filePath); expect(contents).toMatch(/export { AppServerModule } from '\.\/app\/app\.server\.module'/); }); - it('should create a tsconfig file for the workspace project', () => { - const tree = schematicRunner.runSchematic('universal', workspaceUniversalOptions, appTree); + it('should create a tsconfig file for the workspace project', async () => { + const tree = await schematicRunner + .runSchematicAsync('universal', workspaceUniversalOptions, appTree) + .toPromise(); + debugger; const filePath = '/tsconfig.server.json'; expect(tree.exists(filePath)).toEqual(true); const contents = tree.readContent(filePath); @@ -90,8 +95,9 @@ describe('Universal Schematic', () => { .server.options.tsConfig).toEqual('tsconfig.server.json'); }); - it('should create a tsconfig file for a generated application', () => { - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should create a tsconfig file for a generated application', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/tsconfig.server.json'; expect(tree.exists(filePath)).toEqual(true); const contents = tree.readContent(filePath); @@ -109,15 +115,17 @@ describe('Universal Schematic', () => { .server.options.tsConfig).toEqual('projects/bar/tsconfig.server.json'); }); - it('should add dependency: @angular/platform-server', () => { - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should add dependency: @angular/platform-server', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/package.json'; const contents = tree.readContent(filePath); expect(contents).toMatch(/\"@angular\/platform-server\": \"/); }); - it('should update workspace with a server target', () => { - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should update workspace with a server target', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/angular.json'; const contents = tree.readContent(filePath); const config = JSON.parse(contents.toString()); @@ -137,22 +145,24 @@ describe('Universal Schematic', () => { expect(fileReplacements[0].with).toEqual('projects/bar/src/environments/environment.prod.ts'); }); - it('should add a server transition to BrowerModule import', () => { - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should add a server transition to BrowerModule import', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.module.ts'; const contents = tree.readContent(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); + it('should wrap the bootstrap call in a DOMContentLoaded event handler', async () => { + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/main.ts'; const contents = tree.readContent(filePath); expect(contents) .toMatch(/document.addEventListener\('DOMContentLoaded', \(\) => {[\w\W]+;[\r\n]}\);/); }); - it('should wrap the bootstrap declaration in a DOMContentLoaded event handler', () => { + it('should wrap the bootstrap declaration in a DOMContentLoaded event handler', async () => { const filePath = '/projects/bar/src/main.ts'; appTree.overwrite( filePath, @@ -175,15 +185,16 @@ describe('Universal Schematic', () => { `, ); - const tree = schematicRunner.runSchematic('universal', defaultOptions, appTree); + const tree = await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree) + .toPromise(); const contents = tree.readContent(filePath); expect(contents).toMatch( /document.addEventListener\('DOMContentLoaded', \(\) => {[\n\r\s]+bootstrap\(\)/, ); }); - it('should install npm dependencies', () => { - schematicRunner.runSchematic('universal', defaultOptions, appTree); + it('should install npm dependencies', async () => { + await schematicRunner.runSchematicAsync('universal', defaultOptions, appTree).toPromise(); expect(schematicRunner.tasks.length).toBe(1); expect(schematicRunner.tasks[0].name).toBe('node-package'); expect((schematicRunner.tasks[0].options as {command: string}).command).toBe('install'); From 3211303e3fa11fff16a9204c7ea86a87523b8d6b Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 11 Apr 2019 14:06:22 -0400 Subject: [PATCH 07/14] refactor(@schematics/angular): update app-shell to use new workspace rules --- .../schematics/angular/app-shell/index.ts | 141 +++++++++--------- .../angular/app-shell/index_spec.ts | 58 ++++--- 2 files changed, 107 insertions(+), 92 deletions(-) diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index 0431ad605c96..868205182de6 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -5,13 +5,14 @@ * 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 { dirname, experimental, join, normalize } from '@angular-devkit/core'; +import { dirname, join, normalize } from '@angular-devkit/core'; import { Rule, SchematicContext, SchematicsException, Tree, chain, + noop, schematic, } from '@angular-devkit/schematics'; import { Schema as ComponentOptions } from '../component/schema'; @@ -26,11 +27,10 @@ import { isImported, } from '../utility/ast-utils'; import { Change, InsertChange } from '../utility/change'; -import { getWorkspace, updateWorkspace } from '../utility/config'; import { getAppModulePath } from '../utility/ng-ast-utils'; -import { getProject } from '../utility/project'; -import { getProjectTargets, targetBuildNotFoundError } from '../utility/project-targets'; -import { Builders, WorkspaceProject } from '../utility/workspace-models'; +import { targetBuildNotFoundError } from '../utility/project-targets'; +import { getWorkspace, updateWorkspace } from '../utility/workspace'; +import { BrowserBuilderOptions, Builders, ServerBuilderOptions } from '../utility/workspace-models'; import { Schema as AppShellOptions } from './schema'; function getSourceFile(host: Tree, path: string): ts.SourceFile { @@ -46,10 +46,9 @@ function getSourceFile(host: Tree, path: string): ts.SourceFile { function getServerModulePath( host: Tree, - project: experimental.workspace.WorkspaceProject, - architect: experimental.workspace.WorkspaceTool, + projectRoot: string, + mainPath: string, ): string | null { - const mainPath = architect.server.options.main; const mainSource = getSourceFile(host, mainPath); const allNodes = getSourceNodes(mainSource); const expNode = allNodes.filter(node => node.kind === ts.SyntaxKind.ExportDeclaration)[0]; @@ -57,7 +56,7 @@ function getServerModulePath( return null; } const relativePath = (expNode as ts.ExportDeclaration).moduleSpecifier as ts.StringLiteral; - const modulePath = normalize(`/${project.root}/src/${relativePath.text}.ts`); + const modulePath = normalize(`/${projectRoot}/src/${relativePath.text}.ts`); return modulePath; } @@ -97,14 +96,8 @@ function getComponentTemplate(host: Tree, compPath: string, tmplInfo: TemplateIn function getBootstrapComponentPath( host: Tree, - project: WorkspaceProject, + mainPath: string, ): string { - const projectTargets = getProjectTargets(project); - if (!projectTargets.build) { - throw targetBuildNotFoundError(); - } - - const mainPath = projectTargets.build.options.main; const modulePath = getAppModulePath(host, mainPath); const moduleSource = getSourceFile(host, modulePath); @@ -131,15 +124,11 @@ function getBootstrapComponentPath( } // end helper functions. -function validateProject(options: AppShellOptions): Rule { +function validateProject(mainPath: string): Rule { return (host: Tree, context: SchematicContext) => { const routerOutletCheckRegex = /([\s\S]*?)<\/router\-outlet>/; - const clientProject = getProject(host, options.clientProject); - if (clientProject.projectType !== 'application') { - throw new SchematicsException(`App shell requires a project type of "application".`); - } - const componentPath = getBootstrapComponentPath(host, clientProject); + const componentPath = getBootstrapComponentPath(host, mainPath); const tmpl = getComponentTemplateInfo(host, componentPath); const template = getComponentTemplate(host, componentPath, tmpl); if (!routerOutletCheckRegex.test(template)) { @@ -152,12 +141,7 @@ function validateProject(options: AppShellOptions): Rule { } function addUniversalTarget(options: AppShellOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const architect = getProjectTargets(host, options.clientProject); - if (architect.server) { - return host; - } - + return () => { // Copy options. const universalOptions = { ...options, @@ -177,39 +161,38 @@ function addUniversalTarget(options: AppShellOptions): Rule { } function addAppShellConfigToWorkspace(options: AppShellOptions): Rule { - return (host: Tree) => { + return () => { if (!options.route) { throw new SchematicsException(`Route is not defined`); } - const workspace = getWorkspace(host); - const projectTargets = getProjectTargets(workspace, options.clientProject); - projectTargets['app-shell'] = { - builder: Builders.AppShell, - options: { - browserTarget: `${options.clientProject}:build`, - serverTarget: `${options.clientProject}:server`, - route: options.route, - }, - configurations: { - production: { - browserTarget: `${options.clientProject}:build:production`, - serverTarget: `${options.clientProject}:server:production`, - }, - }, - }; + return updateWorkspace(workspace => { + const project = workspace.projects.get(options.clientProject); + if (!project) { + return; + } - return updateWorkspace(workspace); + project.targets.add({ + name: 'app-shell', + builder: Builders.AppShell, + options: { + browserTarget: `${options.clientProject}:build`, + serverTarget: `${options.clientProject}:server`, + route: options.route, + }, + configurations: { + production: { + browserTarget: `${options.clientProject}:build:production`, + serverTarget: `${options.clientProject}:server:production`, + }, + }, + }); + }); }; } -function addRouterModule(options: AppShellOptions): Rule { +function addRouterModule(mainPath: string): Rule { return (host: Tree) => { - const projectTargets = getProjectTargets(host, options.clientProject); - if (!projectTargets.build) { - throw targetBuildNotFoundError(); - } - const mainPath = projectTargets.build.options.main; const modulePath = getAppModulePath(host, mainPath); const moduleSource = getSourceFile(host, modulePath); const changes = addImportToModule(moduleSource, modulePath, 'RouterModule', '@angular/router'); @@ -245,11 +228,22 @@ function getMetadataProperty(metadata: ts.Node, propertyName: string): ts.Proper } function addServerRoutes(options: AppShellOptions): Rule { - return (host: Tree) => { - const clientProject = getProject(host, options.clientProject); - const architect = getProjectTargets(clientProject); - // const mainPath = universalArchitect.build.options.main; - const modulePath = getServerModulePath(host, clientProject, architect); + return async (host: Tree) => { + // The workspace gets updated so this needs to be reloaded + const workspace = await getWorkspace(host); + const clientProject = workspace.projects.get(options.clientProject); + if (!clientProject) { + throw new Error('Universal schematic removed client project.'); + } + const clientServerTarget = clientProject.targets.get('server'); + if (!clientServerTarget) { + throw new Error('Universal schematic did not add server target to client project.'); + } + const clientServerOptions = clientServerTarget.options as unknown as ServerBuilderOptions; + if (!clientServerOptions) { + throw new SchematicsException('Server target does not contain options.'); + } + const modulePath = getServerModulePath(host, clientProject.root, clientServerOptions.main); if (modulePath === null) { throw new SchematicsException('Universal/server module not found.'); } @@ -296,9 +290,6 @@ function addServerRoutes(options: AppShellOptions): Rule { } host.commitUpdate(recorder); } - - - return host; }; } @@ -313,12 +304,26 @@ function addShellComponent(options: AppShellOptions): Rule { } export default function (options: AppShellOptions): Rule { - return chain([ - validateProject(options), - addUniversalTarget(options), - addAppShellConfigToWorkspace(options), - addRouterModule(options), - addServerRoutes(options), - addShellComponent(options), - ]); + return async tree => { + const workspace = await getWorkspace(tree); + const clientProject = workspace.projects.get(options.clientProject); + if (!clientProject || clientProject.extensions.projectType !== 'application') { + throw new SchematicsException(`A client project type of "application" is required.`); + } + const clientBuildTarget = clientProject.targets.get('build'); + if (!clientBuildTarget) { + throw targetBuildNotFoundError(); + } + const clientBuildOptions = + (clientBuildTarget.options || {}) as unknown as BrowserBuilderOptions; + + return chain([ + validateProject(clientBuildOptions.main), + clientProject.targets.has('server') ? noop() : addUniversalTarget(options), + addAppShellConfigToWorkspace(options), + addRouterModule(clientBuildOptions.main), + addServerRoutes(options), + addShellComponent(options), + ]); + }; } diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index 75cb959ff69b..56355b17032a 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -44,22 +44,24 @@ describe('App Shell Schematic', () => { }); - it('should ensure the client app has a router-outlet', () => { + it('should ensure the client app has a router-outlet', async () => { appTree = schematicRunner.runSchematic('workspace', workspaceOptions); appTree = schematicRunner.runSchematic('application', {...appOptions, routing: false}, appTree); - expect(() => { - schematicRunner.runSchematic('appShell', defaultOptions, appTree); - }).toThrowError(); + await expectAsync( + schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree).toPromise(), + ).toBeRejected(); }); - it('should add a universal app', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should add a universal app', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; expect(tree.exists(filePath)).toEqual(true); }); - it('should add app shell configuration', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should add app shell configuration', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/angular.json'; const content = tree.readContent(filePath); const workspace = JSON.parse(content); @@ -71,19 +73,21 @@ describe('App Shell Schematic', () => { expect(target.configurations.production.serverTarget).toEqual('bar:server:production'); }); - it('should add router module to client app module', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should add router module to client app module', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/import { RouterModule } from \'@angular\/router\';/); }); - it('should not fail when AppModule have imported RouterModule already', () => { + it('should not fail when AppModule have imported RouterModule already', async () => { const updateRecorder = appTree.beginUpdate('/projects/bar/src/app/app.module.ts'); updateRecorder.insertLeft(0, 'import { RouterModule } from \'@angular/router\';'); appTree.commitUpdate(updateRecorder); - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/import { RouterModule } from \'@angular\/router\';/); @@ -119,10 +123,11 @@ describe('App Shell Schematic', () => { tree.delete('/projects/bar/src/app/app.component.html'); } - it('should not re-add the router outlet (external template)', () => { + it('should not re-add the router outlet (external template)', async () => { const htmlPath = '/projects/bar/src/app/app.component.html'; appTree.overwrite(htmlPath, ''); - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const content = tree.readContent(htmlPath); const matches = content.match(/<\/router\-outlet>/g); @@ -130,9 +135,10 @@ describe('App Shell Schematic', () => { expect(numMatches).toEqual(1); }); - it('should not re-add the router outlet (inline template)', () => { + it('should not re-add the router outlet (inline template)', async () => { makeInlineTemplate(appTree, ''); - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const content = tree.readContent('/projects/bar/src/app/app.component.ts'); const matches = content.match(/<\/router\-outlet>/g); const numMatches = matches ? matches.length : 0; @@ -140,22 +146,25 @@ describe('App Shell Schematic', () => { }); }); - it('should add router imports to server module', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should add router imports to server module', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/import { Routes, RouterModule } from \'@angular\/router\';/); }); - it('should define a server route', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should define a server route', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/const routes: Routes = \[/); }); - it('should import RouterModule with forRoot', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should import RouterModule with forRoot', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); expect(content) @@ -164,8 +173,9 @@ describe('App Shell Schematic', () => { .toMatch(/ServerModule,\r?\n\s*RouterModule\.forRoot\(routes\),/); }); - it('should create the shell component', () => { - const tree = schematicRunner.runSchematic('appShell', defaultOptions, appTree); + it('should create the shell component', async () => { + const tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree) + .toPromise(); expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true); const content = tree.readContent('/projects/bar/src/app/app.server.module.ts'); expect(content).toMatch(/app\-shell\.component/); From 945de17a7759b5fc1603c0aa2fb8f41217ff9a09 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 11 Apr 2019 20:58:26 -0400 Subject: [PATCH 08/14] refactor(@schematics/angular): update class to use new workspace rules --- packages/schematics/angular/class/index.ts | 14 ++------ .../schematics/angular/class/index_spec.ts | 35 +++++++++++-------- packages/schematics/angular/class/schema.json | 3 +- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/schematics/angular/class/index.ts b/packages/schematics/angular/class/index.ts index ff1a68906ef8..46c1f4626bc7 100644 --- a/packages/schematics/angular/class/index.ts +++ b/packages/schematics/angular/class/index.ts @@ -8,8 +8,6 @@ import { strings } from '@angular-devkit/core'; import { Rule, - SchematicContext, - SchematicsException, Tree, apply, applyTemplates, @@ -22,19 +20,13 @@ import { } from '@angular-devkit/schematics'; import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; -import { buildDefaultPath, getProject } from '../utility/project'; +import { createDefaultPath } from '../utility/workspace'; import { Schema as ClassOptions } from './schema'; export default function (options: ClassOptions): Rule { - return (host: Tree, context: SchematicContext) => { - if (!options.project) { - throw new SchematicsException('Option (project) is required.'); - } - - const project = getProject(host, options.project); - + return async (host: Tree) => { if (options.path === undefined) { - options.path = buildDefaultPath(project); + options.path = await createDefaultPath(host, options.project as string); } options.type = !!options.type ? `.${options.type}` : ''; diff --git a/packages/schematics/angular/class/index_spec.ts b/packages/schematics/angular/class/index_spec.ts index 09a211437d79..0d07324f1407 100644 --- a/packages/schematics/angular/class/index_spec.ts +++ b/packages/schematics/angular/class/index_spec.ts @@ -44,54 +44,61 @@ describe('Class Schematic', () => { appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should create just the class file', () => { - const tree = schematicRunner.runSchematic('class', defaultOptions, appTree); + it('should create just the class file', async () => { + const tree = await schematicRunner.runSchematicAsync('class', defaultOptions, appTree) + .toPromise(); expect(tree.files).toContain('/projects/bar/src/app/foo.ts'); expect(tree.files).not.toContain('/projects/bar/src/app/foo.spec.ts'); }); - it('should create the class and spec file', () => { + it('should create the class and spec file', async () => { const options = { ...defaultOptions, spec: true, }; - const tree = schematicRunner.runSchematic('class', options, appTree); + const tree = await schematicRunner.runSchematicAsync('class', options, appTree) + .toPromise(); expect(tree.files).toContain('/projects/bar/src/app/foo.ts'); expect(tree.files).toContain('/projects/bar/src/app/foo.spec.ts'); }); - it('should create an class named "Foo"', () => { - const tree = schematicRunner.runSchematic('class', defaultOptions, appTree); + it('should create an class named "Foo"', async () => { + const tree = await schematicRunner.runSchematicAsync('class', defaultOptions, appTree) + .toPromise(); const fileContent = tree.readContent('/projects/bar/src/app/foo.ts'); expect(fileContent).toMatch(/export class Foo/); }); - it('should put type in the file name', () => { + it('should put type in the file name', async () => { const options = { ...defaultOptions, type: 'model' }; - const tree = schematicRunner.runSchematic('class', options, appTree); + const tree = await schematicRunner.runSchematicAsync('class', options, appTree) + .toPromise(); expect(tree.files).toContain('/projects/bar/src/app/foo.model.ts'); }); - it('should split the name to name & type with split on "."', () => { + it('should split the name to name & type with split on "."', async () => { const options = {...defaultOptions, name: 'foo.model' }; - const tree = schematicRunner.runSchematic('class', options, appTree); + const tree = await schematicRunner.runSchematicAsync('class', options, appTree) + .toPromise(); const classPath = '/projects/bar/src/app/foo.model.ts'; const content = tree.readContent(classPath); expect(content).toMatch(/export class Foo/); }); - it('should respect the path option', () => { + it('should respect the path option', async () => { const options = { ...defaultOptions, path: 'zzz' }; - const tree = schematicRunner.runSchematic('class', options, appTree); + const tree = await schematicRunner.runSchematicAsync('class', options, appTree) + .toPromise(); expect(tree.files).toContain('/zzz/foo.ts'); }); - it('should respect the sourceRoot value', () => { + it('should respect the sourceRoot value', async () => { const config = JSON.parse(appTree.readContent('/angular.json')); config.projects.bar.sourceRoot = 'projects/bar/custom'; appTree.overwrite('/angular.json', JSON.stringify(config, null, 2)); - appTree = schematicRunner.runSchematic('class', defaultOptions, appTree); + appTree = await schematicRunner.runSchematicAsync('class', defaultOptions, appTree) + .toPromise(); expect(appTree.files).toContain('/projects/bar/custom/app/foo.ts'); }); }); diff --git a/packages/schematics/angular/class/schema.json b/packages/schematics/angular/class/schema.json index e57a09046e66..285cfc55af14 100644 --- a/packages/schematics/angular/class/schema.json +++ b/packages/schematics/angular/class/schema.json @@ -52,7 +52,6 @@ } }, "required": [ - "name", - "project" + "name" ] } From d0eadd083cb2b7ca4282280a2e728bd1a8190553 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 11 Apr 2019 21:01:45 -0400 Subject: [PATCH 09/14] refactor(@schematics/angular): update enum to use new workspace rules --- packages/schematics/angular/enum/index.ts | 13 +++---------- packages/schematics/angular/enum/index_spec.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/schematics/angular/enum/index.ts b/packages/schematics/angular/enum/index.ts index 5c2d489f784f..6219358cba41 100644 --- a/packages/schematics/angular/enum/index.ts +++ b/packages/schematics/angular/enum/index.ts @@ -8,8 +8,6 @@ import { strings } from '@angular-devkit/core'; import { Rule, - SchematicContext, - SchematicsException, Tree, apply, applyTemplates, @@ -21,19 +19,14 @@ import { } from '@angular-devkit/schematics'; import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; -import { buildDefaultPath, getProject } from '../utility/project'; +import { createDefaultPath } from '../utility/workspace'; import { Schema as EnumOptions } from './schema'; export default function (options: EnumOptions): Rule { - return (host: Tree, context: SchematicContext) => { - if (!options.project) { - throw new SchematicsException('Option (project) is required.'); - } - const project = getProject(host, options.project); - + return async (host: Tree) => { if (options.path === undefined) { - options.path = buildDefaultPath(project); + options.path = await createDefaultPath(host, options.project as string); } const parsedPath = parseName(options.path, options.name); diff --git a/packages/schematics/angular/enum/index_spec.ts b/packages/schematics/angular/enum/index_spec.ts index e65376e93e03..9a09afec5ddb 100644 --- a/packages/schematics/angular/enum/index_spec.ts +++ b/packages/schematics/angular/enum/index_spec.ts @@ -41,22 +41,26 @@ describe('Enum Schematic', () => { appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should create an enumeration', () => { - const tree = schematicRunner.runSchematic('enum', defaultOptions, appTree); + it('should create an enumeration', async () => { + const tree = await schematicRunner.runSchematicAsync('enum', defaultOptions, appTree) + .toPromise(); const files = tree.files; expect(files).toContain('/projects/bar/src/app/foo.enum.ts'); }); - it('should create an enumeration', () => { - const tree = schematicRunner.runSchematic('enum', defaultOptions, appTree); + + it('should create an enumeration', async () => { + const tree = await schematicRunner.runSchematicAsync('enum', defaultOptions, appTree) + .toPromise(); const content = tree.readContent('/projects/bar/src/app/foo.enum.ts'); expect(content).toMatch('export enum Foo {'); }); - it('should respect the sourceRoot value', () => { + it('should respect the sourceRoot value', async () => { const config = JSON.parse(appTree.readContent('/angular.json')); config.projects.bar.sourceRoot = 'projects/bar/custom'; appTree.overwrite('/angular.json', JSON.stringify(config, null, 2)); - appTree = schematicRunner.runSchematic('enum', defaultOptions, appTree); + appTree = await schematicRunner.runSchematicAsync('enum', defaultOptions, appTree) + .toPromise(); expect(appTree.files).toContain('/projects/bar/custom/app/foo.enum.ts'); }); }); From 5489857ab0d0b2c25eeb8463ed640dbf0d2bc9c5 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 11 Apr 2019 21:06:25 -0400 Subject: [PATCH 10/14] refactor(@schematics/angular): update guard to use new workspace rules --- packages/schematics/angular/guard/index.ts | 12 +++----- .../schematics/angular/guard/index_spec.ts | 30 +++++++++++-------- packages/schematics/angular/guard/schema.json | 3 +- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/schematics/angular/guard/index.ts b/packages/schematics/angular/guard/index.ts index 95666fea1583..0b16aefd4150 100644 --- a/packages/schematics/angular/guard/index.ts +++ b/packages/schematics/angular/guard/index.ts @@ -21,20 +21,16 @@ import { } from '@angular-devkit/schematics'; import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; -import { buildDefaultPath, getProject } from '../utility/project'; +import { createDefaultPath } from '../utility/workspace'; import { Schema as GuardOptions } from './schema'; export default function (options: GuardOptions): Rule { - return (host: Tree) => { - if (!options.project) { - throw new SchematicsException('Option (project) is required.'); - } - const project = getProject(host, options.project); - + return async (host: Tree) => { if (options.path === undefined) { - options.path = buildDefaultPath(project); + options.path = await createDefaultPath(host, options.project as string); } + if (options.implements === undefined) { options.implements = []; } diff --git a/packages/schematics/angular/guard/index_spec.ts b/packages/schematics/angular/guard/index_spec.ts index a98bf7f148c0..c89ee365d067 100644 --- a/packages/schematics/angular/guard/index_spec.ts +++ b/packages/schematics/angular/guard/index_spec.ts @@ -40,42 +40,47 @@ describe('Guard Schematic', () => { appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should create a guard', () => { - const tree = schematicRunner.runSchematic('guard', defaultOptions, appTree); + it('should create a guard', async () => { + const tree = await schematicRunner.runSchematicAsync('guard', defaultOptions, appTree) + .toPromise(); const files = tree.files; expect(files).toContain('/projects/bar/src/app/foo.guard.spec.ts'); expect(files).toContain('/projects/bar/src/app/foo.guard.ts'); }); - it('should respect the skipTests flag', () => { + it('should respect the skipTests flag', async () => { const options = { ...defaultOptions, skipTests: true }; - const tree = schematicRunner.runSchematic('guard', options, appTree); + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree) + .toPromise(); const files = tree.files; expect(files).not.toContain('/projects/bar/src/app/foo.guard.spec.ts'); expect(files).toContain('/projects/bar/src/app/foo.guard.ts'); }); - it('should respect the flat flag', () => { + it('should respect the flat flag', async () => { const options = { ...defaultOptions, flat: false }; - const tree = schematicRunner.runSchematic('guard', options, appTree); + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree) + .toPromise(); const files = tree.files; expect(files).toContain('/projects/bar/src/app/foo/foo.guard.spec.ts'); expect(files).toContain('/projects/bar/src/app/foo/foo.guard.ts'); }); - it('should respect the sourceRoot value', () => { + it('should respect the sourceRoot value', async () => { const config = JSON.parse(appTree.readContent('/angular.json')); config.projects.bar.sourceRoot = 'projects/bar/custom'; appTree.overwrite('/angular.json', JSON.stringify(config, null, 2)); - appTree = schematicRunner.runSchematic('guard', defaultOptions, appTree); + appTree = await schematicRunner.runSchematicAsync('guard', defaultOptions, appTree) + .toPromise(); expect(appTree.files).toContain('/projects/bar/custom/app/foo.guard.ts'); }); - it('should respect the implements value', () => { + it('should respect the implements value', async () => { const options = { ...defaultOptions, implements: ['CanActivate']}; - const tree = schematicRunner.runSchematic('guard', options, appTree); + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree) + .toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); expect(fileString).toContain('CanActivate'); expect(fileString).toContain('canActivate'); @@ -85,10 +90,11 @@ describe('Guard Schematic', () => { expect(fileString).not.toContain('canLoad'); }); - it('should respect the implements values', () => { + it('should respect the implements values', async () => { const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild']; const options = { ...defaultOptions, implements: implementationOptions}; - const tree = schematicRunner.runSchematic('guard', options, appTree); + const tree = await schematicRunner.runSchematicAsync('guard', options, appTree) + .toPromise(); const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts'); // Should contain all implementations diff --git a/packages/schematics/angular/guard/schema.json b/packages/schematics/angular/guard/schema.json index 660c5b80bac4..e430e62ec7fb 100644 --- a/packages/schematics/angular/guard/schema.json +++ b/packages/schematics/angular/guard/schema.json @@ -70,7 +70,6 @@ } }, "required": [ - "name", - "project" + "name" ] } From 99ca3f05d24d2f5a954a2beffd3aefcf9937d664 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 12 Apr 2019 14:36:19 -0400 Subject: [PATCH 11/14] refactor(@schematics/angular): update service to use new workspace rules --- packages/schematics/angular/service/index.ts | 12 +++-------- .../schematics/angular/service/index_spec.ts | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/schematics/angular/service/index.ts b/packages/schematics/angular/service/index.ts index 6bd3b3f1cad6..2e6681e03b58 100644 --- a/packages/schematics/angular/service/index.ts +++ b/packages/schematics/angular/service/index.ts @@ -8,7 +8,6 @@ import { strings } from '@angular-devkit/core'; import { Rule, - SchematicsException, Tree, apply, applyTemplates, @@ -21,18 +20,13 @@ import { } from '@angular-devkit/schematics'; import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; -import { buildDefaultPath, getProject } from '../utility/project'; +import { createDefaultPath } from '../utility/workspace'; import { Schema as ServiceOptions } from './schema'; export default function (options: ServiceOptions): Rule { - return (host: Tree) => { - if (!options.project) { - throw new SchematicsException('Option (project) is required.'); - } - const project = getProject(host, options.project); - + return async (host: Tree) => { if (options.path === undefined) { - options.path = buildDefaultPath(project); + options.path = await createDefaultPath(host, options.project as string); } const parsedPath = parseName(options.path, options.name); diff --git a/packages/schematics/angular/service/index_spec.ts b/packages/schematics/angular/service/index_spec.ts index f974d83002b4..449f22024031 100644 --- a/packages/schematics/angular/service/index_spec.ts +++ b/packages/schematics/angular/service/index_spec.ts @@ -41,37 +41,41 @@ describe('Service Schematic', () => { appTree = schematicRunner.runSchematic('application', appOptions, appTree); }); - it('should create a service', () => { + it('should create a service', async () => { const options = { ...defaultOptions }; - const tree = schematicRunner.runSchematic('service', options, appTree); + const tree = await schematicRunner.runSchematicAsync('service', options, appTree) + .toPromise(); const files = tree.files; expect(files).toContain('/projects/bar/src/app/foo/foo.service.spec.ts'); expect(files).toContain('/projects/bar/src/app/foo/foo.service.ts'); }); - it('service should be tree-shakeable', () => { + it('service should be tree-shakeable', async () => { const options = { ...defaultOptions}; - const tree = schematicRunner.runSchematic('service', options, appTree); + const tree = await schematicRunner.runSchematicAsync('service', options, appTree) + .toPromise(); const content = tree.readContent('/projects/bar/src/app/foo/foo.service.ts'); expect(content).toMatch(/providedIn: 'root'/); }); - it('should respect the skipTests flag', () => { + it('should respect the skipTests flag', async () => { const options = { ...defaultOptions, skipTests: true }; - const tree = schematicRunner.runSchematic('service', options, appTree); + const tree = await schematicRunner.runSchematicAsync('service', options, appTree) + .toPromise(); const files = tree.files; expect(files).toContain('/projects/bar/src/app/foo/foo.service.ts'); expect(files).not.toContain('/projects/bar/src/app/foo/foo.service.spec.ts'); }); - it('should respect the sourceRoot value', () => { + it('should respect the sourceRoot value', async () => { const config = JSON.parse(appTree.readContent('/angular.json')); config.projects.bar.sourceRoot = 'projects/bar/custom'; appTree.overwrite('/angular.json', JSON.stringify(config, null, 2)); - appTree = schematicRunner.runSchematic('service', defaultOptions, appTree); + appTree = await schematicRunner.runSchematicAsync('service', defaultOptions, appTree) + .toPromise(); expect(appTree.files).toContain('/projects/bar/custom/app/foo/foo.service.ts'); }); }); From 6d8f7eb1d2f6f8551183a8a008da6ee1db308ced Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 12 Apr 2019 22:23:50 -0400 Subject: [PATCH 12/14] refactor(@schematics/angular): update library to use new workspace rules --- packages/schematics/angular/library/index.ts | 99 ++++++++-------- .../schematics/angular/library/index_spec.ts | 106 +++++++++--------- 2 files changed, 102 insertions(+), 103 deletions(-) diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index 034fef945485..4a7d08761b0f 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -21,20 +21,12 @@ import { url, } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; -import { - addProjectToWorkspace, - getWorkspace, -} from '../utility/config'; import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies'; import { latestVersions } from '../utility/latest-versions'; import { applyLintFix } from '../utility/lint-fix'; import { validateProjectName } from '../utility/validation'; -import { - Builders, - ProjectType, - WorkspaceProject, - WorkspaceSchema, -} from '../utility/workspace-models'; +import { getWorkspace, updateWorkspace } from '../utility/workspace'; +import { Builders, ProjectType } from '../utility/workspace-models'; import { Schema as LibraryOptions } from './schema'; interface UpdateJsonFn { @@ -126,50 +118,57 @@ function addDependenciesToPackageJson() { }; } -function addAppToWorkspaceFile(options: LibraryOptions, workspace: WorkspaceSchema, - projectRoot: string, projectName: string): Rule { - - const project: WorkspaceProject = { - root: projectRoot, - sourceRoot: `${projectRoot}/src`, - projectType: ProjectType.Library, - prefix: options.prefix || 'lib', - architect: { - build: { - builder: Builders.NgPackagr, - options: { - tsConfig: `${projectRoot}/tsconfig.lib.json`, - project: `${projectRoot}/ng-package.json`, +function addAppToWorkspaceFile( + options: LibraryOptions, + projectRoot: string, + projectName: string, +): Rule { + return updateWorkspace(workspace => { + if (workspace.projects.size === 0) { + workspace.extensions.defaultProject = projectName; + } + + workspace.projects.add({ + name: projectName, + root: projectRoot, + sourceRoot: `${projectRoot}/src`, + projectType: ProjectType.Library, + prefix: options.prefix || 'lib', + targets: { + build: { + builder: Builders.NgPackagr, + options: { + tsConfig: `${projectRoot}/tsconfig.lib.json`, + project: `${projectRoot}/ng-package.json`, + }, }, - }, - test: { - builder: Builders.Karma, - options: { - main: `${projectRoot}/src/test.ts`, - tsConfig: `${projectRoot}/tsconfig.spec.json`, - karmaConfig: `${projectRoot}/karma.conf.js`, + test: { + builder: Builders.Karma, + options: { + main: `${projectRoot}/src/test.ts`, + tsConfig: `${projectRoot}/tsconfig.spec.json`, + karmaConfig: `${projectRoot}/karma.conf.js`, + }, }, - }, - lint: { - builder: Builders.TsLint, - options: { - tsConfig: [ - `${projectRoot}/tsconfig.lib.json`, - `${projectRoot}/tsconfig.spec.json`, - ], - exclude: [ - '**/node_modules/**', - ], + lint: { + builder: Builders.TsLint, + options: { + tsConfig: [ + `${projectRoot}/tsconfig.lib.json`, + `${projectRoot}/tsconfig.spec.json`, + ], + exclude: [ + '**/node_modules/**', + ], + }, }, }, - }, - }; - - return addProjectToWorkspace(workspace, projectName, project); + }); + }); } export default function (options: LibraryOptions): Rule { - return (host: Tree, context: SchematicContext) => { + return async (host: Tree) => { if (!options.name) { throw new SchematicsException(`Invalid options, "name" is required.`); } @@ -187,8 +186,8 @@ export default function (options: LibraryOptions): Rule { options.name = name; } - const workspace = getWorkspace(host); - const newProjectRoot = workspace.newProjectRoot || ''; + const workspace = await getWorkspace(host); + const newProjectRoot = workspace.extensions.newProjectRoot || ''; const scopeFolder = scopeName ? strings.dasherize(scopeName) + '/' : ''; const folderName = `${scopeFolder}${strings.dasherize(options.name)}`; @@ -215,7 +214,7 @@ export default function (options: LibraryOptions): Rule { return chain([ mergeWith(templateSource), - addAppToWorkspaceFile(options, workspace, projectRoot, projectName), + addAppToWorkspaceFile(options, projectRoot, projectName), options.skipPackageJson ? noop() : addDependenciesToPackageJson(), options.skipTsConfig ? noop() : updateTsConfig(packageName, distRoot), schematic('module', { diff --git a/packages/schematics/angular/library/index_spec.ts b/packages/schematics/angular/library/index_spec.ts index 92bee35a683d..49ad3d5ecd1e 100644 --- a/packages/schematics/angular/library/index_spec.ts +++ b/packages/schematics/angular/library/index_spec.ts @@ -41,8 +41,8 @@ describe('Library Schematic', () => { workspaceTree = schematicRunner.runSchematic('workspace', workspaceOptions); }); - it('should create files', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should create files', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const files = tree.files; expect(files).toEqual(jasmine.arrayContaining([ '/projects/foo/karma.conf.js', @@ -60,81 +60,81 @@ describe('Library Schematic', () => { ])); }); - it('should create a package.json named "foo"', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should create a package.json named "foo"', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const fileContent = getFileContent(tree, '/projects/foo/package.json'); expect(fileContent).toMatch(/"name": "foo"/); }); - it('should have the latest Angular major versions in package.json named "foo"', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should have the latest Angular major versions in package.json named "foo"', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const fileContent = getJsonFileContent(tree, '/projects/foo/package.json'); const angularVersion = latestVersions.Angular.replace('~', '').replace('^', ''); expect(fileContent.peerDependencies['@angular/core']).toBe(`^${angularVersion}`); }); - it('should create a README.md named "foo"', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should create a README.md named "foo"', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const fileContent = getFileContent(tree, '/projects/foo/README.md'); expect(fileContent).toMatch(/# Foo/); }); - it('should create a tsconfig for library', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should create a tsconfig for library', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const fileContent = getJsonFileContent(tree, '/projects/foo/tsconfig.lib.json'); expect(fileContent).toBeDefined(); }); - it('should create a ng-package.json with ngPackage conf', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should create a ng-package.json with ngPackage conf', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const fileContent = getJsonFileContent(tree, '/projects/foo/ng-package.json'); expect(fileContent.lib).toBeDefined(); expect(fileContent.lib.entryFile).toEqual('src/my-index.ts'); expect(fileContent.dest).toEqual('../../dist/foo'); }); - it('should use default value for baseDir and entryFile', () => { - const tree = schematicRunner.runSchematic('library', { + it('should use default value for baseDir and entryFile', async () => { + const tree = await schematicRunner.runSchematicAsync('library', { name: 'foobar', - }, workspaceTree); + }, workspaceTree).toPromise(); expect(tree.files).toContain('/projects/foobar/src/public-api.ts'); }); - it(`should add library to workspace`, () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it(`should add library to workspace`, async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const workspace = getJsonFileContent(tree, '/angular.json'); expect(workspace.projects.foo).toBeDefined(); expect(workspace.defaultProject).toBe('foo'); }); - it('should set the prefix to lib if none is set', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should set the prefix to lib if none is set', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const workspace = JSON.parse(tree.readContent('/angular.json')); expect(workspace.projects.foo.prefix).toEqual('lib'); }); - it('should set the prefix correctly', () => { + it('should set the prefix correctly', async () => { const options = { ...defaultOptions, prefix: 'pre' }; - const tree = schematicRunner.runSchematic('library', options, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', options, workspaceTree).toPromise(); const workspace = JSON.parse(tree.readContent('/angular.json')); expect(workspace.projects.foo.prefix).toEqual('pre'); }); - it('should set the right prefix in the tslint file when provided is kebabed', () => { + it('should set the right prefix in the tslint file when provided is kebabed', async () => { const options: GenerateLibrarySchema = { ...defaultOptions, prefix: 'foo-bar' }; - const tree = schematicRunner.runSchematic('library', options, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', options, workspaceTree).toPromise(); const path = '/projects/foo/tslint.json'; const content = JSON.parse(tree.readContent(path)); expect(content.rules['directive-selector'][2]).toMatch('fooBar'); expect(content.rules['component-selector'][2]).toMatch('foo-bar'); }); - it('should handle a pascalCasedName', () => { + it('should handle a pascalCasedName', async () => { const options = {...defaultOptions, name: 'pascalCasedName'}; - const tree = schematicRunner.runSchematic('library', options, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', options, workspaceTree).toPromise(); const config = getJsonFileContent(tree, '/angular.json'); const project = config.projects.pascalCasedName; expect(project).toBeDefined(); @@ -143,14 +143,14 @@ describe('Library Schematic', () => { expect(svcContent).toMatch(/providedIn: 'root'/); }); - it('should export the component in the NgModule', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should export the component in the NgModule', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const fileContent = getFileContent(tree, '/projects/foo/src/lib/foo.module.ts'); expect(fileContent).toContain('exports: [FooComponent]'); }); - it('should set the right path and prefix in the tslint file', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should set the right path and prefix in the tslint file', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const path = '/projects/foo/tslint.json'; const content = JSON.parse(tree.readContent(path)); expect(content.extends).toMatch('../../tslint.json'); @@ -159,8 +159,8 @@ describe('Library Schematic', () => { }); describe(`update package.json`, () => { - it(`should add ng-packagr to devDependencies`, () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it(`should add ng-packagr to devDependencies`, async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const packageJson = getJsonFileContent(tree, 'package.json'); expect(packageJson.devDependencies['ng-packagr']).toEqual('^5.0.0'); @@ -168,30 +168,30 @@ describe('Library Schematic', () => { .toEqual(latestVersions.DevkitBuildNgPackagr); }); - it('should use the latest known versions in package.json', () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should use the latest known versions in package.json', async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const pkg = JSON.parse(tree.readContent('/package.json')); expect(pkg.devDependencies['@angular/compiler-cli']).toEqual(latestVersions.Angular); expect(pkg.devDependencies['typescript']).toEqual(latestVersions.TypeScript); }); - it(`should not override existing users dependencies`, () => { + it(`should not override existing users dependencies`, async () => { const oldPackageJson = workspaceTree.readContent('package.json'); workspaceTree.overwrite('package.json', oldPackageJson.replace( `"typescript": "${latestVersions.TypeScript}"`, `"typescript": "~2.5.2"`, )); - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const packageJson = getJsonFileContent(tree, 'package.json'); expect(packageJson.devDependencies.typescript).toEqual('~2.5.2'); }); - it(`should not modify the file when --skipPackageJson`, () => { - const tree = schematicRunner.runSchematic('library', { + it(`should not modify the file when --skipPackageJson`, async () => { + const tree = await schematicRunner.runSchematicAsync('library', { name: 'foo', skipPackageJson: true, - }, workspaceTree); + }, workspaceTree).toPromise(); const packageJson = getJsonFileContent(tree, 'package.json'); expect(packageJson.devDependencies['ng-packagr']).toBeUndefined(); @@ -200,8 +200,8 @@ describe('Library Schematic', () => { }); describe(`update tsconfig.json`, () => { - it(`should add paths mapping to empty tsconfig`, () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it(`should add paths mapping to empty tsconfig`, async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json'); expect(tsConfigJson.compilerOptions.paths.foo).toBeTruthy(); @@ -212,7 +212,7 @@ describe('Library Schematic', () => { expect(tsConfigJson.compilerOptions.paths['foo/*'][0]).toEqual('dist/foo/*'); }); - it(`should append to existing paths mappings`, () => { + it(`should append to existing paths mappings`, async () => { workspaceTree.overwrite('tsconfig.json', JSON.stringify({ compilerOptions: { paths: { @@ -221,7 +221,7 @@ describe('Library Schematic', () => { }, }, })); - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json'); expect(tsConfigJson.compilerOptions.paths.foo).toBeTruthy(); @@ -229,19 +229,19 @@ describe('Library Schematic', () => { expect(tsConfigJson.compilerOptions.paths.foo[1]).toEqual('dist/foo'); }); - it(`should not modify the file when --skipTsConfig`, () => { - const tree = schematicRunner.runSchematic('library', { + it(`should not modify the file when --skipTsConfig`, async () => { + const tree = await schematicRunner.runSchematicAsync('library', { name: 'foo', skipTsConfig: true, - }, workspaceTree); + }, workspaceTree).toPromise(); const tsConfigJson = getJsonFileContent(tree, 'tsconfig.json'); expect(tsConfigJson.compilerOptions.paths).toBeUndefined(); }); }); - it('should generate inside of a library', () => { - let tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it('should generate inside of a library', async () => { + let tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const componentOptions: ComponentOptions = { name: 'comp', project: 'foo', @@ -250,10 +250,10 @@ describe('Library Schematic', () => { expect(tree.exists('/projects/foo/src/lib/comp/comp.component.ts')).toBe(true); }); - it(`should support creating scoped libraries`, () => { + it(`should support creating scoped libraries`, async () => { const scopedName = '@myscope/mylib'; const options = { ...defaultOptions, name: scopedName }; - const tree = schematicRunner.runSchematic('library', options, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', options, workspaceTree).toPromise(); const pkgJsonPath = '/projects/myscope/mylib/package.json'; expect(tree.files).toContain(pkgJsonPath); @@ -276,12 +276,12 @@ describe('Library Schematic', () => { expect(karmaConf).toContain(`dir: require('path').join(__dirname, '../../../coverage/myscope/mylib')`); }); - it(`should dasherize scoped libraries`, () => { + it(`should dasherize scoped libraries`, async () => { const scopedName = '@myScope/myLib'; const expectedScopeName = '@my-scope/my-lib'; const expectedFolderName = 'my-scope/my-lib'; const options = { ...defaultOptions, name: scopedName }; - const tree = schematicRunner.runSchematic('library', options, workspaceTree); + const tree = await schematicRunner.runSchematicAsync('library', options, workspaceTree).toPromise(); const pkgJsonPath = '/projects/my-scope/my-lib/package.json'; expect(tree.readContent(pkgJsonPath)).toContain(expectedScopeName); @@ -296,8 +296,8 @@ describe('Library Schematic', () => { expect(cfg.projects['@myScope/myLib']).toBeDefined(); }); - it(`should set coverage folder to "coverage/foo"`, () => { - const tree = schematicRunner.runSchematic('library', defaultOptions, workspaceTree); + it(`should set coverage folder to "coverage/foo"`, async () => { + const tree = await schematicRunner.runSchematicAsync('library', defaultOptions, workspaceTree).toPromise(); const karmaConf = getFileContent(tree, '/projects/foo/karma.conf.js'); expect(karmaConf).toContain(`dir: require('path').join(__dirname, '../../coverage/foo')`); }); From 68b196b0da87a185508f82986418112f64aa1cee Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 12 Apr 2019 22:25:57 -0400 Subject: [PATCH 13/14] fix(@angular-devkit/core): allow scoped names for workspace projects Library projects support scoped package names as project names. --- packages/angular_devkit/core/src/workspace/definitions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/core/src/workspace/definitions.ts b/packages/angular_devkit/core/src/workspace/definitions.ts index 4be3180249b5..95f7e21d4b6f 100644 --- a/packages/angular_devkit/core/src/workspace/definitions.ts +++ b/packages/angular_devkit/core/src/workspace/definitions.ts @@ -201,8 +201,8 @@ export class ProjectDefinitionCollection extends DefinitionCollection Date: Fri, 12 Apr 2019 22:54:16 -0400 Subject: [PATCH 14/14] fix(@angular-devkit/core): escape workspace json pointers Internally, the JSON workspace support uses JSON pointers to represent change locations. These need to be escaped to support slashes within a key. --- .../core/src/workspace/json/reader.ts | 12 ++++++-- .../core/src/workspace/json/reader_spec.ts | 4 +-- .../core/src/workspace/json/utilities.ts | 28 +++++++++++++++---- .../core/src/workspace/json/writer.ts | 3 +- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/packages/angular_devkit/core/src/workspace/json/reader.ts b/packages/angular_devkit/core/src/workspace/json/reader.ts index 7f3e954b5e29..e54382c4acef 100644 --- a/packages/angular_devkit/core/src/workspace/json/reader.ts +++ b/packages/angular_devkit/core/src/workspace/json/reader.ts @@ -23,7 +23,7 @@ import { } from '../definitions'; import { WorkspaceHost } from '../host'; import { JsonWorkspaceMetadata, JsonWorkspaceSymbol } from './metadata'; -import { createVirtualAstObject } from './utilities'; +import { createVirtualAstObject, escapeKey } from './utilities'; interface ParserContext { readonly host: WorkspaceHost; @@ -116,7 +116,13 @@ function parseWorkspace(workspaceNode: JsonAstObject, context: ParserContext): W if (context.trackChanges && projectsNode) { const parentNode = projectsNode; collectionListener = (name, action, newValue) => { - jsonMetadata.addChange(action, `/projects/${name}`, parentNode, newValue, 'project'); + jsonMetadata.addChange( + action, + `/projects/${escapeKey(name)}`, + parentNode, + newValue, + 'project', + ); }; } @@ -212,7 +218,7 @@ function parseProject( collectionListener = (name, action, newValue) => { jsonMetadata.addChange( action, - `/projects/${projectName}/targets/${name}`, + `/projects/${projectName}/targets/${escapeKey(name)}`, parentNode, newValue, 'target', diff --git a/packages/angular_devkit/core/src/workspace/json/reader_spec.ts b/packages/angular_devkit/core/src/workspace/json/reader_spec.ts index 584044d334e4..914e7df007d8 100644 --- a/packages/angular_devkit/core/src/workspace/json/reader_spec.ts +++ b/packages/angular_devkit/core/src/workspace/json/reader_spec.ts @@ -302,7 +302,7 @@ describe('JSON WorkspaceDefinition Tracks Workspace Changes', () => { expect(metadata.hasChanges).toBeTruthy(); expect(metadata.changeCount).toBe(2); - change = metadata.findChangesForPath('/schematics/@angular/schematics:component')[0]; + change = metadata.findChangesForPath('/schematics/@angular~1schematics:component')[0]; expect(change).not.toBeUndefined(); if (change) { expect(change.op).toBe('replace'); @@ -350,7 +350,7 @@ describe('JSON WorkspaceDefinition Tracks Workspace Changes', () => { expect(metadata.hasChanges).toBeTruthy(); expect(metadata.changeCount).toBe(2); - change = metadata.findChangesForPath('/schematics/@angular/schematics:component')[0]; + change = metadata.findChangesForPath('/schematics/@angular~1schematics:component')[0]; expect(change).not.toBeUndefined(); if (change) { expect(change.op).toBe('replace'); diff --git a/packages/angular_devkit/core/src/workspace/json/utilities.ts b/packages/angular_devkit/core/src/workspace/json/utilities.ts index b70ad13dfe06..fa6dd14167f8 100644 --- a/packages/angular_devkit/core/src/workspace/json/utilities.ts +++ b/packages/angular_devkit/core/src/workspace/json/utilities.ts @@ -65,6 +65,22 @@ function createPropertyDescriptor(value: JsonValue | undefined): PropertyDescrip }; } +export function escapeKey(key: string | number): string | number { + if (typeof key === 'number') { + return key; + } + + return key.replace('~', '~0').replace('/', '~1'); +} + +export function unescapeKey(key: string | number): string | number { + if (typeof key === 'number') { + return key; + } + + return key.replace('~1', '/').replace('~0', '~'); +} + export function createVirtualAstObject( root: JsonAstObject, options: { @@ -129,7 +145,7 @@ function create( return undefined; } - const propertyPath = path + '/' + p; + const propertyPath = path + '/' + escapeKey(p); const cacheEntry = cache.get(propertyPath); if (cacheEntry) { if (cacheEntry.value) { @@ -153,7 +169,7 @@ function create( return false; } - return cache.has(path + '/' + p) || findNode(ast, p) !== undefined; + return cache.has(path + '/' + escapeKey(p)) || findNode(ast, p) !== undefined; }, get(target: {}, p: PropertyKey): unknown { if (typeof p === 'symbol' || Reflect.has(target, p)) { @@ -162,7 +178,7 @@ function create( return undefined; } - const propertyPath = path + '/' + p; + const propertyPath = path + '/' + escapeKey(p); const cacheEntry = cache.get(propertyPath); if (cacheEntry) { return cacheEntry.value; @@ -200,7 +216,7 @@ function create( // TODO: Check if is JSON value const jsonValue = value as JsonValue; - const propertyPath = path + '/' + p; + const propertyPath = path + '/' + escapeKey(p); const cacheEntry = cache.get(propertyPath); if (cacheEntry) { const oldValue = cacheEntry.value; @@ -227,7 +243,7 @@ function create( return false; } - const propertyPath = path + '/' + p; + const propertyPath = path + '/' + escapeKey(p); const cacheEntry = cache.get(propertyPath); if (cacheEntry) { const oldValue = cacheEntry.value; @@ -267,7 +283,7 @@ function create( for (const key of cache.keys()) { const relativeKey = key.substr(path.length + 1); if (relativeKey.length > 0 && !relativeKey.includes('/')) { - keys.push(relativeKey); + keys.push(unescapeKey(relativeKey)); } } diff --git a/packages/angular_devkit/core/src/workspace/json/writer.ts b/packages/angular_devkit/core/src/workspace/json/writer.ts index 49430e6c6606..2c087178c65c 100644 --- a/packages/angular_devkit/core/src/workspace/json/writer.ts +++ b/packages/angular_devkit/core/src/workspace/json/writer.ts @@ -15,6 +15,7 @@ import { JsonWorkspaceMetadata, JsonWorkspaceSymbol, } from './metadata'; +import { unescapeKey } from './utilities'; export async function writeJsonWorkspace( workspace: WorkspaceDefinition, @@ -211,7 +212,7 @@ function updateJsonWorkspace(metadata: JsonWorkspaceMetadata): string { const multiline = node.start.line !== node.end.line; const pathSegments = path.split('/'); const depth = pathSegments.length - 1; // TODO: more complete analysis - const propertyOrIndex = pathSegments[depth]; + const propertyOrIndex = unescapeKey(pathSegments[depth]); const jsonValue = normalizeValue(value, type); if (op === 'add' && jsonValue === undefined) { continue;