Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createContentHash } from '../../../utils/hash';
import { BundleContextResult, BundlerContext } from '../bundler-context';
import { type BuildOutputFile, BuildOutputFileType } from '../bundler-files';
import { MemoryCache } from '../cache';
import { MemoryLoadResultCache } from '../load-result-cache';
import {
BundleStylesheetOptions,
createStylesheetBundleOptions,
Expand All @@ -30,11 +31,12 @@ export type ComponentStylesheetResult = BundleContextResult & {
export class ComponentStylesheetBundler {
readonly #fileContexts = new MemoryCache<BundlerContext>();
readonly #inlineContexts = new MemoryCache<BundlerContext>();
readonly #loadCache = new MemoryLoadResultCache();

/**
*
* @param options An object containing the stylesheet bundling options.
* @param cache A load result cache to use when bundling.
* @param defaultInlineLanguage The default language to use for inline component styles.
* @param incremental True if incremental watch mode is enabled.
*/
constructor(
private readonly options: BundleStylesheetOptions,
Expand All @@ -54,29 +56,38 @@ export class ComponentStylesheetBundler {
externalId?: string | boolean,
direct?: boolean,
): Promise<ComponentStylesheetResult> {
const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
if (externalId) {
assert(
typeof externalId === 'string',
'Initial external component stylesheets must have a string identifier',
);

buildOptions.entryPoints = { [externalId]: entry };
buildOptions.entryNames = '[name]';
delete buildOptions.publicPath;
} else {
buildOptions.entryPoints = [entry];
Comment thread
alan-agius4 marked this conversation as resolved.
}
entry = path.normalize(entry);

// Angular encapsulation does not support nesting
// See: https://github.com/angular/angular/issues/58996
buildOptions.supported ??= {};
buildOptions.supported['nesting'] = false;
const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
return new BundlerContext(
this.options.workspaceRoot,
this.incremental,
(loadCache) => {
const buildOptions = createStylesheetBundleOptions(this.options, loadCache);
if (externalId) {
assert(
typeof externalId === 'string',
'Initial external component stylesheets must have a string identifier',
);

buildOptions.entryPoints = { [externalId]: entry };
buildOptions.entryNames = '[name]';
delete buildOptions.publicPath;
} else {
buildOptions.entryPoints = [entry];
}

return buildOptions;
});
// Angular encapsulation does not support nesting
// See: https://github.com/angular/angular/issues/58996
buildOptions.supported ??= {};
buildOptions.supported['nesting'] = false;

return buildOptions;
},
/* useContext */ false,
/* initialFilter */ undefined,
this.#loadCache,
);
});

return this.extractResult(
Expand All @@ -101,6 +112,8 @@ export class ComponentStylesheetBundler {
language = this.defaultInlineLanguage,
externalId?: string,
): Promise<ComponentStylesheetResult> {
filename = path.normalize(filename);

// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
// to the actual stylesheet file path.
const hasher = createContentHash();
Expand All @@ -112,48 +125,55 @@ export class ComponentStylesheetBundler {
const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {
const namespace = 'angular:styles/component';

return new BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
const buildOptions = createStylesheetBundleOptions(this.options, loadCache, {
[entry]: data,
});
if (externalId) {
buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
buildOptions.entryNames = '[name]';
delete buildOptions.publicPath;
} else {
buildOptions.entryPoints = [`${namespace};${entry}`];
}
return new BundlerContext(
this.options.workspaceRoot,
this.incremental,
(loadCache) => {
const buildOptions = createStylesheetBundleOptions(this.options, loadCache, {
[entry]: data,
});
if (externalId) {
buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
buildOptions.entryNames = '[name]';
delete buildOptions.publicPath;
} else {
buildOptions.entryPoints = [`${namespace};${entry}`];
}

// Angular encapsulation does not support nesting
// See: https://github.com/angular/angular/issues/58996
buildOptions.supported ??= {};
buildOptions.supported['nesting'] = false;

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

return {
path: entry,
namespace,
};
});
build.onLoad({ filter: /^css;/, namespace }, () => {
return {
contents: data,
loader: 'css',
resolveDir: path.dirname(filename),
};
});
},
});

return buildOptions;
});
// Angular encapsulation does not support nesting
// See: https://github.com/angular/angular/issues/58996
buildOptions.supported ??= {};
buildOptions.supported['nesting'] = false;

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

return {
path: entry,
namespace,
};
});
build.onLoad({ filter: /^css;/, namespace }, () => {
return {
contents: data,
loader: 'css',
resolveDir: path.dirname(filename),
};
});
},
});

return buildOptions;
},
/* useContext */ false,
/* initialFilter */ undefined,
this.#loadCache,
);
});

