happy-css-modules 3.0.1 → 3.1.1

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.
Files changed (37) hide show
  1. package/dist/emitter/dts.js +34 -17
  2. package/dist/emitter/dts.js.map +1 -1
  3. package/dist/emitter/dts.test.js +3 -3
  4. package/dist/emitter/dts.test.js.map +1 -1
  5. package/dist/emitter/index.test.js +3 -3
  6. package/dist/emitter/index.test.js.map +1 -1
  7. package/dist/locator/index.d.ts +4 -2
  8. package/dist/locator/index.js +41 -15
  9. package/dist/locator/index.js.map +1 -1
  10. package/dist/locator/index.test.js +142 -48
  11. package/dist/locator/index.test.js.map +1 -1
  12. package/dist/locator/postcss.d.ts +41 -3
  13. package/dist/locator/postcss.js +104 -32
  14. package/dist/locator/postcss.js.map +1 -1
  15. package/dist/locator/postcss.test.js +86 -26
  16. package/dist/locator/postcss.test.js.map +1 -1
  17. package/dist/test-util/util.d.ts +3 -2
  18. package/dist/test-util/util.js +22 -18
  19. package/dist/test-util/util.js.map +1 -1
  20. package/dist/transformer/less-transformer.test.js +25 -15
  21. package/dist/transformer/less-transformer.test.js.map +1 -1
  22. package/dist/transformer/postcss-transformer.test.js +5 -3
  23. package/dist/transformer/postcss-transformer.test.js.map +1 -1
  24. package/dist/transformer/scss-transformer.test.js +38 -19
  25. package/dist/transformer/scss-transformer.test.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/emitter/dts.test.ts +3 -3
  28. package/src/emitter/dts.ts +59 -37
  29. package/src/emitter/index.test.ts +3 -3
  30. package/src/locator/index.test.ts +143 -48
  31. package/src/locator/index.ts +51 -23
  32. package/src/locator/postcss.test.ts +135 -26
  33. package/src/locator/postcss.ts +120 -36
  34. package/src/test-util/util.ts +23 -18
  35. package/src/transformer/less-transformer.test.ts +25 -15
  36. package/src/transformer/postcss-transformer.test.ts +5 -3
  37. package/src/transformer/scss-transformer.test.ts +38 -19
@@ -4,7 +4,15 @@ import type { Resolver } from '../resolver/index.js';
4
4
  import { createDefaultResolver } from '../resolver/index.js';
5
5
  import { createDefaultTransformer, type Transformer } from '../transformer/index.js';
6
6
  import { unique, uniqueBy } from '../util.js';
7
- import { getOriginalLocation, generateLocalTokenNames, parseAtImport, type Location, collectNodes } from './postcss.js';
7
+ import {
8
+ getOriginalLocationOfClassSelector,
9
+ getOriginalLocationOfAtValue,
10
+ generateLocalTokenNames,
11
+ parseAtImport,
12
+ type Location,
13
+ collectNodes,
14
+ parseAtValue,
15
+ } from './postcss.js';
8
16
 
9
17
  export { collectNodes, type Location } from './postcss.js';
10
18
 
