Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): support scripts option with esbu…
Browse files Browse the repository at this point in the history
…ild builder

When using the esbuild-based browser application builder, the `scripts` option will
now provide equivalent functionality to the current default Webpack-based builder.
The option provides full node resolution capabilities which allows both workspace
relative paths and package paths with support for the `script` exports condition.
  • Loading branch information
clydin authored and angular-robot[bot] committed Apr 4, 2023
1 parent 8c55030 commit 584b519
Show file tree
Hide file tree
Showing 6 changed files with 598 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { Schema as BrowserBuilderOptions } from '../browser/schema';
const UNSUPPORTED_OPTIONS: Array<keyof BrowserBuilderOptions> = [
'budgets',
'progress',
'scripts',

// * i18n support
'localize',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @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 type { BuildOptions } from 'esbuild';
import MagicString, { Bundle } from 'magic-string';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { NormalizedBrowserOptions } from './options';

/**
* Create an esbuild 'build' options object for all global scripts defined in the user provied
* build options.
* @param options The builder's user-provider normalized options.
* @returns An esbuild BuildOptions object.
*/
export function createGlobalScriptsBundleOptions(options: NormalizedBrowserOptions): BuildOptions {
const {
globalScripts,
optimizationOptions,
outputNames,
preserveSymlinks,
sourcemapOptions,
workspaceRoot,
} = options;

const namespace = 'angular:script/global';
const entryPoints: Record<string, string> = {};
for (const { name } of globalScripts) {
entryPoints[name] = `${namespace}:${name}`;
}

return {
absWorkingDir: workspaceRoot,
bundle: false,
splitting: false,
entryPoints,
entryNames: outputNames.bundles,
assetNames: outputNames.media,
mainFields: ['script', 'browser', 'main'],
conditions: ['script'],
resolveExtensions: ['.mjs', '.js'],
logLevel: options.verbose ? 'debug' : 'silent',
metafile: true,
minify: optimizationOptions.scripts,
outdir: workspaceRoot,
sourcemap: sourcemapOptions.scripts && (sourcemapOptions.hidden ? 'external' : true),
write: false,
platform: 'neutral',
preserveSymlinks,
plugins: [
{
name: 'angular-global-scripts',
setup(build) {
build.onResolve({ filter: /^angular:script\/global:/ }, (args) => {
if (args.kind !== 'entry-point') {
return null;
}

return {
// Add the `js` extension here so that esbuild generates an output file with the extension
path: args.path.slice(namespace.length + 1) + '.js',
namespace,
};
});
// All references within a global script should be considered external. This maintains the runtime
// behavior of the script as if it were added directly to a script element for referenced imports.
build.onResolve({ filter: /./, namespace }, ({ path }) => {
return {
path,
external: true,
};
});
build.onLoad({ filter: /./, namespace }, async (args) => {
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))?.files;
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);

// Global scripts are concatenated using magic-string instead of bundled via esbuild.
const bundleContent = new Bundle();
for (const filename of files) {
const resolveResult = await build.resolve(filename, {
kind: 'entry-point',
resolveDir: workspaceRoot,
});

if (resolveResult.errors.length) {
// Remove resolution failure notes about marking as external since it doesn't apply
// to global scripts.
resolveResult.errors.forEach((error) => (error.notes = []));

return {
errors: resolveResult.errors,
warnings: resolveResult.warnings,
};
}

const fileContent = await readFile(resolveResult.path, 'utf-8');
bundleContent.addSource(new MagicString(fileContent, { filename }));
}

return {
contents: bundleContent.toString(),
loader: 'js',
};
});
},
},
],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { checkCommonJSModules } from './commonjs-checker';
import { SourceFileCache, createCompilerPlugin } from './compiler-plugin';
import { BundlerContext, logMessages } from './esbuild';
import { logExperimentalWarnings } from './experimental-warnings';
import { createGlobalScriptsBundleOptions } from './global-scripts';
import { extractLicenses } from './license-extractor';
import { NormalizedBrowserOptions, normalizeOptions } from './options';
import { shutdownSassWorkerPool } from './sass-plugin';
Expand Down Expand Up @@ -124,17 +125,28 @@ async function execute(
createGlobalStylesBundleOptions(options, target, browsers),
);