// Extract the result of the bundling from the output files
Expand All @@ -170,13 +190,20 @@ export class ComponentStylesheetBundler {
* @param files The group of files that have been modified
* @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined.
*/
invalidate(files: Iterable<string>): string[] | undefined {
invalidate(files: Iterable<string> | ReadonlySet<string>): string[] | undefined {
if (!this.incremental) {
return;
}

const normalizedFiles = [...files].map(path.normalize);
const normalizedFilesSet = new Set(normalizedFiles);
const normalizedFiles = new Set<string>();
for (const file of files) {
const normalized = path.normalize(file);
normalizedFiles.add(normalized);
if (!path.isAbsolute(normalized)) {
normalizedFiles.add(path.normalize(path.join(this.options.workspaceRoot, normalized)));
}
}

let entries: string[] | undefined;

for (const [entry, bundler] of this.#fileContexts.entries()) {
Expand All @@ -190,7 +217,7 @@ export class ComponentStylesheetBundler {
const firstSemi = entry.indexOf(';');
const secondSemi = firstSemi !== -1 ? entry.indexOf(';', firstSemi + 1) : -1;
const filename = secondSemi !== -1 ? entry.slice(secondSemi + 1) : '';
if (filename && normalizedFilesSet.has(path.normalize(filename))) {
if (filename && normalizedFiles.has(path.normalize(filename))) {
this.#inlineContexts.delete(entry);
void bundler.dispose();
} else {
Expand All @@ -214,6 +241,7 @@ export class ComponentStylesheetBundler {
const contexts = [...this.#fileContexts.values(), ...this.#inlineContexts.values()];
this.#fileContexts.clear();
this.#inlineContexts.clear();
this.#loadCache.clear();

await Promise.allSettled(contexts.map((context) => context.dispose()));
}
Expand Down Expand Up @@ -268,14 +296,18 @@ export class ComponentStylesheetBundler {
}
}

const { metafile } = result;
// Remove entryPoint fields from outputs to prevent the internal component styles from being
// treated as initial files. Also mark the entry as a component resource for stat reporting.
Object.values(metafile.outputs).forEach((output) => {
delete output.entryPoint;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(output as any)['ng-component'] = true;
});
// Clone metafile to prevent mutation of the cached result by downstream plugins
const metafile = {
inputs: { ...result.metafile.inputs },
outputs: Object.fromEntries(
Object.entries(result.metafile.outputs).map(([key, output]) => {
const cloned = { ...output, ['ng-component']: true };
delete cloned.entryPoint;

return [key, cloned];
}),
),
};
Comment thread
alan-agius4 marked this conversation as resolved.

return {
errors,
Expand Down
68 changes: 61 additions & 7 deletions packages/angular/build/src/tools/esbuild/bundler-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class BundlerContext {

static bundleAll(
contexts: Iterable<BundlerContext>,
changedFiles?: Iterable<string>,
changedFiles?: Iterable<string> | ReadonlySet<string>,
): Promise<BundleContextResult[]> {
return Promise.all(
[...contexts].map((context) => {
Expand Down Expand Up @@ -482,18 +482,72 @@ export class BundlerContext {
* to be stored.
* @returns True, if the result was invalidated; False, otherwise.
*/
invalidate(files: Iterable<string>): boolean {
invalidate(files: Iterable<string> | ReadonlySet<string>): boolean {
if (!this.incremental) {
return false;
}

let invalid = false;
for (const file of files) {
const normalizedFile = isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file);
let candidateFiles: ReadonlySet<string>;
if (files instanceof Set) {
let isCandidateReady = true;
for (const file of files) {
if (
file !== normalize(file) ||
(!isAbsolute(file) && !files.has(normalize(join(this.workspaceRoot, file))))
) {
isCandidateReady = false;
break;
}
}

if (isCandidateReady) {
candidateFiles = files;
} else {
const normalizedFiles = new Set<string>();
for (const file of files) {
const normalized = normalize(file);
normalizedFiles.add(normalized);
if (!isAbsolute(normalized)) {
normalizedFiles.add(normalize(join(this.workspaceRoot, normalized)));
}
}
candidateFiles = normalizedFiles;
}
} else {
const normalizedFiles = new Set<string>();
for (const file of files) {
const normalized = normalize(file);
normalizedFiles.add(normalized);
if (!isAbsolute(normalized)) {
normalizedFiles.add(normalize(join(this.workspaceRoot, normalized)));
}
}
candidateFiles = normalizedFiles;
}

this.#loadCache?.invalidate(normalizedFile);
let invalid = false;
for (const file of candidateFiles) {
if (this.#loadCache?.invalidate(file)) {
invalid = true;
}
}

invalid ||= this.watchFiles.has(normalizedFile);
if (!invalid) {
if (this.watchFiles.size < candidateFiles.size) {
for (const file of this.watchFiles) {
if (candidateFiles.has(file)) {
invalid = true;
break;
}
}
} else {
for (const file of candidateFiles) {
if (this.watchFiles.has(file)) {
invalid = true;
break;
}
}
}
}

if (invalid) {
Expand Down
Loading