happy-css-modules 0.2.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 (154) hide show
  1. package/LICENSE.txt +23 -0
  2. package/README.md +124 -0
  3. package/bin/hcm.js +9 -0
  4. package/dist/cli.d.ts +6 -0
  5. package/dist/cli.js +69 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cli.test.d.ts +1 -0
  8. package/dist/cli.test.js +34 -0
  9. package/dist/cli.test.js.map +1 -0
  10. package/dist/emitter/dts.d.ts +14 -0
  11. package/dist/emitter/dts.js +106 -0
  12. package/dist/emitter/dts.js.map +1 -0
  13. package/dist/emitter/dts.test.d.ts +1 -0
  14. package/dist/emitter/dts.test.js +205 -0
  15. package/dist/emitter/dts.test.js.map +1 -0
  16. package/dist/emitter/file-system.d.ts +6 -0
  17. package/dist/emitter/file-system.js +26 -0
  18. package/dist/emitter/file-system.js.map +1 -0
  19. package/dist/emitter/file-system.test.d.ts +1 -0
  20. package/dist/emitter/file-system.test.js +34 -0
  21. package/dist/emitter/file-system.test.js.map +1 -0
  22. package/dist/emitter/index.d.ts +34 -0
  23. package/dist/emitter/index.js +42 -0
  24. package/dist/emitter/index.js.map +1 -0
  25. package/dist/emitter/index.test.d.ts +1 -0
  26. package/dist/emitter/index.test.js +118 -0
  27. package/dist/emitter/index.test.js.map +1 -0
  28. package/dist/emitter/source-map.d.ts +9 -0
  29. package/dist/emitter/source-map.js +16 -0
  30. package/dist/emitter/source-map.js.map +1 -0
  31. package/dist/emitter/source-map.test.d.ts +1 -0
  32. package/dist/emitter/source-map.test.js +13 -0
  33. package/dist/emitter/source-map.test.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/integration-test/go-to-definition.test.d.ts +1 -0
  38. package/dist/integration-test/go-to-definition.test.js +367 -0
  39. package/dist/integration-test/go-to-definition.test.js.map +1 -0
  40. package/dist/library/source-map/index.d.ts +8 -0
  41. package/dist/library/source-map/index.js +5 -0
  42. package/dist/library/source-map/index.js.map +1 -0
  43. package/dist/loader/index.d.ts +42 -0
  44. package/dist/loader/index.js +145 -0
  45. package/dist/loader/index.js.map +1 -0
  46. package/dist/loader/index.test.d.ts +1 -0
  47. package/dist/loader/index.test.js +290 -0
  48. package/dist/loader/index.test.js.map +1 -0
  49. package/dist/loader/postcss.d.ts +60 -0
  50. package/dist/loader/postcss.js +209 -0
  51. package/dist/loader/postcss.js.map +1 -0
  52. package/dist/loader/postcss.test.d.ts +1 -0
  53. package/dist/loader/postcss.test.js +236 -0
  54. package/dist/loader/postcss.test.js.map +1 -0
  55. package/dist/resolver/index.d.ts +20 -0
  56. package/dist/resolver/index.js +39 -0
  57. package/dist/resolver/index.js.map +1 -0
  58. package/dist/resolver/index.test.d.ts +1 -0
  59. package/dist/resolver/index.test.js +16 -0
  60. package/dist/resolver/index.test.js.map +1 -0
  61. package/dist/resolver/node-resolver.d.ts +2 -0
  62. package/dist/resolver/node-resolver.js +6 -0
  63. package/dist/resolver/node-resolver.js.map +1 -0
  64. package/dist/resolver/node-resolver.test.d.ts +1 -0
  65. package/dist/resolver/node-resolver.test.js +25 -0
  66. package/dist/resolver/node-resolver.test.js.map +1 -0
  67. package/dist/resolver/relative-resolver.d.ts +2 -0
  68. package/dist/resolver/relative-resolver.js +5 -0
  69. package/dist/resolver/relative-resolver.js.map +1 -0
  70. package/dist/resolver/relative-resolver.test.d.ts +1 -0
  71. package/dist/resolver/relative-resolver.test.js +12 -0
  72. package/dist/resolver/relative-resolver.test.js.map +1 -0
  73. package/dist/resolver/webpack-resolver.d.ts +2 -0
  74. package/dist/resolver/webpack-resolver.js +69 -0
  75. package/dist/resolver/webpack-resolver.js.map +1 -0
  76. package/dist/resolver/webpack-resolver.test.d.ts +1 -0
  77. package/dist/resolver/webpack-resolver.test.js +26 -0
  78. package/dist/resolver/webpack-resolver.test.js.map +1 -0
  79. package/dist/runner.d.ts +33 -0
  80. package/dist/runner.js +75 -0
  81. package/dist/runner.js.map +1 -0
  82. package/dist/runner.test.d.ts +1 -0
  83. package/dist/runner.test.js +113 -0
  84. package/dist/runner.test.js.map +1 -0
  85. package/dist/test/jest/resolver.cjs +30 -0
  86. package/dist/test/jest/resolver.cjs.map +1 -0
  87. package/dist/test/jest/resolver.d.cts +30 -0
  88. package/dist/test/tsserver.d.ts +27 -0
  89. package/dist/test/tsserver.js +104 -0
  90. package/dist/test/tsserver.js.map +1 -0
  91. package/dist/test/util.d.ts +29 -0
  92. package/dist/test/util.js +78 -0
  93. package/dist/test/util.js.map +1 -0
  94. package/dist/transformer/index.d.ts +23 -0
  95. package/dist/transformer/index.js +19 -0
  96. package/dist/transformer/index.js.map +1 -0
  97. package/dist/transformer/less-transformer.d.ts +2 -0
  98. package/dist/transformer/less-transformer.js +43 -0
  99. package/dist/transformer/less-transformer.js.map +1 -0
  100. package/dist/transformer/less-transformer.test.d.ts +1 -0
  101. package/dist/transformer/less-transformer.test.js +126 -0
  102. package/dist/transformer/less-transformer.test.js.map +1 -0
  103. package/dist/transformer/scss-transformer.d.ts +2 -0
  104. package/dist/transformer/scss-transformer.js +84 -0
  105. package/dist/transformer/scss-transformer.js.map +1 -0
  106. package/dist/transformer/scss-transformer.test.d.ts +1 -0
  107. package/dist/transformer/scss-transformer.test.js +132 -0
  108. package/dist/transformer/scss-transformer.test.js.map +1 -0
  109. package/dist/util.d.ts +19 -0
  110. package/dist/util.js +52 -0
  111. package/dist/util.js.map +1 -0
  112. package/dist/util.test.d.ts +1 -0
  113. package/dist/util.test.js +75 -0
  114. package/dist/util.test.js.map +1 -0
  115. package/package.json +106 -0
  116. package/src/__snapshots__/runner.test.ts.snap +34 -0
  117. package/src/cli.test.ts +38 -0
  118. package/src/cli.ts +70 -0
  119. package/src/emitter/dts.test.ts +266 -0
  120. package/src/emitter/dts.ts +134 -0
  121. package/src/emitter/file-system.test.ts +36 -0
  122. package/src/emitter/file-system.ts +24 -0
  123. package/src/emitter/index.test.ts +130 -0
  124. package/src/emitter/index.ts +92 -0
  125. package/src/emitter/source-map.test.ts +20 -0
  126. package/src/emitter/source-map.ts +17 -0
  127. package/src/index.ts +3 -0
  128. package/src/integration-test/go-to-definition.test.ts +371 -0
  129. package/src/library/README.md +3 -0
  130. package/src/library/source-map/index.ts +26 -0
  131. package/src/loader/index.test.ts +306 -0
  132. package/src/loader/index.ts +199 -0
  133. package/src/loader/postcss.test.ts +336 -0
  134. package/src/loader/postcss.ts +239 -0
  135. package/src/resolver/index.test.ts +17 -0
  136. package/src/resolver/index.ts +48 -0
  137. package/src/resolver/node-resolver.test.ts +26 -0
  138. package/src/resolver/node-resolver.ts +7 -0
  139. package/src/resolver/relative-resolver.test.ts +13 -0
  140. package/src/resolver/relative-resolver.ts +6 -0
  141. package/src/resolver/webpack-resolver.test.ts +33 -0
  142. package/src/resolver/webpack-resolver.ts +71 -0
  143. package/src/runner.test.ts +122 -0
  144. package/src/runner.ts +105 -0
  145. package/src/test/jest/resolver.cjs +30 -0
  146. package/src/test/tsserver.ts +176 -0
  147. package/src/test/util.ts +100 -0
  148. package/src/transformer/index.ts +44 -0
  149. package/src/transformer/less-transformer.test.ts +135 -0
  150. package/src/transformer/less-transformer.ts +55 -0
  151. package/src/transformer/scss-transformer.test.ts +142 -0
  152. package/src/transformer/scss-transformer.ts +94 -0
  153. package/src/util.test.ts +89 -0
  154. package/src/util.ts +67 -0
