Skip to content
Merged
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,7 +12,8 @@ import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { assertIsError } from '../../utils/error';
import { NormalizedBrowserOptions } from './options';
import { LoadResultCache, createCachedLoad } from './load-result-cache';
import type { NormalizedBrowserOptions } from './options';
import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';

/**
Expand All @@ -24,6 +25,7 @@ import { createSourcemapIngorelistPlugin } from './sourcemap-ignorelist-plugin';
export function createGlobalScriptsBundleOptions(
options: NormalizedBrowserOptions,
initial: boolean,
loadCache?: LoadResultCache,
): BuildOptions | undefined {
const {
globalScripts,
Expand Down Expand Up @@ -91,51 +93,60 @@ export function createGlobalScriptsBundleOptions(
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}]`);
build.onLoad(
{ filter: /./, namespace },
createCachedLoad(loadCache, 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) {
let fileContent;
try {
// Attempt to read as a relative path from the workspace root
fileContent = await readFile(path.join(workspaceRoot, filename), 'utf-8');
} catch (e) {
assertIsError(e);
if (e.code !== 'ENOENT') {
throw e;
}
// Global scripts are concatenated using magic-string instead of bundled via esbuild.
const bundleContent = new Bundle();
const watchFiles = [];
for (const filename of files) {
let fileContent;
try {
// Attempt to read as a relative path from the workspace root
fileContent = await readFile(path.join(workspaceRoot, filename), 'utf-8');
watchFiles.push(filename);
} catch (e) {
assertIsError(e);
if (e.code !== 'ENOENT') {
throw e;
}

// If not found attempt to resolve as a module specifier
const resolveResult = await build.resolve(filename, {
kind: 'entry-point',
resolveDir: workspaceRoot,
});

// If not found attempt to resolve as a module specifier
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 = []));

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,
};
}

return {
errors: resolveResult.errors,
warnings: resolveResult.warnings,
};
watchFiles.push(path.relative(resolveResult.path, workspaceRoot));
fileContent = await readFile(resolveResult.path, 'utf-8');
}

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

bundleContent.addSource(new MagicString(fileContent, { filename }));
}

return {
contents: bundleContent.toString(),
loader: 'js',
};
});
return {
contents: bundleContent.toString(),
loader: 'js',
watchFiles,
};
}),
);
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,38 @@
* found in the LICENSE file at https://angular.io/license
*/

import type { OnLoadResult } from 'esbuild';
import type { OnLoadResult, PluginBuild } from 'esbuild';

export interface LoadResultCache {
get(path: string): OnLoadResult | undefined;
put(path: string, result: OnLoadResult): Promise<void>;
}

export function createCachedLoad(
cache: LoadResultCache | undefined,
callback: Parameters<PluginBuild['onLoad']>[1],
): Parameters<PluginBuild['onLoad']>[1] {
if (cache === undefined) {
return callback;
}

return async (args) => {
const loadCacheKey = `${args.namespace}:${args.path}`;
let result: OnLoadResult | null | undefined = cache.get(loadCacheKey);

if (result === undefined) {
result = await callback(args);

// Do not cache null or undefined or results with errors
if (result && result.errors === undefined) {
await cache.put(loadCacheKey, result);
}
}

return result;
};
}

