@css-modules-kit/core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import type { CSSModule, Token, TokenImporter } from './type.js';
2
- import { isURLSpecifier, isValidAsJSIdentifier } from './util.js';
2
+ import type { ValidateTokenNameOptions } from './util.js';
3
+ import { isURLSpecifier, validateTokenName } from './util.js';
3
4
 
4
5
  export const STYLES_EXPORT_NAME = 'styles';
5
6
 
@@ -17,6 +18,8 @@ interface CodeMapping {
17
18
  lengths: number[];
18
19
  /** The generated offsets of the tokens in the *.d.ts file. */
19
20
  generatedOffsets: number[];
21
+ /** The lengths of the tokens in the *.d.ts file. If not provided, it is assumed to be the same as `lengths`. */
22
+ generatedLengths?: number[];
20
23
  }
21
24
 
22
25
  /** The map linking the two codes in *.d.ts */
@@ -35,6 +38,7 @@ interface LinkedCodeMapping extends CodeMapping {
35
38
  interface GenerateDtsResult {
36
39
  text: string;
37
40
  mapping: CodeMapping;
41
+ secondaryMapping?: CodeMapping;
38
42
  linkedCodeMapping: LinkedCodeMapping;
39
43
  }
40
44
 
@@ -43,7 +47,7 @@ interface GenerateDtsResult {
43
47
  */
44
48
  export function generateDts(cssModule: CSSModule, options: GenerateDtsOptions): GenerateDtsResult {
45
49
  // Exclude invalid tokens
46
- const localTokens = cssModule.localTokens.filter((token) => isValidName(token.name, options));
50
+ const localTokens = cssModule.localTokens.filter((token) => isValidTokenName(token.name, options));
47
51
  const tokenImporters = cssModule.tokenImporters
48
52
  // Exclude invalid imported tokens
49
53
  .map((tokenImporter) => {
@@ -52,8 +56,8 @@ export function generateDts(cssModule: CSSModule, options: GenerateDtsOptions):
52
56
  ...tokenImporter,
53
57
  values: tokenImporter.values.filter(
54
58
  (value) =>
55
- isValidName(value.name, options) &&
56
- (value.localName === undefined || isValidName(value.localName, options)),
59
+ isValidTokenName(value.name, options) &&
60
+ (value.localName === undefined || isValidTokenName(value.localName, options)),
57
61
  ),
58
62
  };
59
63
  } else {
@@ -106,28 +110,7 @@ export function generateDts(cssModule: CSSModule, options: GenerateDtsOptions):
106
110
  }
107
111
  }
108
112
 
109
- /**
110
- * Generate a d.ts file with named exports.
111
- * @example
112
- * If the CSS module file is:
113
- * ```css
114
- * @import './a.module.css';
115
- * @value local1: string;
116
- * @value imported1, imported2 as aliasedImported2 from './b.module.css';
117
- * .local2 { color: red }
118
- * ```
119
- * The d.ts file would be:
120
- * ```ts
121
- * // @ts-nocheck
122
- * export var local1: string;
123
- * export var local2: string;
124
- * export * from './a.module.css';
125
- * export {
126
- * imported1,
127
- * imported2 as aliasedImported2,
128
- * } from './b.module.css';
129
- * ```
130
- */
113
+ /** Generate a d.ts file with named exports. */
131
114
  function generateNamedExportsDts(
132
115
  localTokens: Token[],
133
116
  tokenImporters: TokenImporter[],
@@ -149,6 +132,25 @@ function generateNamedExportsDts(
149
132
  let text = `// @ts-nocheck\n`;
150
133
 
151
134
  for (const token of localTokens) {
135
+ /**
136
+ * The mapping is created as follows:
137
+ * a.module.css:
138
+ * 1 | .a_1 { color: red; }
139
+ * | ^ mapping.sourceOffsets[0]
140
+ * |
141
+ * 2 | .a_2 { color: blue; }
142
+ * | ^ mapping.sourceOffsets[1]
143
+ * |
144
+ *
145
+ * a.module.css.d.ts:
146
+ * 1 | // @ts-nocheck
147
+ * 2 | export var a_1: string;
148
+ * | ^ mapping.generatedOffsets[0]
149
+ * |
150
+ * 3 | export var a_2: string;
151
+ * | ^ mapping.generatedOffsets[1]
152
+ */
153
+
152
154
  text += `export var `;
153
155
  mapping.sourceOffsets.push(token.loc.start.offset);
154
156
  mapping.generatedOffsets.push(text.length);
@@ -157,12 +159,71 @@ function generateNamedExportsDts(
157
159
  }
158
160
  for (const tokenImporter of tokenImporters) {
159
161
  if (tokenImporter.type === 'import') {
162
+ /**
163
+ * The mapping is created as follows:
164
+ * a.module.css:
165
+ * 1 | @import './b.module.css';
166
+ * | ^ mapping.sourceOffsets[0]
167
+ * |
168
+ * 2 | @import './c.module.css';
169
+ * | ^ mapping.sourceOffsets[1]
170
+ * |
171
+ *
172
+ * a.module.css.d.ts:
173
+ * 1 | // @ts-nocheck
174
+ * 2 | export * from './b.module.css';
175
+ * | ^ mapping.generatedOffsets[0]
176
+ * |
177
+ * 3 | export * from './c.module.css';
178
+ * | ^ mapping.generatedOffsets[1]
179
+ *
180
+ * NOTE: Not only the specifier but also the surrounding quotes are included in the mapping.
181
+ */
182
+
160
183
  text += `export * from `;
161
184
  mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
162
185
  mapping.lengths.push(tokenImporter.from.length + 2);
163
186
  mapping.generatedOffsets.push(text.length);
164
187
  text += `'${tokenImporter.from}';\n`;
165
188
  } else {
189
+ /**
190
+ * The mapping is created as follows:
191
+ * a.module.css:
192
+ * 1 | @value b_1, b_2 from './b.module.css';
193
+ * | ^ ^ ^ mapping.sourceOffsets[2]
194
+ * | ^ ^ mapping.sourceOffsets[1]
195
+ * | ^ mapping.sourceOffsets[0]
196
+ * |
197
+ * 2 | @value c_1 as aliased_c_1 from './c.module.css';
198
+ * | ^ ^ ^ mapping.sourceOffsets[5]
199
+ * | ^ ^ mapping.sourceOffsets[4]
200
+ * | ^ mapping.sourceOffsets[3]
201
+ * |
202
+ *
203
+ * a.module.css.d.ts:
204
+ * 1 | // @ts-nocheck
205
+ * 2 | export {
206
+ * 3 | b_1,
207
+ * | ^ mapping.generatedOffsets[0]
208
+ * |
209
+ * 4 | b_2,
210
+ * | ^ mapping.generatedOffsets[1]
211
+ * |
212
+ * 5 | } from './b.module.css';
213
+ * | ^ mapping.generatedOffsets[2]
214
+ * |
215
+ * 6 | export {
216
+ * 7 | c_1 as aliased_c_1,
217
+ * | ^ ^ mapping.generatedOffsets[4], linkedCodeMapping.sourceOffsets[0]
218
+ * | ^ mapping.generatedOffsets[3], linkedCodeMapping.generatedOffsets[0]
219
+ * |
220
+ * 8 | } from './c.module.css';
221
+ * | ^ mapping.generatedOffsets[5]
222
+ *
223
+ * NOTE: Not only the specifier but also the surrounding quotes are included in the mapping.
224
+ * NOTE: linkedCodeMapping is only generated for tokens that have a `localName` (i.e., aliased tokens).
225
+ */
226
+
166
227
  text += `export {\n`;
167
228
  // eslint-disable-next-line no-loop-func
168
229
  tokenImporter.values.forEach((value) => {
@@ -203,34 +264,28 @@ function generateNamedExportsDts(
203
264
  return { text, mapping, linkedCodeMapping };
204
265
  }
205
266
 
206
- /**
207
- * Generate a d.ts file with a default export.
208
- * @example
209
- * If the CSS module file is:
210
- * ```css
211
- * @import './a.module.css';
212
- * @value local1: string;
213
- * @value imported1, imported2 as aliasedImported2 from './b.module.css';
214
- * .local2 { color: red }
215
- * ```
216
- * The d.ts file would be:
217
- * ```ts
218
- * // @ts-nocheck
219
- * const styles = {
220
- * local1: '' as readonly string,
221
- * local2: '' as readonly string,
222
- * ...(await import('./a.module.css')).default,
223
- * imported1: (await import('./b.module.css')).default.imported1,
224
- * aliasedImported2: (await import('./b.module.css')).default.imported2,
225
- * };
226
- * export default styles;
227
- * ```
228
- */
267
+ /** Generate a d.ts file with a default export. */
229
268
  function generateDefaultExportDts(
230
269
  localTokens: Token[],
231
270
  tokenImporters: TokenImporter[],
232
- ): { text: string; mapping: CodeMapping; linkedCodeMapping: LinkedCodeMapping } {
271
+ ): {
272
+ text: string;
273
+ mapping: CodeMapping;
274
+ secondaryMapping: CodeMapping;
275
+ linkedCodeMapping: LinkedCodeMapping;
276
+ } {
233
277
  const mapping: CodeMapping = { sourceOffsets: [], lengths: [], generatedOffsets: [] };
278
+ /**
279
+ * In "Go to Definition", mapping only the inner part of the quotes does not work.
280
+ * Therefore, we also generate a mapping that includes the quotes.
281
+ * For more details, see https://github.com/mizdra/volar-single-quote-span-problem.
282
+ */
283
+ const secondaryMapping: CodeMapping & { generatedLengths: number[] } = {
284
+ sourceOffsets: [],
285
+ lengths: [],
286
+ generatedOffsets: [],
287
+ generatedLengths: [],
288
+ };
234
289
  const linkedCodeMapping: LinkedCodeMapping = {
235
290
  sourceOffsets: [],
236
291
  lengths: [],
@@ -256,54 +311,150 @@ function generateDefaultExportDts(
256
311
 
257
312
  text += `declare const ${STYLES_EXPORT_NAME} = {\n`;
258
313
  for (const token of localTokens) {
259
- text += ` `;
314
+ /**
315
+ * The mapping is created as follows:
316
+ * a.module.css:
317
+ * 1 | .a_1 { color: red; }
318
+ * | ^ mapping.sourceOffsets[0], secondaryMapping.sourceOffsets[0]
319
+ * |
320
+ * 2 | .a_2 { color: blue; }
321
+ * | ^ mapping.sourceOffsets[1], secondaryMapping.sourceOffsets[1]
322
+ * |
323
+ *
324
+ * a.module.css.d.ts:
325
+ * 1 | declare const styles = {
326
+ * 2 | 'a_1': '' as readonly string,
327
+ * | ^^ mapping.generatedOffsets[0]
328
+ * | ^ secondaryMapping.generatedOffsets[0]
329
+ * |
330
+ * 3 | 'a_2': '' as readonly string,
331
+ * | ^^ mapping.generatedOffsets[1]
332
+ * | ^ secondaryMapping.generatedOffsets[1]
333
+ * |
334
+ * 4 | };
335
+ */
336
+
337
+ text += ` '`;
260
338
  mapping.sourceOffsets.push(token.loc.start.offset);
261
- mapping.generatedOffsets.push(text.length);
262
339
  mapping.lengths.push(token.name.length);
263
- text += `${token.name}: '' as readonly string,\n`;
340
+ mapping.generatedOffsets.push(text.length);
341
+ secondaryMapping.sourceOffsets.push(token.loc.start.offset);
342
+ secondaryMapping.lengths.push(token.name.length);
343
+ secondaryMapping.generatedOffsets.push(text.length - 1);
344
+ secondaryMapping.generatedLengths.push(token.name.length + 2);
345
+ text += `${token.name}': '' as readonly string,\n`;
264
346
  }
265
347
  for (const tokenImporter of tokenImporters) {
266
348
  if (tokenImporter.type === 'import') {
349
+ /**
350
+ * The mapping is created as follows:
351
+ * a.module.css:
352
+ * 1 | @import './b.module.css';
353
+ * | ^ mapping.sourceOffsets[0]
354
+ * |
355
+ * 2 | @import './c.module.css';
356
+ * | ^ mapping.sourceOffsets[1]
357
+ * |
358
+ *
359
+ * a.module.css.d.ts:
360
+ * 1 | declare const styles = {
361
+ * 2 | ...blockErrorType((await import('./b.module.css')).default),
362
+ * | ^ mapping.generatedOffsets[0]
363
+ * |
364
+ * 3 | ...blockErrorType((await import('./c.module.css')).default),
365
+ * | ^ mapping.generatedOffsets[1]
366
+ * |
367
+ * 4 | };
368
+ *
369
+ * NOTE: Not only the specifier but also the surrounding quotes are included in the mapping.
370
+ */
371
+
267
372
  text += ` ...blockErrorType((await import(`;
268
373
  mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
269
374
  mapping.lengths.push(tokenImporter.from.length + 2);
270
375
  mapping.generatedOffsets.push(text.length);
271
376
  text += `'${tokenImporter.from}')).default),\n`;
272
377
  } else {
378
+ /**
379
+ * The mapping is created as follows:
380
+ * a.module.css:
381
+ * 1 | @value b_1, b_2 from './b.module.css';
382
+ * | ^ ^ ^ mapping.sourceOffsets[1]
383
+ * | ^ ^ mapping.sourceOffsets[2], secondaryMapping.sourceOffsets[1]
384
+ * | ^ mapping.sourceOffsets[0], secondaryMapping.sourceOffsets[0]
385
+ * |
386
+ * 2 | @value c_1 as aliased_c_1 from './c.module.css';
387
+ * | ^ ^ ^ mapping.sourceOffsets[4]
388
+ * | ^ ^ mapping.sourceOffsets[3], secondaryMapping.sourceOffsets[2]
389
+ * | ^ mapping.sourceOffsets[5], secondaryMapping.sourceOffsets[3]
390
+ * |
391
+ *
392
+ * a.module.css.d.ts:
393
+ * 1 | declare const styles = {
394
+ * 2 | 'b_1': (await import('./b.module.css')).default['b_1'],
395
+ * | ^^ ^ ^ linkedCodeMapping.generatedOffsets[0]
396
+ * | ^^ ^ mapping.generatedOffsets[1]
397
+ * | ^^ mapping.generatedOffsets[0]
398
+ * | ^ secondaryMapping.generatedOffsets[0], linkedCodeMapping.sourceOffsets[0]
399
+ * |
400
+ * 3 | 'b_2': (await import('./b.module.css')).default['b_2'],
401
+ * | ^^ ^ linkedCodeMapping.generatedOffsets[1]
402
+ * | ^^ mapping.generatedOffsets[2]
403
+ * | ^ secondaryMapping.generatedOffsets[1], linkedCodeMapping.sourceOffsets[1]
404
+ * |
405
+ * 4 | 'aliased_c_1': (await import('./c.module.css')).default['c_1'],
406
+ * | ^^ ^ ^^ mapping.generatedOffsets[5]
407
+ * | ^^ ^ ^ secondaryMapping.generatedOffsets[3], linkedCodeMapping.generatedOffsets[2]
408
+ * | ^^ ^ mapping.generatedOffsets[4]
409
+ * | ^^ mapping.generatedOffsets[3]
410
+ * | ^ secondaryMapping.generatedOffsets[2], linkedCodeMapping.sourceOffsets[2]
411
+ * |
412
+ * 5 | };
413
+ *
414
+ * NOTE: Not only the specifier but also the surrounding quotes are included in the mapping.
415
+ */
416
+
273
417
  // eslint-disable-next-line no-loop-func
274
418
  tokenImporter.values.forEach((value, i) => {
275
419
  const localName = value.localName ?? value.name;
276
420
  const localLoc = value.localLoc ?? value.loc;
277
421
 
278
- text += ` `;
422
+ text += ` '`;
279
423
  mapping.sourceOffsets.push(localLoc.start.offset);
280
424
  mapping.lengths.push(localName.length);
281
425
  mapping.generatedOffsets.push(text.length);
282
- linkedCodeMapping.sourceOffsets.push(text.length);
283
- linkedCodeMapping.lengths.push(localName.length);
284
- text += `${localName}: (await import(`;
426
+ secondaryMapping.sourceOffsets.push(localLoc.start.offset);
427
+ secondaryMapping.lengths.push(localName.length);
428
+ secondaryMapping.generatedOffsets.push(text.length - 1);
429
+ secondaryMapping.generatedLengths.push(localName.length + 2);
430
+ linkedCodeMapping.sourceOffsets.push(text.length - 1);
431
+ linkedCodeMapping.lengths.push(localName.length + 2);
432
+ text += `${localName}': (await import(`;
285
433
  if (i === 0) {
286
434
  mapping.sourceOffsets.push(tokenImporter.fromLoc.start.offset - 1);
287
435
  mapping.lengths.push(tokenImporter.from.length + 2);
288
436
  mapping.generatedOffsets.push(text.length);
289
437
  }
290
- text += `'${tokenImporter.from}')).default.`;
291
- mapping.sourceOffsets.push(value.loc.start.offset);
292
- mapping.lengths.push(value.name.length);
293
- mapping.generatedOffsets.push(text.length);
294
- linkedCodeMapping.generatedOffsets.push(text.length);
295
- linkedCodeMapping.generatedLengths.push(value.name.length);
296
- text += `${value.name},\n`;
438
+ text += `'${tokenImporter.from}')).default['`;
439
+ if ('localName' in value) {
440
+ mapping.sourceOffsets.push(value.loc.start.offset);
441
+ mapping.lengths.push(value.name.length);
442
+ mapping.generatedOffsets.push(text.length);
443
+ secondaryMapping.sourceOffsets.push(value.loc.start.offset);
444
+ secondaryMapping.lengths.push(value.name.length);
445
+ secondaryMapping.generatedOffsets.push(text.length - 1);
446
+ secondaryMapping.generatedLengths.push(value.name.length + 2);
447
+ }
448
+ linkedCodeMapping.generatedOffsets.push(text.length - 1);
449
+ linkedCodeMapping.generatedLengths.push(value.name.length + 2);
450
+ text += `${value.name}'],\n`;
297
451
  });
298
452
  }
299
453
  }
300
454
  text += `};\nexport default ${STYLES_EXPORT_NAME};\n`;
301
- return { text, mapping, linkedCodeMapping };
455
+ return { text, mapping, linkedCodeMapping, secondaryMapping };
302
456
  }
303
457
 
304
- function isValidName(name: string, options: GenerateDtsOptions): boolean {
305
- if (!isValidAsJSIdentifier(name)) return false;
306
- if (name === '__proto__') return false;
307
- if (options.namedExports && name === 'default') return false;
308
- return true;
458
+ function isValidTokenName(name: string, options: ValidateTokenNameOptions): boolean {
459
+ return validateTokenName(name, options) === undefined;
309
460
  }
@@ -119,14 +119,24 @@ export function parseRule(rule: Rule): ParseRuleResult {
119
119
  column: rule.source!.start!.column + className.source!.start!.column,
120
120
  offset: rule.source!.start!.offset + className.sourceIndex + 1,
121
121
  };
122
+ /**
123
+ * When there is a selector like `.\31 backslash`, `className.value` becomes `"1backslash"`.
124
+ * In other words, it is the string after escape sequences have been interpreted.
125
+ * However, here we need the raw string as written in the CSS source code.
126
+ * So we use `className.toString()`.
127
+ *
128
+ * The return value of `className.toString()` may contain leading dots and spaces like `" .1backslash"`.
129
+ * Therefore, we remove the leading spaces and dot with a regular expression.
130
+ */
131
+ const rawClassName = className.toString().replace(/^\s*\./u, '');
122
132
  const end = {
123
133
  // The end line is always the same as the start line, as a class selector cannot break in the middle.
124
134
  line: start.line,
125
- column: start.column + className.value.length,
126
- offset: start.offset + className.value.length,
135
+ column: start.column + rawClassName.length,
136
+ offset: start.offset + rawClassName.length,
127
137
  };
128
138
  return {
129
- name: className.value,
139
+ name: rawClassName,
130
140
  loc: { start, end },
131
141
  declarationLoc: { start: rule.source!.start!, end: rule.positionBy({ index: rule.toString().length }) },
132
142
  };
package/src/resolver.ts CHANGED
@@ -12,8 +12,8 @@ export function createResolver(
12
12
  const host: ts.ModuleResolutionHost = {
13
13
  ...ts.sys,
14
14
  fileExists: (fileName) => {
15
- if (fileName.endsWith('.module.d.css.ts')) {
16
- return ts.sys.fileExists(fileName.replace(/\.module\.d\.css\.ts$/u, '.module.css'));
15
+ if (fileName.endsWith('.d.css.ts')) {
16
+ return ts.sys.fileExists(fileName.replace(/\.d\.css\.ts$/u, '.css'));
17
17
  }
18
18
  return ts.sys.fileExists(fileName);
19
19
  },
@@ -26,8 +26,7 @@ export function createResolver(
26
26
  moduleResolutionCache,
27
27
  );
28
28
  if (resolvedModule) {
29
- // TODO: Logging that the paths is used.
30
- return resolvedModule.resolvedFileName.replace(/\.module\.d\.css\.ts$/u, '.module.css');
29
+ return resolvedModule.resolvedFileName.replace(/\.d\.css\.ts$/u, '.css');
31
30
  }
32
31
  return undefined;
33
32
  };
@@ -5,7 +5,7 @@ declare module 'typescript' {
5
5
  readonly files: readonly string[];
6
6
  readonly directories: readonly string[];
7
7
  }
8
- // eslint-disable-next-line max-params
8
+
9
9
  export function matchFiles(
10
10
  path: string,
11
11
  extensions: readonly string[] | undefined,
package/src/util.ts CHANGED
@@ -4,24 +4,48 @@ export function isPosixRelativePath(path: string): boolean {
4
4
 
5
5
  const JS_IDENTIFIER_PATTERN = /^[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*$/u;
6
6
 
7
- export function isValidAsJSIdentifier(name: string): boolean {
8
- return JS_IDENTIFIER_PATTERN.test(name);
7
+ /** The type of token name violation. */
8
+ export type TokenNameViolation =
9
+ | 'invalid-js-identifier' // Invalid as a JavaScript identifier
10
+ | 'proto-not-allowed' // `__proto__` is not allowed
11
+ | 'default-not-allowed' // `default` is not allowed when namedExports is true
12
+ | 'backslash-not-allowed'; // Backslash (`\`) is not allowed
13
+
14
+ export interface ValidateTokenNameOptions {
15
+ namedExports: boolean;
16
+ }
17
+
18
+ /**
19
+ * Validates a token name and returns the violation if any.
20
+ * @param name The token name to validate.
21
+ * @param options The validation options.
22
+ * @returns The violation, or `undefined` if the name is valid.
23
+ */
24
+ export function validateTokenName(name: string, options: ValidateTokenNameOptions): TokenNameViolation | undefined {
25
+ if (name === '__proto__') return 'proto-not-allowed';
26
+ if (options.namedExports) {
27
+ if (name === 'default') return 'default-not-allowed';
28
+ if (!JS_IDENTIFIER_PATTERN.test(name)) return 'invalid-js-identifier';
29
+ } else {
30
+ if (name.includes('\\')) return 'backslash-not-allowed';
31
+ }
32
+ return undefined;
9
33
  }
10
34
 
11
35
  /**
12
36
  * The syntax pattern for consuming tokens imported from CSS Module.
13
- * @example `styles.foo`
37
+ * @example `styles.foo`, `styles['foo']`, `styles["foo"]`
14
38
  */
15
- // TODO(#125): Support `styles['foo']` and `styles["foo"]`
16
39
  // MEMO: The `xxxStyles.foo` format is not supported, because the css module file for current component file is usually imported with `styles`.
17
40
  // It is sufficient to support only the `styles.foo` format.
18
- const TOKEN_CONSUMER_PATTERN = /styles\.([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)/gu;
41
+ const TOKEN_CONSUMER_PATTERN =
42
+ /styles(?:\.([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)|\['([^']*?)'\]|\["([^"]*?)"\])/gu;
19
43
 
20
44
  export function findUsedTokenNames(componentText: string): Set<string> {
21
45
  const usedClassNames = new Set<string>();
22
- let match;
23
- while ((match = TOKEN_CONSUMER_PATTERN.exec(componentText)) !== null) {
24
- usedClassNames.add(match[1]!);
46
+ for (const match of componentText.matchAll(TOKEN_CONSUMER_PATTERN)) {
47
+ const name = match[1] ?? match[2] ?? match[3];
48
+ if (name) usedClassNames.add(name);
25
49
  }
26
50
  return usedClassNames;
27
51
  }