"use strict"; /** * @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.dev/license */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ComponentStylesheetBundler = void 0; const node_assert_1 = __importDefault(require("node:assert")); const node_crypto_1 = require("node:crypto"); const node_path_1 = __importDefault(require("node:path")); const bundler_context_1 = require("../bundler-context"); const cache_1 = require("../cache"); const bundle_options_1 = require("../stylesheets/bundle-options"); /** * Bundles component stylesheets. A stylesheet can be either an inline stylesheet that * is contained within the Component's metadata definition or an external file referenced * from the Component's metadata definition. */ class ComponentStylesheetBundler { options; defaultInlineLanguage; incremental; #fileContexts = new cache_1.MemoryCache(); #inlineContexts = new cache_1.MemoryCache(); /** * * @param options An object containing the stylesheet bundling options. * @param cache A load result cache to use when bundling. */ constructor(options, defaultInlineLanguage, incremental) { this.options = options; this.defaultInlineLanguage = defaultInlineLanguage; this.incremental = incremental; } /** * Bundle a file-based component stylesheet for use within an AOT compiled Angular application. * @param entry The file path of the stylesheet. * @param externalId Either an external identifier string for initial bundling or a boolean for rebuilds, if external. * @param direct If true, the output will be used directly by the builder; false if used inside the compiler plugin. * @returns A component bundle result object. */ async bundleFile(entry, externalId, direct) { const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => { return new bundler_context_1.BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => { const buildOptions = (0, bundle_options_1.createStylesheetBundleOptions)(this.options, loadCache); if (externalId) { (0, node_assert_1.default)(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]; } // Angular encapsulation does not support nesting // See: https://github.com/angular/angular/issues/58996 buildOptions.supported ??= {}; buildOptions.supported['nesting'] = false; return buildOptions; }); }); return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, !!externalId, !!direct); } bundleAllFiles(external, direct) { return Promise.all(Array.from(this.#fileContexts.entries()).map(([entry]) => this.bundleFile(entry, external, direct))); } async bundleInline(data, filename, language = this.defaultInlineLanguage, externalId) { // 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 = (0, node_crypto_1.createHash)('sha256') .update(data) .update(externalId ?? '') .digest('hex'); const entry = [language, id, filename].join(';'); const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => { const namespace = 'angular:styles/component'; return new bundler_context_1.BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => { const buildOptions = (0, bundle_options_1.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: node_path_1.default.dirname(filename), }; }); }, }); return buildOptions; }); }); // Extract the result of the bundling from the output files return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, !!externalId, false); } /** * Invalidates both file and inline based component style bundling state for a set of modified files. * @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) { if (!this.incremental) { return; } const normalizedFiles = [...files].map(node_path_1.default.normalize); let entries; for (const [entry, bundler] of this.#fileContexts.entries()) { if (bundler.invalidate(normalizedFiles)) { entries ??= []; entries.push(entry); } } for (const bundler of this.#inlineContexts.values()) { bundler.invalidate(normalizedFiles); } return entries; } collectReferencedFiles() { const files = []; for (const context of this.#fileContexts.values()) { files.push(...context.watchFiles); } return files; } async dispose() { const contexts = [...this.#fileContexts.values(), ...this.#inlineContexts.values()]; this.#fileContexts.clear(); this.#inlineContexts.clear(); await Promise.allSettled(contexts.map((context) => context.dispose())); } extractResult(result, referencedFiles, external, direct) { let contents = ''; const outputFiles = []; const { errors, warnings } = result; if (errors) { return { errors, warnings, referencedFiles, contents: '' }; } for (const outputFile of result.outputFiles) { const filename = node_path_1.default.basename(outputFile.path); if (outputFile.type === bundler_context_1.BuildOutputFileType.Media || filename.endsWith('.css.map')) { // The output files could also contain resources (images/fonts/etc.) that were referenced and the map files. // Clone the output file to avoid amending the original path which would causes problems during rebuild. const clonedOutputFile = outputFile.clone(); // Needed for Bazel as otherwise the files will not be written in the correct place, // this is because esbuild will resolve the output file from the outdir which is currently set to `workspaceRoot` twice, // once in the stylesheet and the other in the application code bundler. // Ex: `../../../../../app.component.css.map`. if (!direct) { clonedOutputFile.path = node_path_1.default.join(this.options.workspaceRoot, outputFile.path); } outputFiles.push(clonedOutputFile); } else if (filename.endsWith('.css')) { if (external) { const clonedOutputFile = outputFile.clone(); if (!direct) { clonedOutputFile.path = node_path_1.default.join(this.options.workspaceRoot, outputFile.path); } outputFiles.push(clonedOutputFile); contents = node_path_1.default.posix.join(this.options.publicPath ?? '', filename); } else { contents = outputFile.text; } } else { throw new Error(`Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`); } } const metafile = result.metafile; // 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['ng-component'] = true; }); return { errors, warnings, contents, outputFiles, metafile, referencedFiles, externalImports: result.externalImports, initialFiles: new Map(), }; } } exports.ComponentStylesheetBundler = ComponentStylesheetBundler;