Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): avoid hash filenames for non-inje…
Browse files Browse the repository at this point in the history
…cted global styles/scripts

When using the esbuild-based browser application builder, non-injected global styles and scripts
were unintentionally being output with filenames that contain a hash. This can prevent the filenames
from being discoverable and therefore usable at runtime. The output filenames will now no longer
contain a hash component which matches the behavior of the Webpack-based builder.

(cherry picked from commit 2a2817d)
  • Loading branch information
clydin authored and dgp1130 committed May 4, 2023
1 parent 95e1b40 commit 3083c4e
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
* @param options The builder's user-provider normalized options.
* @returns An esbuild BuildOptions object.
*/
export function createGlobalScriptsBundleOptions(options: NormalizedBrowserOptions): BuildOptions {
export function createGlobalScriptsBundleOptions(
options: NormalizedBrowserOptions,
initial: boolean,
): BuildOptions | undefined {
const {
globalScripts,
optimizationOptions,
Expand All @@ -31,16 +34,25 @@ export function createGlobalScriptsBundleOptions(options: NormalizedBrowserOptio

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

// Skip if there are no entry points for the style loading type
if (found === false) {
return;
}

return {
absWorkingDir: workspaceRoot,
bundle: false,
splitting: false,
entryPoints,
entryNames: outputNames.bundles,
entryNames: initial ? outputNames.bundles : '[name]',
assetNames: outputNames.media,
mainFields: ['script', 'browser', 'main'],
conditions: ['script'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @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 assert from 'node:assert';
import { LoadResultCache } from './load-result-cache';
import { NormalizedBrowserOptions } from './options';
import { createStylesheetBundleOptions } from './stylesheets/bundle-options';

export function createGlobalStylesBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
browsers: string[],
initial: boolean,
cache?: LoadResultCache,
): BuildOptions | undefined {
const {
workspaceRoot,
optimizationOptions,
sourcemapOptions,
outputNames,
globalStyles,
preserveSymlinks,
externalDependencies,
stylePreprocessorOptions,
tailwindConfiguration,
} = options;

const namespace = 'angular:styles/global';
const entryPoints: Record<string, string> = {};
let found = false;
for (const style of globalStyles) {
if (style.initial === initial) {
found = true;
entryPoints[style.name] = `${namespace};${style.name}`;
}
}

// Skip if there are no entry points for the style loading type
if (found === false) {
return;
}

const buildOptions = createStylesheetBundleOptions(
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
externalDependencies,
outputNames: initial
? outputNames
: {
...outputNames,
bundles: '[name]',
},
includePaths: stylePreprocessorOptions?.includePaths,
browsers,
tailwindConfiguration,
},
cache,
);
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';
buildOptions.entryPoints = entryPoints;

buildOptions.plugins.unshift({
name: 'angular-global-styles',
setup(build) {
build.onResolve({ filter: /^angular:styles\/global;/ }, (args) => {
if (args.kind !== 'entry-point') {
return null;
}

return {
path: args.path.split(';', 2)[1],
namespace,
};
});
build.onLoad({ filter: /./, namespace }, (args) => {
const files = globalStyles.find(({ name }) => name === args.path)?.files;
assert(files, `global style name should always be found [${args.path}]`);

return {
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
loader: 'css',
resolveDir: workspaceRoot,
};
});
},
});

return buildOptions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { BuildOptions, Metafile, OutputFile } from 'esbuild';
import assert from 'node:assert';
import { constants as fsConstants } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
Expand All @@ -26,18 +25,16 @@ import { logBuilderStatusWarnings } from './builder-status-warnings';
import { checkCommonJSModules } from './commonjs-checker';
import { BundlerContext, logMessages } from './esbuild';
import { createGlobalScriptsBundleOptions } from './global-scripts';
import { createGlobalStylesBundleOptions } from './global-styles';
import { extractLicenses } from './license-extractor';
import { LoadResultCache } from './load-result-cache';
import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options';
import { Schema as BrowserBuilderOptions } from './schema';
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
import { createStylesheetBundleOptions } from './stylesheets/bundle-options';
import { shutdownSassWorkerPool } from './stylesheets/sass-plugin';
import type { ChangedFiles } from './watcher';

interface RebuildState {
codeRebuild?: BundlerContext;
globalStylesRebuild?: BundlerContext;
rebuildContexts: BundlerContext[];
codeBundleCache?: SourceFileCache;
fileChanges: ChangedFiles;
}
Expand All @@ -50,8 +47,7 @@ class ExecutionResult {
readonly assetFiles: { source: string; destination: string }[] = [];

constructor(
private codeRebuild?: BundlerContext,
private globalStylesRebuild?: BundlerContext,
private rebuildContexts: BundlerContext[],
private codeBundleCache?: SourceFileCache,
) {}

Expand All @@ -77,15 +73,14 @@ class ExecutionResult {
this.codeBundleCache?.invalidate([...fileChanges.modified, ...fileChanges.removed]);

return {
codeRebuild: this.codeRebuild,
globalStylesRebuild: this.globalStylesRebuild,
rebuildContexts: this.rebuildContexts,
codeBundleCache: this.codeBundleCache,
fileChanges,
};
}

async dispose(): Promise<void> {
await Promise.allSettled([this.codeRebuild?.dispose(), this.globalStylesRebuild?.dispose()]);
await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose()));
}
}

Expand All @@ -109,57 +104,55 @@ async function execute(
const target = transformSupportedBrowsersToTargets(browsers);

// Reuse rebuild state or create new bundle contexts for code and global stylesheets
const bundlerContexts = [];

// Application code
let bundlerContexts = rebuildState?.rebuildContexts;
const codeBundleCache = options.watch
? rebuildState?.codeBundleCache ?? new SourceFileCache()
: undefined;
const codeBundleContext =
rebuildState?.codeRebuild ??
new BundlerContext(
workspaceRoot,
!!options.watch,
createCodeBundleOptions(options, target, browsers, codeBundleCache),
);
bundlerContexts.push(codeBundleContext);
// Global Stylesheets
let globalStylesBundleContext;
if (options.globalStyles.length > 0) {
globalStylesBundleContext =
rebuildState?.globalStylesRebuild ??
if (bundlerContexts === undefined) {
bundlerContexts = [];

// Application code
bundlerContexts.push(
new BundlerContext(
workspaceRoot,
!!options.watch,
createGlobalStylesBundleOptions(
createCodeBundleOptions(options, target, browsers, codeBundleCache),
),
);

// Global Stylesheets
if (options.globalStyles.length > 0) {
for (const initial of [true, false]) {
const bundleOptions = createGlobalStylesBundleOptions(
options,
target,
browsers,
initial,
codeBundleCache?.loadResultCache,
),
);
bundlerContexts.push(globalStylesBundleContext);
}
// Global Scripts
if (options.globalScripts.length > 0) {
const globalScriptsBundleContext = new BundlerContext(
workspaceRoot,
!!options.watch,
createGlobalScriptsBundleOptions(options),
);
bundlerContexts.push(globalScriptsBundleContext);
);
if (bundleOptions) {
bundlerContexts.push(new BundlerContext(workspaceRoot, !!options.watch, bundleOptions));
}
}
}

// Global Scripts
if (options.globalScripts.length > 0) {
for (const initial of [true, false]) {
const bundleOptions = createGlobalScriptsBundleOptions(options, initial);
if (bundleOptions) {
bundlerContexts.push(new BundlerContext(workspaceRoot, !!options.watch, bundleOptions));
}
}
}
}

const bundlingResult = await BundlerContext.bundleAll(bundlerContexts);

// Log all warnings and errors generated during bundling
await logMessages(context, bundlingResult);

const executionResult = new ExecutionResult(
codeBundleContext,
globalStylesBundleContext,
codeBundleCache,
);
const executionResult = new ExecutionResult(bundlerContexts, codeBundleCache);

// Return if the bundling has errors
if (bundlingResult.errors) {
Expand Down Expand Up @@ -501,76 +494,6 @@ function getFeatureSupport(target: string[]): BuildOptions['supported'] {
return supported;
}

function createGlobalStylesBundleOptions(
options: NormalizedBrowserOptions,
target: string[],
browsers: string[],
cache?: LoadResultCache,
): BuildOptions {
const {
workspaceRoot,
optimizationOptions,
sourcemapOptions,
outputNames,
globalStyles,
preserveSymlinks,
externalDependencies,
stylePreprocessorOptions,
tailwindConfiguration,
} = options;

const buildOptions = createStylesheetBundleOptions(
{
workspaceRoot,
optimization: !!optimizationOptions.styles.minify,
sourcemap: !!sourcemapOptions.styles,
preserveSymlinks,
target,
externalDependencies,
outputNames,
includePaths: stylePreprocessorOptions?.includePaths,
browsers,
tailwindConfiguration,
},
cache,
);
buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof';

const namespace = 'angular:styles/global';
buildOptions.entryPoints = {};
for (const { name } of globalStyles) {
buildOptions.entryPoints[name] = `${namespace};${name}`;
}

buildOptions.plugins.unshift({
name: 'angular-global-styles',
setup(build) {
build.onResolve({ filter: /^angular:styles\/global;/ }, (args) => {
if (args.kind !== 'entry-point') {
return null;
}

return {
path: args.path.split(';', 2)[1],
namespace,
};
});
build.onLoad({ filter: /./, namespace }, (args) => {
const files = globalStyles.find(({ name }) => name === args.path)?.files;
assert(files, `global style name should always be found [${args.path}]`);

return {
contents: files.map((file) => `@import '${file.replace(/\\/g, '/')}';`).join('\n'),
loader: 'css',
resolveDir: workspaceRoot,
};
});
},
});

return buildOptions;
}

async function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T> {
const spinner = new Spinner(text);
spinner.start();
Expand Down

0 comments on commit 3083c4e

Please sign in to comment.