@@ -20,8 +28,10 @@ function isIgnoredSpecifier(specifier: string): boolean {
20
28
  export type Token = {
21
29
  /** The token name. */
22
30
  name: string;
23
- /** The original locations of the token in the source file. */
24
- originalLocations: Location[];
31
+ /** The name of the imported token. */
32
+ importedName?: string;
33
+ /** The original location of the token in the source file. */
34
+ originalLocation: Location;
25
35
  };
26
36
 
27
37
  type CacheEntry = {
@@ -37,22 +47,6 @@ export type LoadResult = {
37
47
  tokens: Token[];
38
48
  };
39
49
 
40
- function normalizeTokens(tokens: Token[]): Token[] {
41
- const tokenNameToOriginalLocations = new Map<string, Location[]>();
42
- for (const token of tokens) {
43
- tokenNameToOriginalLocations.set(
44
- token.name,
45
- uniqueBy([...(tokenNameToOriginalLocations.get(token.name) ?? []), ...token.originalLocations], (location) =>
46
- JSON.stringify(location),
47
- ),
48
- );
49
- }
50
- return Array.from(tokenNameToOriginalLocations.entries()).map(([name, originalLocations]) => ({
51
- name,
52
- originalLocations,
53
- }));
54
- }
55
-
56
50
  export type LocatorOptions = {
57
51
  /** The function to transform source code. */
58
52
  transformer?: Transformer | undefined;
@@ -158,7 +152,7 @@ export class Locator {
158
152
 
159
153
  const tokens: Token[] = [];
160
154
 
161
- const { atImports, classSelectors } = collectNodes(ast);
155
+ const { atImports, atValues, classSelectors } = collectNodes(ast);
162
156
 
163
157
  // Load imported sheets recursively.
164
158
  for (const atImport of atImports) {
@@ -180,17 +174,51 @@ export class Locator {
180
174
  // NOTE: This method has false positives. However, it works as expected in many cases.
181
175
  if (!localTokenNames.includes(classSelector.value)) continue;
182
176
 
183
- const originalLocation = getOriginalLocation(rule, classSelector);
177
+ const originalLocation = getOriginalLocationOfClassSelector(rule, classSelector);
184
178
 
185
179
  tokens.push({
186
180
  name: classSelector.value,
187
- originalLocations: [originalLocation],
181
+ originalLocation,
188
182
  });
189
183
  }
190
184
 
185
+ for (const atValue of atValues) {
186
+ const parsedAtValue = parseAtValue(atValue);
187
+
188
+ if (parsedAtValue.type === 'valueDeclaration') {
189
+ tokens.push({
190
+ name: parsedAtValue.tokenName,
191
+ originalLocation: getOriginalLocationOfAtValue(atValue, parsedAtValue),
192
+ });
193
+ } else if (parsedAtValue.type === 'valueImportDeclaration') {
194
+ if (isIgnoredSpecifier(parsedAtValue.from)) continue;
195
+ // eslint-disable-next-line no-await-in-loop
196
+ const from = await this.resolver(parsedAtValue.from, { request: filePath });
197
+ // eslint-disable-next-line no-await-in-loop
198
+ const result = await this._load(from);
199
+ dependencies.push(from, ...result.dependencies);
200
+ for (const token of result.tokens) {
201
+ const matchedImport = parsedAtValue.imports.find((i) => i.importedTokenName === token.name);
202
+ if (!matchedImport) continue;
203
+ if (matchedImport.localTokenName === matchedImport.importedTokenName) {
204
+ tokens.push({
205
+ name: matchedImport.localTokenName,
206
+ originalLocation: token.originalLocation,
207
+ });
208
+ } else {
209
+ tokens.push({
210
+ name: matchedImport.localTokenName,
211
+ importedName: matchedImport.importedTokenName,
212
+ originalLocation: token.originalLocation,
213
+ });
214
+ }
215
+ }
216
+ }
217
+ }
218
+
191
219
  const result: LoadResult = {
192
220
  dependencies: unique(dependencies).filter((dep) => dep !== filePath),
193
- tokens: normalizeTokens(tokens),
221
+ tokens: uniqueBy(tokens, (token) => JSON.stringify(token)),
194
222
  };
195
223
  this.cache.set(filePath, { mtime, result });
196
224
  return result;
@@ -1,6 +1,20 @@
1
1
  import dedent from 'dedent';
2
- import { createRoot, createClassSelectors, createAtImports, createFixtures } from '../test-util/util.js';
3
- import { generateLocalTokenNames, getOriginalLocation, parseAtImport, collectNodes } from './postcss.js';
2
+ import type { AtRule } from 'postcss';
3
+ import {
4
+ createRoot,
5
+ createClassSelectors,
6
+ createAtImports,
7
+ createFixtures,
8
+ createAtValues,
9
+ } from '../test-util/util.js';
10
+ import {
11
+ generateLocalTokenNames,
12
+ getOriginalLocationOfClassSelector,
13
+ parseAtImport,
14
+ parseAtValue,
15
+ collectNodes,
16
+ getOriginalLocationOfAtValue,
17
+ } from './postcss.js';
4
18
 
5
19
  describe('generateLocalTokenNames', () => {
6
20
  test('basic', async () => {
@@ -27,9 +41,11 @@ describe('generateLocalTokenNames', () => {
27
41
  .local_class_name_3 {}
28
42
  }
29
43
  :local(.local_class_name_4) {}
44
+ @value value: #BF4040;
30
45
  `),
31
46
  ),
32
47
  ).toStrictEqual([
48
+ 'value',
33
49
  'basic',
34
50
  'cascading',
35
51
  'pseudo_class_1',
@@ -63,14 +79,18 @@ describe('generateLocalTokenNames', () => {
63
79
  ).toStrictEqual([]);
64
80
  });
65
81
  test('does not track styles imported by @value in other file because it is not a local token', async () => {
66
- createFixtures({});
82
+ createFixtures({
83
+ '/test/1.css': dedent`
84
+ .a {}
85
+ `,
86
+ });
67
87
  expect(
68
88
  await generateLocalTokenNames(
69
89
  createRoot(`
70
- @value something from "/test/1.css";
90
+ @value a from "/test/1.css";
71
91
  `),
72
92
  ),
73
- ).toStrictEqual([]);
93
+ ).toStrictEqual(['a']);
74
94
  });
75
95
  test('does not track styles imported by composes in other file because it is not a local token', async () => {
76
96
  createFixtures({
@@ -90,14 +110,14 @@ describe('generateLocalTokenNames', () => {
90
110
  });
91
111
  });
92
112
 
93
- describe('getOriginalLocation', () => {
113
+ describe('getOriginalLocationOfClassSelector', () => {
94
114
  test('basic', () => {
95
115
  const [basic] = createClassSelectors(
96
116
  createRoot(dedent`
97
117
  .basic {}
98
118
  `),
99
119
  );
100
- expect(getOriginalLocation(basic!.rule, basic!.classSelector)).toMatchInlineSnapshot(
120
+ expect(getOriginalLocationOfClassSelector(basic!.rule, basic!.classSelector)).toMatchInlineSnapshot(
101
121
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }`,
102
122
  );
103
123
  });
@@ -109,10 +129,10 @@ describe('getOriginalLocation', () => {
109
129
  .cascading {}
110
130
  `),
111
131
  );
112
- expect(getOriginalLocation(cascading_1!.rule, cascading_1!.classSelector)).toMatchInlineSnapshot(
132
+ expect(getOriginalLocationOfClassSelector(cascading_1!.rule, cascading_1!.classSelector)).toMatchInlineSnapshot(
113
133
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 10 } }`,
114
134
  );
115
- expect(getOriginalLocation(cascading_2!.rule, cascading_2!.classSelector)).toMatchInlineSnapshot(
135
+ expect(getOriginalLocationOfClassSelector(cascading_2!.rule, cascading_2!.classSelector)).toMatchInlineSnapshot(
116
136
  `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 10 } }`,
117
137
  );
118
138
  });
@@ -125,13 +145,19 @@ describe('getOriginalLocation', () => {
125
145
  :not(.pseudo_class_3) {}
126
146
  `),
127
147
  );
128
- expect(getOriginalLocation(pseudo_class_1!.rule, pseudo_class_1!.classSelector)).toMatchInlineSnapshot(
148
+ expect(
149
+ getOriginalLocationOfClassSelector(pseudo_class_1!.rule, pseudo_class_1!.classSelector),
150
+ ).toMatchInlineSnapshot(
129
151
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 15 } }`,
130
152
  );
131
- expect(getOriginalLocation(pseudo_class_2!.rule, pseudo_class_2!.classSelector)).toMatchInlineSnapshot(
153
+ expect(
154
+ getOriginalLocationOfClassSelector(pseudo_class_2!.rule, pseudo_class_2!.classSelector),
155
+ ).toMatchInlineSnapshot(
132
156
  `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 15 } }`,
133
157
  );
134
- expect(getOriginalLocation(pseudo_class_3!.rule, pseudo_class_3!.classSelector)).toMatchInlineSnapshot(
158
+ expect(
159
+ getOriginalLocationOfClassSelector(pseudo_class_3!.rule, pseudo_class_3!.classSelector),
160
+ ).toMatchInlineSnapshot(
135
161
  `{ filePath: "/test/test.css", start: { line: 3, column: 6 }, end: { line: 3, column: 20 } }`,
136
162
  );
137
163
  });
@@ -142,10 +168,14 @@ describe('getOriginalLocation', () => {
142
168
  .multiple_selector_1.multiple_selector_2 {}
143
169
  `),
144
170
  );
145
- expect(getOriginalLocation(multiple_selector_1!.rule, multiple_selector_1!.classSelector)).toMatchInlineSnapshot(
171
+ expect(
172
+ getOriginalLocationOfClassSelector(multiple_selector_1!.rule, multiple_selector_1!.classSelector),
173
+ ).toMatchInlineSnapshot(
146
174
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 20 } }`,
147
175
  );
148
- expect(getOriginalLocation(multiple_selector_2!.rule, multiple_selector_2!.classSelector)).toMatchInlineSnapshot(
176
+ expect(
177
+ getOriginalLocationOfClassSelector(multiple_selector_2!.rule, multiple_selector_2!.classSelector),
178
+ ).toMatchInlineSnapshot(
149
179
  `{ filePath: "/test/test.css", start: { line: 1, column: 21 }, end: { line: 1, column: 40 } }`,
150
180
  );
151
181
  });
@@ -157,10 +187,10 @@ describe('getOriginalLocation', () => {
157
187
  .combinator_1 + .combinator_2 {}
158
188
  `),
159
189
  );
160
- expect(getOriginalLocation(combinator_1!.rule, combinator_1!.classSelector)).toMatchInlineSnapshot(
190
+ expect(getOriginalLocationOfClassSelector(combinator_1!.rule, combinator_1!.classSelector)).toMatchInlineSnapshot(
161
191
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 13 } }`,
162
192
  );
163
- expect(getOriginalLocation(combinator_2!.rule, combinator_2!.classSelector)).toMatchInlineSnapshot(
193
+ expect(getOriginalLocationOfClassSelector(combinator_2!.rule, combinator_2!.classSelector)).toMatchInlineSnapshot(
164
194
  `{ filePath: "/test/test.css", start: { line: 1, column: 17 }, end: { line: 1, column: 29 } }`,
165
195
  );
166
196
  });
@@ -175,7 +205,7 @@ describe('getOriginalLocation', () => {
175
205
  }
176
206
  `),
177
207
  );
178
- expect(getOriginalLocation(at_rule!.rule, at_rule!.classSelector)).toMatchInlineSnapshot(
208
+ expect(getOriginalLocationOfClassSelector(at_rule!.rule, at_rule!.classSelector)).toMatchInlineSnapshot(
179
209
  `{ filePath: "/test/test.css", start: { line: 3, column: 5 }, end: { line: 3, column: 12 } }`,
180
210
  );
181
211
  });
@@ -186,10 +216,14 @@ describe('getOriginalLocation', () => {
186
216
  .selector_list_1, .selector_list_2 {}
187
217
  `),
188
218
  );
189
- expect(getOriginalLocation(selector_list_1!.rule, selector_list_1!.classSelector)).toMatchInlineSnapshot(
219
+ expect(
220
+ getOriginalLocationOfClassSelector(selector_list_1!.rule, selector_list_1!.classSelector),
221
+ ).toMatchInlineSnapshot(
190
222
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 16 } }`,
191
223
  );
192
- expect(getOriginalLocation(selector_list_2!.rule, selector_list_2!.classSelector)).toMatchInlineSnapshot(
224
+ expect(
225
+ getOriginalLocationOfClassSelector(selector_list_2!.rule, selector_list_2!.classSelector),
226
+ ).toMatchInlineSnapshot(
193
227
  `{ filePath: "/test/test.css", start: { line: 1, column: 19 }, end: { line: 1, column: 34 } }`,
194
228
  );
195
229
  });
@@ -205,16 +239,24 @@ describe('getOriginalLocation', () => {
205
239
  :local(.local_class_name_4) {}
206
240
  `),
207
241
  );
208
- expect(getOriginalLocation(local_class_name_1!.rule, local_class_name_1!.classSelector)).toMatchInlineSnapshot(
242
+ expect(
243
+ getOriginalLocationOfClassSelector(local_class_name_1!.rule, local_class_name_1!.classSelector),
244
+ ).toMatchInlineSnapshot(
209
245
  `{ filePath: "/test/test.css", start: { line: 1, column: 8 }, end: { line: 1, column: 26 } }`,
210
246
  );
211
- expect(getOriginalLocation(local_class_name_2!.rule, local_class_name_2!.classSelector)).toMatchInlineSnapshot(
247
+ expect(
248
+ getOriginalLocationOfClassSelector(local_class_name_2!.rule, local_class_name_2!.classSelector),
249
+ ).toMatchInlineSnapshot(
212
250
  `{ filePath: "/test/test.css", start: { line: 3, column: 3 }, end: { line: 3, column: 21 } }`,
213
251
  );
214
- expect(getOriginalLocation(local_class_name_3!.rule, local_class_name_3!.classSelector)).toMatchInlineSnapshot(
252
+ expect(
253
+ getOriginalLocationOfClassSelector(local_class_name_3!.rule, local_class_name_3!.classSelector),
254
+ ).toMatchInlineSnapshot(
215
255
  `{ filePath: "/test/test.css", start: { line: 4, column: 3 }, end: { line: 4, column: 21 } }`,
216
256
  );
217
- expect(getOriginalLocation(local_class_name_4!.rule, local_class_name_4!.classSelector)).toMatchInlineSnapshot(
257
+ expect(
258
+ getOriginalLocationOfClassSelector(local_class_name_4!.rule, local_class_name_4!.classSelector),
259
+ ).toMatchInlineSnapshot(
218
260
  `{ filePath: "/test/test.css", start: { line: 6, column: 8 }, end: { line: 6, column: 26 } }`,
219
261
  );
220
262
  });
@@ -227,19 +269,44 @@ describe('getOriginalLocation', () => {
227
269
  + .with_newline_3, {}
228
270
  `),
229
271
  );
230
- expect(getOriginalLocation(with_newline_1!.rule, with_newline_1!.classSelector)).toMatchInlineSnapshot(
272
+ expect(
273
+ getOriginalLocationOfClassSelector(with_newline_1!.rule, with_newline_1!.classSelector),
274
+ ).toMatchInlineSnapshot(
231
275
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 15 } }`,
232
276
  );
233
- expect(getOriginalLocation(with_newline_2!.rule, with_newline_2!.classSelector)).toMatchInlineSnapshot(
277
+ expect(
278
+ getOriginalLocationOfClassSelector(with_newline_2!.rule, with_newline_2!.classSelector),
279
+ ).toMatchInlineSnapshot(
234
280
  `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 15 } }`,
235
281
  );
236
282
 
237
- expect(getOriginalLocation(with_newline_3!.rule, with_newline_3!.classSelector)).toMatchInlineSnapshot(
283
+ expect(
284
+ getOriginalLocationOfClassSelector(with_newline_3!.rule, with_newline_3!.classSelector),
285
+ ).toMatchInlineSnapshot(
238
286
  `{ filePath: "/test/test.css", start: { line: 3, column: 5 }, end: { line: 3, column: 19 } }`,
239
287
  );
240
288
  });
241
289
  });
242
290
 
291
+ test('getOriginalLocationOfAtValue', () => {
292
+ function tryGetOriginalLocationOfAtValue(atValue: AtRule) {
293
+ const parsed = parseAtValue(atValue);
294
+ if (parsed.type === 'valueDeclaration') {
295
+ return getOriginalLocationOfAtValue(atValue, parsed);
296
+ } else {
297
+ throw new Error('Unexpected type');
298
+ }
299
+ }
300
+ const [basic] = createAtValues(
301
+ createRoot(dedent`
302
+ @value basic: #000;
303
+ `),
304
+ );
305
+ expect(tryGetOriginalLocationOfAtValue(basic!)).toMatchInlineSnapshot(
306
+ `{ filePath: "/test/test.css", start: { line: 1, column: 8 }, end: { line: 1, column: 13 } }`,
307
+ );
308
+ });
309
+
243
310
  test('collectNodes', () => {
244
311
  const ast = createRoot(dedent`
245
312
  @import;
@@ -277,3 +344,45 @@ test('parseAtImport', () => {
277
344
  expect(parseAtImport(atImports[3]!)).toBe('test.css');
278
345
  expect(parseAtImport(atImports[4]!)).toBe('test.css');
279
346
  });
347
+
348
+ test('parseAtValue', () => {
349
+ const atValues = createAtValues(
350
+ createRoot(dedent`
351
+ @value basic: #000;
352
+ @value withoutColon #000;
353
+ @value empty:;
354
+ @value comment:/* comment */;
355
+ @value complex: (max-width: 599px);
356
+ @value import from "test.css";
357
+ @value import1, import2 from "test.css";
358
+ @value import as alias from "test.css";
359
+ /*
360
+ * NOTE: happy-css-modules intentionally does not support module specifier as variable.
361
+ * e.g. \`@value d, e from moduleName;\`
362
+ */
363
+ `),
364
+ );
365
+ expect(parseAtValue(atValues[0]!)).toStrictEqual({ type: 'valueDeclaration', tokenName: 'basic' });
366
+ expect(parseAtValue(atValues[1]!)).toStrictEqual({ type: 'valueDeclaration', tokenName: 'withoutColon' });
367
+ expect(parseAtValue(atValues[2]!)).toStrictEqual({ type: 'valueDeclaration', tokenName: 'empty' });
368
+ expect(parseAtValue(atValues[3]!)).toStrictEqual({ type: 'valueDeclaration', tokenName: 'comment' });
369
+ expect(parseAtValue(atValues[4]!)).toStrictEqual({ type: 'valueDeclaration', tokenName: 'complex' });
370
+ expect(parseAtValue(atValues[5]!)).toStrictEqual({
371
+ type: 'valueImportDeclaration',
372
+ imports: [{ importedTokenName: 'import', localTokenName: 'import' }],
373
+ from: 'test.css',
374
+ });
375
+ expect(parseAtValue(atValues[6]!)).toStrictEqual({
376
+ type: 'valueImportDeclaration',
377
+ imports: [
378
+ { importedTokenName: 'import1', localTokenName: 'import1' },
379
+ { importedTokenName: 'import2', localTokenName: 'import2' },
380
+ ],
381
+ from: 'test.css',
382
+ });
383
+ expect(parseAtValue(atValues[7]!)).toStrictEqual({
384
+ type: 'valueImportDeclaration',
385
+ imports: [{ importedTokenName: 'import', localTokenName: 'alias' }],
386
+ from: 'test.css',
387
+ });
388
+ });
@@ -1,4 +1,4 @@
1
- import postcss, { type Rule, type AtRule, type Root, type Node, type Declaration, type Plugin } from 'postcss';
1
+ import postcss, { type Rule, type AtRule, type Root, type Node } from 'postcss';
2
2
  import modules from 'postcss-modules';
3
3
  import selectorParser, { type ClassName } from 'postcss-selector-parser';
4
4
  import valueParser from 'postcss-value-parser';
@@ -26,40 +26,27 @@ export type Location =
26
26
  end: undefined;
27
27
  };
28
28
 
29
- function removeDependenciesPlugin(): Plugin {
30
- return {
31
- postcssPlugin: 'remove-dependencies',
32
- // eslint-disable-next-line @typescript-eslint/naming-convention
33
- AtRule(atRule) {
34
- if (isAtImportNode(atRule) || isAtValueNode(atRule)) {
35
- atRule.remove();
36
- }
37
- },
38
- // eslint-disable-next-line @typescript-eslint/naming-convention
39
- Declaration(declaration) {
40
- if (isComposesDeclaration(declaration)) {
41
- declaration.remove();
42
- }
43
- },
44
- };
45
- }
46
-
47
29
  /**
48
30
  * Traverses a local token from the AST and returns its name.
49
31
  * @param ast The AST to traverse.
50
32
  * @returns The name of the local token.
51
33
  */
52
34
  export async function generateLocalTokenNames(ast: Root): Promise<string[]> {
35
+ class EmptyLoader {
36
+ async fetch(_file: string, _relativeTo: string, _depTrace: string): Promise<{ [key: string]: string }> {
37
+ // Return an empty object because we do not want to load external tokens in `generateLocalTokenNames`.
38
+ return Promise.resolve({});
39
+ }
40
+ }
53
41
  return new Promise((resolve, reject) => {
54
42
  postcss
55
43
  .default()
56
- // postcss-modules collects tokens (i.e., includes external tokens) by following
57
- // the dependencies specified in the @import.
58
- // However, we do not want `generateLocalTokenNames` to return external tokens.
59
- // So we remove the @import beforehand.
60
- .use(removeDependenciesPlugin())
61
44
  .use(
62
45
  modules({
46
+ // `@import`, `@value`, and `composes` can read tokens from external files.
47
+ // However, we want to collect only local tokens. So we will fake that
48
+ // an empty token is exported from the external file.
49
+ Loader: EmptyLoader,
63
50
  getJSON: (_cssFileName, json) => {
64
51
  resolve(Object.keys(json));
65
52
  },
@@ -72,12 +59,12 @@ export async function generateLocalTokenNames(ast: Root): Promise<string[]> {
72
59
  }
73
60
 
74
61
  /**
75
- * Get the token's location on the source file.
62
+ * Get the original location of the class selector.
76
63
  * @param rule The rule node that contains the token.
77
64
  * @param classSelector The class selector node that contains the token.
78
- * @returns The token's location on the source file.
65
+ * @returns The original location of the class selector.
79
66
  */
80
- export function getOriginalLocation(rule: Rule, classSelector: ClassName): Location {
67
+ export function getOriginalLocationOfClassSelector(rule: Rule, classSelector: ClassName): Location {
81
68
  // The node derived from `postcss.parse` always has `source` property. Therefore, this line is unreachable.
82
69
  if (rule.source === undefined || classSelector.source === undefined) throw new Error('Node#source is undefined');
83
70
  // The node derived from `postcss.parse` always has `start` and `end` property. Therefore, this line is unreachable.
@@ -127,6 +114,32 @@ export function getOriginalLocation(rule: Rule, classSelector: ClassName): Locat
127
114
  };
128
115
  }
129
116
 
117
+ /**
118
+ * Get the original location of `@value`.
119
+ * @param atValue The `@value` rule.
120
+ * @returns The location of the `@value` rule.
121
+ */
122
+ export function getOriginalLocationOfAtValue(atValue: AtRule, valueDeclaration: ValueDeclaration): Location {
123
+ // The node derived from `postcss.parse` always has `source` property. Therefore, this line is unreachable.
124
+ if (atValue.source === undefined) throw new Error('Node#source is undefined');
125
+ // The node derived from `postcss.parse` always has `start` and `end` property. Therefore, this line is unreachable.
126
+ if (atValue.source.start === undefined) throw new Error('Node#start is undefined');
127
+ if (atValue.source.end === undefined) throw new Error('Node#end is undefined');
128
+ if (atValue.source.input.file === undefined) throw new Error('Node#input.file is undefined');
129
+
130
+ return {
131
+ filePath: atValue.source.input.file,
132
+ start: {
133
+ line: atValue.source.start.line,
134
+ column: atValue.source.start.column + 7, // Add for `@value `
135
+ },
136
+ end: {
137
+ line: atValue.source.start.line,
138
+ column: atValue.source.start.column + 7 + valueDeclaration.tokenName.length, // Add for `@value ` and token name
139
+ },
140
+ };
141
+ }
142
+
130
143
  function isAtRuleNode(node: Node): node is AtRule {
131
144
  return node.type === 'atrule';
132
145
  }
@@ -143,16 +156,9 @@ function isRuleNode(node: Node): node is Rule {
143
156
  return node.type === 'rule';
144
157
  }
145
158
 
146
- function isDeclaration(node: Node): node is Declaration {
147
- return node.type === 'decl';
148
- }
149
-
150
- function isComposesDeclaration(node: Node): node is Declaration {
151
- return isDeclaration(node) && node.prop === 'composes';
152
- }
153
-
154
159
  type CollectNodesResult = {
155
160
  atImports: AtRule[];
161
+ atValues: AtRule[];
156
162
  classSelectors: { rule: Rule; classSelector: ClassName }[];
157
163
  };
158
164
 
@@ -162,10 +168,13 @@ type CollectNodesResult = {
162
168
  */
163
169
  export function collectNodes(ast: Root): CollectNodesResult {
164
170
  const atImports: AtRule[] = [];
171
+ const atValues: AtRule[] = [];
165
172
  const classSelectors: { rule: Rule; classSelector: ClassName }[] = [];
166
173
  ast.walk((node) => {
167
174
  if (isAtImportNode(node)) {
168
175
  atImports.push(node);
176
+ } else if (isAtValueNode(node)) {
177
+ atValues.push(node);
169
178
  } else if (isRuleNode(node)) {
170
179
  // In `rule.selector` comes the following string:
171
180
  // 1. ".foo"
@@ -183,7 +192,7 @@ export function collectNodes(ast: Root): CollectNodesResult {
183
192
  }).processSync(node);
184
193
  }
185
194
  });
186
- return { atImports, classSelectors };
195
+ return { atImports, atValues, classSelectors };
187
196
  }
188
197
 
189
198
  /**
@@ -202,3 +211,78 @@ export function parseAtImport(atImport: AtRule): string | undefined {
202
211
  }
203
212
  return undefined;
204
213
  }
214
+
215
+ type ValueDeclaration = {
216
+ type: 'valueDeclaration';
217
+ tokenName: string;
218
+ // value: string; // unneeded
219
+ };
220
+ type ValueImportDeclaration = {
221
+ type: 'valueImportDeclaration';
222
+ imports: { importedTokenName: string; localTokenName: string }[];
223
+ from: string;
224
+ };
225
+
226
+ type ParsedAtValue = ValueDeclaration | ValueImportDeclaration;
227
+
228
+ const matchImports = /^(.+?|\([\s\S]+?\))\s+from\s+("[^"]*"|'[^']*'|[\w-]+)$/u;
229
+ const matchValueDefinition = /(?:\s+|^)([\w-]+):?(.*?)$/u;
230
+ const matchImport = /^([\w-]+)(?:\s+as\s+([\w-]+))?/u;
231
+
232
+ /**
233
+ * Parse the `@value` rule.
234
+ * Forked from https://github.com/css-modules/postcss-modules-values/blob/v4.0.0/src/index.js.
235
+ *
236
+ * @license
237
+ * ISC License (ISC)
238
+ * Copyright (c) 2015, Glen Maddern
239
+ *
240
+ * Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,
241
+ * provided that the above copyright notice and this permission notice appear in all copies.
242
+ *
243
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING
244
+ * ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
245
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
246
+ * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH
247
+ * THE USE OR PERFORMANCE OF THIS SOFTWARE.
248
+ */
249
+ export function parseAtValue(atValue: AtRule): ParsedAtValue {
250
+ const matchesForImports = atValue.params.match(matchImports);
251
+ if (matchesForImports) {
252
+ const [, aliases, path] = matchesForImports;
253
+
254
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
255
+ if (aliases === undefined || path === undefined) throw new Error(`unreachable`);
256
+
257
+ const imports = aliases
258
+ .replace(/^\(\s*([\s\S]+)\s*\)$/u, '$1')
259
+ .split(/\s*,\s*/u)
260
+ .map((alias) => {
261
+ const tokens = matchImport.exec(alias);
262
+
263
+ if (tokens) {
264
+ const [, theirName, myName] = tokens;
265
+ if (theirName === undefined) throw new Error(`unreachable`);
266
+ return { importedTokenName: theirName, localTokenName: myName ?? theirName };
267
+ } else {
268
+ throw new Error(`@import statement "${alias}" is invalid!`);
269
+ }
270
+ });
271
+
272
+ // Remove quotes from the path.
273
+ // NOTE: This is a restriction unique to "happy-css-modules" and not a specification of CSS Modules.
274
+ const normalizedPath = path.replace(/^['"]|['"]$/gu, '');
275
+
276
+ return { type: 'valueImportDeclaration', imports, from: normalizedPath };
277
+ }
278
+
279
+ const matchesForValueDefinitions = `${atValue.params}${atValue.raws.between!}`.match(matchValueDefinition);
280
+ if (matchesForValueDefinitions) {
281
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
282
+ const [, key, value] = matchesForValueDefinitions;
283
+ if (key === undefined) throw new Error(`unreachable`);
284
+ return { type: 'valueDeclaration', tokenName: key };
285
+ }
286
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
287
+ throw new Error(`@value statement "${atValue.source!}" is invalid!`);
288
+ }
@@ -21,31 +21,36 @@ export function createAtImports(root: Root): AtRule[] {
21
21
  return collectNodes(root).atImports;
22
22
  }
23
23
 
24
+ export function createAtValues(root: Root): AtRule[] {
25
+ return collectNodes(root).atValues;
26
+ }
27
+
24
28
  export function createClassSelectors(root: Root): { rule: Rule; classSelector: ClassName }[] {
25
29
  return collectNodes(root).classSelectors;
26
30
  }
27
31
 
28
32
  export function fakeToken(args: {
29
33
  name: Token['name'];
30
- originalLocations: { filePath?: Location['filePath']; start?: Location['start'] }[];
34
+ originalLocation: { filePath?: Location['filePath']; start?: Location['start'] };
31
35
  }): Token {
32
- return {
33
- name: args.name,
34
- originalLocations: args.originalLocations.map((location) => {
35
- if (location.filePath === undefined || location.start === undefined) {
36
- return { filePath: undefined, start: undefined, end: undefined };
37
- } else {
38
- return {
39
- filePath: location.filePath ?? getFixturePath('/test/1.css'),
40
- start: location.start,
41
- end: {
42
- line: location.start.line,
43
- column: location.start.column + args.name.length - 1,
44
- },
45
- };
46
- }
47
- }),
48
- };
36
+ if (args.originalLocation.filePath === undefined || args.originalLocation.start === undefined) {
37
+ return {
38
+ name: args.name,
39
+ originalLocation: { filePath: undefined, start: undefined, end: undefined },
40
+ };
41
+ } else {
42
+ return {
43
+ name: args.name,
44
+ originalLocation: {
45
+ filePath: args.originalLocation.filePath ?? getFixturePath('/test/1.css'),
46
+ start: args.originalLocation.start,
47
+ end: {
48
+ line: args.originalLocation.start.line,
49
+ column: args.originalLocation.start.column + args.name.length - 1,
50
+ },
51
+ },
52
+ };
53
+ }
49
54
  }
50
55
 
51
56
  export async function waitForAsyncTask(ms?: number): Promise<void> {