@@ -3,6 +3,7 @@ import externalizeAllPackagesExcept from 'esbuild-plugin-noexternal';
3
3
import MagicString from 'magic-string' ;
4
4
import path from 'path' ;
5
5
import postcss from 'postcss' ;
6
+ import postcssModules from 'postcss-modules' ;
6
7
import postcssNested from 'postcss-nested' ;
7
8
import postcssScss from 'postcss-scss' ;
8
9
import { ancestor as walk } from 'acorn-walk' ;
@@ -102,14 +103,10 @@ export function ecsstatic(options: Options = {}) {
102
103
103
104
const parsedAst = this . parse ( code ) as ESTree . Program ;
104
105
105
- const {
106
- cssImportName,
107
- scssImportName,
108
- statements : ecsstaticImportStatements ,
109
- } = findEcsstaticImports ( parsedAst ) ;
110
- if ( ecsstaticImportStatements . length === 0 ) return ;
106
+ const ecsstaticImports = findEcsstaticImports ( parsedAst ) ;
107
+ if ( ecsstaticImports . size === 0 ) return ;
111
108
112
- const importNames = [ cssImportName , scssImportName ] . filter ( Boolean ) as string [ ] ;
109
+ const importNames = [ ... ecsstaticImports . keys ( ) ] ;
113
110
114
111
const cssTemplateLiterals = findCssTaggedTemplateLiterals ( parsedAst , importNames ) ;
115
112
if ( cssTemplateLiterals . length === 0 ) return ;
@@ -119,7 +116,8 @@ export function ecsstatic(options: Options = {}) {
119
116
120
117
for ( const node of cssTemplateLiterals ) {
121
118
const { start, end, quasi, tag, _originalName } = node ;
122
- const isScss = tag . type === 'Identifier' && tag . name === scssImportName ;
119
+ const isScss = tag . type === 'Identifier' && ecsstaticImports . get ( tag . name ) ?. isScss ;
120
+ const isModule = tag . type === 'Identifier' && ecsstaticImports . get ( tag . name ) ?. isModule ;
123
121
124
122
// lazy populate inlinedVars until we need it, to delay problems that come with this mess
125
123
if ( quasi . expressions . length && evaluateExpressions && ! inlinedVars ) {
@@ -131,27 +129,34 @@ export function ecsstatic(options: Options = {}) {
131
129
evaluateExpressions && quasi . expressions . length
132
130
? await processTemplateLiteral ( rawTemplate , { inlinedVars } )
133
131
: rawTemplate . slice ( 1 , rawTemplate . length - 2 ) ;
134
- const [ css , className ] = processCss ( templateContents , isScss ) ;
132
+
133
+ // do the scoping!
134
+ const [ css , modulesOrClass ] = await processCss ( templateContents , { isScss, isModule } ) ;
135
+
136
+ let returnValue = '' ; // what we will replace the tagged template literal with
137
+ if ( isModule ) {
138
+ returnValue = JSON . stringify ( modulesOrClass ) ;
139
+ } else {
140
+ returnValue = `"${ modulesOrClass } "` ;
141
+ // add the original variable name in DEV mode
142
+ if ( _originalName && viteConfigObj . command === 'serve' ) {
143
+ returnValue = `"🎈-${ _originalName } ${ modulesOrClass } "` ;
144
+ }
145
+ }
135
146
136
147
// add processed css to a .css file
137
148
const extension = isScss ? 'scss' : 'css' ;
138
- const cssFilename = `${ className } .acab.${ extension } ` . toLowerCase ( ) ;
149
+ const cssFilename = `${ hash ( templateContents . trim ( ) ) } .acab.${ extension } ` . toLowerCase ( ) ;
139
150
magicCode . append ( `import "./${ cssFilename } ";\n` ) ;
140
151
const fullCssPath = normalizePath ( path . join ( path . dirname ( id ) , cssFilename ) ) ;
141
152
cssList . set ( fullCssPath , css ) ;
142
153
143
- // add the original variable name in DEV mode
144
- let _className = `"${ className } "` ;
145
- if ( _originalName && viteConfigObj . command === 'serve' ) {
146
- _className = `"🎈-${ _originalName } ${ className } "` ;
147
- }
148
-
149
- // replace the tagged template literal with the generated className
150
- magicCode . update ( start , end , _className ) ;
154
+ // replace the tagged template literal with the generated class names
155
+ magicCode . update ( start , end , returnValue ) ;
151
156
}
152
157
153
158
// remove ecsstatic imports, we don't need them anymore
154
- ecsstaticImportStatements . forEach ( ( { start, end } ) => magicCode . update ( start , end , '' ) ) ;
159
+ for ( const { start, end } of ecsstaticImports . values ( ) ) magicCode . remove ( start , end ) ;
155
160
156
161
return {
157
162
code : magicCode . toString ( ) ,
@@ -161,11 +166,8 @@ export function ecsstatic(options: Options = {}) {
161
166
} ;
162
167
}
163
168
164
- /**
165
- * processes template strings using postcss and
166
- * returns it along with a hashed classname based on the string contents.
167
- */
168
- function processCss ( templateContents : string , isScss = false ) {
169
+ /** processes css and returns it along with hashed classeses */
170
+ async function processCss ( templateContents : string , { isScss = false , isModule = false } ) {
169
171
const isImportOrUse = ( line : string ) =>
170
172
line . trim ( ) . startsWith ( '@import' ) || line . trim ( ) . startsWith ( '@use' ) ;
171
173
@@ -180,15 +182,37 @@ function processCss(templateContents: string, isScss = false) {
180
182
. join ( '\n' ) ;
181
183
182
184
const className = `🎈-${ hash ( templateContents . trim ( ) ) } ` ;
183
- const unprocessedCss = `${ importsAndUses } \n.${ className } {${ codeWithoutImportsAndUses } }` ;
185
+ const unprocessedCss = isModule
186
+ ? templateContents
187
+ : `${ importsAndUses } \n.${ className } {${ codeWithoutImportsAndUses } }` ;
184
188
185
- const plugins = ! isScss
186
- ? [ postcssNested ( ) , autoprefixer ( autoprefixerOptions ) ]
187
- : [ autoprefixer ( autoprefixerOptions ) ] ;
188
- const options = isScss ? { parser : postcssScss } : { } ;
189
- const { css } = postcss ( plugins ) . process ( unprocessedCss , options ) ;
189
+ const { css, modules } = await postprocessCss ( unprocessedCss , { isScss, isModule } ) ;
190
190
191
- return [ css , className ] ;
191
+ if ( isModule ) {
192
+ return [ css , modules ] as const ;
193
+ }
194
+
195
+ return [ css , className ] as const ;
196
+ }
197
+
198
+ /** runs postcss with autoprefixer and optionally css-modules */
199
+ async function postprocessCss ( rawCss : string , { isScss = false , isModule = false } ) {
200
+ let modules : Record < string , string > = { } ;
201
+
202
+ const plugins = [
203
+ ! isScss && postcssNested ( ) ,
204
+ autoprefixer ( autoprefixerOptions ) ,
205
+ isModule &&
206
+ postcssModules ( {
207
+ generateScopedName : '🎈-[local]-[hash:base64:6]' ,
208
+ getJSON : ( _ , json ) => void ( modules = json ) ,
209
+ } ) ,
210
+ ] . flatMap ( ( value ) => ( value ? [ value ] : [ ] ) ) ;
211
+
212
+ const options = isScss ? { parser : postcssScss , from : undefined } : { from : undefined } ;
213
+ const { css } = await postcss ( plugins ) . process ( rawCss , options ) ;
214
+
215
+ return { css, modules } ;
192
216
}
193
217
194
218
/** resolves all expressions in the template literal and returns a plain string */
@@ -204,28 +228,32 @@ async function processTemplateLiteral(rawTemplate: string, { inlinedVars = '' })
204
228
205
229
/** parses ast and returns info about all css/scss ecsstatic imports */
206
230
function findEcsstaticImports ( ast : ESTree . Program ) {
207
- let cssImportName : string | undefined ;
208
- let scssImportName : string | undefined ;
209
- let statements : Array < { start : number ; end : number } > = [ ] ;
231
+ const statements = new Map <
232
+ string ,
233
+ { isScss : boolean ; isModule : boolean ; start : number ; end : number }
234
+ > ( ) ;
210
235
211
236
for ( const node of ast . body . filter ( ( node ) => node . type === 'ImportDeclaration' ) ) {
212
- if ( node . type === 'ImportDeclaration' && node . source . value === '@acab/ecsstatic' ) {
237
+ if (
238
+ node . type === 'ImportDeclaration' &&
239
+ node . source . value ?. toString ( ) . startsWith ( '@acab/ecsstatic' )
240
+ ) {
241
+ const isModule = node . source . value ?. toString ( ) . endsWith ( 'modules' ) ;
213
242
const { start, end } = node ;
214
- if ( node . specifiers . some ( ( { imported } : any ) => [ 'css' , 'scss' ] . includes ( imported . name ) ) ) {
215
- statements . push ( { start, end } ) ;
216
- }
217
243
node . specifiers . forEach ( ( specifier ) => {
218
- if ( specifier . type === 'ImportSpecifier' && specifier . imported . name === 'css' ) {
219
- cssImportName = specifier . local . name ;
220
- }
221
- if ( specifier . type === 'ImportSpecifier' && specifier . imported . name === 'scss' ) {
222
- scssImportName = specifier . local . name ;
244
+ if (
245
+ specifier . type === 'ImportSpecifier' &&
246
+ [ 'css' , 'scss' ] . includes ( specifier . imported . name )
247
+ ) {
248
+ const tagName = specifier . local . name ;
249
+ const isScss = specifier . imported . name === 'scss' ;
250
+ statements . set ( tagName , { isScss, isModule, start, end } ) ;
223
251
}
224
252
} ) ;
225
253
}
226
254
}
227
255
228
- return { cssImportName , scssImportName , statements } ;
256
+ return statements ;
229
257
}
230
258
231
259
/**
@@ -330,25 +358,29 @@ function findCssTaggedTemplateLiterals(ast: ESTree.Program, tagNames: string[])
330
358
function loadDummyEcsstatic ( ) {
331
359
const hashStr = hash . toString ( ) ;
332
360
const getHashFromTemplateStr = getHashFromTemplate . toString ( ) ;
333
- const contents = `${ hashStr } \n${ getHashFromTemplateStr } \n
361
+ const indexContents = `${ hashStr } \n${ getHashFromTemplateStr } \n
334
362
export const css = getHashFromTemplate;
335
363
export const scss = getHashFromTemplate;
336
364
` ;
365
+ const modulesContents = `new Proxy({}, {
366
+ get() { throw 'please don't do this. css modules are hard to evaluate inside other strings :(' }
367
+ })` ;
337
368
338
369
return < esbuild . Plugin > {
339
370
name : 'load-dummy-ecsstatic' ,
340
371
setup ( build ) {
341
372
build . onResolve ( { filter : / ^ @ a c a b \/ e c s s t a t i c $ / } , ( args ) => {
342
- return {
343
- namespace : 'ecsstatic' ,
344
- path : args . path ,
345
- } ;
373
+ return { namespace : 'ecsstatic' , path : args . path } ;
346
374
} ) ;
347
375
build . onLoad ( { filter : / ( .* ) / , namespace : 'ecsstatic' } , ( ) => {
348
- return {
349
- contents,
350
- loader : 'js' ,
351
- } ;
376
+ return { contents : indexContents , loader : 'js' } ;
377
+ } ) ;
378
+
379
+ build . onResolve ( { filter : / ^ @ a c a b \/ e c s s t a t i c \/ m o d u l e s $ / } , ( args ) => {
380
+ return { namespace : 'ecsstatic-modules' , path : args . path } ;
381
+ } ) ;
382
+ build . onLoad ( { filter : / ( .* ) / , namespace : 'ecsstatic-modules' } , ( ) => {
383
+ return { contents : modulesContents , loader : 'js' } ;
352
384
} ) ;
353
385
} ,
354
386
} ;
0 commit comments