From ebfc2a3717651ddd993e80de616d1c1b387adafe Mon Sep 17 00:00:00 2001 From: KCM Date: Wed, 21 Jan 2026 09:01:42 -0600 Subject: [PATCH 1/5] fix: walk dependency graph in combined imports to collect transitive styles. --- packages/css/package.json | 2 +- packages/css/src/loaderBridge.ts | 78 ++++++++++++++++++- .../components/button.module.scss | 3 + .../bridge-graph/components/button.tsx | 3 + .../css/test/fixtures/bridge-graph/entry.tsx | 3 + packages/css/test/loaderBridge.test.ts | 29 +++++++ packages/playwright/package.json | 2 +- 7 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 packages/css/test/fixtures/bridge-graph/components/button.module.scss create mode 100644 packages/css/test/fixtures/bridge-graph/components/button.tsx create mode 100644 packages/css/test/fixtures/bridge-graph/entry.tsx diff --git a/packages/css/package.json b/packages/css/package.json index b1c75fd..40f6bc1 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/css", - "version": "1.1.0-rc.6", + "version": "1.1.0-rc.7", "description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.", "type": "module", "main": "./dist/css.js", diff --git a/packages/css/src/loaderBridge.ts b/packages/css/src/loaderBridge.ts index f5d2d06..827598a 100644 --- a/packages/css/src/loaderBridge.ts +++ b/packages/css/src/loaderBridge.ts @@ -14,6 +14,7 @@ import { shouldEmitCombinedDefault, TYPES_QUERY_FLAG, } from './loaderInternals.js' +import { collectStyleImports } from './moduleGraph.js' export interface KnightedCssBridgeLoaderOptions { emitCssModules?: boolean @@ -25,6 +26,12 @@ type BridgeModuleLike = { } const DEFAULT_EXPORT_NAME = 'knightedCss' +const CSS_MODULE_EXTENSIONS = [ + '.module.css', + '.module.scss', + '.module.sass', + '.module.less', +] const loader: LoaderDefinitionFunction = function loader( source, @@ -42,10 +49,8 @@ export const pitch: PitchLoaderDefinitionFunction { - const cssRequests = collectCssModuleRequests(source).map(request => - buildBridgeCssRequest(request), - ) + .then(async source => { + const cssRequests = await collectCombinedCssRequests(this, source) const upstreamRequest = buildUpstreamRequest(resolvedRemainingRequest) callback( null, @@ -124,6 +129,71 @@ function readResourceSource( }) } +async function collectCombinedCssRequests( + ctx: LoaderContext, + source: string, +): Promise { + const directSpecifiers = collectCssModuleRequests(source) + const graphImports = await collectCssModuleImports(ctx) + const graphRequests = graphImports.map(filePath => buildBridgeCssRequest(filePath)) + const graphPaths = new Set(graphImports.map(filePath => path.resolve(filePath))) + const directRequests = directSpecifiers + .map(specifier => { + const [resource, query] = specifier.split('?') + if (query) { + return buildBridgeCssRequest(specifier) + } + const resolved = resolveCssModuleSpecifier(resource, ctx.resourcePath) + if (resolved && graphPaths.has(resolved)) { + return undefined + } + return buildBridgeCssRequest(specifier) + }) + .filter((request): request is string => Boolean(request)) + return dedupeRequests([...graphRequests, ...directRequests]) +} + +async function collectCssModuleImports( + ctx: LoaderContext, +): Promise { + const cwd = ctx.rootContext ?? path.dirname(ctx.resourcePath) + const filter = (filePath: string) => !filePath.includes('node_modules') + try { + return await collectStyleImports(ctx.resourcePath, { + cwd, + styleExtensions: CSS_MODULE_EXTENSIONS, + filter, + }) + } catch { + return [] + } +} + +function resolveCssModuleSpecifier( + specifier: string, + importer: string, +): string | undefined { + if (!specifier) return undefined + if (specifier.startsWith('.')) { + return path.resolve(path.dirname(importer), specifier) + } + if (path.isAbsolute(specifier)) { + return path.resolve(specifier) + } + return undefined +} + +function dedupeRequests(requests: string[]): string[] { + const seen = new Set() + const output: string[] = [] + for (const request of requests) { + if (seen.has(request)) continue + seen.add(request) + output.push(request) + } + return output +} + function collectCssModuleRequests(source: string): string[] { const matches = new Set() const importPattern = diff --git a/packages/css/test/fixtures/bridge-graph/components/button.module.scss b/packages/css/test/fixtures/bridge-graph/components/button.module.scss new file mode 100644 index 0000000..dcffeaa --- /dev/null +++ b/packages/css/test/fixtures/bridge-graph/components/button.module.scss @@ -0,0 +1,3 @@ +.button { + background: hotpink; +} diff --git a/packages/css/test/fixtures/bridge-graph/components/button.tsx b/packages/css/test/fixtures/bridge-graph/components/button.tsx new file mode 100644 index 0000000..814d6e2 --- /dev/null +++ b/packages/css/test/fixtures/bridge-graph/components/button.tsx @@ -0,0 +1,3 @@ +import styles from './button.module.scss' + +export const Button = () => diff --git a/packages/css/test/fixtures/bridge-graph/entry.tsx b/packages/css/test/fixtures/bridge-graph/entry.tsx new file mode 100644 index 0000000..33e32f9 --- /dev/null +++ b/packages/css/test/fixtures/bridge-graph/entry.tsx @@ -0,0 +1,3 @@ +import { Button } from './components/button.js' + +export const Entry = () =>