mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-16 18:43:42 +08:00
feat(@schematics/angular): introduce a utility subpath export for Angular rules and utilities
The `@schematics/angular` package now contains a defined set of package `exports` including a `utility` subpath export. A wildcard export is also temporarily defined to support transition away from existing deep-import usage. The `@schematics/angular/utility` subpath export will contain supported utility methods used by the first-party schematics contained within the `@schematics/angular` package and can be considered public API that will follow SemVer stability constraints. The first group of utilities introduced in this change are used to modify the `angular.json` workspace file within the schematics and include the `updateWorkspace` rule and `readWorkspace`/`writeWorkspace` helpers.
This commit is contained in:
parent
3fa38b08ba
commit
b07ccfbb1b
@ -19,11 +19,11 @@ import {
|
||||
strings,
|
||||
url,
|
||||
} from '@angular-devkit/schematics';
|
||||
import { readWorkspace, writeWorkspace } from '../utility';
|
||||
import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies';
|
||||
import { JSONFile } from '../utility/json-file';
|
||||
import { latestVersions } from '../utility/latest-versions';
|
||||
import { relativePathToWorkspaceRoot } from '../utility/paths';
|
||||
import { getWorkspace, updateWorkspace } from '../utility/workspace';
|
||||
import { Builders } from '../utility/workspace-models';
|
||||
import { Schema as E2eOptions } from './schema';
|
||||
|
||||
@ -41,7 +41,7 @@ function addScriptsToPackageJson(): Rule {
|
||||
export default function (options: E2eOptions): Rule {
|
||||
return async (host: Tree) => {
|
||||
const appProject = options.relatedAppName;
|
||||
const workspace = await getWorkspace(host);
|
||||
const workspace = await readWorkspace(host);
|
||||
const project = workspace.projects.get(appProject);
|
||||
if (!project) {
|
||||
throw new SchematicsException(`Project name "${appProject}" doesn't not exist.`);
|
||||
@ -66,8 +66,9 @@ export default function (options: E2eOptions): Rule {
|
||||
},
|
||||
});
|
||||
|
||||
await writeWorkspace(host, workspace);
|
||||
|
||||
return chain([
|
||||
updateWorkspace(workspace),
|
||||
mergeWith(
|
||||
apply(url('./files'), [
|
||||
applyTemplates({
|
||||
|
@ -8,6 +8,13 @@
|
||||
"code generation",
|
||||
"schematics"
|
||||
],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
"./utility": "./utility/index.js",
|
||||
"./utility/*": "./utility/*.js",
|
||||
"./migrations/migration-collection.json": "./migrations/migration-collection.json",
|
||||
"./*": "./*.js"
|
||||
},
|
||||
"schematics": "./collection.json",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "0.0.0-PLACEHOLDER",
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from '@angular-devkit/schematics';
|
||||
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
|
||||
import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
|
||||
import { readWorkspace, writeWorkspace } from '../utility';
|
||||
import {
|
||||
addSymbolToNgModuleMetadata,
|
||||
getEnvironmentExportName,
|
||||
@ -32,7 +33,6 @@ import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/d
|
||||
import { getAppModulePath } from '../utility/ng-ast-utils';
|
||||
import { relativePathToWorkspaceRoot } from '../utility/paths';
|
||||
import { targetBuildNotFoundError } from '../utility/project-targets';
|
||||
import { getWorkspace, updateWorkspace } from '../utility/workspace';
|
||||
import { BrowserBuilderOptions } from '../utility/workspace-models';
|
||||
import { Schema as ServiceWorkerOptions } from './schema';
|
||||
|
||||
@ -130,7 +130,7 @@ function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
|
||||
|
||||
export default function (options: ServiceWorkerOptions): Rule {
|
||||
return async (host: Tree, context: SchematicContext) => {
|
||||
const workspace = await getWorkspace(host);
|
||||
const workspace = await readWorkspace(host);
|
||||
const project = workspace.projects.get(options.project);
|
||||
if (!project) {
|
||||
throw new SchematicsException(`Invalid project name (${options.project})`);
|
||||
@ -163,9 +163,10 @@ export default function (options: ServiceWorkerOptions): Rule {
|
||||
|
||||
context.addTask(new NodePackageInstallTask());
|
||||
|
||||
await writeWorkspace(host, workspace);
|
||||
|
||||
return chain([
|
||||
mergeWith(templateSource),
|
||||
updateWorkspace(workspace),
|
||||
addDependencies(),
|
||||
updateAppModule(buildOptions.main),
|
||||
]);
|
||||
|
21
packages/schematics/angular/utility/index.ts
Normal file
21
packages/schematics/angular/utility/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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
|
||||
*/
|
||||
|
||||
// Workspace related rules and types
|
||||
export {
|
||||
ProjectDefinition,
|
||||
TargetDefinition,
|
||||
WorkspaceDefinition,
|
||||
getWorkspace as readWorkspace,
|
||||
updateWorkspace,
|
||||
writeWorkspace,
|
||||
} from './workspace';
|
||||
export { Builders as AngularBuilder } from './workspace-models';
|
||||
|
||||
// Package dependency related rules and types
|
||||
export { DependencyType, addDependency } from './dependency';
|
@ -11,6 +11,12 @@ export enum ProjectType {
|
||||
Library = 'library',
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum of the official Angular builders.
|
||||
* Each enum value provides the fully qualified name of the associated builder.
|
||||
* This enum can be used when analyzing the `builder` fields of project configurations from the
|
||||
* `angular.json` workspace file.
|
||||
*/
|
||||
export enum Builders {
|
||||
AppShell = '@angular-devkit/build-angular:app-shell',
|
||||
Server = '@angular-devkit/build-angular:server',
|
||||
|
@ -10,60 +10,109 @@ import { json, workspaces } from '@angular-devkit/core';
|
||||
import { Rule, Tree, noop } from '@angular-devkit/schematics';
|
||||
import { ProjectType } from './workspace-models';
|
||||
|
||||
function createHost(tree: Tree): workspaces.WorkspaceHost {
|
||||
return {
|
||||
async readFile(path: string): Promise<string> {
|
||||
return tree.readText(path);
|
||||
},
|
||||
async writeFile(path: string, data: string): Promise<void> {
|
||||
return tree.overwrite(path, data);
|
||||
},
|
||||
async isDirectory(path: string): Promise<boolean> {
|
||||
// approximate a directory check
|
||||
return !tree.exists(path) && tree.getDir(path).subfiles.length > 0;
|
||||
},
|
||||
async isFile(path: string): Promise<boolean> {
|
||||
return tree.exists(path);
|
||||
},
|
||||
};
|
||||
const DEFAULT_WORKSPACE_PATH = '/angular.json';
|
||||
|
||||
// re-export the workspace definition types for convenience
|
||||
export type WorkspaceDefinition = workspaces.WorkspaceDefinition;
|
||||
export type ProjectDefinition = workspaces.ProjectDefinition;
|
||||
export type TargetDefinition = workspaces.TargetDefinition;
|
||||
|
||||
/**
|
||||
* A {@link workspaces.WorkspaceHost} backed by a Schematics {@link Tree} instance.
|
||||
*/
|
||||
class TreeWorkspaceHost implements workspaces.WorkspaceHost {
|
||||
constructor(private readonly tree: Tree) {}
|
||||
|
||||
async readFile(path: string): Promise<string> {
|
||||
return this.tree.readText(path);
|
||||
}
|
||||
|
||||
async writeFile(path: string, data: string): Promise<void> {
|
||||
if (this.tree.exists(path)) {
|
||||
this.tree.overwrite(path, data);
|
||||
} else {
|
||||
this.tree.create(path, data);
|
||||
}
|
||||
}
|
||||
|
||||
async isDirectory(path: string): Promise<boolean> {
|
||||
// approximate a directory check
|
||||
return !this.tree.exists(path) && this.tree.getDir(path).subfiles.length > 0;
|
||||
}
|
||||
|
||||
async isFile(path: string): Promise<boolean> {
|
||||
return this.tree.exists(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the workspace file (`angular.json`) found within the root of the schematic's tree.
|
||||
* The workspace object model can be directly modified within the provided updater function
|
||||
* with changes being written to the workspace file after the updater function returns.
|
||||
* The spacing and overall layout of the file (including comments) will be maintained where
|
||||
* possible when updating the file.
|
||||
*
|
||||
* @param updater An update function that can be used to modify the object model for the
|
||||
* workspace. A {@link WorkspaceDefinition} is provided as the first argument to the function.
|
||||
*/
|
||||
export function updateWorkspace(
|
||||
updater: (workspace: workspaces.WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>,
|
||||
): Rule;
|
||||
export function updateWorkspace(workspace: workspaces.WorkspaceDefinition): Rule;
|
||||
export function updateWorkspace(
|
||||
updaterOrWorkspace:
|
||||
| workspaces.WorkspaceDefinition
|
||||
| ((workspace: workspaces.WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>),
|
||||
updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>,
|
||||
): Rule {
|
||||
return async (tree: Tree) => {
|
||||
const host = createHost(tree);
|
||||
const host = new TreeWorkspaceHost(tree);
|
||||
|
||||
if (typeof updaterOrWorkspace === 'function') {
|
||||
const { workspace } = await workspaces.readWorkspace('/', host);
|
||||
const { workspace } = await workspaces.readWorkspace(DEFAULT_WORKSPACE_PATH, host);
|
||||
|
||||
const result = await updaterOrWorkspace(workspace);
|
||||
const result = await updater(workspace);
|
||||
|
||||
await workspaces.writeWorkspace(workspace, host);
|
||||
await workspaces.writeWorkspace(workspace, host);
|
||||
|
||||
return result || noop;
|
||||
} else {
|
||||
await workspaces.writeWorkspace(updaterOrWorkspace, host);
|
||||
|
||||
return noop;
|
||||
}
|
||||
return result || noop;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWorkspace(tree: Tree, path = '/') {
|
||||
const host = createHost(tree);
|
||||
// TODO: This should be renamed `readWorkspace` once deep imports are restricted (already exported from `utility` with that name)
|
||||
/**
|
||||
* Reads a workspace file (`angular.json`) from the provided {@link Tree} instance.
|
||||
*
|
||||
* @param tree A schematics {@link Tree} instance used to access the workspace file.
|
||||
* @param path The path where a workspace file should be found. If a file is specified, the file
|
||||
* path will be used. If a directory is specified, the file `angular.json` will be used from
|
||||
* within the specified directory. Defaults to `/angular.json`.
|
||||
* @returns A {@link WorkspaceDefinition} representing the workspace found at the specified path.
|
||||
*/
|
||||
export async function getWorkspace(
|
||||
tree: Tree,
|
||||
path = DEFAULT_WORKSPACE_PATH,
|
||||
): Promise<WorkspaceDefinition> {
|
||||
const host = new TreeWorkspaceHost(tree);
|
||||
|
||||
const { workspace } = await workspaces.readWorkspace(path, host);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a workspace file (`angular.json`) to the provided {@link Tree} instance.
|
||||
* The spacing and overall layout of an exisitng file (including comments) will be maintained where
|
||||
* possible when writing the file.
|
||||
*
|
||||
* @param tree A schematics {@link Tree} instance used to access the workspace file.
|
||||
* @param workspace The {@link WorkspaceDefinition} to write.
|
||||
* @param path The path where a workspace file should be written. If a file is specified, the file
|
||||
* path will be used. If not provided, the definition's underlying file path stored during reading
|
||||
* will be used.
|
||||
*/
|
||||
export async function writeWorkspace(
|
||||
tree: Tree,
|
||||
workspace: WorkspaceDefinition,
|
||||
path?: string,
|
||||
): Promise<void> {
|
||||
const host = new TreeWorkspaceHost(tree);
|
||||
|
||||
return workspaces.writeWorkspace(workspace, host, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a default project path for generating.
|
||||
* @param project The project which will have its default path generated.
|
||||
|
126
packages/schematics/angular/utility/workspace_spec.ts
Normal file
126
packages/schematics/angular/utility/workspace_spec.ts
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 { EmptyTree, Rule, SchematicContext, Tree, callRule } from '@angular-devkit/schematics';
|
||||
import { getWorkspace as readWorkspace, updateWorkspace, writeWorkspace } from './workspace';
|
||||
|
||||
const TEST_WORKSPACE_CONTENT = JSON.stringify({
|
||||
version: 1,
|
||||
projects: {
|
||||
'test': {},
|
||||
},
|
||||
});
|
||||
|
||||
async function testRule(rule: Rule, tree: Tree): Promise<void> {
|
||||
await callRule(rule, tree, {} as unknown as SchematicContext).toPromise();
|
||||
}
|
||||
|
||||
describe('readWorkspace', () => {
|
||||
it('reads a workspace using the default path value', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
|
||||
const workspace = await readWorkspace(tree);
|
||||
expect(workspace.projects.has('test')).toBeTrue();
|
||||
});
|
||||
|
||||
it('reads a workspace when specifying a directory path', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/xyz/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
|
||||
const workspace = await readWorkspace(tree, '/xyz/');
|
||||
expect(workspace.projects.has('test')).toBeTrue();
|
||||
});
|
||||
|
||||
it('reads a workspace when specifying a file path', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/xyz/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
|
||||
const workspace = await readWorkspace(tree, '/xyz/angular.json');
|
||||
expect(workspace.projects.has('test')).toBeTrue();
|
||||
});
|
||||
|
||||
it('throws if workspace file does not exist when using the default path value', async () => {
|
||||
const tree = new EmptyTree();
|
||||
|
||||
await expectAsync(readWorkspace(tree)).toBeRejectedWithError();
|
||||
});
|
||||
|
||||
it('throws if workspace file does not exist when specifying a file path', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
|
||||
await expectAsync(readWorkspace(tree, 'abc.json')).toBeRejectedWithError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeWorkspace', () => {
|
||||
it('writes a workspace using the default path value', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
const workspace = await readWorkspace(tree);
|
||||
|
||||
workspace.extensions['x-abc'] = 1;
|
||||
await writeWorkspace(tree, workspace);
|
||||
expect(tree.readJson('/angular.json')).toEqual(jasmine.objectContaining({ 'x-abc': 1 }));
|
||||
});
|
||||
|
||||
it('writes a workspace when specifying a path', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
const workspace = await readWorkspace(tree);
|
||||
|
||||
workspace.extensions['x-abc'] = 1;
|
||||
await writeWorkspace(tree, workspace, '/xyz/angular.json');
|
||||
expect(tree.readJson('/xyz/angular.json')).toEqual(jasmine.objectContaining({ 'x-abc': 1 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateWorkspace', () => {
|
||||
it('updates a workspace using the default path value', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
|
||||
const rule = updateWorkspace((workspace) => {
|
||||
workspace.projects.add({
|
||||
name: 'abc',
|
||||
root: 'src',
|
||||
});
|
||||
});
|
||||
|
||||
await testRule(rule, tree);
|
||||
|
||||
expect(tree.read('angular.json')?.toString()).toContain('"abc"');
|
||||
});
|
||||
|
||||
it('throws if workspace file does not exist', async () => {
|
||||
const tree = new EmptyTree();
|
||||
|
||||
const rule = updateWorkspace((workspace) => {
|
||||
workspace.projects.add({
|
||||
name: 'abc',
|
||||
root: 'src',
|
||||
});
|
||||
});
|
||||
|
||||
await expectAsync(testRule(rule, tree)).toBeRejectedWithError();
|
||||
});
|
||||
|
||||
it('allows executing a returned followup rule', async () => {
|
||||
const tree = new EmptyTree();
|
||||
tree.create('/angular.json', TEST_WORKSPACE_CONTENT);
|
||||
|
||||
const rule = updateWorkspace(() => {
|
||||
return (tree) => tree.create('/followup.txt', '12345');
|
||||
});
|
||||
|
||||
await testRule(rule, tree);
|
||||
|
||||
expect(tree.read('/followup.txt')?.toString()).toContain('12345');
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user