|
7 | 7 | */
|
8 | 8 |
|
9 | 9 | 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'; |
13 | 15 | import { BuilderInfo } from '../src';
|
14 | 16 | import { Schema as BuilderSchema } from '../src/builders-schema';
|
15 | 17 | import { Target } from '../src/input-schema';
|
16 | 18 | import { ArchitectHost, Builder, BuilderSymbol } from '../src/internal';
|
17 | 19 |
|
| 20 | +// TODO_ESM: Update to use import.meta.url |
| 21 | +const localRequire = createRequire(__filename); |
| 22 | + |
18 | 23 | export type NodeModulesBuilderInfo = BuilderInfo & {
|
19 | 24 | import: string;
|
20 | 25 | };
|
@@ -121,41 +126,83 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost<NodeModu
|
121 | 126 | * @param builderStr The name of the builder to be used.
|
122 | 127 | * @returns All the info needed for the builder itself.
|
123 | 128 | */
|
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 | + |
125 | 136 | const [packageName, builderName] = builderStr.split(':', 2);
|
126 | 137 | if (!builderName) {
|
127 | 138 | throw new Error('No builder name specified.');
|
128 | 139 | }
|
129 | 140 |
|
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', { |
131 | 143 | paths: [this._root],
|
132 | 144 | });
|
133 | 145 |
|
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) { |
136 | 149 | throw new Error(`Package ${JSON.stringify(packageName)} has no builders defined.`);
|
137 | 150 | }
|
138 | 151 |
|
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 | + } |
141 | 158 |
|
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); |
143 | 164 |
|
| 165 | + // Attempt to locate an entry for the specified builder by name |
| 166 | + const builder = buildersManifest.builders?.[builderName]; |
144 | 167 | if (!builder) {
|
145 | 168 | throw new Error(`Cannot find builder ${JSON.stringify(builderStr)}.`);
|
146 | 169 | }
|
147 | 170 |
|
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) { |
150 | 179 | throw new Error('Could not find the implementation for builder ' + builderStr);
|
151 | 180 | }
|
| 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'); |
152 | 199 |
|
153 | 200 | return Promise.resolve({
|
154 | 201 | name: builderStr,
|
155 | 202 | builderName,
|
156 | 203 | 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), |
159 | 206 | });
|
160 | 207 | }
|
161 | 208 |
|
@@ -248,12 +295,12 @@ async function getBuilder(builderPath: string): Promise<any> {
|
248 | 295 | // changed to a direct dynamic import.
|
249 | 296 | return (await loadEsmModule<{ default: unknown }>(pathToFileURL(builderPath))).default;
|
250 | 297 | case '.cjs':
|
251 |
| - return require(builderPath); |
| 298 | + return localRequire(builderPath); |
252 | 299 | default:
|
253 | 300 | // The file could be either CommonJS or ESM.
|
254 | 301 | // CommonJS is tried first then ESM if loading fails.
|
255 | 302 | try {
|
256 |
| - return require(builderPath); |
| 303 | + return localRequire(builderPath); |
257 | 304 | } catch (e) {
|
258 | 305 | if ((e as NodeJS.ErrnoException).code === 'ERR_REQUIRE_ESM') {
|
259 | 306 | // Load the ESM configuration file using the TypeScript dynamic import workaround.
|
|
0 commit comments