mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-15 18:13:38 +08:00
Prepares the `@angular-devkit/architect` package for the eventual change of enabling the TypeScript `useUnknownInCatchVariables` option. This option provides additional code safety by ensuring that the catch clause variable is the proper type before attempting to access its properties. Similar changes will be needed in the other packages in the repository prior to enabling `useUnknownInCatchVariables`.
259 lines
9.1 KiB
TypeScript
259 lines
9.1 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 { json, workspaces } from '@angular-devkit/core';
|
|
import * as path from 'path';
|
|
import { URL, pathToFileURL } from 'url';
|
|
import { deserialize, serialize } from 'v8';
|
|
import { BuilderInfo } from '../src';
|
|
import { Schema as BuilderSchema } from '../src/builders-schema';
|
|
import { Target } from '../src/input-schema';
|
|
import { ArchitectHost, Builder, BuilderSymbol } from '../src/internal';
|
|
|
|
export type NodeModulesBuilderInfo = BuilderInfo & {
|
|
import: string;
|
|
};
|
|
|
|
function clone(obj: unknown): unknown {
|
|
try {
|
|
return deserialize(serialize(obj));
|
|
} catch {
|
|
return JSON.parse(JSON.stringify(obj));
|
|
}
|
|
}
|
|
|
|
export interface WorkspaceHost {
|
|
getBuilderName(project: string, target: string): Promise<string>;
|
|
getMetadata(project: string): Promise<json.JsonObject>;
|
|
getOptions(project: string, target: string, configuration?: string): Promise<json.JsonObject>;
|
|
hasTarget(project: string, target: string): Promise<boolean>;
|
|
getDefaultConfigurationName(project: string, target: string): Promise<string | undefined>;
|
|
}
|
|
|
|
function findProjectTarget(
|
|
workspace: workspaces.WorkspaceDefinition,
|
|
project: string,
|
|
target: string,
|
|
): workspaces.TargetDefinition {
|
|
const projectDefinition = workspace.projects.get(project);
|
|
if (!projectDefinition) {
|
|
throw new Error(`Project "${project}" does not exist.`);
|
|
}
|
|
|
|
const targetDefinition = projectDefinition.targets.get(target);
|
|
if (!targetDefinition) {
|
|
throw new Error('Project target does not exist.');
|
|
}
|
|
|
|
return targetDefinition;
|
|
}
|
|
|
|
export class WorkspaceNodeModulesArchitectHost implements ArchitectHost<NodeModulesBuilderInfo> {
|
|
private workspaceHost: WorkspaceHost;
|
|
|
|
constructor(workspaceHost: WorkspaceHost, _root: string);
|
|
|
|
constructor(workspace: workspaces.WorkspaceDefinition, _root: string);
|
|
|
|
constructor(
|
|
workspaceOrHost: workspaces.WorkspaceDefinition | WorkspaceHost,
|
|
protected _root: string,
|
|
) {
|
|
if ('getBuilderName' in workspaceOrHost) {
|
|
this.workspaceHost = workspaceOrHost;
|
|
} else {
|
|
this.workspaceHost = {
|
|
async getBuilderName(project, target) {
|
|
const targetDefinition = findProjectTarget(workspaceOrHost, project, target);
|
|
|
|
return targetDefinition.builder;
|
|
},
|
|
async getOptions(project, target, configuration) {
|
|
const targetDefinition = findProjectTarget(workspaceOrHost, project, target);
|
|
|
|
if (configuration === undefined) {
|
|
return (targetDefinition.options ?? {}) as json.JsonObject;
|
|
}
|
|
|
|
if (!targetDefinition.configurations?.[configuration]) {
|
|
throw new Error(`Configuration '${configuration}' is not set in the workspace.`);
|
|
}
|
|
|
|
return (targetDefinition.configurations?.[configuration] ?? {}) as json.JsonObject;
|
|
},
|
|
async getMetadata(project) {
|
|
const projectDefinition = workspaceOrHost.projects.get(project);
|
|
if (!projectDefinition) {
|
|
throw new Error(`Project "${project}" does not exist.`);
|
|
}
|
|
|
|
return {
|
|
root: projectDefinition.root,
|
|
sourceRoot: projectDefinition.sourceRoot,
|
|
prefix: projectDefinition.prefix,
|
|
...(clone(workspaceOrHost.extensions) as {}),
|
|
...(clone(projectDefinition.extensions) as {}),
|
|
} as unknown as json.JsonObject;
|
|
},
|
|
async hasTarget(project, target) {
|
|
return !!workspaceOrHost.projects.get(project)?.targets.has(target);
|
|
},
|
|
async getDefaultConfigurationName(project, target) {
|
|
return workspaceOrHost.projects.get(project)?.targets.get(target)?.defaultConfiguration;
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
async getBuilderNameForTarget(target: Target) {
|
|
return this.workspaceHost.getBuilderName(target.project, target.target);
|
|
}
|
|
|
|
/**
|
|
* Resolve a builder. This needs to be a string which will be used in a dynamic `import()`
|
|
* clause. This should throw if no builder can be found. The dynamic import will throw if
|
|
* it is unsupported.
|
|
* @param builderStr The name of the builder to be used.
|
|
* @returns All the info needed for the builder itself.
|
|
*/
|
|
resolveBuilder(builderStr: string): Promise<NodeModulesBuilderInfo> {
|
|
const [packageName, builderName] = builderStr.split(':', 2);
|
|
if (!builderName) {
|
|
throw new Error('No builder name specified.');
|
|
}
|
|
|
|
const packageJsonPath = require.resolve(packageName + '/package.json', {
|
|
paths: [this._root],
|
|
});
|
|
|
|
const packageJson = require(packageJsonPath);
|
|
if (!packageJson['builders']) {
|
|
throw new Error(`Package ${JSON.stringify(packageName)} has no builders defined.`);
|
|
}
|
|
|
|
const builderJsonPath = path.resolve(path.dirname(packageJsonPath), packageJson['builders']);
|
|
const builderJson = require(builderJsonPath) as BuilderSchema;
|
|
|
|
const builder = builderJson.builders && builderJson.builders[builderName];
|
|
|
|
if (!builder) {
|
|
throw new Error(`Cannot find builder ${JSON.stringify(builderStr)}.`);
|
|
}
|
|
|
|
const importPath = builder.implementation;
|
|
if (!importPath) {
|
|
throw new Error('Could not find the implementation for builder ' + builderStr);
|
|
}
|
|
|
|
return Promise.resolve({
|
|
name: builderStr,
|
|
builderName,
|
|
description: builder['description'],
|
|
optionSchema: require(path.resolve(path.dirname(builderJsonPath), builder.schema)),
|
|
import: path.resolve(path.dirname(builderJsonPath), importPath),
|
|
});
|
|
}
|
|
|
|
async getCurrentDirectory() {
|
|
return process.cwd();
|
|
}
|
|
|
|
async getWorkspaceRoot() {
|
|
return this._root;
|
|
}
|
|
|
|
async getOptionsForTarget(target: Target): Promise<json.JsonObject | null> {
|
|
if (!(await this.workspaceHost.hasTarget(target.project, target.target))) {
|
|
return null;
|
|
}
|
|
|
|
let options = await this.workspaceHost.getOptions(target.project, target.target);
|
|
const targetConfiguration =
|
|
target.configuration ||
|
|
(await this.workspaceHost.getDefaultConfigurationName(target.project, target.target));
|
|
|
|
if (targetConfiguration) {
|
|
const configurations = targetConfiguration.split(',').map((c) => c.trim());
|
|
for (const configuration of configurations) {
|
|
options = {
|
|
...options,
|
|
...(await this.workspaceHost.getOptions(target.project, target.target, configuration)),
|
|
};
|
|
}
|
|
}
|
|
|
|
return clone(options) as json.JsonObject;
|
|
}
|
|
|
|
async getProjectMetadata(target: Target | string): Promise<json.JsonObject | null> {
|
|
const projectName = typeof target === 'string' ? target : target.project;
|
|
const metadata = this.workspaceHost.getMetadata(projectName);
|
|
|
|
return metadata;
|
|
}
|
|
|
|
async loadBuilder(info: NodeModulesBuilderInfo): Promise<Builder> {
|
|
const builder = await getBuilder(info.import);
|
|
|
|
if (builder[BuilderSymbol]) {
|
|
return builder;
|
|
}
|
|
|
|
// Default handling code is for old builders that incorrectly export `default` with non-ESM module
|
|
if (builder?.default[BuilderSymbol]) {
|
|
return builder.default;
|
|
}
|
|
|
|
throw new Error('Builder is not a builder');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This uses a dynamic import to load a module which may be ESM.
|
|
* CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
|
|
* will currently, unconditionally downlevel dynamic import into a require call.
|
|
* require calls cannot load ESM code and will result in a runtime error. To workaround
|
|
* this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
|
|
* Once TypeScript provides support for keeping the dynamic import this workaround can
|
|
* be dropped.
|
|
*
|
|
* @param modulePath The path of the module to load.
|
|
* @returns A Promise that resolves to the dynamically imported module.
|
|
*/
|
|
function loadEsmModule<T>(modulePath: string | URL): Promise<T> {
|
|
return new Function('modulePath', `return import(modulePath);`)(modulePath) as Promise<T>;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async function getBuilder(builderPath: string): Promise<any> {
|
|
switch (path.extname(builderPath)) {
|
|
case '.mjs':
|
|
// Load the ESM configuration file using the TypeScript dynamic import workaround.
|
|
// Once TypeScript provides support for keeping the dynamic import this workaround can be
|
|
// changed to a direct dynamic import.
|
|
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(builderPath))).default;
|
|
case '.cjs':
|
|
return require(builderPath);
|
|
default:
|
|
// The file could be either CommonJS or ESM.
|
|
// CommonJS is tried first then ESM if loading fails.
|
|
try {
|
|
return require(builderPath);
|
|
} catch (e) {
|
|
if ((e as NodeJS.ErrnoException).code === 'ERR_REQUIRE_ESM') {
|
|
// Load the ESM configuration file using the TypeScript dynamic import workaround.
|
|
// Once TypeScript provides support for keeping the dynamic import this workaround can be
|
|
// changed to a direct dynamic import.
|
|
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(builderPath))).default;
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
}
|
|
}
|