happy-css-modules 3.0.0 → 3.1.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.
Files changed (143) hide show
  1. package/package.json +1 -1
  2. package/src/emitter/dts.test.ts +3 -3
  3. package/src/emitter/dts.ts +59 -37
  4. package/src/emitter/index.test.ts +3 -3
  5. package/src/integration-test/go-to-definition.test.ts +0 -35
  6. package/src/locator/index.test.ts +152 -158
  7. package/src/locator/index.ts +44 -37
  8. package/src/locator/postcss.test.ts +118 -76
  9. package/src/locator/postcss.ts +116 -73
  10. package/src/test-util/util.ts +24 -23
  11. package/src/transformer/less-transformer.test.ts +26 -24
  12. package/src/transformer/postcss-transformer.test.ts +5 -3
  13. package/src/transformer/scss-transformer.test.ts +39 -28
  14. package/src/util.ts +1 -1
  15. package/dist/cli.d.ts +0 -6
  16. package/dist/cli.js +0 -121
  17. package/dist/cli.js.map +0 -1
  18. package/dist/cli.test.d.ts +0 -1
  19. package/dist/cli.test.js +0 -66
  20. package/dist/cli.test.js.map +0 -1
  21. package/dist/config.d.ts +0 -1
  22. package/dist/config.js +0 -2
  23. package/dist/config.js.map +0 -1
  24. package/dist/emitter/dts.d.ts +0 -14
  25. package/dist/emitter/dts.js +0 -111
  26. package/dist/emitter/dts.js.map +0 -1
  27. package/dist/emitter/dts.test.d.ts +0 -1
  28. package/dist/emitter/dts.test.js +0 -205
  29. package/dist/emitter/dts.test.js.map +0 -1
  30. package/dist/emitter/file-system.d.ts +0 -6
  31. package/dist/emitter/file-system.js +0 -26
  32. package/dist/emitter/file-system.js.map +0 -1
  33. package/dist/emitter/file-system.test.d.ts +0 -1
  34. package/dist/emitter/file-system.test.js +0 -34
  35. package/dist/emitter/file-system.test.js.map +0 -1
  36. package/dist/emitter/index.d.ts +0 -34
  37. package/dist/emitter/index.js +0 -49
  38. package/dist/emitter/index.js.map +0 -1
  39. package/dist/emitter/index.test.d.ts +0 -1
  40. package/dist/emitter/index.test.js +0 -68
  41. package/dist/emitter/index.test.js.map +0 -1
  42. package/dist/emitter/source-map.d.ts +0 -8
  43. package/dist/emitter/source-map.js +0 -16
  44. package/dist/emitter/source-map.js.map +0 -1
  45. package/dist/emitter/source-map.test.d.ts +0 -1
  46. package/dist/emitter/source-map.test.js +0 -13
  47. package/dist/emitter/source-map.test.js.map +0 -1
  48. package/dist/index.d.ts +0 -5
  49. package/dist/index.js +0 -6
  50. package/dist/index.js.map +0 -1
  51. package/dist/integration-test/go-to-definition.test.d.ts +0 -1
  52. package/dist/integration-test/go-to-definition.test.js +0 -369
  53. package/dist/integration-test/go-to-definition.test.js.map +0 -1
  54. package/dist/library/source-map/index.d.ts +0 -8
  55. package/dist/library/source-map/index.js +0 -5
  56. package/dist/library/source-map/index.js.map +0 -1
  57. package/dist/locator/index.d.ts +0 -44
  58. package/dist/locator/index.js +0 -162
  59. package/dist/locator/index.js.map +0 -1
  60. package/dist/locator/index.test.d.ts +0 -1
  61. package/dist/locator/index.test.js +0 -395
  62. package/dist/locator/index.test.js.map +0 -1
  63. package/dist/locator/postcss.d.ts +0 -64
  64. package/dist/locator/postcss.js +0 -208
  65. package/dist/locator/postcss.js.map +0 -1
  66. package/dist/locator/postcss.test.d.ts +0 -1
  67. package/dist/locator/postcss.test.js +0 -242
  68. package/dist/locator/postcss.test.js.map +0 -1
  69. package/dist/logger.d.ts +0 -9
  70. package/dist/logger.js +0 -28
  71. package/dist/logger.js.map +0 -1
  72. package/dist/regression-test/issue-168.test.d.ts +0 -1
  73. package/dist/regression-test/issue-168.test.js +0 -30
  74. package/dist/regression-test/issue-168.test.js.map +0 -1
  75. package/dist/resolver/index.d.ts +0 -22
  76. package/dist/resolver/index.js +0 -44
  77. package/dist/resolver/index.js.map +0 -1
  78. package/dist/resolver/index.test.d.ts +0 -1
  79. package/dist/resolver/index.test.js +0 -16
  80. package/dist/resolver/index.test.js.map +0 -1
  81. package/dist/resolver/node-resolver.d.ts +0 -2
  82. package/dist/resolver/node-resolver.js +0 -6
  83. package/dist/resolver/node-resolver.js.map +0 -1
  84. package/dist/resolver/node-resolver.test.d.ts +0 -1
  85. package/dist/resolver/node-resolver.test.js +0 -25
  86. package/dist/resolver/node-resolver.test.js.map +0 -1
  87. package/dist/resolver/relative-resolver.d.ts +0 -2
  88. package/dist/resolver/relative-resolver.js +0 -5
  89. package/dist/resolver/relative-resolver.js.map +0 -1
  90. package/dist/resolver/relative-resolver.test.d.ts +0 -1
  91. package/dist/resolver/relative-resolver.test.js +0 -12
  92. package/dist/resolver/relative-resolver.test.js.map +0 -1
  93. package/dist/resolver/webpack-resolver.d.ts +0 -24
  94. package/dist/resolver/webpack-resolver.js +0 -91
  95. package/dist/resolver/webpack-resolver.js.map +0 -1
  96. package/dist/resolver/webpack-resolver.test.d.ts +0 -1
  97. package/dist/resolver/webpack-resolver.test.js +0 -89
  98. package/dist/resolver/webpack-resolver.test.js.map +0 -1
  99. package/dist/runner.d.ts +0 -76
  100. package/dist/runner.js +0 -120
  101. package/dist/runner.js.map +0 -1
  102. package/dist/runner.test.d.ts +0 -1
  103. package/dist/runner.test.js +0 -287
  104. package/dist/runner.test.js.map +0 -1
  105. package/dist/test-util/jest/resolver.cjs +0 -31
  106. package/dist/test-util/jest/resolver.cjs.map +0 -1
  107. package/dist/test-util/jest/resolver.d.cts +0 -16
  108. package/dist/test-util/tsserver.d.ts +0 -31
  109. package/dist/test-util/tsserver.js +0 -112
  110. package/dist/test-util/tsserver.js.map +0 -1
  111. package/dist/test-util/util.d.ts +0 -29
  112. package/dist/test-util/util.js +0 -84
  113. package/dist/test-util/util.js.map +0 -1
  114. package/dist/transformer/index.d.ts +0 -30
  115. package/dist/transformer/index.js +0 -25
  116. package/dist/transformer/index.js.map +0 -1
  117. package/dist/transformer/index.test.d.ts +0 -1
  118. package/dist/transformer/index.test.js +0 -66
  119. package/dist/transformer/index.test.js.map +0 -1
  120. package/dist/transformer/less-transformer.d.ts +0 -2
  121. package/dist/transformer/less-transformer.js +0 -45
  122. package/dist/transformer/less-transformer.js.map +0 -1
  123. package/dist/transformer/less-transformer.test.d.ts +0 -1
  124. package/dist/transformer/less-transformer.test.js +0 -132
  125. package/dist/transformer/less-transformer.test.js.map +0 -1
  126. package/dist/transformer/postcss-transformer.d.ts +0 -12
  127. package/dist/transformer/postcss-transformer.js +0 -32
  128. package/dist/transformer/postcss-transformer.js.map +0 -1
  129. package/dist/transformer/postcss-transformer.test.d.ts +0 -1
  130. package/dist/transformer/postcss-transformer.test.js +0 -176
  131. package/dist/transformer/postcss-transformer.test.js.map +0 -1
  132. package/dist/transformer/scss-transformer.d.ts +0 -2
  133. package/dist/transformer/scss-transformer.js +0 -40
  134. package/dist/transformer/scss-transformer.js.map +0 -1
  135. package/dist/transformer/scss-transformer.test.d.ts +0 -1
  136. package/dist/transformer/scss-transformer.test.js +0 -138
  137. package/dist/transformer/scss-transformer.test.js.map +0 -1
  138. package/dist/util.d.ts +0 -21
  139. package/dist/util.js +0 -72
  140. package/dist/util.js.map +0 -1
  141. package/dist/util.test.d.ts +0 -1
  142. package/dist/util.test.js +0 -74
  143. package/dist/util.test.js.map +0 -1