export class MemoryLoadResultCache implements LoadResultCache {
#loadResults = new Map<string, OnLoadResult>();
#fileDependencies = new Map<string, Set<string>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type { BuildOptions, OutputFile } from 'esbuild';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { BundlerContext } from '../esbuild';
import { LoadResultCache } from '../load-result-cache';
Expand Down Expand Up @@ -69,18 +70,24 @@ export function createStylesheetBundleOptions(
},
cache,
),
createLessPlugin({
sourcemap: !!options.sourcemap,
includePaths,
inlineComponentData,
}),
createCssPlugin({
sourcemap: !!options.sourcemap,
inlineComponentData,
browsers: options.browsers,
tailwindConfiguration: options.tailwindConfiguration,
}),
createCssResourcePlugin(),
createLessPlugin(
{
sourcemap: !!options.sourcemap,
includePaths,
inlineComponentData,
},
cache,
),
createCssPlugin(
{
sourcemap: !!options.sourcemap,
inlineComponentData,
browsers: options.browsers,
tailwindConfiguration: options.tailwindConfiguration,
},
cache,
),
createCssResourcePlugin(cache),
],
};
}
Expand Down Expand Up @@ -108,7 +115,11 @@ export async function bundleComponentStylesheet(
cache?: LoadResultCache,
) {
const namespace = 'angular:styles/component';
const entry = [language, componentStyleCounter++, filename].join(';');
// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
// to the actual stylesheet file path.
// TODO: Consider xxhash instead for hashing
const id = inline ? createHash('sha256').update(data).digest('hex') : componentStyleCounter++;
const entry = [language, id, filename].join(';');

const buildOptions = createStylesheetBundleOptions(options, cache, { [entry]: data });
buildOptions.entryPoints = [`${namespace};${entry}`];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import createAutoPrefixerPlugin from 'autoprefixer';
import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';

/**
* The lazy-loaded instance of the postcss stylesheet postprocessor.
Expand Down Expand Up @@ -46,7 +47,7 @@ export interface CssPluginOptions {
* @param options An object containing the plugin options.
* @returns An esbuild Plugin instance.
*/
export function createCssPlugin(options: CssPluginOptions): Plugin {
export function createCssPlugin(options: CssPluginOptions, cache?: LoadResultCache): Plugin {
return {
name: 'angular-css',
async setup(build: PluginBuild): Promise<void> {
Expand Down Expand Up @@ -78,24 +79,30 @@ export function createCssPlugin(options: CssPluginOptions): Plugin {
}

// Add a load callback to support inline Component styles
build.onLoad({ filter: /^css;/, namespace: 'angular:styles/component' }, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(
typeof data === 'string',
`component style name should always be found [${args.path}]`,
);
build.onLoad(
{ filter: /^css;/, namespace: 'angular:styles/component' },
createCachedLoad(cache, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(
typeof data === 'string',
`component style name should always be found [${args.path}]`,
);

const [, , filePath] = args.path.split(';', 3);
const [, , filePath] = args.path.split(';', 3);

return compileString(data, filePath, postcssProcessor, options);
});
return compileString(data, filePath, postcssProcessor, options);
}),
);

// Add a load callback to support files from disk
build.onLoad({ filter: /\.css$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');

return compileString(data, args.path, postcssProcessor, options);
});
build.onLoad(
{ filter: /\.css$/ },
createCachedLoad(cache, async (args) => {
const data = await readFile(args.path, 'utf-8');

return compileString(data, args.path, postcssProcessor, options);
}),
);
},
};
}
Expand Down Expand Up @@ -157,6 +164,7 @@ async function compileString(
contents: result.css,
loader: 'css',
warnings,
watchFiles: [filename],
};
} catch (error) {
postcss ??= (await import('postcss')).default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { Plugin, PluginBuild } from 'esbuild';
import { readFile } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';

/**
* Symbol marker used to indicate CSS resource resolution is being attempted.
Expand All @@ -24,7 +25,7 @@ const CSS_RESOURCE_RESOLUTION = Symbol('CSS_RESOURCE_RESOLUTION');
*
* @returns An esbuild {@link Plugin} instance.
*/
export function createCssResourcePlugin(): Plugin {
export function createCssResourcePlugin(cache?: LoadResultCache): Plugin {
return {
name: 'angular-css-resource',
setup(build: PluginBuild): void {
Expand Down Expand Up @@ -80,12 +81,18 @@ export function createCssResourcePlugin(): Plugin {
};
});

build.onLoad({ filter: /.*/, namespace: 'css-resource' }, async (args) => {
return {
contents: await readFile(join(build.initialOptions.absWorkingDir ?? '', args.path)),
loader: 'file',
};
});
build.onLoad(
{ filter: /./, namespace: 'css-resource' },
createCachedLoad(cache, async (args) => {
const resourcePath = join(build.initialOptions.absWorkingDir ?? '', args.path);

return {
contents: await readFile(resourcePath),
loader: 'file',
watchFiles: [resourcePath],
};
}),
);
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { OnLoadResult, Plugin, PluginBuild } from 'esbuild';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { LoadResultCache, createCachedLoad } from '../load-result-cache';

/**
* The lazy-loaded instance of the less stylesheet preprocessor.
Expand All @@ -33,29 +34,35 @@ function isLessException(error: unknown): error is LessException {
return !!error && typeof error === 'object' && 'column' in error;
}

export function createLessPlugin(options: LessPluginOptions): Plugin {
export function createLessPlugin(options: LessPluginOptions, cache?: LoadResultCache): Plugin {
return {
name: 'angular-less',
setup(build: PluginBuild): void {
// Add a load callback to support inline Component styles
build.onLoad({ filter: /^less;/, namespace: 'angular:styles/component' }, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(
typeof data === 'string',
`component style name should always be found [${args.path}]`,
);
build.onLoad(
{ filter: /^less;/, namespace: 'angular:styles/component' },
createCachedLoad(cache, async (args) => {
const data = options.inlineComponentData?.[args.path];
assert(
typeof data === 'string',
`component style name should always be found [${args.path}]`,
);

const [, , filePath] = args.path.split(';', 3);
const [, , filePath] = args.path.split(';', 3);

return compileString(data, filePath, options, build.resolve.bind(build));
});
return compileString(data, filePath, options, build.resolve.bind(build));
}),
);

// Add a load callback to support files from disk
build.onLoad({ filter: /\.less$/ }, async (args) => {
const data = await readFile(args.path, 'utf-8');

return compileString(data, args.path, options, build.resolve.bind(build));
});
build.onLoad(
{ filter: /\.less$/ },
createCachedLoad(cache, async (args) => {
const data = await readFile(args.path, 'utf-8');

return compileString(data, args.path, options, build.resolve.bind(build));
}),
);
},
};
}
Expand Down Expand Up @@ -127,6 +134,7 @@ async function compileString(
return {
contents: result.css,
loader: 'css',
watchFiles: [filename, ...result.imports],
};
} catch (error) {
if (isLessException(error)) {
Expand Down
Loading