@@ -2,51 +2,78 @@ import type { Plugin, OutputChunk, NormalizedOutputOptions, OutputBundle, Output
22import type JavaScriptTypes from '@ast-grep/napi/lang/JavaScript' ;
33import type { Kinds } from '@ast-grep/napi/types/staticTypes' ;
44import { Lang , parse , type SgNode } from '@ast-grep/napi' ;
5+ import { basename } from 'node:path' ;
56
67type CSSFiles = Set < string > | undefined ;
78
89type NodePos = SgNode < JavaScriptTypes > | undefined ;
910
1011/**
11- * @name extractCssImports
12- * @description Extract CSS imports from source code
12+ * @name extractStyleImports
13+ * @description Extract CSS imports (including CSS Modules) from source code
1314 * @example
14- * const s = 'import "./index.css";'
15- * const arr = extractCssImports (s) // ["./index.css"]
15+ * const s = 'import "./index.css"; import styles from "./Button.module.css"; '
16+ * const arr = extractStyleImports (s) // ["./index.css", "./Button.module .css"]
1617 * @param code - The source code to analyze
1718 * @returns An array of CSS import paths
1819 */
19- const extractCssImports = ( code : string ) : string [ ] => {
20- const cssImports : string [ ] = [ ] ;
20+ const extractStyleImports = ( code : string ) : string [ ] => {
21+ const styleImports : string [ ] = [ ] ;
2122
22- // Match CSS import statements
23- // Handles: import './style.css', import 'style.css', import './style.css?inline'
24- const importRegex = / i m p o r t \s + ( [ ' " ] ) ( .* ?\. c s s (?: \? [ ^ ' " ] * ) ? ) \1/ g;
23+ // Match CSS file extensions (including CSS Modules)
24+ const cssExtensions = / \. c s s (?: \? [ ^ ' " ] * ) ? / ;
25+
26+ // Pattern 1: import './style.css' or import 'style.css'
27+ const sideEffectImportRegex = / i m p o r t \s + ( [ ' " ] ) ( .* ?) \1/ g;
28+
29+ // Pattern 2: import styles from './style.module.css'
30+ const namedImportRegex = / i m p o r t \s + (?: \* \s + a s \s + ) ? ( \w + ) \s + f r o m \s + ( [ ' " ] ) ( .* ?) \2/ g;
31+
32+ // Pattern 3: import { something } from './style.css'
33+ const destructuredImportRegex = / i m p o r t \s + { [ ^ } ] + } \s + f r o m \s + ( [ ' " ] ) ( .* ?) \1/ g;
34+
35+ // Check side-effect imports
2536 let match ;
37+ while ( ( match = sideEffectImportRegex . exec ( code ) ) !== null ) {
38+ const importPath = match [ 2 ] ;
39+ if ( cssExtensions . test ( importPath ) ) {
40+ styleImports . push ( importPath ) ;
41+ }
42+ }
2643
27- while ( ( match = importRegex . exec ( code ) ) !== null ) {
28- const cssPath = match [ 2 ] ;
29- cssImports . push ( cssPath ) ;
44+ // Check named imports (e.g., CSS modules)
45+ while ( ( match = namedImportRegex . exec ( code ) ) !== null ) {
46+ const importPath = match [ 3 ] ;
47+ if ( cssExtensions . test ( importPath ) ) {
48+ styleImports . push ( importPath ) ;
49+ }
3050 }
3151
32- return cssImports ;
52+ // Check destructured imports
53+ while ( ( match = destructuredImportRegex . exec ( code ) ) !== null ) {
54+ const importPath = match [ 2 ] ;
55+ if ( cssExtensions . test ( importPath ) ) {
56+ styleImports . push ( importPath ) ;
57+ }
58+ }
59+
60+ return styleImports ;
3361} ;
3462
3563/**
3664 * @description Inject CSS files at the top of each generated chunk file for tsdown builds.
3765 * @return {Plugin } A Rolldown plugin to inject CSS imports into library chunks.
3866 */
39- export const injectCssPlugin = ( ) : Plugin => {
40- // Track CSS imports per module
41- const cssImportMap = new Map < string , string [ ] > ( ) ;
67+ const injectCssPlugin = ( ) : Plugin => {
68+ // Track style imports per module
69+ const styleImportMap = new Map < string , string [ ] > ( ) ;
4270 // Track which modules are included in which chunks
4371 const moduleToChunkMap = new Map < string , string > ( ) ;
4472
4573 return {
4674 name : 'tsdown:lib-inject-css' ,
4775
4876 // Set default config for better library bundling
49- // Not sure if this is required
5077 outputOptions ( outputOptions : OutputOptions ) : OutputOptions {
5178 // Prevent hoisting transitive imports to avoid tree-shaking issues
5279 if ( typeof outputOptions . hoistTransitiveImports !== 'boolean' ) {
@@ -59,17 +86,17 @@ export const injectCssPlugin = (): Plugin => {
5986 return outputOptions ;
6087 } ,
6188
62- // Capture CSS imports before they're stripped by the build
89+ // Capture style imports before they're stripped by the build
6390 transform ( code , id ) {
6491 // Only process TypeScript/JavaScript files (ignore .d.ts files)
6592 if ( ! / \. ( t s x ? | j s x ? ) $ / . test ( id ) ) {
6693 return null ;
6794 }
6895
69- const cssImports = extractCssImports ( code ) ;
96+ const styleImports = extractStyleImports ( code ) ;
7097
71- if ( cssImports . length > 0 ) {
72- cssImportMap . set ( id , cssImports ) ;
98+ if ( styleImports . length > 0 ) {
99+ styleImportMap . set ( id , styleImports ) ;
73100 }
74101
75102 return null ;
@@ -94,9 +121,10 @@ export const injectCssPlugin = (): Plugin => {
94121 }
95122
96123 // Build a map of chunk -> CSS files
124+ // This aggregates ALL style imports from ALL modules in each chunk
97125 const chunkCssMap = new Map < string , Set < string > > ( ) ;
98126
99- for ( const [ moduleId , cssImports ] of cssImportMap . entries ( ) ) {
127+ for ( const [ moduleId , styleImports ] of styleImportMap . entries ( ) ) {
100128 const chunkName = moduleToChunkMap . get ( moduleId ) ;
101129
102130 if ( chunkName ) {
@@ -105,13 +133,29 @@ export const injectCssPlugin = (): Plugin => {
105133 }
106134
107135 const chunkCss = chunkCssMap . get ( chunkName ) ! ;
108- for ( const cssImport of cssImports ) {
109- // Normalize CSS import path to match the output file name
110- const normalizedPath = cssImport . replace ( / ^ \. \/ / , '' ) ;
111-
112- // Only add CSS files have been bundled
113- if ( outputCssFiles . has ( normalizedPath ) ) {
114- chunkCss . add ( normalizedPath ) ;
136+ for ( const styleImport of styleImports ) {
137+ // Remove query parameters
138+ const cleanPath = styleImport . split ( '?' ) [ 0 ] ;
139+
140+ // Get the base filename
141+ const fileName = basename ( cleanPath ) ;
142+
143+ // Try to find matching CSS file in output
144+ const possibleMatches = Array . from ( outputCssFiles ) . filter ( ( cssFile ) => {
145+ const cssBaseName = basename ( cssFile ) ;
146+
147+ // Exact filename match (including .module.css files)
148+ return cssBaseName === fileName ;
149+ } ) ;
150+
151+ // If we found exact matches, add them
152+ if ( possibleMatches . length > 0 ) {
153+ possibleMatches . forEach ( ( match ) => chunkCss . add ( match ) ) ;
154+ } else if ( outputCssFiles . size === 1 ) {
155+ // If there's only one CSS file in the output, assume all styles
156+ // are bundled into it (common case for libraries)
157+ const [ singleCssFile ] = Array . from ( outputCssFiles ) ;
158+ chunkCss . add ( singleCssFile ) ;
115159 }
116160 }
117161 }
@@ -175,122 +219,12 @@ export const injectCssPlugin = (): Plugin => {
175219 code = code . slice ( 0 , position ) + injections . join ( '\n' ) + '\n' + code . slice ( position ) ;
176220 }
177221
178- // Update code and sourcemap
222+ // Update code
179223 outputChunk . code = code ;
180224 }
181225 }
182-
183- // generateBundle(options: NormalizedOutputOptions, bundle: OutputBundle) {
184- // // First, get all CSS files that actually exist in the output bundle
185- // const outputCssFiles = new Set<string>();
186- // for (const file of Object.keys(bundle)) {
187- // if (file.endsWith('.css')) {
188- // outputCssFiles.add(file);
189- // }
190- // }
191- //
192- // console.log('BOSH: CSS files in output bundle:', Array.from(outputCssFiles));
193- //
194- // // Build a map of chunk -> CSS files
195- // const chunkCssMap = new Map<string, Set<string>>();
196- //
197- // for (const [moduleId, cssImports] of cssImportMap.entries()) {
198- // const chunkName = moduleToChunkMap.get(moduleId);
199- //
200- // if (chunkName) {
201- // if (!chunkCssMap.has(chunkName)) {
202- // chunkCssMap.set(chunkName, new Set());
203- // }
204- //
205- // const chunkCss = chunkCssMap.get(chunkName)!;
206- // for (const cssImport of cssImports) {
207- // // Normalize the CSS import path to match output file names
208- // const normalizedPath = cssImport.replace(/^\.\//, '');
209- //
210- // // Only add CSS files that actually exist in the output bundle
211- // if (outputCssFiles.has(normalizedPath)) {
212- // chunkCss.add(normalizedPath);
213- // console.log(`BOSH: Adding CSS ${normalizedPath} to chunk ${chunkName}`);
214- // } else {
215- // console.log(
216- // `BOSH: Skipping CSS ${normalizedPath} (not in output bundle, likely bundled into another CSS file)`
217- // );
218- // }
219- // }
220- // }
221- // }
222- //
223- // console.log('BOSH: Chunk CSS Map:', Array.from(chunkCssMap.entries()));
224- //
225- // // Inject CSS imports into chunks
226- // for (const chunk of Object.values(bundle)) {
227- // if (chunk.type !== 'chunk') {
228- // continue;
229- // }
230- //
231- // const outputChunk = chunk as OutputChunk;
232- //
233- // // Skip non-JavaScript files (like .d.ts files)
234- // if (
235- // !outputChunk.fileName.endsWith('.js') &&
236- // !outputChunk.fileName.endsWith('.mjs') &&
237- // !outputChunk.fileName.endsWith('.cjs')
238- // ) {
239- // continue;
240- // }
241- //
242- // const cssFiles = chunkCssMap.get(outputChunk.fileName);
243- //
244- // if (!cssFiles || cssFiles.size === 0) {
245- // console.log(`BOSH: No CSS imports for ${outputChunk.fileName}`);
246- // continue;
247- // }
248- //
249- // console.log(`BOSH: Injecting ${cssFiles.size} CSS imports into ${outputChunk.fileName}`);
250- //
251- // // Find the position to inject CSS imports
252- // const node = parse<JavaScriptTypes>(Lang.JavaScript, outputChunk.code)
253- // .root()
254- // .children()
255- // .find((node) => !excludeTokens.includes(node.kind()));
256- //
257- // const position = node?.range().start.index ?? 0;
258- //
259- // // Inject CSS imports at the top of the chunk
260- // let code = outputChunk.code;
261- // const injections: string[] = [];
262- //
263- // for (const cssFileName of cssFiles) {
264- // // Resolve the CSS file path relative to the chunk
265- // let cssFilePath = cssFileName;
266- //
267- // // If it's a relative import, keep it relative
268- // if (cssFilePath.startsWith('./') || cssFilePath.startsWith('../')) {
269- // // Already relative, use as-is
270- // } else {
271- // // Make it relative
272- // cssFilePath = `./${cssFilePath}`;
273- // }
274- //
275- // const injection = options.format === 'es' ? `import '${cssFilePath}';` : `require('${cssFilePath}');`;
276- //
277- // injections.push(injection);
278- // }
279- //
280- // if (injections.length > 0) {
281- // code = code.slice(0, position) + injections.join('\n') + '\n' + code.slice(position);
282- // }
283- //
284- // // Update code and sourcemap
285- // outputChunk.code = code;
286- //
287- // if (sourcemap && options.sourcemap) {
288- // const ms = new MagicString(code);
289- // outputChunk.map = ms.generateMap({ hires: 'boundary' }) as any;
290- // }
291- //
292- // console.log(`BOSH: Successfully injected CSS into ${outputChunk.fileName}`);
293- // }
294- // }
295226 } ;
296227} ;
228+
229+ export default injectCssPlugin ;
230+ export { injectCssPlugin } ;
0 commit comments