@@ -1,17 +1,19 @@
1
1
  import dedent from 'dedent';
2
+ import type { AtRule } from 'postcss';
2
3
  import {
3
4
  createRoot,
4
5
  createClassSelectors,
5
6
  createAtImports,
6
- createComposesDeclarations,
7
7
  createFixtures,
8
+ createAtValues,
8
9
  } from '../test-util/util.js';
9
10
  import {
10
11
  generateLocalTokenNames,
11
- getOriginalLocation,
12
+ getOriginalLocationOfClassSelector,
12
13
  parseAtImport,
13
- parseComposesDeclarationWithFromUrl,
14
+ parseAtValue,
14
15
  collectNodes,
16
+ getOriginalLocationOfAtValue,
15
17
  } from './postcss.js';
16
18
 
17
19
  describe('generateLocalTokenNames', () => {
@@ -39,13 +41,11 @@ describe('generateLocalTokenNames', () => {
39
41
  .local_class_name_3 {}
40
42
  }
41
43
  :local(.local_class_name_4) {}
42
- .composes_target {}
43
- .composes {
44
- composes: composes_target;
45
- }
44
+ @value value: #BF4040;
46
45
  `),
47
46
  ),
48
47
  ).toStrictEqual([
48
+ 'value',
49
49
  'basic',
50
50
  'cascading',
51
51
  'pseudo_class_1',
@@ -62,8 +62,6 @@ describe('generateLocalTokenNames', () => {
62
62
  'local_class_name_2',
63
63
  'local_class_name_3',
64
64
  'local_class_name_4',
65
- 'composes_target',
66
- 'composes',
67
65
  ]);
68
66
  });
69
67
  test('does not track styles imported by @import in other file because it is not a local token', async () => {
@@ -81,14 +79,18 @@ describe('generateLocalTokenNames', () => {
81
79
  ).toStrictEqual([]);
82
80
  });
83
81
  test('does not track styles imported by @value in other file because it is not a local token', async () => {
84
- createFixtures({});
82
+ createFixtures({
83
+ '/test/1.css': dedent`
84
+ .a {}
85
+ `,
86
+ });
85
87
  expect(
86
88
  await generateLocalTokenNames(
87
89
  createRoot(`
88
- @value something from "/test/1.css";
90
+ @value a from "/test/1.css";
89
91
  `),
90
92
  ),
91
- ).toStrictEqual([]);
93
+ ).toStrictEqual(['a']);
92
94
  });
93
95
  test('does not track styles imported by composes in other file because it is not a local token', async () => {
94
96
  createFixtures({
@@ -108,14 +110,14 @@ describe('generateLocalTokenNames', () => {
108
110
  });
109
111
  });
110
112
 
111
- describe('getOriginalLocation', () => {
113
+ describe('getOriginalLocationOfClassSelector', () => {
112
114
  test('basic', () => {
113
115
  const [basic] = createClassSelectors(
114
116
  createRoot(dedent`
115
117
  .basic {}
116
118
  `),
117
119
  );
118
- expect(getOriginalLocation(basic!.rule, basic!.classSelector)).toMatchInlineSnapshot(
120
+ expect(getOriginalLocationOfClassSelector(basic!.rule, basic!.classSelector)).toMatchInlineSnapshot(
119
121
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }`,
120
122
  );
121
123
  });
