@@ -14,6 +14,7 @@ import {
1414 shouldEmitCombinedDefault ,
1515 TYPES_QUERY_FLAG ,
1616} from './loaderInternals.js'
17+ import { collectTransitiveStyleImports } from './styleGraph.js'
1718
1819export interface KnightedCssBridgeLoaderOptions {
1920 emitCssModules ?: boolean
@@ -25,6 +26,7 @@ type BridgeModuleLike = {
2526}
2627
2728const DEFAULT_EXPORT_NAME = 'knightedCss'
29+ const BRIDGE_STYLE_EXTENSIONS = [ '.css' , '.scss' , '.sass' , '.less' , '.css.ts' ]
2830
2931const loader : LoaderDefinitionFunction < KnightedCssBridgeLoaderOptions > = function loader (
3032 source ,
@@ -37,15 +39,13 @@ export const pitch: PitchLoaderDefinitionFunction<KnightedCssBridgeLoaderOptions
3739 const resolvedRemainingRequest = resolveRemainingRequest ( this , remainingRequest )
3840
3941 if ( isJsLikeResource ( this . resourcePath ) && hasCombinedQuery ( this . resourceQuery ) ) {
40- const callback = this . async ( )
42+ const callback = getAsyncCallback ( this )
4143 if ( ! callback ) {
4244 return createCombinedJsBridgeModuleSync ( resolvedRemainingRequest )
4345 }
4446 readResourceSource ( this )
45- . then ( source => {
46- const cssRequests = collectCssModuleRequests ( source ) . map ( request =>
47- buildBridgeCssRequest ( request ) ,
48- )
47+ . then ( async source => {
48+ const cssRequests = await collectBridgeStyleRequests ( this , source )
4949 const upstreamRequest = buildUpstreamRequest ( resolvedRemainingRequest )
5050 callback (
5151 null ,
@@ -59,6 +59,41 @@ export const pitch: PitchLoaderDefinitionFunction<KnightedCssBridgeLoaderOptions
5959 . catch ( error => callback ( error as Error ) )
6060 return
6161 }
62+ const callback = getAsyncCallback ( this )
63+ if ( ! callback ) {
64+ const localsRequest = buildProxyRequest ( this )
65+ const upstreamRequest = buildUpstreamRequest ( resolvedRemainingRequest )
66+ const { emitCssModules } = resolveLoaderOptions ( this )
67+ const combined = hasCombinedQuery ( this . resourceQuery )
68+ const skipSyntheticDefault = hasNamedOnlyQueryFlag ( this . resourceQuery )
69+
70+ if ( hasQueryFlag ( this . resourceQuery , TYPES_QUERY_FLAG ) ) {
71+ emitKnightedWarning (
72+ this ,
73+ 'The bridge loader does not generate stableSelectors. Remove the "types" query flag.' ,
74+ )
75+ }
76+
77+ const emitDefault = combined
78+ ? shouldEmitCombinedDefault ( {
79+ detection : 'unknown' ,
80+ request : localsRequest ,
81+ skipSyntheticDefault,
82+ } )
83+ : false
84+
85+ const resolvedUpstream = upstreamRequest || localsRequest
86+ const resolvedLocals = upstreamRequest || localsRequest
87+
88+ return createBridgeModule ( {
89+ localsRequest : resolvedLocals ,
90+ upstreamRequest : resolvedUpstream ,
91+ combined,
92+ emitDefault,
93+ emitCssModules,
94+ } )
95+ }
96+
6297 const localsRequest = buildProxyRequest ( this )
6398 const upstreamRequest = buildUpstreamRequest ( resolvedRemainingRequest )
6499 const { emitCssModules } = resolveLoaderOptions ( this )
@@ -83,13 +118,27 @@ export const pitch: PitchLoaderDefinitionFunction<KnightedCssBridgeLoaderOptions
83118 const resolvedUpstream = upstreamRequest || localsRequest
84119 const resolvedLocals = upstreamRequest || localsRequest
85120
86- return createBridgeModule ( {
87- localsRequest : resolvedLocals ,
88- upstreamRequest : resolvedUpstream ,
89- combined,
90- emitDefault,
91- emitCssModules,
92- } )
121+ const collectSource = isJsLikeResource ( this . resourcePath )
122+ ? readResourceSource ( this )
123+ : Promise . resolve ( undefined )
124+
125+ collectSource
126+ . then ( async source => {
127+ const cssRequests = await collectBridgeStyleRequests ( this , source )
128+ callback (
129+ null ,
130+ createBridgeModule ( {
131+ localsRequest : resolvedLocals ,
132+ upstreamRequest : resolvedUpstream ,
133+ combined,
134+ emitDefault,
135+ emitCssModules,
136+ cssRequests,
137+ } ) ,
138+ )
139+ } )
140+ . catch ( error => callback ( error as Error ) )
141+ return
93142 }
94143; ( loader as LoaderDefinitionFunction & { pitch ?: typeof pitch } ) . pitch = pitch
95144
@@ -106,6 +155,12 @@ function resolveLoaderOptions(
106155 }
107156}
108157
158+ function getAsyncCallback (
159+ ctx : LoaderContext < KnightedCssBridgeLoaderOptions > ,
160+ ) : ( ( error : Error | null , result ?: string ) => void ) | undefined {
161+ return typeof ctx . async === 'function' ? ctx . async ( ) : undefined
162+ }
163+
109164function readResourceSource (
110165 ctx : LoaderContext < KnightedCssBridgeLoaderOptions > ,
111166) : Promise < string > {
@@ -124,10 +179,80 @@ function readResourceSource(
124179 } )
125180}
126181
127- function collectCssModuleRequests ( source : string ) : string [ ] {
182+ async function collectBridgeStyleRequests (
183+ ctx : LoaderContext < KnightedCssBridgeLoaderOptions > ,
184+ source ?: string ,
185+ ) : Promise < string [ ] > {
186+ const graphImports = await collectStyleGraphImports ( ctx )
187+ const graphPaths = new Set ( graphImports . map ( filePath => path . resolve ( filePath ) ) )
188+ const graphRequests = graphImports
189+ . filter ( filePath => path . resolve ( filePath ) !== path . resolve ( ctx . resourcePath ) )
190+ . map ( filePath => buildBridgeCssRequest ( filePath ) )
191+
192+ if ( ! source ) {
193+ return dedupeRequests ( graphRequests )
194+ }
195+
196+ const directSpecifiers = collectStyleImportSpecifiers ( source )
197+ const directRequests = directSpecifiers
198+ . map ( specifier => {
199+ const [ resource , query ] = specifier . split ( '?' )
200+ if ( query ) {
201+ return buildBridgeCssRequest ( specifier )
202+ }
203+ const resolved = resolveStyleSpecifier ( resource , ctx . resourcePath )
204+ if ( resolved && graphPaths . has ( resolved ) ) {
205+ return undefined
206+ }
207+ return buildBridgeCssRequest ( specifier )
208+ } )
209+ . filter ( ( request ) : request is string => Boolean ( request ) )
210+
211+ return dedupeRequests ( [ ...graphRequests , ...directRequests ] )
212+ }
213+
214+ async function collectStyleGraphImports (
215+ ctx : LoaderContext < KnightedCssBridgeLoaderOptions > ,
216+ ) : Promise < string [ ] > {
217+ const cwd = ctx . rootContext ?? path . dirname ( ctx . resourcePath )
218+ const filter = ( filePath : string ) => ! filePath . includes ( 'node_modules' )
219+ try {
220+ return await collectTransitiveStyleImports ( ctx . resourcePath , {
221+ cwd,
222+ styleExtensions : BRIDGE_STYLE_EXTENSIONS ,
223+ filter,
224+ } )
225+ } catch {
226+ return [ ]
227+ }
228+ }
229+
230+ function resolveStyleSpecifier ( specifier : string , importer : string ) : string | undefined {
231+ if ( ! specifier ) return undefined
232+ if ( specifier . startsWith ( '.' ) ) {
233+ return path . resolve ( path . dirname ( importer ) , specifier )
234+ }
235+ if ( path . isAbsolute ( specifier ) ) {
236+ return path . resolve ( specifier )
237+ }
238+ return undefined
239+ }
240+
241+ function dedupeRequests ( requests : string [ ] ) : string [ ] {
242+ const seen = new Set < string > ( )
243+ const output : string [ ] = [ ]
244+ for ( const request of requests ) {
245+ if ( seen . has ( request ) ) continue
246+ seen . add ( request )
247+ output . push ( request )
248+ }
249+ return output
250+ }
251+
252+ function collectStyleImportSpecifiers ( source : string ) : string [ ] {
128253 const matches = new Set < string > ( )
129254 const importPattern =
130- / (?: i m p o r t | e x p o r t ) \s + (?: [ ^ ' " \n ] + \s + f r o m \s + ) ? [ ' " ] ( [ ^ ' " \n ] + ?\. m o d u l e \. (?: c s s | s c s s | s a s s | l e s s ) (?: \? [ ^ ' " \n ] + ) ? ) [ ' " ] / g
255+ / (?: i m p o r t | e x p o r t ) \s + (?: [ ^ ' " \n ] + \s + f r o m \s + ) ? [ ' " ] ( [ ^ ' " \n ] + ?\. (?: c s s | s c s s | s a s s | l e s s | c s s \. t s ) (?: \? [ ^ ' " \n ] + ) ? ) [ ' " ] / g
131256 let match : RegExpExecArray | null
132257 while ( ( match = importPattern . exec ( source ) ) ) {
133258 if ( match [ 1 ] ) {
@@ -167,11 +292,15 @@ function createCombinedJsBridgeModule(options: CombinedJsBridgeOptions): string
167292 const upstreamLiteral = JSON . stringify ( options . upstreamRequest )
168293 const cssImports = options . cssRequests . map ( ( request , index ) => {
169294 const literal = JSON . stringify ( request )
170- return `import { knightedCss as __knightedCss ${ index } , knightedCssModules as __knightedCssModules ${ index } } from ${ literal } ;`
295+ return `import * as __knightedStyle ${ index } from ${ literal } ;`
171296 } )
172- const cssValues = options . cssRequests . map ( ( _ , index ) => `__knightedCss${ index } ` )
173- const cssModulesValues = options . cssRequests . map (
174- ( _ , index ) => `__knightedCssModules${ index } ` ,
297+ const cssValues = options . cssRequests . map (
298+ ( _ , index ) => `__knightedStyle${ index } .knightedCss` ,
299+ )
300+ const cssModulesValues = options . cssRequests . map ( ( request , index ) =>
301+ isCssModuleRequest ( request )
302+ ? `__knightedStyle${ index } .knightedCssModules`
303+ : 'undefined' ,
175304 )
176305 const lines = [
177306 `import * as __knightedUpstream from ${ upstreamLiteral } ;` ,
@@ -259,26 +388,42 @@ interface BridgeModuleOptions {
259388 combined : boolean
260389 emitDefault : boolean
261390 emitCssModules : boolean
391+ cssRequests ?: string [ ]
262392}
263393
264394function createBridgeModule ( options : BridgeModuleOptions ) : string {
265395 const localsLiteral = JSON . stringify ( options . localsRequest )
266396 const upstreamLiteral = JSON . stringify ( options . upstreamRequest )
397+ const cssRequests = options . cssRequests ?? [ ]
398+ const cssImports = cssRequests . map ( ( request , index ) => {
399+ const literal = JSON . stringify ( request )
400+ return `import * as __knightedStyle${ index } from ${ literal } ;`
401+ } )
402+ const cssValues = cssRequests . map ( ( _ , index ) => `__knightedStyle${ index } .knightedCss` )
403+ const cssModulesValues = cssRequests . map ( ( request , index ) =>
404+ isCssModuleRequest ( request )
405+ ? `__knightedStyle${ index } .knightedCssModules`
406+ : 'undefined' ,
407+ )
267408 const lines = [
268409 `import * as __knightedLocals from ${ localsLiteral } ;` ,
269410 `import * as __knightedUpstream from ${ upstreamLiteral } ;` ,
411+ ...cssImports ,
270412 `const __knightedDefault =\ntypeof __knightedUpstream.default !== 'undefined'\n ? __knightedUpstream.default\n : __knightedUpstream;` ,
271413 `const __knightedResolveCss = ${ resolveCssText . toString ( ) } ;` ,
272414 `const __knightedResolveCssModules = ${ resolveCssModules . toString ( ) } ;` ,
273415 `const __knightedUpstreamLocals =\n __knightedResolveCssModules(__knightedUpstream, __knightedUpstream);` ,
274416 `const __knightedLocalsExport =\n __knightedUpstreamLocals ??\n __knightedResolveCssModules(__knightedLocals, __knightedLocals) ??\n __knightedLocals;` ,
275- `const __knightedCss = __knightedResolveCss(__knightedDefault, __knightedUpstream);` ,
417+ `const __knightedBaseCss = __knightedResolveCss(__knightedDefault, __knightedUpstream);` ,
418+ `const __knightedCss = [__knightedBaseCss, ${ cssValues . join ( ', ' ) } ].filter(Boolean).join('\\n');` ,
276419 `export const ${ DEFAULT_EXPORT_NAME } = __knightedCss;` ,
277420 ]
278421
279422 if ( options . emitCssModules ) {
280423 lines . push (
281- `const __knightedCssModules = __knightedLocalsExport ?? __knightedResolveCssModules(\n __knightedDefault,\n __knightedUpstream,\n);` ,
424+ `const __knightedCssModules = Object.assign({}, ...[__knightedLocalsExport ?? __knightedResolveCssModules(\n __knightedDefault,\n __knightedUpstream,\n), ${ cssModulesValues . join (
425+ ', ' ,
426+ ) } ].filter(Boolean));`,
282427 'export const knightedCssModules = __knightedCssModules;' ,
283428 )
284429 }
@@ -305,6 +450,12 @@ function buildUpstreamRequest(remainingRequest?: string): string {
305450 return request
306451}
307452
453+ function isCssModuleRequest ( request : string ) : boolean {
454+ const [ resource ] = request . split ( '?' )
455+ const lower = resource . toLowerCase ( )
456+ return / \. m o d u l e \. ( c s s | s c s s | s a s s | l e s s | c s s \. t s ) $ / . test ( lower )
457+ }
458+
308459function buildProxyRequest ( ctx : LoaderContext < KnightedCssBridgeLoaderOptions > ) : string {
309460 const sanitizedQuery = buildSanitizedQuery ( ctx . resourceQuery )
310461 const rawRequest = getRawRequest ( ctx )
@@ -449,7 +600,7 @@ function emitKnightedWarning(
449600}
450601
451602export const __loaderBridgeInternals = {
452- collectCssModuleRequests ,
603+ collectStyleImportSpecifiers ,
453604 buildBridgeCssRequest,
454605 createCombinedJsBridgeModule,
455606 isJsLikeResource,
0 commit comments