const [codeResults, styleResults] = await Promise.all([
const globalScriptsBundleContext = new BundlerContext(
workspaceRoot,
!!options.watch,
createGlobalScriptsBundleOptions(options),
);

const [codeResults, styleResults, scriptResults] = await Promise.all([
// Execute esbuild to bundle the application code
codeBundleContext.bundle(),
// Execute esbuild to bundle the global stylesheets
globalStylesBundleContext.bundle(),
globalScriptsBundleContext.bundle(),
]);

// Log all warnings and errors generated during bundling
await logMessages(context, {
errors: [...(codeResults.errors || []), ...(styleResults.errors || [])],
warnings: [...codeResults.warnings, ...styleResults.warnings],
errors: [
...(codeResults.errors || []),
...(styleResults.errors || []),
...(scriptResults.errors || []),
],
warnings: [...codeResults.warnings, ...styleResults.warnings, ...scriptResults.warnings],
});

const executionResult = new ExecutionResult(
Expand All @@ -144,7 +156,7 @@ async function execute(
);

// Return if the bundling has errors
if (codeResults.errors || styleResults.errors) {
if (codeResults.errors || styleResults.errors || scriptResults.errors) {
return executionResult;
}

Expand All @@ -154,13 +166,29 @@ async function execute(
);

// Combine the bundling output files
const initialFiles: FileInfo[] = [...codeResults.initialFiles, ...styleResults.initialFiles];
executionResult.outputFiles.push(...codeResults.outputFiles, ...styleResults.outputFiles);
const initialFiles: FileInfo[] = [
...codeResults.initialFiles,
...styleResults.initialFiles,
...scriptResults.initialFiles,
];
executionResult.outputFiles.push(
...codeResults.outputFiles,
...styleResults.outputFiles,
...scriptResults.outputFiles,
);

// Combine metafiles used for the stats option as well as bundle budgets and console output
const metafile = {
inputs: { ...codeResults.metafile?.inputs, ...styleResults.metafile?.inputs },
outputs: { ...codeResults.metafile?.outputs, ...styleResults.metafile?.outputs },
inputs: {
...codeResults.metafile?.inputs,
...styleResults.metafile?.inputs,
...scriptResults.metafile?.inputs,
},
outputs: {
...codeResults.metafile?.outputs,
...styleResults.metafile?.outputs,
...scriptResults.metafile?.outputs,
},
};

// Check metafile for CommonJS module usage if optimizing scripts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } fr
import { normalizeCacheOptions } from '../../utils/normalize-cache';
import { generateEntryPoints } from '../../utils/package-chunk-sort';
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
import { normalizeGlobalStyles } from '../../webpack/utils/helpers';
import { globalScriptsByBundleName, normalizeGlobalStyles } from '../../webpack/utils/helpers';
import { Schema as BrowserBuilderOptions, OutputHashing } from './schema';

export type NormalizedBrowserOptions = Awaited<ReturnType<typeof normalizeOptions>>;
Expand Down Expand Up @@ -88,6 +88,13 @@ export async function normalizeOptions(
}
}

const globalScripts: { name: string; files: string[]; initial: boolean }[] = [];
if (options.scripts?.length) {
for (const { bundleName, paths, inject } of globalScriptsByBundleName(options.scripts)) {
globalScripts.push({ name: bundleName, files: paths, initial: inject });
}
}

let tailwindConfiguration: { file: string; package: string } | undefined;
const tailwindConfigurationPath = findTailwindConfigurationFile(workspaceRoot, projectRoot);
if (tailwindConfigurationPath) {
Expand Down Expand Up @@ -186,6 +193,7 @@ export async function normalizeOptions(
outputNames,
fileReplacements,
globalStyles,
globalScripts,
serviceWorkerOptions,
indexHtmlOptions,
tailwindConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@
},
{
"type": "string",
"description": "The file to include.",
"pattern": "\\.[cm]?jsx?$"
"description": "The JavaScript/TypeScript file or package containing the file to include."
}
]
}
Expand Down
Loading

0 comments on commit 584b519

Please sign in to comment.