Skip to content

Commit ada40e6

Browse files
committed
refactor: add absolute entry point support to browser-esbuild
This allows `browser-esbuild` to consume absolute file paths and `entryPoints`. Absolute paths will always output in the root of the output directory with the same basename, since they are not within the workspace root and cannot exist at any guaranteed unique relative path. No attempt is made to check if the absolute path is actually within the workspace root, since this would require a call to `fs.realpath()` and make this logic dependent on the actual file system structure which introduces a lot of complexity we'd rather avoid. Longer term, the ideal approach is probably to leverage virtual files in some capacity, but this should be sufficient for now. `main` functionality is left alone, and absolute paths like `/main.ts` are treated as relative to the workspace root. This is to preserve existing functionality and discourage public API usage of file paths outside the workspace. (cherry picked from commit e22352b)
1 parent 1b2f902 commit ada40e6

File tree

3 files changed

+98
-11
lines changed

3 files changed

+98
-11
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,36 @@ function normalizeEntryPoints(
252252
return { 'main': path.join(workspaceRoot, main) };
253253
} else {
254254
// Use `entryPoints` alone.
255-
return Object.fromEntries(
256-
Array.from(entryPoints).map((entryPoint) => {
257-
const parsedEntryPoint = path.parse(entryPoint);
255+
const entryPointPaths: Record<string, string> = {};
256+
for (const entryPoint of entryPoints) {
257+
const parsedEntryPoint = path.parse(entryPoint);
258258

259-
return [
260-
// File path without extension.
261-
path.join(parsedEntryPoint.dir, parsedEntryPoint.name),
262-
// Full file path.
263-
path.join(workspaceRoot, entryPoint),
264-
];
265-
}),
266-
);
259+
// Use the input file path without an extension as the "name" of the entry point dictating its output location.
260+
// Relative entry points are generated at the same relative path in the output directory.
261+
// Absolute entry points are always generated with the same file name in the root of the output directory. This includes absolute
262+
// paths pointing at files actually within the workspace root.
263+
const entryPointName = path.isAbsolute(entryPoint)
264+
? parsedEntryPoint.name
265+
: path.join(parsedEntryPoint.dir, parsedEntryPoint.name);
266+
267+
// Get the full file path to the entry point input.
268+
const entryPointPath = path.isAbsolute(entryPoint)
269+
? entryPoint
270+
: path.join(workspaceRoot, entryPoint);
271+
272+
// Check for conflicts with previous entry points.
273+
const existingEntryPointPath = entryPointPaths[entryPointName];
274+
if (existingEntryPointPath) {
275+
throw new Error(
276+
`\`${existingEntryPointPath}\` and \`${entryPointPath}\` both output to the same location \`${entryPointName}\`.` +
277+
' Rename or move one of the files to fix the conflict.',
278+
);
279+
}
280+
281+
entryPointPaths[entryPointName] = entryPointPath;
282+
}
283+
284+
return entryPointPaths;
267285
}
268286
}
269287

packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/entry-points_spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,23 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import { promises as fs } from 'fs';
10+
import { tmpdir } from 'os';
11+
import * as path from 'path';
912
import { buildEsbuildBrowserInternal } from '../../index';
1013
import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
1114

1215
describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) => {
16+
let tempDir!: string;
17+
18+
beforeEach(async () => {
19+
tempDir = await fs.mkdtemp(path.join(tmpdir(), 'angular-cli-e2e-browser-esbuild-main-spec-'));
20+
});
21+
22+
afterEach(async () => {
23+
await fs.rm(tempDir, { recursive: true });
24+
});
25+
1326
describe('Option: "entryPoints"', () => {
1427
it('provides multiple entry points', async () => {
1528
await harness.writeFiles({
@@ -62,5 +75,46 @@ describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) =>
6275

6376
expect(error?.message).toContain('Only one of `main` or `entryPoints` may be provided.');
6477
});
78+
79+
it('resolves entry points outside the workspace root', async () => {
80+
const entry = path.join(tempDir, 'entry.mjs');
81+
await fs.writeFile(entry, `console.log('entry');`);
82+
83+
harness.useTarget('build', {
84+
...BASE_OPTIONS,
85+
main: undefined,
86+
entryPoints: new Set([entry]),
87+
});
88+
89+
const { result } = await harness.executeOnce();
90+
expect(result?.success).toBeTrue();
91+
92+
harness.expectFile('dist/entry.js').toExist();
93+
});
94+
95+
it('throws an error when multiple entry points output to the same location', async () => {
96+
// Would generate `/entry.mjs` in the output directory.
97+
const entry1 = path.join(tempDir, 'entry.mjs');
98+
await fs.writeFile(entry1, `console.log('entry1');`);
99+
100+
// Would also generate `/entry.mjs` in the output directory.
101+
const subDir = path.join(tempDir, 'subdir');
102+
await fs.mkdir(subDir);
103+
const entry2 = path.join(subDir, 'entry.mjs');
104+
await fs.writeFile(entry2, `console.log('entry2');`);
105+
106+
harness.useTarget('build', {
107+
...BASE_OPTIONS,
108+
main: undefined,
109+
entryPoints: new Set([entry1, entry2]),
110+
});
111+
112+
const { result, error } = await harness.executeOnce();
113+
expect(result).toBeUndefined();
114+
115+
expect(error?.message).toContain(entry1);
116+
expect(error?.message).toContain(entry2);
117+
expect(error?.message).toContain('both output to the same location');
118+
});
65119
});
66120
});

packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/main_spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,20 @@ describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => {
7171

7272
expect(error?.message).toContain('cannot be an empty string');
7373
});
74+
75+
it('resolves an absolute path as relative inside the workspace root', async () => {
76+
await harness.writeFile('file.mjs', `console.log('Hello!');`);
77+
78+
harness.useTarget('build', {
79+
...BASE_OPTIONS,
80+
main: '/file.mjs',
81+
});
82+
83+
const { result } = await harness.executeOnce();
84+
expect(result?.success).toBeTrue();
85+
86+
// Always uses the name `main.js` for the `main` option.
87+
harness.expectFile('dist/main.js').toExist();
88+
});
7489
});
7590
});

0 commit comments

Comments
 (0)