@@ -0,0 +1,336 @@
1
+ import dedent from 'dedent';
2
+ import {
3
+ createRoot,
4
+ createClassSelectors,
5
+ createAtImports,
6
+ createComposesDeclarations,
7
+ createFixtures,
8
+ } from '../test/util.js';
9
+ import {
10
+ generateLocalTokenNames,
11
+ getOriginalLocation,
12
+ parseAtImport,
13
+ parseComposesDeclarationWithFromUrl,
14
+ collectNodes,
15
+ } from './postcss.js';
16
+
17
+ describe('generateLocalTokenNames', () => {
18
+ test('basic', async () => {
19
+ expect(
20
+ await generateLocalTokenNames(
21
+ createRoot(`
22
+ .basic {}
23
+ .cascading {}
24
+ .cascading {}
25
+ .pseudo_class_1 {}
26
+ .pseudo_class_2:hover {}
27
+ :not(.pseudo_class_3) {}
28
+ .multiple_selector_1.multiple_selector_2 {}
29
+ .combinator_1 + .combinator_2 {}
30
+ @supports (display: flex) {
31
+ @media screen and (min-width: 900px) {
32
+ .at_rule {}
33
+ }
34
+ }
35
+ .selector_list_1, .selector_list_2 {}
36
+ :local .local_class_name_1 {}
37
+ :local {
38
+ .local_class_name_2 {}
39
+ .local_class_name_3 {}
40
+ }
41
+ :local(.local_class_name_4) {}
42
+ .composes_target {}
43
+ .composes {
44
+ composes: composes_target;
45
+ }
46
+ `),
47
+ ),
48
+ ).toStrictEqual([
49
+ 'basic',
50
+ 'cascading',
51
+ 'pseudo_class_1',
52
+ 'pseudo_class_2',
53
+ 'pseudo_class_3',
54
+ 'multiple_selector_1',
55
+ 'multiple_selector_2',
56
+ 'combinator_1',
57
+ 'combinator_2',
58
+ 'at_rule',
59
+ 'selector_list_1',
60
+ 'selector_list_2',
61
+ 'local_class_name_1',
62
+ 'local_class_name_2',
63
+ 'local_class_name_3',
64
+ 'local_class_name_4',
65
+ 'composes_target',
66
+ 'composes',
67
+ ]);
68
+ });
69
+ test('does not track styles imported by @import in other file because it is not a local token', async () => {
70
+ createFixtures({
71
+ '/test/1.css': dedent`
72
+ .a {}
73
+ `,
74
+ });
75
+ expect(
76
+ await generateLocalTokenNames(
77
+ createRoot(`
78
+ @import "/test/1.css";
79
+ `),
80
+ ),
81
+ ).toStrictEqual([]);
82
+ });
83
+ test('does not track styles imported by composes in other file because it is not a local token', async () => {
84
+ createFixtures({
85
+ '/test/1.css': dedent`
86
+ .b {}
87
+ `,
88
+ });
89
+ expect(
90
+ await generateLocalTokenNames(
91
+ createRoot(`
92
+ .a {
93
+ composes: b from "/test/1.css";
94
+ }
95
+ `),
96
+ ),
97
+ ).toStrictEqual(['a']);
98
+ });
99
+ });
100
+
101
+ describe('getOriginalLocation', () => {
102
+ test('basic', () => {
103
+ const [basic] = createClassSelectors(
104
+ createRoot(dedent`
105
+ .basic {}
106
+ `),
107
+ );
108
+ expect(getOriginalLocation(basic!.rule, basic!.classSelector)).toMatchInlineSnapshot(
109
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }`,
110
+ );
111
+ });
112
+ test('cascading', () => {
113
+ // eslint-disable-next-line @typescript-eslint/naming-convention
114
+ const [cascading_1, cascading_2] = createClassSelectors(
115
+ createRoot(dedent`
116
+ .cascading {}
117
+ .cascading {}
118
+ `),
119
+ );
120
+ expect(getOriginalLocation(cascading_1!.rule, cascading_1!.classSelector)).toMatchInlineSnapshot(
121
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 10 } }`,
122
+ );
123
+ expect(getOriginalLocation(cascading_2!.rule, cascading_2!.classSelector)).toMatchInlineSnapshot(
124
+ `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 10 } }`,
125
+ );
126
+ });
127
+ test('pseudo_class', () => {
128
+ // eslint-disable-next-line @typescript-eslint/naming-convention
129
+ const [pseudo_class_1, pseudo_class_2, pseudo_class_3] = createClassSelectors(
130
+ createRoot(dedent`
131
+ .pseudo_class_1 {}
132
+ .pseudo_class_2:hover {}
133
+ :not(.pseudo_class_3) {}
134
+ `),
135
+ );
136
+ expect(getOriginalLocation(pseudo_class_1!.rule, pseudo_class_1!.classSelector)).toMatchInlineSnapshot(
137
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 15 } }`,
138
+ );
139
+ expect(getOriginalLocation(pseudo_class_2!.rule, pseudo_class_2!.classSelector)).toMatchInlineSnapshot(
140
+ `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 15 } }`,
141
+ );
142
+ expect(getOriginalLocation(pseudo_class_3!.rule, pseudo_class_3!.classSelector)).toMatchInlineSnapshot(
143
+ `{ filePath: "/test/test.css", start: { line: 3, column: 6 }, end: { line: 3, column: 20 } }`,
144
+ );
145
+ });
146
+ test('multiple_selector', () => {
147
+ // eslint-disable-next-line @typescript-eslint/naming-convention
148
+ const [multiple_selector_1, multiple_selector_2] = createClassSelectors(
149
+ createRoot(dedent`
150
+ .multiple_selector_1.multiple_selector_2 {}
151
+ `),
152
+ );
153
+ expect(getOriginalLocation(multiple_selector_1!.rule, multiple_selector_1!.classSelector)).toMatchInlineSnapshot(
154
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 20 } }`,
155
+ );
156
+ expect(getOriginalLocation(multiple_selector_2!.rule, multiple_selector_2!.classSelector)).toMatchInlineSnapshot(
157
+ `{ filePath: "/test/test.css", start: { line: 1, column: 21 }, end: { line: 1, column: 40 } }`,
158
+ );
159
+ });
160
+
161
+ test('combinator', () => {
162
+ // eslint-disable-next-line @typescript-eslint/naming-convention
163
+ const [combinator_1, combinator_2] = createClassSelectors(
164
+ createRoot(dedent`
165
+ .combinator_1 + .combinator_2 {}
166
+ `),
167
+ );
168
+ expect(getOriginalLocation(combinator_1!.rule, combinator_1!.classSelector)).toMatchInlineSnapshot(
169
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 13 } }`,
170
+ );
171
+ expect(getOriginalLocation(combinator_2!.rule, combinator_2!.classSelector)).toMatchInlineSnapshot(
172
+ `{ filePath: "/test/test.css", start: { line: 1, column: 17 }, end: { line: 1, column: 29 } }`,
173
+ );
174
+ });
175
+ test('at_rule', () => {
176
+ // eslint-disable-next-line @typescript-eslint/naming-convention
177
+ const [at_rule] = createClassSelectors(
178
+ createRoot(dedent`
179
+ @supports (display: flex) {
180
+ @media screen and (min-width: 900px) {
181
+ .at_rule {}
182
+ }
183
+ }
184
+ `),
185
+ );
186
+ expect(getOriginalLocation(at_rule!.rule, at_rule!.classSelector)).toMatchInlineSnapshot(
187
+ `{ filePath: "/test/test.css", start: { line: 3, column: 5 }, end: { line: 3, column: 12 } }`,
188
+ );
189
+ });
190
+ test('selector_list', () => {
191
+ // eslint-disable-next-line @typescript-eslint/naming-convention
192
+ const [selector_list_1, selector_list_2] = createClassSelectors(
193
+ createRoot(dedent`
194
+ .selector_list_1, .selector_list_2 {}
195
+ `),
196
+ );
197
+ expect(getOriginalLocation(selector_list_1!.rule, selector_list_1!.classSelector)).toMatchInlineSnapshot(
198
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 16 } }`,
199
+ );
200
+ expect(getOriginalLocation(selector_list_2!.rule, selector_list_2!.classSelector)).toMatchInlineSnapshot(
201
+ `{ filePath: "/test/test.css", start: { line: 1, column: 19 }, end: { line: 1, column: 34 } }`,
202
+ );
203
+ });
204
+ test('local_class_name', () => {
205
+ // eslint-disable-next-line @typescript-eslint/naming-convention
206
+ const [local_class_name_1, local_class_name_2, local_class_name_3, local_class_name_4] = createClassSelectors(
207
+ createRoot(dedent`
208
+ :local .local_class_name_1 {}
209
+ :local {
210
+ .local_class_name_2 {}
211
+ .local_class_name_3 {}
212
+ }
213
+ :local(.local_class_name_4) {}
214
+ `),
215
+ );
216
+ expect(getOriginalLocation(local_class_name_1!.rule, local_class_name_1!.classSelector)).toMatchInlineSnapshot(
217
+ `{ filePath: "/test/test.css", start: { line: 1, column: 8 }, end: { line: 1, column: 26 } }`,
218
+ );
219
+ expect(getOriginalLocation(local_class_name_2!.rule, local_class_name_2!.classSelector)).toMatchInlineSnapshot(
220
+ `{ filePath: "/test/test.css", start: { line: 3, column: 3 }, end: { line: 3, column: 21 } }`,
221
+ );
222
+ expect(getOriginalLocation(local_class_name_3!.rule, local_class_name_3!.classSelector)).toMatchInlineSnapshot(
223
+ `{ filePath: "/test/test.css", start: { line: 4, column: 3 }, end: { line: 4, column: 21 } }`,
224
+ );
225
+ expect(getOriginalLocation(local_class_name_4!.rule, local_class_name_4!.classSelector)).toMatchInlineSnapshot(
226
+ `{ filePath: "/test/test.css", start: { line: 6, column: 8 }, end: { line: 6, column: 26 } }`,
227
+ );
228
+ });
229
+ test('composes', () => {
230
+ // eslint-disable-next-line @typescript-eslint/naming-convention
231
+ const [composes_target, composes] = createClassSelectors(
232
+ createRoot(dedent`
233
+ .composes_target {}
234
+ .composes {
235
+ composes: composes_target;
236
+ }
237
+ `),
238
+ );
239
+ expect(getOriginalLocation(composes_target!.rule, composes_target!.classSelector)).toMatchInlineSnapshot(
240
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 16 } }`,
241
+ );
242
+ expect(getOriginalLocation(composes!.rule, composes!.classSelector)).toMatchInlineSnapshot(
243
+ `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 9 } }`,
244
+ );
245
+ });
246
+ test('with_newline', () => {
247
+ // eslint-disable-next-line @typescript-eslint/naming-convention
248
+ const [with_newline_1, with_newline_2, with_newline_3] = createClassSelectors(
249
+ createRoot(dedent`
250
+ .with_newline_1,
251
+ .with_newline_2
252
+ + .with_newline_3, {}
253
+ `),
254
+ );
255
+ expect(getOriginalLocation(with_newline_1!.rule, with_newline_1!.classSelector)).toMatchInlineSnapshot(
256
+ `{ filePath: "/test/test.css", start: { line: 1, column: 1 }, end: { line: 1, column: 15 } }`,
257
+ );
258
+ expect(getOriginalLocation(with_newline_2!.rule, with_newline_2!.classSelector)).toMatchInlineSnapshot(
259
+ `{ filePath: "/test/test.css", start: { line: 2, column: 1 }, end: { line: 2, column: 15 } }`,
260
+ );
261
+
262
+ expect(getOriginalLocation(with_newline_3!.rule, with_newline_3!.classSelector)).toMatchInlineSnapshot(
263
+ `{ filePath: "/test/test.css", start: { line: 3, column: 5 }, end: { line: 3, column: 19 } }`,
264
+ );
265
+ });
266
+ });
267
+
268
+ test('collectNodes', () => {
269
+ const ast = createRoot(dedent`
270
+ @import;
271
+ @import "test.css";
272
+ @ignored;
273
+ .a { ignored: "ignored"; composes: a; }
274
+ .b { ignored: "ignored"; composes: b; }
275
+ `);
276
+
277
+ const { atImports, classSelectors, composesDeclarations } = collectNodes(ast);
278
+
279
+ expect(atImports).toHaveLength(2);
280
+ expect(atImports[0]!.toString()).toEqual('@import');
281
+ expect(atImports[1]!.toString()).toEqual('@import "test.css"');
282
+ expect(classSelectors).toHaveLength(2);
283
+ expect(classSelectors[0]!.rule.toString()).toEqual('.a { ignored: "ignored"; composes: a; }');
284
+ expect(classSelectors[0]!.classSelector.toString()).toEqual('.a');
285
+ expect(classSelectors[1]!.rule.toString()).toEqual('.b { ignored: "ignored"; composes: b; }');
286
+ expect(classSelectors[1]!.classSelector.toString()).toEqual('.b');
287
+ expect(composesDeclarations).toHaveLength(2);
288
+ expect(composesDeclarations[0]!.toString()).toEqual('composes: a');
289
+ expect(composesDeclarations[1]!.toString()).toEqual('composes: b');
290
+ });
291
+
292
+ test('parseAtImport', () => {
293
+ const atImports = createAtImports(
294
+ createRoot(dedent`
295
+ @import;
296
+ @import "test.css";
297
+ @import url("test.css");
298
+ @import url(test.css);
299
+ @import "test.css" print;
300
+ `),
301
+ );
302
+ expect(parseAtImport(atImports[0]!)).toBe(undefined);
303
+ expect(parseAtImport(atImports[1]!)).toBe('test.css');
304
+ expect(parseAtImport(atImports[2]!)).toBe('test.css');
305
+ expect(parseAtImport(atImports[3]!)).toBe('test.css');
306
+ expect(parseAtImport(atImports[4]!)).toBe('test.css');
307
+ });
308
+
309
+ test('parseComposesDeclarationWithFromUrl', () => {
310
+ const composesDeclarations = createComposesDeclarations(
311
+ createRoot(dedent`
312
+ .a {
313
+ composes: a;
314
+ composes: a b c;
315
+ composes: a from "test.css";
316
+ composes: a b c from "test.css";
317
+ composes: from from from from "test.css";
318
+ /* NOTE: CSS Modules do not support '... from url("test.css")'. */
319
+ }
320
+ `),
321
+ );
322
+ expect(parseComposesDeclarationWithFromUrl(composesDeclarations[0]!)).toStrictEqual(undefined);
323
+ expect(parseComposesDeclarationWithFromUrl(composesDeclarations[1]!)).toStrictEqual(undefined);
324
+ expect(parseComposesDeclarationWithFromUrl(composesDeclarations[2]!)).toStrictEqual({
325
+ from: 'test.css',
326
+ tokenNames: ['a'],
327
+ });
328
+ expect(parseComposesDeclarationWithFromUrl(composesDeclarations[3]!)).toStrictEqual({
329
+ from: 'test.css',
330
+ tokenNames: ['a', 'b', 'c'],
331
+ });
332
+ expect(parseComposesDeclarationWithFromUrl(composesDeclarations[4]!)).toStrictEqual({
333
+ from: 'test.css',
334
+ tokenNames: ['from', 'from', 'from'], // do not deduplicate.
335
+ });
336
+ });
@@ -0,0 +1,239 @@
1
+ import postcss, { type Rule, type AtRule, type Root, type Node, type Declaration, type Plugin } from 'postcss';
2
+ import modules from 'postcss-modules';
3
+ import selectorParser, { type ClassName } from 'postcss-selector-parser';
4
+ import valueParser from 'postcss-value-parser';
5
+
6
+ /** The pair of line number and column number. */
7
+ export type Position = {
8
+ /** The line number in the source file. It is 1-based (compatible with postcss). */
9
+ line: number;
10
+ /** The column number in the source file. It is 1-based (compatible with postcss). */
11
+ column: number;
12
+ };
13
+
14
+ /** The location of class selector. */
15
+ export type Location = {
16
+ filePath: string;
17
+ /** The inclusive starting position of the node's source (compatible with postcss). */
18
+ start: Position;
19
+ /** The inclusive ending position of the node's source (compatible with postcss). */
20
+ end: Position;
21
+ };
22
+
23
+ function removeDependenciesPlugin(): Plugin {
24
+ return {
25
+ postcssPlugin: 'remove-dependencies',
26
+ // eslint-disable-next-line @typescript-eslint/naming-convention
27
+ AtRule(atRule) {
28
+ if (isAtImportNode(atRule)) {
29
+ atRule.remove();
30
+ }
31
+ },
32
+ // eslint-disable-next-line @typescript-eslint/naming-convention
33
+ Declaration(declaration) {
34
+ if (isComposesDeclaration(declaration)) {
35
+ declaration.remove();
36
+ }
37
+ },
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Traverses a local token from the AST and returns its name.
43
+ * @param ast The AST to traverse.
44
+ * @returns The name of the local token.
45
+ */
46
+ export async function generateLocalTokenNames(ast: Root): Promise<string[]> {
47
+ return new Promise((resolve, reject) => {
48
+ postcss
49
+ .default()
50
+ // postcss-modules collects tokens (i.e., includes external tokens) by following
51
+ // the dependencies specified in the @import and composes properties.
52
+ // However, we do not want `generateLocalTokenNames` to return external tokens.
53
+ // So we remove the @import and composes properties beforehand.
54
+ .use(removeDependenciesPlugin())
55
+ .use(
56
+ modules({
57
+ getJSON: (_cssFileName, json) => {
58
+ resolve(Object.keys(json));
59
+ },
60
+ }),
61
+ )
62
+ // NOTE: `process` modifies ast, so clone it.
63
+ .process(ast.clone())
64
+ .catch(reject);
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Get the token's location on the source file.
70
+ * @param rule The rule node that contains the token.
71
+ * @param classSelector The class selector node that contains the token.
72
+ * @returns The token's location on the source file.
73
+ */
74
+ export function getOriginalLocation(rule: Rule, classSelector: ClassName): Location {
75
+ // The node derived from `postcss.parse` always has `source` property. Therefore, this line is unreachable.
76
+ if (rule.source === undefined || classSelector.source === undefined) throw new Error('Node#source is undefined');
77
+ // The node derived from `postcss.parse` always has `start` and `end` property. Therefore, this line is unreachable.
78
+ if (rule.source.start === undefined || classSelector.source.start === undefined)
79
+ throw new Error('Node#start is undefined');
80
+ if (rule.source.end === undefined || classSelector.source.end === undefined) throw new Error('Node#end is undefined');
81
+ if (rule.source.input.file === undefined) throw new Error('Node#input.file is undefined');
82
+
83
+ const start = {
84
+ // The line is 1-based.
85
+ line: rule.source.start.line + (classSelector.source.start.line - 1),
86
+ // The column is 1-based.
87
+ column: rule.source.start.column + (classSelector.source.start.column - 1),
88
+ };
89
+ const end = {
90
+ line: start.line,
91
+ // The column is inclusive.
92
+ column: start.column + classSelector.value.length,
93
+ };
94
+ let location = {
95
+ filePath: rule.source.input.file,
96
+ start,
97
+ end,
98
+ };
99
+
100
+ if (rule.source.input.map) {
101
+ const origin = rule.source.input.origin(
102
+ location.start.line,
103
+ // The column of `Input#origin` is 0-based. This behavior is undocumented and probably a postcss's bug.
104
+ // TODO: Open PR to postcss/postcss
105
+ location.start.column - 1,
106
+ );
107
+ if (origin === false) throw new Error('`Input#origin` returned false');
108
+ if (origin.file === undefined) throw new Error('`FilePosition#file` is undefined');
109
+
110
+ location = {
111
+ filePath: origin.file,
112
+ start: {
113
+ line: origin.line,
114
+ // The column of `Input#origin` is 0-based.
115
+ column: origin.column + 1,
116
+ },
117
+ end: {
118
+ line: origin.line,
119
+ // The column of `Input#origin` is 0-based. Also, the column of happy-css-modules is inclusive.
120
+ column: origin.column + 1 + (classSelector.value.length - 1),
121
+ },
122
+ };
123
+ }
124
+
125
+ return location;
126
+ }
127
+
128
+ function isAtRuleNode(node: Node): node is AtRule {
129
+ return node.type === 'atrule';
130
+ }
131
+
132
+ function isAtImportNode(node: Node): node is AtRule {
133
+ return isAtRuleNode(node) && node.name === 'import';
134
+ }
135
+
136
+ function isRuleNode(node: Node): node is Rule {
137
+ return node.type === 'rule';
138
+ }
139
+
140
+ function isDeclaration(node: Node): node is Declaration {
141
+ return node.type === 'decl';
142
+ }
143
+
144
+ function isComposesDeclaration(node: Node): node is Declaration {
145
+ return isDeclaration(node) && node.prop === 'composes';
146
+ }
147
+
148
+ type CollectNodesResult = {
149
+ atImports: AtRule[];
150
+ classSelectors: { rule: Rule; classSelector: ClassName }[];
151
+ composesDeclarations: Declaration[];
152
+ };
153
+
154
+ /**
155
+ * Collect nodes from the AST.
156
+ * @param ast The AST.
157
+ */
158
+ export function collectNodes(ast: Root): CollectNodesResult {
159
+ const atImports: AtRule[] = [];
160
+ const classSelectors: { rule: Rule; classSelector: ClassName }[] = [];
161
+ const composesDeclarations: Declaration[] = [];
162
+ ast.walk((node) => {
163
+ if (isAtImportNode(node)) {
164
+ atImports.push(node);
165
+ } else if (isRuleNode(node)) {
166
+ // In `rule.selector` comes the following string:
167
+ // 1. ".foo"
168
+ // 2. ".foo:hover"
169
+ // 3. ".foo, .bar"
170
+ selectorParser((selectors) => {
171
+ selectors.walk((selector) => {
172
+ if (selector.type === 'class') {
173
+ // In `selector.value` comes the following string:
174
+ // 1. "foo"
175
+ // 2. "bar"
176
+ classSelectors.push({ rule: node, classSelector: selector });
177
+ }
178
+ });
179
+ }).processSync(node);
180
+ } else if (isComposesDeclaration(node)) {
181
+ composesDeclarations.push(node);
182
+ }
183
+ });
184
+ return { atImports, classSelectors, composesDeclarations };
185
+ }
186
+
187
+ /**
188
+ * Parse the `@import` rule.
189
+ * @param atImport The `@import` rule to parse.
190
+ * @returns The imported sheet path.
191
+ */
192
+ export function parseAtImport(atImport: AtRule): string | undefined {
193
+ const firstNode = valueParser(atImport.params).nodes[0];
194
+ if (firstNode === undefined) return undefined;
195
+ if (firstNode.type === 'string') return firstNode.value;
196
+ if (firstNode.type === 'function' && firstNode.value === 'url') {
197
+ if (firstNode.nodes[0] === undefined) return undefined;
198
+ if (firstNode.nodes[0].type === 'string') return firstNode.nodes[0].value;
199
+ if (firstNode.nodes[0].type === 'word') return firstNode.nodes[0].value;
200
+ }
201
+ return undefined;
202
+ }
203
+
204
+ /**
205
+ * Parse `composes` declaration with `from <url>`.
206
+ * If the declaration is not found or do not have `from <url>`, return `undefined`.
207
+ * @param composesDeclaration The `composes` declaration to parse.
208
+ * @returns The information of the declaration.
209
+ */
210
+ export function parseComposesDeclarationWithFromUrl(
211
+ composesDeclaration: Declaration,
212
+ ): { from: string; tokenNames: string[] } | undefined {
213
+ // NOTE: `composes` property syntax is...
214
+ // - syntax: `composes: <class-name> [...<class-name>] [from <url>];`
215
+ // - variables:
216
+ // - `<class-name>`: `<sting>`
217
+ // - `<url>`: `<string>`
218
+ // - ref:
219
+ // - https://github.com/css-modules/css-modules#composition
220
+ // - https://github.com/css-modules/css-modules#composing-from-other-files
221
+ // - https://github.com/css-modules/postcss-modules-extract-imports#specification
222
+
223
+ const nodes = valueParser(composesDeclaration.value).nodes;
224
+ if (nodes.length < 5) return undefined;
225
+
226
+ const classNamesOrSpaces = nodes.slice(0, -3);
227
+ const [from, , url] = nodes.slice(-3);
228
+
229
+ const classNames = classNamesOrSpaces.filter((node) => node.type === 'word');
230
+
231
+ // validate nodes
232
+ if (from === undefined) return undefined;
233
+ if (from.type !== 'word' || from.value !== 'from') return undefined;
234
+ if (url === undefined) return undefined;
235
+ if (url.type !== 'string') return undefined;
236
+ if (classNames.length === 0) return undefined;
237
+
238
+ return { from: url.value, tokenNames: classNames.map((node) => node.value) };
239
+ }
@@ -0,0 +1,17 @@
1
+ import { createFixtures, getFixturePath } from '../test/util.js';
2
+ import { createDefaultResolver } from './index.js';
3
+
4
+ const defaultResolver = createDefaultResolver();
5
+ const request = getFixturePath('/test/1.css');
6
+
7
+ test('resolve with webpackResolver when other resolvers fail to resolve', async () => {
8
+ createFixtures({
9
+ '/test/2.css': `.a {}`,
10
+ '/test/3.css': `.a {}`,
11
+ '/node_modules/3.css/index.css': `.a {}`,
12
+ '/node_modules/4.css/index.css': `.a {}`,
13
+ });
14
+ expect(await defaultResolver('2.css', { request })).toBe(getFixturePath('/test/2.css'));
15
+ expect(await defaultResolver('3.css', { request })).toBe(getFixturePath('/test/3.css'));
16
+ expect(await defaultResolver('~4.css', { request })).toBe(getFixturePath('/node_modules/4.css/index.css'));
17
+ });
@@ -0,0 +1,48 @@
1
+ import { exists } from '../util.js';
2
+ import { createNodeResolver } from './node-resolver.js';
3
+ import { createRelativeResolver } from './relative-resolver.js';
4
+ import { createWebpackResolver } from './webpack-resolver.js';
5
+
6
+ export type ResolverOptions = {
7
+ request: string;
8
+ };
9
+
10
+ /**
11
+ * The function to resolve the path of the imported file.
12
+ * @returns The resolved path of the imported file. `false` means to skip resolving.
13
+ * */
14
+ export type Resolver = (specifier: string, options: ResolverOptions) => string | false | Promise<string | false>;
15
+
16
+ /**
17
+ * The Default resolver.
18
+ *
19
+ * This resolver implements a resolve algorithm that is as compatible as possible with the major toolchains,
20
+ * including Node.js, webpack (css-loader, sass-loader, less-loader) and vite. It is difficult to completely
21
+ * mimic the behavior of these toolchains, so the behavior may differ in some cases.
22
+ *
23
+ * @param specifier The specifier to resolve (i.e. './foo.css', '~bootstrap', etc.)
24
+ * @param options The options to resolve
25
+ * @returns The resolved path (absolute). `false` means to skip resolving.
26
+ */
27
+ export const createDefaultResolver: () => Resolver = () => async (specifier, options) => {
28
+ const relativeResolver = createRelativeResolver();
29
+ const nodeResolver = createNodeResolver();
30
+ const webpackResolver = createWebpackResolver();
31
+
32
+ // In less-loader, `relativeResolver` has priority over `webpackResolver`.
33
+ // happy-css-modules follows suit.
34
+ // ref: https://github.com/webpack-contrib/less-loader/tree/454e187f58046356c3d383d67fda763db8bfc528#webpack-resolver
35
+ const resolvers = [relativeResolver, nodeResolver, webpackResolver];
36
+ for (const resolver of resolvers) {
37
+ try {
38
+ const resolved = await resolver(specifier, options);
39
+ if (resolved !== false) {
40
+ const isExists = await exists(resolved);
41
+ if (isExists) return resolved;
42
+ }
43
+ } catch (e) {
44
+ // noop
45
+ }
46
+ }
47
+ return false;
48
+ };
@@ -0,0 +1,26 @@
1
+ import { createFixtures, getFixturePath } from '../test/util.js';
2
+ import { createNodeResolver } from './node-resolver.js';
3
+
4
+ const nodeResolver = createNodeResolver();
5
+ const request = getFixturePath('/test/1.css');
6
+
7
+ test('resolves specifier with node mechanism', async () => {
8
+ createFixtures({
9
+ '/test/2.css': `.a {}`,
10
+ '/test/dir/3.css': `.a {}`,
11
+ '/4.css': `.a {}`,
12
+ '/test/5.css': `.a {}`,
13
+ '/node_modules/package-1/6.css': `.a {}`,
14
+ '/package.json': `{ "imports": { "#subpath-1": "./test/7.css", "#subpath-2/*.css": "./test/*.css" } }`,
15
+ '/test/7.css': `.a {}`,
16
+ '/test/8.css': `.a {}`,
17
+ });
18
+ expect(await nodeResolver('./2.css', { request })).toBe(getFixturePath('/test/2.css'));
19
+ expect(await nodeResolver('./dir/3.css', { request })).toBe(getFixturePath('/test/dir/3.css'));
20
+ expect(await nodeResolver('../4.css', { request })).toBe(getFixturePath('/4.css'));
21
+ expect(await nodeResolver(getFixturePath('/test/5.css'), { request })).toBe(getFixturePath('/test/5.css'));
22
+ expect(await nodeResolver('package-1/6.css', { request })).toBe(getFixturePath('/node_modules/package-1/6.css'));
23
+ expect(await nodeResolver('#subpath-1', { request })).toBe(getFixturePath('/test/7.css'));
24
+ // FIXME: For some reason, it does not pass...
25
+ // expect(await nodeResolver('#subpath-2/8.css', { request })).toBe(getFixturePath('/test/8.css'));
26
+ });
@@ -0,0 +1,7 @@
1
+ import { fileURLToPath, pathToFileURL } from 'url';
2
+ import { resolve } from 'import-meta-resolve';
3
+ import type { Resolver } from './index.js';
4
+
5
+ export const createNodeResolver: () => Resolver = () => async (specifier, options) => {
6
+ return fileURLToPath(await resolve(specifier, pathToFileURL(options.request).href));
7
+ };