Skip to content

Commit b83dc44

Browse files
alan-agius4clydin
authored andcommitted
refactor: create a common plugin to generate virtual modules
This commit adds a new plugin to enable us to create virtual modules more easily.
1 parent 851fe31 commit b83dc44

File tree

4 files changed

+132
-109
lines changed

4 files changed

+132
-109
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/global-scripts.ts

Lines changed: 49 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { assertIsError } from '../../utils/error';
1515
import { LoadResultCache, createCachedLoad } from './load-result-cache';
1616
import type { NormalizedBrowserOptions } from './options';
1717
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
18+
import { createVirtualModulePlugin } from './virtual-module-plugin';
1819

1920
/**
2021
* Create an esbuild 'build' options object for all global scripts defined in the user provied
@@ -71,84 +72,62 @@ export function createGlobalScriptsBundleOptions(
7172
preserveSymlinks,
7273
plugins: [
7374
createSourcemapIngorelistPlugin(),
74-
{
75-
name: 'angular-global-scripts',
76-
setup(build) {
77-
build.onResolve({ filter: /^angular:script\/global:/ }, (args) => {
78-
if (args.kind !== 'entry-point') {
79-
return null;
80-
}
81-
82-
return {
83-
// Add the `js` extension here so that esbuild generates an output file with the extension
84-
path: args.path.slice(namespace.length + 1) + '.js',
85-
namespace,
86-
};
87-
});
88-
// All references within a global script should be considered external. This maintains the runtime
89-
// behavior of the script as if it were added directly to a script element for referenced imports.
90-
build.onResolve({ filter: /./, namespace }, ({ path }) => {
91-
return {
92-
path,
93-
external: true,
94-
};
95-
});
96-
build.onLoad(
97-
{ filter: /./, namespace },
98-
createCachedLoad(loadCache, async (args) => {
99-
const files = globalScripts.find(
100-
({ name }) => name === args.path.slice(0, -3),
101-
)?.files;
102-
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
75+
createVirtualModulePlugin({
76+
namespace,
77+
external: true,
78+
// Add the `js` extension here so that esbuild generates an output file with the extension
79+
transformPath: (path) => path.slice(namespace.length + 1) + '.js',
80+
loadContent: (args, build) =>
81+
createCachedLoad(loadCache, async (args) => {
82+
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))?.files;
83+
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
10384

104-
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
105-
const bundleContent = new Bundle();
106-
const watchFiles = [];
107-
for (const filename of files) {
108-
let fileContent;
109-
try {
110-
// Attempt to read as a relative path from the workspace root
111-
fileContent = await readFile(path.join(workspaceRoot, filename), 'utf-8');
112-
watchFiles.push(filename);
113-
} catch (e) {
114-
assertIsError(e);
115-
if (e.code !== 'ENOENT') {
116-
throw e;
117-
}
118-
119-
// If not found attempt to resolve as a module specifier
120-
const resolveResult = await build.resolve(filename, {
121-
kind: 'entry-point',
122-
resolveDir: workspaceRoot,
123-
});
85+
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
86+
const bundleContent = new Bundle();
87+
const watchFiles = [];
88+
for (const filename of files) {
89+
let fileContent;
90+
try {
91+
// Attempt to read as a relative path from the workspace root
92+
fileContent = await readFile(path.join(workspaceRoot, filename), 'utf-8');
93+
watchFiles.push(filename);
94+
} catch (e) {
95+
assertIsError(e);
96+
if (e.code !== 'ENOENT') {
97+
throw e;
98+
}
12499

125-
if (resolveResult.errors.length) {
126-
// Remove resolution failure notes about marking as external since it doesn't apply
127-
// to global scripts.
128-
resolveResult.errors.forEach((error) => (error.notes = []));
100+
// If not found attempt to resolve as a module specifier
101+
const resolveResult = await build.resolve(filename, {
102+
kind: 'entry-point',
103+
resolveDir: workspaceRoot,
104+
});
129105

130-
return {
131-
errors: resolveResult.errors,
132-
warnings: resolveResult.warnings,
133-
};
134-
}
106+
if (resolveResult.errors.length) {
107+
// Remove resolution failure notes about marking as external since it doesn't apply
108+
// to global scripts.
109+
resolveResult.errors.forEach((error) => (error.notes = []));
135110

136-
watchFiles.push(path.relative(resolveResult.path, workspaceRoot));
137-
fileContent = await readFile(resolveResult.path, 'utf-8');
111+
return {
112+
errors: resolveResult.errors,
113+
warnings: resolveResult.warnings,
114+
};
138115
}
139116

140-
bundleContent.addSource(new MagicString(fileContent, { filename }));
117+
watchFiles.push(path.relative(resolveResult.path, workspaceRoot));
118+
fileContent = await readFile(resolveResult.path, 'utf-8');
141119
}
142120

143-
return {
144-
contents: bundleContent.toString(),
145-
loader: 'js',
146-
watchFiles,
147-
};
148-
}),
149-
);
150-
},
151-
},
121+
bundleContent.addSource(new MagicString(fileContent, { filename }));
122+
}
123+
124+
return {
125+
contents: bundleContent.toString(),
126+
loader: 'js',
127+
watchFiles,
128+
};
129+
}).call(build, args),
130+
}),
152131
],
153132
};
154133
}

packages/angular_devkit/build_angular/src/builders/browser-esbuild/global-styles.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import assert from 'node:assert';
1111
import { LoadResultCache } from './load-result-cache';
1212
import { NormalizedBrowserOptions } from './options';
1313
import { createStylesheetBundleOptions } from './stylesheets/bundle-options';
14+
import { createVirtualModulePlugin } from './virtual-module-plugin';
1415

1516
export function createGlobalStylesBundleOptions(
1617
options: NormalizedBrowserOptions,
@@ -69,20 +70,11 @@ export function createGlobalStylesBundleOptions(
6970
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';
7071
buildOptions.entryPoints = entryPoints;
7172

72-
buildOptions.plugins.unshift({
73-
name: 'angular-global-styles',
74-
setup(build) {
75-
build.onResolve({ filter: /^angular:styles\/global;/ }, (args) => {
76-
if (args.kind !== 'entry-point') {
77-
return null;
78-
}
79-
80-
return {
81-
path: args.path.split(';', 2)[1],
82-
namespace,
83-
};
84-
});
85-
build.onLoad({ filter: /./, namespace }, (args) => {
73+
buildOptions.plugins.unshift(
74+
createVirtualModulePlugin({
75+
namespace,
76+
transformPath: (path) => path.split(';', 2)[1],
77+
loadContent: (args) => {
8678
const files = globalStyles.find(({ name }) => name === args.path)?.files;
8779
assert(files, `global style name should always be found [${args.path}]`);
8880

@@ -91,9 +83,9 @@ export function createGlobalStylesBundleOptions(
9183
loader: 'css',
9284
resolveDir: workspaceRoot,
9385
};
94-
});
95-
},
96-
});
86+
},
87+
}),
88+
);
9789

9890
return buildOptions;
9991
}

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

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } fro
3333
import { Schema as BrowserBuilderOptions } from './schema';
3434
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
3535
import { shutdownSassWorkerPool } from './stylesheets/sass-language';
36+
import { createVirtualModulePlugin } from './virtual-module-plugin';
3637
import type { ChangedFiles } from './watcher';
3738

3839
const compressAsync = promisify(brotliCompress);
@@ -470,28 +471,16 @@ function createCodeBundleOptions(
470471
['polyfills']: namespace,
471472
};
472473

473-
buildOptions.plugins?.unshift({
474-
name: 'angular-polyfills',
475-
setup(build) {
476-
build.onResolve({ filter: /^angular:polyfills$/ }, (args) => {
477-
if (args.kind !== 'entry-point') {
478-
return null;
479-
}
480-
481-
return {
482-
path: 'entry',
483-
namespace,
484-
};
485-
});
486-
build.onLoad({ filter: /./, namespace }, () => {
487-
return {
488-
contents: polyfills.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n'),
489-
loader: 'js',
490-
resolveDir: workspaceRoot,
491-
};
492-
});
493-
},
494-
});
474+
buildOptions.plugins?.unshift(
475+
createVirtualModulePlugin({
476+
namespace,
477+
loadContent: () => ({
478+
contents: polyfills.map((file) => `import '${file.replace(/\\/g, '/')}';`).join('\n'),
479+
loader: 'js',
480+
resolveDir: workspaceRoot,
481+
}),
482+
}),
483+
);
495484
}
496485

497486
return buildOptions;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import type { OnLoadArgs, Plugin, PluginBuild } from 'esbuild';
10+
11+
/**
12+
* Options for the createVirtualModulePlugin
13+
* @see createVirtualModulePlugin
14+
*/
15+
export interface VirtualModulePluginOptions {
16+
/** Namespace. Example: `angular:polyfills`. */
17+
namespace: string;
18+
/** If the generated module should be marked as external. */
19+
external?: boolean;
20+
/** Method to transform the onResolve path. */
21+
transformPath?: (path: string) => string;
22+
/** Method to provide the module content. */
23+
loadContent: (
24+
args: OnLoadArgs,
25+
build: PluginBuild,
26+
) => ReturnType<Parameters<PluginBuild['onLoad']>[1]>;
27+
}
28+
29+
/**
30+
* Creates an esbuild plugin that generated virtual modules.
31+
*
32+
* @returns An esbuild plugin.
33+
*/
34+
export function createVirtualModulePlugin(options: VirtualModulePluginOptions): Plugin {
35+
const { namespace, external, transformPath: pathTransformer, loadContent } = options;
36+
37+
return {
38+
name: namespace.replace(/[/:]/g, '-'),
39+
setup(build): void {
40+
build.onResolve({ filter: new RegExp('^' + namespace) }, ({ kind, path }) => {
41+
if (kind !== 'entry-point') {
42+
return null;
43+
}
44+
45+
return {
46+
path: pathTransformer?.(path) ?? path,
47+
namespace,
48+
};
49+
});
50+
51+
if (external) {
52+
build.onResolve({ filter: /./, namespace }, ({ path }) => {
53+
return {
54+
path,
55+
external: true,
56+
};
57+
});
58+
}
59+
60+
build.onLoad({ filter: /./, namespace }, (args) => loadContent(args, build));
61+
},
62+
};
63+
}

0 commit comments

Comments
 (0)