Skip to content

Commit 6d9a06a

Browse files
committed
refactor(@angular-devkit/architect): allow aliasing builder names in package builder manifest
A builder's manifest definition within a package (typically `builders.json`) can now contain a string value that will be used as a builder specifier when loading the named builder. This allows a builder name to be aliased to another builder. The other builder may also be in another package. The build resolution logic has also been updated to remove use of the global `require` to support eventual direct ESM usage.
1 parent 2a5caf1 commit 6d9a06a

File tree

2 files changed

+72
-17
lines changed

2 files changed

+72
-17
lines changed

packages/angular_devkit/architect/node/node-modules-architect-host.ts

+63-16
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@
77
*/
88

99
import { json, workspaces } from '@angular-devkit/core';
10-
import * as path from 'path';
11-
import { URL, pathToFileURL } from 'url';
12-
import { deserialize, serialize } from 'v8';
10+
import { readFileSync } from 'node:fs';
11+
import { createRequire } from 'node:module';
12+
import * as path from 'node:path';
13+
import { pathToFileURL } from 'node:url';
14+
import { deserialize, serialize } from 'node:v8';
1315
import { BuilderInfo } from '../src';
1416
import { Schema as BuilderSchema } from '../src/builders-schema';
1517
import { Target } from '../src/input-schema';
1618
import { ArchitectHost, Builder, BuilderSymbol } from '../src/internal';
1719

20+
// TODO_ESM: Update to use import.meta.url
21+
const localRequire = createRequire(__filename);
22+
1823
export type NodeModulesBuilderInfo = BuilderInfo & {
1924
import: string;
2025
};
@@ -121,41 +126,83 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost<NodeModu
121126
* @param builderStr The name of the builder to be used.
122127
* @returns All the info needed for the builder itself.
123128
*/
124-
resolveBuilder(builderStr: string): Promise<NodeModulesBuilderInfo> {
129+
resolveBuilder(builderStr: string, seenBuilders?: Set<string>): Promise<NodeModulesBuilderInfo> {
130+
if (seenBuilders?.has(builderStr)) {
131+
throw new Error(
132+
'Circular builder alias references detected: ' + [...seenBuilders, builderStr],
133+
);
134+
}
135+
125136
const [packageName, builderName] = builderStr.split(':', 2);
126137
if (!builderName) {
127138
throw new Error('No builder name specified.');
128139
}
129140

130-
const packageJsonPath = require.resolve(packageName + '/package.json', {
141+
// Resolve and load the builders manifest from the package's `builders` field, if present
142+
const packageJsonPath = localRequire.resolve(packageName + '/package.json', {
131143
paths: [this._root],
132144
});
133145

134-
const packageJson = require(packageJsonPath);
135-
if (!packageJson['builders']) {
146+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { builders?: string };
147+
const buildersManifestRawPath = packageJson['builders'];
148+
if (!buildersManifestRawPath) {
136149
throw new Error(`Package ${JSON.stringify(packageName)} has no builders defined.`);
137150
}
138151

139-
const builderJsonPath = path.resolve(path.dirname(packageJsonPath), packageJson['builders']);
140-
const builderJson = require(builderJsonPath) as BuilderSchema;
152+
let buildersManifestPath = path.normalize(buildersManifestRawPath);
153+
if (path.isAbsolute(buildersManifestRawPath) || buildersManifestRawPath.startsWith('..')) {
154+
throw new Error(
155+
`Package "${packageName}" has an invalid builders manifest path: "${buildersManifestRawPath}"`,
156+
);
157+
}
141158

142-
const builder = builderJson.builders && builderJson.builders[builderName];
159+
buildersManifestPath = path.join(path.dirname(packageJsonPath), buildersManifestPath);
160+
const buildersManifest = JSON.parse(
161+
readFileSync(buildersManifestPath, 'utf-8'),
162+
) as BuilderSchema;
163+
const buildersManifestDirectory = path.dirname(buildersManifestPath);
143164

165+
// Attempt to locate an entry for the specified builder by name
166+
const builder = buildersManifest.builders?.[builderName];
144167
if (!builder) {
145168
throw new Error(`Cannot find builder ${JSON.stringify(builderStr)}.`);
146169
}
147170

148-
const importPath = builder.implementation;
149-
if (!importPath) {
171+
// Resolve alias reference if entry is a string
172+
if (typeof builder === 'string') {
173+
return this.resolveBuilder(builder, (seenBuilders ?? new Set()).add(builderStr));
174+
}
175+
176+
// Determine builder implementation path (relative within package only)
177+
const implementationPath = builder.implementation && path.normalize(builder.implementation);
178+
if (!implementationPath) {
150179
throw new Error('Could not find the implementation for builder ' + builderStr);
151180
}
181+
if (path.isAbsolute(implementationPath) || implementationPath.startsWith('..')) {
182+
throw new Error(
183+
`Package "${packageName}" has an invalid builder implementation path: "${builderName}" --> "${builder.implementation}"`,
184+
);
185+
}
186+
187+
// Determine builder option schema path (relative within package only)
188+
const schemaPath = builder.schema && path.normalize(builder.schema);
189+
if (!schemaPath) {
190+
throw new Error('Could not find the schema for builder ' + builderStr);
191+
}
192+
if (path.isAbsolute(schemaPath) || schemaPath.startsWith('..')) {
193+
throw new Error(
194+
`Package "${packageName}" has an invalid builder implementation path: "${builderName}" --> "${builder.schema}"`,
195+
);
196+
}
197+
198+
const schemaText = readFileSync(path.join(buildersManifestDirectory, schemaPath), 'utf-8');
152199

153200
return Promise.resolve({
154201
name: builderStr,
155202
builderName,
156203
description: builder['description'],
157-
optionSchema: require(path.resolve(path.dirname(builderJsonPath), builder.schema)),
158-
import: path.resolve(path.dirname(builderJsonPath), importPath),
204+
optionSchema: JSON.parse(schemaText) as json.schema.JsonSchema,
205+
import: path.join(buildersManifestDirectory, implementationPath),
159206
});
160207
}
161208

@@ -248,12 +295,12 @@ async function getBuilder(builderPath: string): Promise<any> {
248295
// changed to a direct dynamic import.
249296
return (await loadEsmModule<{ default: unknown }>(pathToFileURL(builderPath))).default;
250297
case '.cjs':
251-
return require(builderPath);
298+
return localRequire(builderPath);
252299
default:
253300
// The file could be either CommonJS or ESM.
254301
// CommonJS is tried first then ESM if loading fails.
255302
try {
256-
return require(builderPath);
303+
return localRequire(builderPath);
257304
} catch (e) {
258305
if ((e as NodeJS.ErrnoException).code === 'ERR_REQUIRE_ESM') {
259306
// Load the ESM configuration file using the TypeScript dynamic import workaround.

packages/angular_devkit/architect/src/builders-schema.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@
1111
"builders": {
1212
"type": "object",
1313
"additionalProperties": {
14-
"$ref": "#/definitions/builder"
14+
"oneOf": [
15+
{
16+
"$ref": "#/definitions/builder"
17+
},
18+
{
19+
"type": "string",
20+
"minLength": 1
21+
}
22+
]
1523
}
1624
}
1725
},

0 commit comments

Comments
 (0)