Charles Lyding b07ccfbb1b 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.
2022-05-03 14:04:47 -07:00

175 lines
5.8 KiB
TypeScript

/**
* @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 { join, normalize, tags } from '@angular-devkit/core';
import {
Rule,
SchematicContext,
SchematicsException,
Tree,
apply,
applyTemplates,
chain,
mergeWith,
move,
url,
} 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,
insertImport,
isImported,
} from '../utility/ast-utils';
import { applyToUpdateRecorder } from '../utility/change';
import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies';
import { getAppModulePath } from '../utility/ng-ast-utils';
import { relativePathToWorkspaceRoot } from '../utility/paths';
import { targetBuildNotFoundError } from '../utility/project-targets';
import { BrowserBuilderOptions } from '../utility/workspace-models';
import { Schema as ServiceWorkerOptions } from './schema';
function addDependencies(): Rule {
return (host: Tree, context: SchematicContext) => {
const packageName = '@angular/service-worker';
context.logger.debug(`adding dependency (${packageName})`);
const coreDep = getPackageJsonDependency(host, '@angular/core');
if (coreDep === null) {
throw new SchematicsException('Could not find version.');
}
const serviceWorkerDep = {
...coreDep,
name: packageName,
};
addPackageJsonDependency(host, serviceWorkerDep);
return host;
};
}
function updateAppModule(mainPath: string): Rule {
return (host: Tree, context: SchematicContext) => {
context.logger.debug('Updating appmodule');
const modulePath = getAppModulePath(host, mainPath);
context.logger.debug(`module path: ${modulePath}`);
// add import
let moduleSource = getTsSourceFile(host, modulePath);
let importModule = 'ServiceWorkerModule';
let importPath = '@angular/service-worker';
if (!isImported(moduleSource, importModule, importPath)) {
const change = insertImport(moduleSource, modulePath, importModule, importPath);
if (change) {
const recorder = host.beginUpdate(modulePath);
applyToUpdateRecorder(recorder, [change]);
host.commitUpdate(recorder);
}
}
// add import for environments
// import { environment } from '../environments/environment';
moduleSource = getTsSourceFile(host, modulePath);
const environmentExportName = getEnvironmentExportName(moduleSource);
// if environemnt import already exists then use the found one
// otherwise use the default name
importModule = environmentExportName || 'environment';
// TODO: dynamically find environments relative path
importPath = '../environments/environment';
if (!environmentExportName) {
// if environment import was not found then insert the new one
// with default path and default export name
const change = insertImport(moduleSource, modulePath, importModule, importPath);
if (change) {
const recorder = host.beginUpdate(modulePath);
applyToUpdateRecorder(recorder, [change]);
host.commitUpdate(recorder);
}
}
// register SW in application module
const importText = tags.stripIndent`
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: ${importModule}.production,
// Register the ServiceWorker as soon as the application is stable
// or after 30 seconds (whichever comes first).
registrationStrategy: 'registerWhenStable:30000'
})
`;
moduleSource = getTsSourceFile(host, modulePath);
const metadataChanges = addSymbolToNgModuleMetadata(
moduleSource,
modulePath,
'imports',
importText,
);
if (metadataChanges) {
const recorder = host.beginUpdate(modulePath);
applyToUpdateRecorder(recorder, metadataChanges);
host.commitUpdate(recorder);
}
return host;
};
}
function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
const content = host.readText(path);
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
return source;
}
export default function (options: ServiceWorkerOptions): Rule {
return async (host: Tree, context: SchematicContext) => {
const workspace = await readWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
throw new SchematicsException(`Invalid project name (${options.project})`);
}
if (project.extensions.projectType !== 'application') {
throw new SchematicsException(`Service worker requires a project type of "application".`);
}
const buildTarget = project.targets.get('build');
if (!buildTarget) {
throw targetBuildNotFoundError();
}
const buildOptions = (buildTarget.options || {}) as unknown as BrowserBuilderOptions;
const root = project.root;
buildOptions.serviceWorker = true;
buildOptions.ngswConfigPath = join(normalize(root), 'ngsw-config.json');
let { resourcesOutputPath = '' } = buildOptions;
if (resourcesOutputPath) {
resourcesOutputPath = normalize(`/${resourcesOutputPath}`);
}
const templateSource = apply(url('./files'), [
applyTemplates({
...options,
resourcesOutputPath,
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(project.root),
}),
move(project.root),
]);
context.addTask(new NodePackageInstallTask());
await writeWorkspace(host, workspace);
return chain([
mergeWith(templateSource),
addDependencies(),
updateAppModule(buildOptions.main),
]);
};
}