From 63bc1bf9fac9cdaf4472808613483933835686e4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 01:09:57 +0000 Subject: [PATCH 1/9] fix: bundle CSS is not injected properly --- packages/bundle/tsup.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/bundle/tsup.config.ts b/packages/bundle/tsup.config.ts index 58bff54be9..6357d7ea2f 100644 --- a/packages/bundle/tsup.config.ts +++ b/packages/bundle/tsup.config.ts @@ -66,7 +66,10 @@ export default defineConfig([ }, esbuildPlugins: [ ...(commonConfig.esbuildPlugins ?? []), - injectCSSPlugin({ stylesPlaceholder: bundleStyleContentPlaceholder }), + injectCSSPlugin({ + stylesPlaceholder: bundleStyleContentPlaceholder, + getCSSText: (_source, cssFiles) => cssFiles.find(({ path }) => path.endsWith('botframework-webchat.css'))?.text + }), resolveReact ], format: 'iife', From fc9ba68f63347a825daf263cd018e71b9f08b8e3 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 01:13:53 +0000 Subject: [PATCH 2/9] Chnglg --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c2686b7a..1761653ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,7 +147,7 @@ Breaking changes in this release: - New debug API, by [@compulim](https://github.com/compulim) in PR [#5663](https://github.com/microsoft/BotFramework-WebChat/pull/5663) and PR [#5664](https://github.com/microsoft/BotFramework-WebChat/pull/5664), see [`DEBUGGING.md`](docs/DEBUGGING.md) for more - Debug into element: open F12, select the subject in Element pane, type `$0.webChat.debugger` - Breakpoint: open F12, select the subject in Element pane, type `$0.webChat.breakpoint.incomingActivity` -- The `botframework-webchat` package now uses CSS modules for styling purposes, in PR [#5666](https://github.com/microsoft/BotFramework-WebChat/pull/5666), by [@OEvgeny](https://github.com/OEvgeny) +- The `botframework-webchat` package now uses CSS modules for styling purposes, in PR [#5666](https://github.com/microsoft/BotFramework-WebChat/pull/5666), in PR [#5677](https://github.com/microsoft/BotFramework-WebChat/pull/5677) by [@OEvgeny](https://github.com/OEvgeny) - 👷🏻 Added `npm run build-browser` script for building test harness package only, in PR [#5667](https://github.com/microsoft/BotFramework-WebChat/pull/5667), by [@compulim](https://github.com/compulim) ### Changed From 0e412e5cbec34051cf8c68022b8ee7418979efa2 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 01:35:19 +0000 Subject: [PATCH 3/9] Add vg rule and migrate other --- packages/api/package.json | 2 +- packages/bundle/package.json | 7 +++++-- packages/core/package.json | 2 +- packages/fluent-theme/package.json | 3 ++- packages/vibe-grep/src/rules/css-inject.yaml | 14 ++++++++++++++ 5 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 packages/vibe-grep/src/rules/css-inject.yaml diff --git a/packages/api/package.json b/packages/api/package.json index c302546fc0..6e6fea218c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -78,7 +78,7 @@ "build": "npm run --if-present build:pre && npm run build:run && npm run --if-present build:post", "build:post": "npm run build:post:dtsroll && npm run build:post:validate:dts", "build:post:dtsroll": "dtsroll ./dist/*.d.*", - "build:post:validate:dts": "if grep -q -P '@msinternal\\/' dist/*.d.* 2>/dev/null; then echo \"Error: dist/*.d.* is not compiled by dtsroll\" >&2; exit 1; fi", + "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch && npm run build:pre:globalize", "build:pre:globalize": "node scripts/createPrecompiledGlobalize.mjs", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", diff --git a/packages/bundle/package.json b/packages/bundle/package.json index 4d0ff7c2a5..5df3471162 100644 --- a/packages/bundle/package.json +++ b/packages/bundle/package.json @@ -100,9 +100,12 @@ }, "scripts": { "build": "npm run --if-present build:pre && npm run build:run && npm run --if-present build:post", - "build:post": "npm run build:post:dtsroll && npm run build:post:validate:dts", + "build:post": "npm run build:post:dtsroll && npm run build:post:validate", "build:post:dtsroll": "dtsroll ./dist/*.d.*", - "build:post:validate:dts": "if grep -q -P '@msinternal\\/' dist/*.d.* 2>/dev/null; then echo \"Error: dist/*.d.* is not compiled by dtsroll\" >&2; exit 1; fi", + "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", + "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", + "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", "build:pre:watch": "../../scripts/npm/build-watch.sh", diff --git a/packages/core/package.json b/packages/core/package.json index d9cb31eaba..72ddd1c3e8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -80,7 +80,7 @@ "build": "npm run --if-present build:pre && npm run build:run && npm run --if-present build:post", "build:post": "npm run build:post:dtsroll && npm run build:post:validate:dts", "build:post:dtsroll": "dtsroll ./dist/*.d.*", - "build:post:validate:dts": "if grep -q -P '@msinternal\\/' dist/*.d.* 2>/dev/null; then echo \"Error: dist/*.d.* is not compiled by dtsroll\" >&2; exit 1; fi", + "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", "build:pre:watch": "../../scripts/npm/build-watch.sh", diff --git a/packages/fluent-theme/package.json b/packages/fluent-theme/package.json index 2482963648..abf73b2639 100644 --- a/packages/fluent-theme/package.json +++ b/packages/fluent-theme/package.json @@ -43,8 +43,9 @@ "build": "npm run --if-present build:pre && npm run build:run && npm run --if-present build:post", "build:post": "npm run build:post:dtsroll && npm run build:post:validate", "build:post:dtsroll": "dtsroll ./dist/*.d.*", - "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:dts", + "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", diff --git a/packages/vibe-grep/src/rules/css-inject.yaml b/packages/vibe-grep/src/rules/css-inject.yaml new file mode 100644 index 0000000000..7a9a877480 --- /dev/null +++ b/packages/vibe-grep/src/rules/css-inject.yaml @@ -0,0 +1,14 @@ +--- + +# This rule checks that CSS is injected properly by verifying there are no placeholders + +id: css-inject-placeholder +language: JavaScript +rule: + kind: string + regex: "\\@\\-\\-[A-Z\\-]+\\-\\-\\@" + +description: "Ensure CSS placeholders are replaced during bundling" +message: "Found CSS placeholder. Ensure CSS is properly injected during bundling." +severity: error +args: [dist/*.js dist/*.mjs static/*.js] From b879fa2dc52b3e29bdcc335e952d3acd9a848bb9 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 20:49:21 +0000 Subject: [PATCH 4/9] Improve css root lookup --- packages/bundle/esbuild.static.mjs | 2 +- packages/bundle/tsup.config.ts | 16 +- packages/component/package.json | 4 +- packages/component/tsup.config.ts | 3 +- packages/fluent-theme/esbuild.static.mjs | 1 + packages/fluent-theme/tsup.config.ts | 4 +- .../src/build/private/injectCSSPlugin.ts | 254 +++++++++++++++--- 7 files changed, 234 insertions(+), 50 deletions(-) diff --git a/packages/bundle/esbuild.static.mjs b/packages/bundle/esbuild.static.mjs index 235a55d6e7..863974e53e 100644 --- a/packages/bundle/esbuild.static.mjs +++ b/packages/bundle/esbuild.static.mjs @@ -83,7 +83,7 @@ const BASE_CONFIG = { plugins: [ cssPlugin, injectCSSPlugin({ - getCSSText: (_source, cssFiles) => cssFiles.find(({ path }) => path.endsWith('botframework-webchat.css'))?.text, + ignoreCSSEntries: ['static/botframework-webchat/component.css'], stylesPlaceholder: bundleStyleContentPlaceholder }), { diff --git a/packages/bundle/tsup.config.ts b/packages/bundle/tsup.config.ts index 6357d7ea2f..f10dedd7dc 100644 --- a/packages/bundle/tsup.config.ts +++ b/packages/bundle/tsup.config.ts @@ -47,6 +47,13 @@ const commonConfig = applyConfig(config => ({ // The way `microsoft-cognitiveservices-speech-sdk` imported the `uuid` package (in their `Guid.js`) is causing esbuild/tsup to proxy require() into __require() for dynamic loading. // Webpack 4 cannot statically analyze the code and failed with error "Critical dependency: require function is used in a way in which dependencies cannot be statically extracted". 'uuid' + ], + esbuildPlugins: [ + ...(config.esbuildPlugins ?? []), + injectCSSPlugin({ + ignoreCSSEntries: ['dist/botframework-webchat.component.css'], + stylesPlaceholder: bundleStyleContentPlaceholder, + }) ] })); @@ -64,14 +71,7 @@ export default defineConfig([ 'webchat-es5': './src/boot/iife/webchat-es5.ts', 'webchat-minimal': './src/boot/iife/webchat-minimal.ts' }, - esbuildPlugins: [ - ...(commonConfig.esbuildPlugins ?? []), - injectCSSPlugin({ - stylesPlaceholder: bundleStyleContentPlaceholder, - getCSSText: (_source, cssFiles) => cssFiles.find(({ path }) => path.endsWith('botframework-webchat.css'))?.text - }), - resolveReact - ], + esbuildPlugins: [...(commonConfig.esbuildPlugins ?? []), resolveReact], format: 'iife', outExtension() { return { js: '.js' }; diff --git a/packages/component/package.json b/packages/component/package.json index 3ce5a4df1e..6d5d9aac57 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -76,8 +76,10 @@ "homepage": "https://github.com/microsoft/BotFramework-WebChat/tree/main/packages/component#readme", "scripts": { "build": "npm run --if-present build:pre && npm run build:run && npm run --if-present build:post", - "build:post": "npm run build:post:dtsroll && npm run build:post:validate:css && npm run build:post:validate:dts", + "build:post": "npm run build:post:dtsroll && npm run build:post:validate", "build:post:dtsroll": "dtsroll ./dist/*.d.*", + "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", diff --git a/packages/component/tsup.config.ts b/packages/component/tsup.config.ts index 9ff33e0e85..23963b16b0 100644 --- a/packages/component/tsup.config.ts +++ b/packages/component/tsup.config.ts @@ -19,8 +19,7 @@ const commonConfig = applyConfig(config => ({ injectCSSPlugin({ // esbuild does not fully support CSS code splitting, every entry point has its own CSS file. // Related to https://github.com/evanw/esbuild/issues/608. - getCSSText: (_source, cssFiles) => - cssFiles.find(({ path }) => path.endsWith('botframework-webchat-component.component.css'))?.text, + ignoreCSSEntries: ['dist/botframework-webchat-component.component.css'], stylesPlaceholder: componentStyleContentPlaceholder }), injectCSSPlugin({ stylesPlaceholder: decoratorStyleContentPlaceholder }) diff --git a/packages/fluent-theme/esbuild.static.mjs b/packages/fluent-theme/esbuild.static.mjs index 2d18829063..305cbb1a23 100644 --- a/packages/fluent-theme/esbuild.static.mjs +++ b/packages/fluent-theme/esbuild.static.mjs @@ -51,6 +51,7 @@ const config = { format: 'esm', loader: { '.js': 'jsx' }, minify: true, + metafile: true, outdir: resolve(fileURLToPath(import.meta.url), `../static/`), platform: 'browser', plugins: [ diff --git a/packages/fluent-theme/tsup.config.ts b/packages/fluent-theme/tsup.config.ts index 2786f66cee..4dff24e2e6 100644 --- a/packages/fluent-theme/tsup.config.ts +++ b/packages/fluent-theme/tsup.config.ts @@ -102,7 +102,9 @@ export default defineConfig([ entry: { 'botframework-webchat-fluent-theme.production.min': './src/bundle.ts' }, esbuildPlugins: [ ...(config.esbuildPlugins ?? []), - injectCSSPlugin({ stylesPlaceholder: fluentStyleContentPlaceholder }), + injectCSSPlugin({ + stylesPlaceholder: fluentStyleContentPlaceholder + }), umdResolvePlugin ], format: 'iife', diff --git a/packages/styles/src/build/private/injectCSSPlugin.ts b/packages/styles/src/build/private/injectCSSPlugin.ts index d69e2ad098..b1ca449f3c 100644 --- a/packages/styles/src/build/private/injectCSSPlugin.ts +++ b/packages/styles/src/build/private/injectCSSPlugin.ts @@ -1,8 +1,10 @@ +/* eslint-disable complexity */ import { decode, encode } from '@jridgewell/sourcemap-codec'; +import path from 'node:path'; import type { OutputFile, Plugin } from 'esbuild'; export interface InjectCSSPluginOptions { - getCSSText?: ((source: OutputFile, cssFiles: OutputFile[]) => string | undefined | void) | undefined; + ignoreCSSEntries?: string[]; stylesPlaceholder: string; } @@ -20,63 +22,241 @@ function updateMappings(encoded: string, startIndex: number, offset: number) { return encode(mappings); } -export default function injectCSSPlugin({ getCSSText, stylesPlaceholder }: InjectCSSPluginOptions): Plugin { - if (!stylesPlaceholder) { - throw new Error('inject-css-plugin: no placeholder for styles provided'); +type Metafile = { + outputs: Record< + string, + { + entryPoint?: string; + imports?: Array<{ + path: string; + kind?: string; + external?: boolean; + }>; + } + >; +}; + +export function mapOutputsToRootOutputs(metafile: Metafile): { + roots: string[]; + outputToRoots: Map; +} { + const outputs = metafile.outputs ?? {}; + const outFiles = Object.keys(outputs); + + const rootSet = new Set(); + for (const outKey of outFiles) { + // eslint-disable-next-line security/detect-object-injection + if (outputs[outKey]?.entryPoint) { + rootSet.add(outKey); + } } + const roots = [...rootSet].sort(); + + const outputKeySet = new Set(outFiles); + + const adj = new Map(); + for (const outKey of outFiles) { + // eslint-disable-next-line security/detect-object-injection + const imps = outputs[outKey]?.imports ?? []; + const list: string[] = []; + + for (const imp of imps) { + if (!imp || imp.external) { + continue; + } + + const raw = imp.path; + let target: string | null = null; + + if (outputKeySet.has(raw)) { + target = raw; + } else { + const resolved = path.posix.normalize(path.posix.resolve(path.posix.dirname(outKey), raw)); + if (outputKeySet.has(resolved)) { + target = resolved; + } + } - getCSSText = - getCSSText || - ((source, cssFiles) => { - const entryName = source.path.replace(/(\.js|\.mjs)$/u, ''); - const css = cssFiles.find(f => f.path.replace(/(\.css)$/u, '') === entryName); + if (target) { + list.push(target); + } + } + + adj.set(outKey, list); + } + + const outToRootSet = new Map>(); + for (const outKey of outFiles) { + outToRootSet.set(outKey, new Set()); + } + + for (const rootKey of roots) { + const stack: string[] = [rootKey]; + const seen = new Set(); + + while (stack.length) { + const cur = stack.pop()!; + if (seen.has(cur)) { + continue; + } + seen.add(cur); + + outToRootSet.get(cur)?.add(rootKey); + + const nexts = adj.get(cur); + if (!nexts || nexts.length === 0) { + continue; + } + + for (const n of nexts) { + if (!seen.has(n)) { + stack.push(n); + } + } + } + } + + const outputToRoots: Map = new Map(); + for (const outKey of outFiles) { + const s = outToRootSet.get(outKey) ?? new Set(); + outputToRoots.set(outKey, Object.freeze([...s].sort())); + } + + return { roots, outputToRoots }; +} - return css?.text; - }); +function findOutputKeyForFile(filePath: string, outputToRoots: Map): string | undefined { + const fp = path.normalize(filePath); + + // output keys in esbuild metafile are relative e.g. "dist/chunk-XYZ.js" + for (const outKey of outputToRoots.keys()) { + const k1 = path.normalize(outKey); // "dist/chunk-XYZ.js" + const k2 = path.normalize(path.join(path.sep, outKey)); // "/dist/chunk-XYZ.js" + if (fp.endsWith(k1) || fp.endsWith(k2)) { + return outKey; + } + } + + return undefined; +} + +function diffSets(self: Set, other: Set): Set { + const result = new Set(); + for (const element of self) { + if (!other.has(element)) { + result.add(element); + } + } + return result; +} + +export default function injectCSSPlugin({ ignoreCSSEntries, stylesPlaceholder }: InjectCSSPluginOptions): Plugin { + if (!stylesPlaceholder) { + throw new Error('inject-css-plugin: no placeholder for styles provided'); + } const stylesPlaceholderQuoted = JSON.stringify(stylesPlaceholder); + const ignoreCSSEntriesSet = new Set(ignoreCSSEntries); + return { name: `inject-css-plugin(${stylesPlaceholder})`, setup(build) { - build.onEnd(({ outputFiles = [] }) => { + if (build.initialOptions.metafile) { + build.initialOptions.metafile = true; + } + + build.onEnd(({ outputFiles = [], metafile }) => { const cssFiles = outputFiles.filter(({ path }) => path.match(/(\.css)$/u)); + const jsFiles = outputFiles.filter(({ path }) => path.match(/(\.js|\.mjs)$/u)); + + const jsToCssMap = new Map( + cssFiles + .map(cssFile => { + const jsFilePath = jsFiles.find( + jsFile => jsFile.path.replace(/(\.js|\.mjs)$/u, '') === cssFile.path.replace(/(\.css)$/u, '') + )?.path; + if (!jsFilePath) { + return; + } + return [jsFilePath, cssFile] as const; + }) + .filter((entry): entry is readonly [string, OutputFile] => entry !== undefined) + ); + + if (!metafile) { + throw new Error('inject-css-plugin: metafile is required for proper CSS injection'); + } + + const { outputToRoots } = mapOutputsToRootOutputs(metafile); for (const file of outputFiles) { if (file.path.match(/(\.js|\.mjs)$/u)) { - const cssText = getCSSText(file, cssFiles); const jsText = file?.text; - if (cssText && jsText?.includes(stylesPlaceholderQuoted)) { - const index = jsText.indexOf(stylesPlaceholderQuoted); - const map = outputFiles.find(f => f.path.replace(/(\.map)$/u, '') === file.path); + const shouldProccess = jsText?.includes(stylesPlaceholderQuoted); - const updatedJsText = [ - jsText.slice(0, index), - JSON.stringify(cssText), - jsText.slice(index + stylesPlaceholderQuoted.length) - ].join(''); + if (!shouldProccess) { + continue; + } - file.contents = Buffer.from(updatedJsText); + const outKey = findOutputKeyForFile(file.path, outputToRoots); + const owners = (outKey && outputToRoots.get(outKey)) || []; + const cssFilesMap = new Map( + owners + ?.map(owner => { + const cssFile = jsToCssMap.get(path.join(process.cwd(), owner)); + if (!cssFile) { + return; + } + const cssKey = cssFile ? path.relative(process.cwd(), cssFile.path) : undefined; + return [cssKey, cssFile] as const; + }) + .filter((entry): entry is readonly [string, OutputFile] => entry !== undefined) + ); - // eslint-disable-next-line no-magic-numbers - if (updatedJsText.indexOf(stylesPlaceholder) !== -1) { - throw new Error( - `Duplicate placeholders are not supported.\nFound ${stylesPlaceholder} in ${file.path}.` - ); - } + const cssCandidateKeys = diffSets(new Set(cssFilesMap.keys()), ignoreCSSEntriesSet); - if (map) { - const parsed = JSON.parse(map.text); + if (cssCandidateKeys.size !== 1) { + throw new Error( + `inject-css-plugin: unable to uniquely determine CSS for ${outKey}. Found CSS entries: \n[\n${[ + ...cssCandidateKeys + ] + .map(entry => ` '${entry}'`) + .join(',\n')}\n]\n Add the appropriate CSS file to ignoreCSSEntries to fix this issue.` + ); + } - parsed.mappings = updateMappings( - parsed.mappings, - index, - cssText.length - stylesPlaceholderQuoted.length - ); + const cssText = cssFilesMap.get(Array.from(cssCandidateKeys).at(0)!)?.text; - map.contents = Buffer.from(JSON.stringify(parsed)); - } + if (!cssText) { + throw new Error( + `inject-css-plugin: unable to find CSS text for ${outKey}.${ignoreCSSEntries ? '\n The following entries were ignored:\n' + ignoreCSSEntries.map(entry => ` '${entry}'`).join('\n') : ''}` + ); + } + + const index = jsText.indexOf(stylesPlaceholderQuoted); + const map = outputFiles.find(f => f.path.replace(/(\.map)$/u, '') === file.path); + + const updatedJsText = [ + jsText.slice(0, index), + JSON.stringify(cssText), + jsText.slice(index + stylesPlaceholderQuoted.length) + ].join(''); + + file.contents = Buffer.from(updatedJsText); + + // eslint-disable-next-line no-magic-numbers + if (updatedJsText.indexOf(stylesPlaceholder) !== -1) { + throw new Error(`Duplicate placeholders are not supported.\nFound ${stylesPlaceholder} in ${file.path}.`); + } + + if (map) { + const parsed = JSON.parse(map.text); + + parsed.mappings = updateMappings(parsed.mappings, index, cssText.length - stylesPlaceholderQuoted.length); + + map.contents = Buffer.from(JSON.stringify(parsed)); } } } From d3447931b3faa31429af1b8660e27644a550d136 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 21:23:01 +0000 Subject: [PATCH 5/9] Simplify outputs lookup --- .../src/build/private/injectCSSPlugin.ts | 111 +++++++----------- 1 file changed, 43 insertions(+), 68 deletions(-) diff --git a/packages/styles/src/build/private/injectCSSPlugin.ts b/packages/styles/src/build/private/injectCSSPlugin.ts index b1ca449f3c..e54ef6e96f 100644 --- a/packages/styles/src/build/private/injectCSSPlugin.ts +++ b/packages/styles/src/build/private/injectCSSPlugin.ts @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { decode, encode } from '@jridgewell/sourcemap-codec'; import path from 'node:path'; import type { OutputFile, Plugin } from 'esbuild'; @@ -27,102 +26,78 @@ type Metafile = { string, { entryPoint?: string; - imports?: Array<{ - path: string; - kind?: string; - external?: boolean; - }>; + imports?: Array<{ path: string; kind?: string; external?: boolean }>; } >; }; -export function mapOutputsToRootOutputs(metafile: Metafile): { - roots: string[]; - outputToRoots: Map; -} { - const outputs = metafile.outputs ?? {}; - const outFiles = Object.keys(outputs); - - const rootSet = new Set(); - for (const outKey of outFiles) { - // eslint-disable-next-line security/detect-object-injection - if (outputs[outKey]?.entryPoint) { - rootSet.add(outKey); - } - } - const roots = [...rootSet].sort(); +export function mapOutputsToRootOutputs(metafile: Metafile) { + const outputs = metafile.outputs || {}; + const allFiles = new Set(Object.keys(outputs)); - const outputKeySet = new Set(outFiles); + const roots: string[] = []; + const graph = new Map(); + + for (const [file, meta] of Object.entries(outputs)) { + if (meta.entryPoint) { + roots.push(file); + } - const adj = new Map(); - for (const outKey of outFiles) { - // eslint-disable-next-line security/detect-object-injection - const imps = outputs[outKey]?.imports ?? []; - const list: string[] = []; + const edges: string[] = []; + const imports = meta.imports || []; - for (const imp of imps) { - if (!imp || imp.external) { + for (const imp of imports) { + if (imp.external) { continue; } - const raw = imp.path; - let target: string | null = null; - - if (outputKeySet.has(raw)) { - target = raw; + if (allFiles.has(imp.path)) { + edges.push(imp.path); } else { - const resolved = path.posix.normalize(path.posix.resolve(path.posix.dirname(outKey), raw)); - if (outputKeySet.has(resolved)) { - target = resolved; + const resolved = path.posix.normalize(path.posix.resolve(path.posix.dirname(file), imp.path)); + if (allFiles.has(resolved)) { + edges.push(resolved); } } - - if (target) { - list.push(target); - } } - - adj.set(outKey, list); + graph.set(file, edges); } - const outToRootSet = new Map>(); - for (const outKey of outFiles) { - outToRootSet.set(outKey, new Set()); - } + roots.sort(); - for (const rootKey of roots) { - const stack: string[] = [rootKey]; - const seen = new Set(); + const outputToRoots = new Map>(); - while (stack.length) { - const cur = stack.pop()!; - if (seen.has(cur)) { - continue; - } - seen.add(cur); + for (const file of allFiles) { + outputToRoots.set(file, new Set()); + } - outToRootSet.get(cur)?.add(rootKey); + for (const root of roots) { + const stack = [root]; + const visited = new Set(); - const nexts = adj.get(cur); - if (!nexts || nexts.length === 0) { + while (stack.length > 0) { + const node = stack.pop()!; + + if (visited.has(node)) { continue; } + visited.add(node); - for (const n of nexts) { - if (!seen.has(n)) { - stack.push(n); - } + outputToRoots.get(node)?.add(root); + + const children = graph.get(node); + if (children) { + stack.push(...children); } } } - const outputToRoots: Map = new Map(); - for (const outKey of outFiles) { - const s = outToRootSet.get(outKey) ?? new Set(); - outputToRoots.set(outKey, Object.freeze([...s].sort())); + const result = new Map(); + for (const [file, rootSet] of outputToRoots) { + result.set(file, Object.freeze([...rootSet].sort())); } - return { roots, outputToRoots }; + return { roots, outputToRoots: result }; } function findOutputKeyForFile(filePath: string, outputToRoots: Map): string | undefined { From 4c75685a0af06b877af821c34532c979277f5d5f Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 21:38:42 +0000 Subject: [PATCH 6/9] Cleanup --- packages/styles/src/build/private/injectCSSPlugin.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/styles/src/build/private/injectCSSPlugin.ts b/packages/styles/src/build/private/injectCSSPlugin.ts index e54ef6e96f..f2e54cb408 100644 --- a/packages/styles/src/build/private/injectCSSPlugin.ts +++ b/packages/styles/src/build/private/injectCSSPlugin.ts @@ -137,10 +137,6 @@ export default function injectCSSPlugin({ ignoreCSSEntries, stylesPlaceholder }: return { name: `inject-css-plugin(${stylesPlaceholder})`, setup(build) { - if (build.initialOptions.metafile) { - build.initialOptions.metafile = true; - } - build.onEnd(({ outputFiles = [], metafile }) => { const cssFiles = outputFiles.filter(({ path }) => path.match(/(\.css)$/u)); const jsFiles = outputFiles.filter(({ path }) => path.match(/(\.js|\.mjs)$/u)); From 6f3bc8a0e3fb7fcf5ab5b435fbb8c30a35e408ea Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 23:26:17 +0000 Subject: [PATCH 7/9] Use Set.difference --- .../styles/src/build/private/injectCSSPlugin.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/styles/src/build/private/injectCSSPlugin.ts b/packages/styles/src/build/private/injectCSSPlugin.ts index f2e54cb408..9573e1a25c 100644 --- a/packages/styles/src/build/private/injectCSSPlugin.ts +++ b/packages/styles/src/build/private/injectCSSPlugin.ts @@ -31,7 +31,7 @@ type Metafile = { >; }; -export function mapOutputsToRootOutputs(metafile: Metafile) { +function mapOutputsToRootOutputs(metafile: Metafile) { const outputs = metafile.outputs || {}; const allFiles = new Set(Object.keys(outputs)); @@ -115,16 +115,6 @@ function findOutputKeyForFile(filePath: string, outputToRoots: Map(self: Set, other: Set): Set { - const result = new Set(); - for (const element of self) { - if (!other.has(element)) { - result.add(element); - } - } - return result; -} - export default function injectCSSPlugin({ ignoreCSSEntries, stylesPlaceholder }: InjectCSSPluginOptions): Plugin { if (!stylesPlaceholder) { throw new Error('inject-css-plugin: no placeholder for styles provided'); @@ -186,7 +176,7 @@ export default function injectCSSPlugin({ ignoreCSSEntries, stylesPlaceholder }: .filter((entry): entry is readonly [string, OutputFile] => entry !== undefined) ); - const cssCandidateKeys = diffSets(new Set(cssFilesMap.keys()), ignoreCSSEntriesSet); + const cssCandidateKeys = new Set(cssFilesMap.keys()).difference(ignoreCSSEntriesSet); if (cssCandidateKeys.size !== 1) { throw new Error( From b129b997cbfe675a413365ec8bb14fea6d00ac33 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 23:28:01 +0000 Subject: [PATCH 8/9] Sort --- packages/bundle/package.json | 2 +- packages/component/package.json | 2 +- packages/fluent-theme/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bundle/package.json b/packages/bundle/package.json index 5df3471162..5022e2ce4a 100644 --- a/packages/bundle/package.json +++ b/packages/bundle/package.json @@ -104,8 +104,8 @@ "build:post:dtsroll": "dtsroll ./dist/*.d.*", "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", - "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", "build:pre:watch": "../../scripts/npm/build-watch.sh", diff --git a/packages/component/package.json b/packages/component/package.json index 6d5d9aac57..e49902471c 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -79,9 +79,9 @@ "build:post": "npm run build:post:dtsroll && npm run build:post:validate", "build:post:dtsroll": "dtsroll ./dist/*.d.*", "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", - "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", "build:pre:watch": "../../scripts/npm/build-watch.sh", diff --git a/packages/fluent-theme/package.json b/packages/fluent-theme/package.json index abf73b2639..36dee197c4 100644 --- a/packages/fluent-theme/package.json +++ b/packages/fluent-theme/package.json @@ -45,8 +45,8 @@ "build:post:dtsroll": "dtsroll ./dist/*.d.*", "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", - "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", "build:pre:watch": "../../scripts/npm/build-watch.sh", From b8518745bd72f5884bf0e08a56e8f8392a3d809e Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 21 Jan 2026 23:38:15 +0000 Subject: [PATCH 9/9] Fix warning --- packages/component/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/component/package.json b/packages/component/package.json index e49902471c..ad7610253c 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -81,7 +81,7 @@ "build:post:validate": "npm run build:post:validate:css && npm run build:post:validate:inject-css && npm run build:post:validate:dts", "build:post:validate:css": "vg ast-check lightning-css ./dist/*.css", "build:post:validate:dts": "vg ast-check dist-types ./dist/*.d.*", - "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs static/*.js", + "build:post:validate:inject-css": "vg ast-check css-inject dist/*.js dist/*.mjs", "build:pre": "npm run build:pre:local-dependencies && npm run build:pre:watch", "build:pre:local-dependencies": "../../scripts/npm/build-local-dependencies.sh", "build:pre:watch": "../../scripts/npm/build-watch.sh",