@@ -127,10 +129,10 @@ describe('getOriginalLocation', () => {
127
129
  .cascading {}
128
130
  `),
129
131
  );
130
- expect(getOriginalLocation(cascading_1!.rule, cascading_1!.classSelector)).toMatchInlineSnapshot(
132
+ expect(getOriginalLocationOfClassSelector(cascading_1!.rule, cascading_1!.classSelector)).toMatchInlineSnapshot(
131
133
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 10 } }`,
132
134
  );
133
- expect(getOriginalLocation(cascading_2!.rule, cascading_2!.classSelector)).toMatchInlineSnapshot(
135
+ expect(getOriginalLocationOfClassSelector(cascading_2!.rule, cascading_2!.classSelector)).toMatchInlineSnapshot(
134
136
  `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 10 } }`,
135
137
  );
136
138
  });
@@ -143,13 +145,19 @@ describe('getOriginalLocation', () => {
143
145
  :not(.pseudo_class_3) {}
144
146
  `),
145
147
  );
146
- 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(
147
151
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 15 } }`,
148
152
  );
149
- 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(
150
156
  `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 15 } }`,
151
157
  );
152
- 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(
153
161
  `{ filePath: "/test/test.css", start: { line: 3, column: 6 }, end: { line: 3, column: 20 } }`,
154
162
  );
155
163
  });
