@@ -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' ;
@@ -107,6 +108,7 @@ export function ecsstatic(options: Options = {}) {
107
108
for ( const node of cssTemplateLiterals ) {
108
109
const { start, end, quasi, tag, _originalName } = node ;
109
110
const isScss = tag . type === 'Identifier' && ecsstaticImports . get ( tag . name ) ?. isScss ;
111
+ const isModule = tag . type === 'Identifier' && ecsstaticImports . get ( tag . name ) ?. isModule ;
110
112
111
113
// lazy populate inlinedVars until we need it, to delay problems that come with this mess
112
114
if ( quasi . expressions . length && ! inlinedVars ) {
@@ -117,23 +119,30 @@ export function ecsstatic(options: Options = {}) {
117
119
const templateContents = quasi . expressions . length
118
120
? await processTemplateLiteral ( rawTemplate , { inlinedVars } )
119
121
: rawTemplate . slice ( 1 , rawTemplate . length - 2 ) ;
120
- const [ css , className ] = processCss ( templateContents , isScss ) ;
122
+
123
+ // do the scoping!
124
+ const [ css , modulesOrClass ] = await processCss ( templateContents , { isScss, isModule } ) ;
125
+
126
+ let returnValue = '' ; // what we will replace the tagged template literal with
127
+ if ( isModule ) {
128
+ returnValue = JSON . stringify ( modulesOrClass ) ;
129
+ } else {
130
+ returnValue = `"${ modulesOrClass } "` ;
131
+ // add the original variable name in DEV mode
132
+ if ( _originalName && viteConfigObj . command === 'serve' ) {
133
+ returnValue = `"🎈-${ _originalName } ${ modulesOrClass } "` ;
134
+ }
135
+ }
121
136
122
137
// add processed css to a .css file
123
138
const extension = isScss ? 'scss' : 'css' ;
124
- const cssFilename = `${ className } .acab.${ extension } ` . toLowerCase ( ) ;
139
+ const cssFilename = `${ hash ( templateContents . trim ( ) ) } .acab.${ extension } ` . toLowerCase ( ) ;
125
140
magicCode . append ( `import "./${ cssFilename } ";\n` ) ;
126
141
const fullCssPath = normalizePath ( path . join ( path . dirname ( id ) , cssFilename ) ) ;
127
142
cssList . set ( fullCssPath , css ) ;
128
143
129
- // add the original variable name in DEV mode
130
- let _className = `"${ className } "` ;
131
- if ( _originalName && viteConfigObj . command === 'serve' ) {
132
- _className = `"🎈-${ _originalName } ${ className } "` ;
133
- }
134
-
135
- // replace the tagged template literal with the generated className
136
- magicCode . update ( start , end , _className ) ;
144
+ // replace the tagged template literal with the generated class names
145
+ magicCode . update ( start , end , returnValue ) ;
137
146
}
138
147
139
148
// remove ecsstatic imports, we don't need them anymore
@@ -147,11 +156,8 @@ export function ecsstatic(options: Options = {}) {
147
156
} ;
148
157
}
149
158
150
- /**
151
- * processes template strings using postcss and
152
- * returns it along with a hashed classname based on the string contents.
153
- */
154
- function processCss ( templateContents : string , isScss = false ) {
159
+ /** processes css and returns it along with hashed classeses */
160
+ async function processCss ( templateContents : string , { isScss = false , isModule = false } ) {
155
161
const isImportOrUse = ( line : string ) =>
156
162
line . trim ( ) . startsWith ( '@import' ) || line . trim ( ) . startsWith ( '@use' ) ;
157
163
@@ -166,15 +172,37 @@ function processCss(templateContents: string, isScss = false) {
166
172
. join ( '\n' ) ;
167
173
168
174
const className = `🎈-${ hash ( templateContents . trim ( ) ) } ` ;
169
- const unprocessedCss = `${ importsAndUses } \n.${ className } {${ codeWithoutImportsAndUses } }` ;
175
+ const unprocessedCss = isModule
176
+ ? templateContents
177
+ : `${ importsAndUses } \n.${ className } {${ codeWithoutImportsAndUses } }` ;
170
178
171
- const plugins = ! isScss
172
- ? [ postcssNested ( ) , autoprefixer ( autoprefixerOptions ) ]
173
- : [ autoprefixer ( autoprefixerOptions ) ] ;
174
- const options = isScss ? { parser : postcssScss } : { } ;
175
- const { css } = postcss ( plugins ) . process ( unprocessedCss , options ) ;
179
+ const { css , modules } = await postprocessCss ( unprocessedCss , { isScss, isModule } ) ;
180
+
181
+ if ( isModule ) {
182
+ return [ css , modules ] as const ;
183
+ }
176
184
177
- return [ css , className ] ;
185
+ return [ css , className ] as const ;
186
+ }
187
+
188
+ /** runs postcss with autoprefixer and optionally css-modules */
189
+ async function postprocessCss ( rawCss : string , { isScss = false , isModule = false } ) {
190
+ let modules : Record < string , string > = { } ;
191
+
192
+ const plugins = [
193
+ ! isScss && postcssNested ( ) ,
194
+ autoprefixer ( autoprefixerOptions ) ,
195
+ isModule &&
196
+ postcssModules ( {
197
+ generateScopedName : '🎈-[local]-[hash:base64:6]' ,
198
+ getJSON : ( _ , json ) => void ( modules = json ) ,
199
+ } ) ,
200
+ ] . flatMap ( ( value ) => ( value ? [ value ] : [ ] ) ) ;
201
+
202
+ const options = isScss ? { parser : postcssScss , from : undefined } : { from : undefined } ;
203
+ const { css } = await postcss ( plugins ) . process ( rawCss , options ) ;
204
+
205
+ return { css, modules } ;
178
206
}
179
207
180
208
/** resolves all expressions in the template literal and returns a plain string */
@@ -190,13 +218,17 @@ async function processTemplateLiteral(rawTemplate: string, { inlinedVars = '' })
190
218
191
219
/** parses ast and returns info about all css/scss ecsstatic imports */
192
220
function findEcsstaticImports ( ast : ESTree . Program ) {
193
- const statements = new Map < string , { isScss : boolean ; start : number ; end : number } > ( ) ;
221
+ const statements = new Map <
222
+ string ,
223
+ { isScss : boolean ; isModule : boolean ; start : number ; end : number }
224
+ > ( ) ;
194
225
195
226
for ( const node of ast . body . filter ( ( node ) => node . type === 'ImportDeclaration' ) ) {
196
227
if (
197
228
node . type === 'ImportDeclaration' &&
198
229
node . source . value ?. toString ( ) . startsWith ( '@acab/ecsstatic' )
199
230
) {
231
+ const isModule = node . source . value ?. toString ( ) . endsWith ( 'modules' ) ;
200
232
const { start, end } = node ;
201
233
node . specifiers . forEach ( ( specifier ) => {
202
234
if (
@@ -205,7 +237,7 @@ function findEcsstaticImports(ast: ESTree.Program) {
205
237
) {
206
238
const tagName = specifier . local . name ;
207
239
const isScss = specifier . imported . name === 'scss' ;
208
- statements . set ( tagName , { isScss, start, end } ) ;
240
+ statements . set ( tagName , { isScss, isModule , start, end } ) ;
209
241
}
210
242
} ) ;
211
243
}
@@ -316,25 +348,29 @@ function findCssTaggedTemplateLiterals(ast: ESTree.Program, tagNames: string[])
316
348
function loadDummyEcsstatic ( ) {
317
349
const hashStr = hash . toString ( ) ;
318
350
const getHashFromTemplateStr = getHashFromTemplate . toString ( ) ;
319
- const contents = `${ hashStr } \n${ getHashFromTemplateStr } \n
351
+ const indexContents = `${ hashStr } \n${ getHashFromTemplateStr } \n
320
352
export const css = getHashFromTemplate;
321
353
export const scss = getHashFromTemplate;
322
354
` ;
355
+ const modulesContents = `new Proxy({}, {
356
+ get() { throw 'please don't do this. css modules are hard to evaluate inside other strings :(' }
357
+ })` ;
323
358
324
359
return < esbuild . Plugin > {
325
360
name : 'load-dummy-ecsstatic' ,
326
361
setup ( build ) {
327
362
build . onResolve ( { filter : / ^ @ a c a b \/ e c s s t a t i c $ / } , ( args ) => {
328
- return {
329
- namespace : 'ecsstatic' ,
330
- path : args . path ,
331
- } ;
363
+ return { namespace : 'ecsstatic' , path : args . path } ;
332
364
} ) ;
333
365
build . onLoad ( { filter : / ( .* ) / , namespace : 'ecsstatic' } , ( ) => {
334
- return {
335
- contents,
336
- loader : 'js' ,
337
- } ;
366
+ return { contents : indexContents , loader : 'js' } ;
367
+ } ) ;
368
+
369
+ build . onResolve ( { filter : / ^ @ a c a b \/ e c s s t a t i c \/ m o d u l e s $ / } , ( args ) => {
370
+ return { namespace : 'ecsstatic-modules' , path : args . path } ;
371
+ } ) ;
372
+ build . onLoad ( { filter : / ( .* ) / , namespace : 'ecsstatic-modules' } , ( ) => {
373
+ return { contents : modulesContents , loader : 'js' } ;
338
374
} ) ;
339
375
} ,
340
376
} ;
0 commit comments