From 3a1bf5c8a52d6ec1eb337f0937bf073de2ea0b62 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 10 Jun 2024 10:14:33 +0000 Subject: [PATCH] fix(@angular/build): Initiate PostCSS only once Previously, PostCSS was initialized three times (once for each preprocessor), resulting in plugins being applied multiple times to each instance. This issue occured due to a race condition triggered by multiple esbuild plugins. Fixes #27804 (cherry picked from commit f102f815e404bcc2f627b7a52e92b3385eb9be5f) --- .../stylesheets/stylesheet-plugin-factory.ts | 104 +++++++++--------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts index 0ef09ec4dbd9..8c021d8e8e6a 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts @@ -96,68 +96,22 @@ export interface StylesheetLanguage { const postcssProcessors = new Map>(); export class StylesheetPluginFactory { - private postcssProcessor?: PostcssProcessor; - constructor( private readonly options: StylesheetPluginOptions, private readonly cache?: LoadResultCache, ) {} create(language: Readonly): Plugin { + const { cache, options, setupPostcss } = this; + // Return a noop plugin if no load actions are required - if ( - !language.process && - !this.options.postcssConfiguration && - !this.options.tailwindConfiguration - ) { + if (!language.process && !options.postcssConfiguration && !options.tailwindConfiguration) { return { name: 'angular-' + language.name, setup() {}, }; } - const { cache, options } = this; - const setupPostcss = async () => { - // Return already created processor if present - if (this.postcssProcessor) { - return this.postcssProcessor; - } - - if (options.postcssConfiguration) { - const postCssInstanceKey = JSON.stringify(options.postcssConfiguration); - - this.postcssProcessor = postcssProcessors.get(postCssInstanceKey)?.deref(); - - if (!this.postcssProcessor) { - postcss ??= (await import('postcss')).default; - this.postcssProcessor = postcss(); - - for (const [pluginName, pluginOptions] of options.postcssConfiguration.plugins) { - const { default: plugin } = await import(pluginName); - if (typeof plugin !== 'function' || plugin.postcss !== true) { - throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`); - } - this.postcssProcessor.use(plugin(pluginOptions)); - } - - postcssProcessors.set(postCssInstanceKey, new WeakRef(this.postcssProcessor)); - } - } else if (options.tailwindConfiguration) { - const { package: tailwindPackage, file: config } = options.tailwindConfiguration; - const postCssInstanceKey = tailwindPackage + ':' + config; - this.postcssProcessor = postcssProcessors.get(postCssInstanceKey)?.deref(); - - if (!this.postcssProcessor) { - postcss ??= (await import('postcss')).default; - const tailwind = await import(tailwindPackage); - this.postcssProcessor = postcss().use(tailwind.default({ config })); - postcssProcessors.set(postCssInstanceKey, new WeakRef(this.postcssProcessor)); - } - } - - return this.postcssProcessor; - }; - return { name: 'angular-' + language.name, async setup(build) { @@ -165,7 +119,7 @@ export class StylesheetPluginFactory { let postcssProcessor: PostcssProcessor | undefined; build.onStart(async () => { try { - postcssProcessor = await setupPostcss(); + postcssProcessor = await setupPostcss; } catch { return { errors: [ @@ -229,6 +183,56 @@ export class StylesheetPluginFactory { }, }; } + + private setupPostcssPromise: Promise | undefined; + private get setupPostcss(): Promise { + return (this.setupPostcssPromise ??= this.initPostcss()); + } + + private initPostcssCallCount = 0; + /** + * This method should not be called directly. + * Use {@link setupPostcss} instead. + */ + private async initPostcss(): Promise { + assert.equal(++this.initPostcssCallCount, 1, '`initPostcss` was called more than once.'); + + const { options } = this; + if (options.postcssConfiguration) { + const postCssInstanceKey = JSON.stringify(options.postcssConfiguration); + let postcssProcessor = postcssProcessors.get(postCssInstanceKey)?.deref(); + + if (!postcssProcessor) { + postcss ??= (await import('postcss')).default; + postcssProcessor = postcss(); + for (const [pluginName, pluginOptions] of options.postcssConfiguration.plugins) { + const { default: plugin } = await import(pluginName); + if (typeof plugin !== 'function' || plugin.postcss !== true) { + throw new Error(`Attempted to load invalid Postcss plugin: "${pluginName}"`); + } + + postcssProcessor.use(plugin(pluginOptions)); + } + + postcssProcessors.set(postCssInstanceKey, new WeakRef(postcssProcessor)); + } + + return postcssProcessor; + } else if (options.tailwindConfiguration) { + const { package: tailwindPackage, file: config } = options.tailwindConfiguration; + const postCssInstanceKey = tailwindPackage + ':' + config; + let postcssProcessor = postcssProcessors.get(postCssInstanceKey)?.deref(); + + if (!postcssProcessor) { + postcss ??= (await import('postcss')).default; + const tailwind = await import(tailwindPackage); + postcssProcessor = postcss().use(tailwind.default({ config })); + postcssProcessors.set(postCssInstanceKey, new WeakRef(postcssProcessor)); + } + + return postcssProcessor; + } + } } async function processStylesheet(