@@ -160,10 +168,14 @@ describe('getOriginalLocation', () => {
160
168
  .multiple_selector_1.multiple_selector_2 {}
161
169
  `),
162
170
  );
163
- 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(
164
174
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 20 } }`,
165
175
  );
166
- 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(
167
179
  `{ filePath: "/test/test.css", start: { line: 1, column: 21 }, end: { line: 1, column: 40 } }`,
168
180
  );
169
181
  });
@@ -175,10 +187,10 @@ describe('getOriginalLocation', () => {
175
187
  .combinator_1 + .combinator_2 {}
176
188
  `),
177
189
  );
178
- expect(getOriginalLocation(combinator_1!.rule, combinator_1!.classSelector)).toMatchInlineSnapshot(
190
+ expect(getOriginalLocationOfClassSelector(combinator_1!.rule, combinator_1!.classSelector)).toMatchInlineSnapshot(
179
191
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 13 } }`,
180
192
  );
181
- expect(getOriginalLocation(combinator_2!.rule, combinator_2!.classSelector)).toMatchInlineSnapshot(
193
+ expect(getOriginalLocationOfClassSelector(combinator_2!.rule, combinator_2!.classSelector)).toMatchInlineSnapshot(
182
194
  `{ filePath: "/test/test.css", start: { line: 1, column: 17 }, end: { line: 1, column: 29 } }`,
183
195
  );
184
196
  });
@@ -193,7 +205,7 @@ describe('getOriginalLocation', () => {
193
205
  }
194
206
  `),
195
207
  );
196
- expect(getOriginalLocation(at_rule!.rule, at_rule!.classSelector)).toMatchInlineSnapshot(
208
+ expect(getOriginalLocationOfClassSelector(at_rule!.rule, at_rule!.classSelector)).toMatchInlineSnapshot(
197
209
  `{ filePath: "/test/test.css", start: { line: 3, column: 5 }, end: { line: 3, column: 12 } }`,
198
210
  );
199
211
  });
@@ -204,10 +216,14 @@ describe('getOriginalLocation', () => {
204
216
  .selector_list_1, .selector_list_2 {}
205
217
  `),
206
218
  );
207
- 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(
208
222
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 16 } }`,
209
223
  );
210
- 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(
211
227
  `{ filePath: "/test/test.css", start: { line: 1, column: 19 }, end: { line: 1, column: 34 } }`,
212
228
  );
213
229
  });
@@ -223,36 +239,27 @@ describe('getOriginalLocation', () => {
223
239
  :local(.local_class_name_4) {}
224
240
  `),
225
241
  );
226
- 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(
227
245
  `{ filePath: "/test/test.css", start: { line: 1, column: 8 }, end: { line: 1, column: 26 } }`,
228
246
  );
229
- 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(
230
250
  `{ filePath: "/test/test.css", start: { line: 3, column: 3 }, end: { line: 3, column: 21 } }`,
231
251
  );
232
- 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(
233
255
  `{ filePath: "/test/test.css", start: { line: 4, column: 3 }, end: { line: 4, column: 21 } }`,
234
256
  );
235
- 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(
236
260
  `{ filePath: "/test/test.css", start: { line: 6, column: 8 }, end: { line: 6, column: 26 } }`,
237
261
  );
238
262
  });
239
- test('composes', () => {
240
- // eslint-disable-next-line @typescript-eslint/naming-convention
241
- const [composes_target, composes] = createClassSelectors(
242
- createRoot(dedent`
243
- .composes_target {}
244
- .composes {
245
- composes: composes_target;
246
- }
247
- `),
248
- );
249
- expect(getOriginalLocation(composes_target!.rule, composes_target!.classSelector)).toMatchInlineSnapshot(
250
- `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 16 } }`,
251
- );
252
- expect(getOriginalLocation(composes!.rule, composes!.classSelector)).toMatchInlineSnapshot(
253
- `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 9 } }`,
254
- );
255
- });
256
263
  test('with_newline', () => {
257
264
  // eslint-disable-next-line @typescript-eslint/naming-convention
258
265
  const [with_newline_1, with_newline_2, with_newline_3] = createClassSelectors(
@@ -262,41 +269,63 @@ describe('getOriginalLocation', () => {
262
269
  + .with_newline_3, {}
263
270
  `),
264
271
  );
265
- 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(
266
275
  `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 15 } }`,
267
276
  );
268
- 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(
269
280
  `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 15 } }`,
270
281
  );
271
282
 
272
- 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(
273
286
  `{ filePath: "/test/test.css", start: { line: 3, column: 5 }, end: { line: 3, column: 19 } }`,
274
287
  );
275
288
  });
276
289
  });
277
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
+
278
310
  test('collectNodes', () => {
279
311
  const ast = createRoot(dedent`
280
312
  @import;
281
313
  @import "test.css";
282
314
  @ignored;
283
- .a { ignored: "ignored"; composes: a; }
284
- .b { ignored: "ignored"; composes: b; }
315
+ .a { ignored: "ignored"; }
316
+ .b { ignored: "ignored"; }
285
317
  `);
286
318
 
287
- const { atImports, classSelectors, composesDeclarations } = collectNodes(ast);
319
+ const { atImports, classSelectors } = collectNodes(ast);
288
320
 
289
321
  expect(atImports).toHaveLength(2);
290
322
  expect(atImports[0]!.toString()).toEqual('@import');
291
323
  expect(atImports[1]!.toString()).toEqual('@import "test.css"');
292
324
  expect(classSelectors).toHaveLength(2);
293
- expect(classSelectors[0]!.rule.toString()).toEqual('.a { ignored: "ignored"; composes: a; }');
325
+ expect(classSelectors[0]!.rule.toString()).toEqual('.a { ignored: "ignored"; }');
294
326
  expect(classSelectors[0]!.classSelector.toString()).toEqual('.a');
295
- expect(classSelectors[1]!.rule.toString()).toEqual('.b { ignored: "ignored"; composes: b; }');
327
+ expect(classSelectors[1]!.rule.toString()).toEqual('.b { ignored: "ignored"; }');
296
328
  expect(classSelectors[1]!.classSelector.toString()).toEqual('.b');
297
- expect(composesDeclarations).toHaveLength(2);
298
- expect(composesDeclarations[0]!.toString()).toEqual('composes: a');
299
- expect(composesDeclarations[1]!.toString()).toEqual('composes: b');
300
329
  });
301
330
 
302
331
  test('parseAtImport', () => {
@@ -316,31 +345,44 @@ test('parseAtImport', () => {
316
345
  expect(parseAtImport(atImports[4]!)).toBe('test.css');
317
346
  });
318
347
 
319
- test('parseComposesDeclarationWithFromUrl', () => {
320
- const composesDeclarations = createComposesDeclarations(
348
+ test('parseAtValue', () => {
349
+ const atValues = createAtValues(
321
350
  createRoot(dedent`
322
- .a {
323
- composes: a;
324
- composes: a b c;
325
- composes: a from "test.css";
326
- composes: a b c from "test.css";
327
- composes: from from from from "test.css";
328
- /* NOTE: CSS Modules do not support '... from url("test.css")'. */
329
- }
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
+ */
330
363
  `),
331
364
  );
332
- expect(parseComposesDeclarationWithFromUrl(composesDeclarations[0]!)).toStrictEqual(undefined);
333
- expect(parseComposesDeclarationWithFromUrl(composesDeclarations[1]!)).toStrictEqual(undefined);
334
- expect(parseComposesDeclarationWithFromUrl(composesDeclarations[2]!)).toStrictEqual({
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' }],
335
373
  from: 'test.css',
336
- tokenNames: ['a'],
337
374
  });
338
- expect(parseComposesDeclarationWithFromUrl(composesDeclarations[3]!)).toStrictEqual({
375
+ expect(parseAtValue(atValues[6]!)).toStrictEqual({
376
+ type: 'valueImportDeclaration',
377
+ imports: [
378
+ { importedTokenName: 'import1', localTokenName: 'import1' },
379
+ { importedTokenName: 'import2', localTokenName: 'import2' },
380
+ ],
339
381
  from: 'test.css',
340
- tokenNames: ['a', 'b', 'c'],
341
382
  });
342
- expect(parseComposesDeclarationWithFromUrl(composesDeclarations[4]!)).toStrictEqual({
383
+ expect(parseAtValue(atValues[7]!)).toStrictEqual({
384
+ type: 'valueImportDeclaration',
385
+ imports: [{ importedTokenName: 'import', localTokenName: 'alias' }],
343
386
  from: 'test.css',
344
- tokenNames: ['from', 'from', 'from'], // do not deduplicate.
345
387
  });
346
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 and composes properties.
58
- // However, we do not want `generateLocalTokenNames` to return external tokens.
59
- // So we remove the @import and composes properties 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,18 +156,10 @@ 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
- composesDeclarations: Declaration[];
158
163
  };
159
164
 
160
165
  /**
@@ -163,11 +168,13 @@ type CollectNodesResult = {
163
168
  */
164
169
  export function collectNodes(ast: Root): CollectNodesResult {
165
170
  const atImports: AtRule[] = [];
171
+ const atValues: AtRule[] = [];
166
172
  const classSelectors: { rule: Rule; classSelector: ClassName }[] = [];
167
- const composesDeclarations: Declaration[] = [];
168
173
  ast.walk((node) => {
169
174
  if (isAtImportNode(node)) {
170
175
  atImports.push(node);
176
+ } else if (isAtValueNode(node)) {
177
+ atValues.push(node);
171
178
  } else if (isRuleNode(node)) {
172
179
  // In `rule.selector` comes the following string:
173
180
  // 1. ".foo"
@@ -183,11 +190,9 @@ export function collectNodes(ast: Root): CollectNodesResult {
183
190
  }
184
191
  });
185
192
  }).processSync(node);
186
- } else if (isComposesDeclaration(node)) {
187
- composesDeclarations.push(node);
188
193
  }
189
194
  });
190
- return { atImports, classSelectors, composesDeclarations };
195
+ return { atImports, atValues, classSelectors };
191
196
  }
192
197
 
193
198
  /**
@@ -207,39 +212,77 @@ export function parseAtImport(atImport: AtRule): string | undefined {
207
212
  return undefined;
208
213
  }
209
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
+
210
232
  /**
211
- * Parse `composes` declaration with `from <url>`.
212
- * If the declaration is not found or do not have `from <url>`, return `undefined`.
213
- * @param composesDeclaration The `composes` declaration to parse.
214
- * @returns The information of the declaration.
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.
215
248
  */
216
- export function parseComposesDeclarationWithFromUrl(
217
- composesDeclaration: Declaration,
218
- ): { from: string; tokenNames: string[] } | undefined {
219
- // NOTE: `composes` property syntax is...
220
- // - syntax: `composes: <class-name> [...<class-name>] [from <url>];`
221
- // - variables:
222
- // - `<class-name>`: `<sting>`
223
- // - `<url>`: `<string>`
224
- // - ref:
225
- // - https://github.com/css-modules/css-modules#composition
226
- // - https://github.com/css-modules/css-modules#composing-from-other-files
227
- // - https://github.com/css-modules/postcss-modules-extract-imports#specification
228
-
229
- const nodes = valueParser(composesDeclaration.value).nodes;
230
- if (nodes.length < 5) return undefined;
231
-
232
- const classNamesOrSpaces = nodes.slice(0, -3);
233
- const [from, , url] = nodes.slice(-3);
234
-
235
- const classNames = classNamesOrSpaces.filter((node) => node.type === 'word');
236
-
237
- // validate nodes
238
- if (from === undefined) return undefined;
239
- if (from.type !== 'word' || from.value !== 'from') return undefined;
240
- if (url === undefined) return undefined;
241
- if (url.type !== 'string') return undefined;
242
- if (classNames.length === 0) return undefined;
243
-
244
- return { from: url.value, tokenNames: classNames.map((node) => node.value) };
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!`);
245
288
  }