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,34 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`generates .d.ts and .d.ts.map 1`] = `
4
+ "declare const styles:
5
+ & Readonly<{ "a": string }>
6
+ ;
7
+ export default styles;
8
+ //# sourceMappingURL=./1.css.d.ts.map
9
+ "
10
+ `;
11
+
12
+ exports[`generates .d.ts and .d.ts.map 2`] = `"{"version":3,"sources":["./1.css"],"names":["a"],"mappings":"AAAA;AAAA,E,aAAAA,G,WAAA;AAAA;AAAA","file":"1.css.d.ts","sourceRoot":""}"`;
13
+
14
+ exports[`generates .d.ts and .d.ts.map 3`] = `
15
+ "declare const styles:
16
+ & Readonly<{ "b": string }>
17
+ ;
18
+ export default styles;
19
+ //# sourceMappingURL=./2.css.d.ts.map
20
+ "
21
+ `;
22
+
23
+ exports[`generates .d.ts and .d.ts.map 4`] = `"{"version":3,"sources":["./2.css"],"names":["b"],"mappings":"AAAA;AAAA,E,aAAAA,G,WAAA;AAAA;AAAA","file":"2.css.d.ts","sourceRoot":""}"`;
24
+
25
+ exports[`supports transformer 1`] = `
26
+ "declare const styles:
27
+ & Readonly<{ "a": string }>
28
+ ;
29
+ export default styles;
30
+ //# sourceMappingURL=./1.scss.d.ts.map
31
+ "
32
+ `;
33
+
34
+ exports[`supports transformer 2`] = `"{"version":3,"sources":["./1.scss"],"names":["a"],"mappings":"AAAA;AAAA,E,aAAAA,G,WAAA;AAAA;AAAA","file":"1.scss.d.ts","sourceRoot":""}"`;
@@ -0,0 +1,38 @@
1
+ import { parseArgv } from './cli.js';
2
+
3
+ const baseArgs = ['node', 'hcm'];
4
+
5
+ describe('parseArgv', () => {
6
+ test('pattern', () => {
7
+ expect(parseArgv([...baseArgs, 'foo']).pattern).toStrictEqual('foo');
8
+ expect(parseArgv([...baseArgs, '1']).pattern).toStrictEqual('1');
9
+
10
+ // TODO: Support multiple patterns
11
+ // parseArgv([...baseArgs, 'foo', 'bar']);
12
+ });
13
+ test('--outDir', () => {
14
+ expect(parseArgv([...baseArgs, '1.css', '--outDir', 'foo']).outDir).toStrictEqual('foo');
15
+ expect(parseArgv([...baseArgs, '1.css', '--outDir', '1']).outDir).toStrictEqual('1');
16
+ });
17
+ test('--watch', () => {
18
+ expect(parseArgv([...baseArgs, '1.css', '--watch']).watch).toBe(true);
19
+ expect(parseArgv([...baseArgs, '1.css', '--no-watch']).watch).toBe(false);
20
+ });
21
+ test('--localsConvention', () => {
22
+ expect(parseArgv([...baseArgs, '1.css']).localsConvention).toBe(undefined);
23
+ expect(parseArgv([...baseArgs, '1.css', '--localsConvention', 'camelCaseOnly']).localsConvention).toBe(
24
+ 'camelCaseOnly',
25
+ );
26
+ expect(parseArgv([...baseArgs, '1.css', '--localsConvention', 'camelCase']).localsConvention).toBe('camelCase');
27
+ expect(parseArgv([...baseArgs, '1.css', '--localsConvention', 'dashesOnly']).localsConvention).toBe('dashesOnly');
28
+ expect(parseArgv([...baseArgs, '1.css', '--localsConvention', 'dashes']).localsConvention).toBe('dashes');
29
+ });
30
+ test('--declarationMap', () => {
31
+ expect(parseArgv([...baseArgs, '1.css', '--declarationMap']).declarationMap).toBe(true);
32
+ expect(parseArgv([...baseArgs, '1.css', '--no-declarationMap']).declarationMap).toBe(false);
33
+ });
34
+ test('--silent', () => {
35
+ expect(parseArgv([...baseArgs, '1.css', '--silent']).silent).toBe(true);
36
+ expect(parseArgv([...baseArgs, '1.css', '--no-silent']).silent).toBe(false);
37
+ });
38
+ });
package/src/cli.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import yargs from 'yargs';
5
+ import { hideBin } from 'yargs/helpers';
6
+ import { type RunnerOptions } from './runner.js';
7
+
8
+ const pkgJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf-8'));
9
+
10
+ /**
11
+ * Parse command line arguments.
12
+ * @returns Runner options.
13
+ */
14
+ export function parseArgv(argv: string[]): RunnerOptions {
15
+ const parsedArgv = yargs(hideBin(argv))
16
+ .parserConfiguration({
17
+ // workaround for https://github.com/yargs/yargs/issues/1318
18
+ 'duplicate-arguments-array': false,
19
+ })
20
+ .scriptName('hcm')
21
+ .usage('Create .d.ts and .d.ts.map from CSS modules *.css files.\n\n$0 [options] <glob>')
22
+ .example("$0 'src/**/*.module.css'", 'Generate .d.ts and .d.ts.map.')
23
+ .example("$0 'src/**/*.module.{css,scss,less}'", 'Also generate files for sass and less.')
24
+ .example("$0 'src/**/*.module.css' --watch", 'Watch for changes and generate .d.ts and .d.ts.map.')
25
+ .example("$0 'src/**/*.module.css' --declarationMap=false", 'Generate .d.ts only.')
26
+ .detectLocale(false)
27
+ .option('outDir', {
28
+ type: 'string',
29
+ describe: 'Output directory',
30
+ })
31
+ .option('watch', {
32
+ type: 'boolean',
33
+ alias: 'w',
34
+ default: false,
35
+ describe: "Watch input directory's css files or pattern",
36
+ })
37
+ .option('localsConvention', {
38
+ choices: ['camelCase', 'camelCaseOnly', 'dashes', 'dashesOnly'] as const,
39
+ describe: 'Style of exported class names.',
40
+ })
41
+ .option('declarationMap', {
42
+ type: 'boolean',
43
+ default: true,
44
+ describe: 'Create sourcemaps for d.ts files',
45
+ })
46
+ .option('silent', {
47
+ type: 'boolean',
48
+ default: false,
49
+ describe: 'Silent output. Do not show "files written" messages',
50
+ })
51
+ .alias('h', 'help')
52
+ .alias('v', 'version')
53
+ .version(pkgJson.version)
54
+ .check((argv) => {
55
+ const patterns = argv._;
56
+ // TODO: support multiple patterns
57
+ if (patterns.length !== 1) throw new Error('Only one pattern is allowed.');
58
+ return true;
59
+ })
60
+ .parseSync();
61
+ const patterns: string[] = parsedArgv._.map((pattern) => pattern.toString());
62
+ return {
63
+ pattern: patterns[0]!,
64
+ outDir: parsedArgv.outDir,
65
+ watch: parsedArgv.watch,
66
+ localsConvention: parsedArgv.localsConvention,
67
+ declarationMap: parsedArgv.declarationMap,
68
+ silent: parsedArgv.silent,
69
+ };
70
+ }
@@ -0,0 +1,266 @@
1
+ import dedent from 'dedent';
2
+ import { SourceMapConsumer } from 'source-map';
3
+ import { Loader } from '../loader/index.js';
4
+ import { getFixturePath, createFixtures } from '../test/util.js';
5
+ import { generateDtsContentWithSourceMap, getDtsFilePath } from './dts.js';
6
+ import { type DtsFormatOptions } from './index.js';
7
+
8
+ const loader = new Loader();
9
+ const isExternalFile = () => false;
10
+
11
+ test('getDtsFilePath', () => {
12
+ expect(getDtsFilePath('/app/src/dir/1.css', undefined)).toBe('/app/src/dir/1.css.d.ts');
13
+ expect(getDtsFilePath('/app/src/dir/1.css', { rootDir: '/app', outDir: '/app/dist' })).toBe(
14
+ '/app/dist/src/dir/1.css.d.ts',
15
+ );
16
+ expect(() => getDtsFilePath('/tmp/src/dir/1.css', { rootDir: '/app', outDir: '/app/dist' })).toThrow();
17
+ expect(() => getDtsFilePath('/app/src/dir/1.css', { rootDir: '/app', outDir: '/tmp/dist' })).toThrow();
18
+ });
19
+
20
+ describe('generateDtsContentWithSourceMap', () => {
21
+ const filePath = getFixturePath('/test/1.css');
22
+ const dtsFilePath = getFixturePath('/test/1.css.d.ts');
23
+ const sourceMapFilePath = getFixturePath('/test/1.css.map');
24
+ const dtsFormatOptions: DtsFormatOptions = {
25
+ localsConvention: undefined,
26
+ };
27
+ test('generate dts content with source map', async () => {
28
+ createFixtures({
29
+ '/test/1.css': dedent`
30
+ @import './2.css';
31
+ .a {}
32
+ .b {}
33
+ .b {}
34
+ `,
35
+ '/test/2.css': dedent`
36
+ @import './3.css';
37
+ .c {}
38
+ `,
39
+ '/test/3.css': dedent`
40
+ .d {}
41
+ `,
42
+ });
43
+ const result = await loader.load(filePath);
44
+ const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
45
+ filePath,
46
+ dtsFilePath,
47
+ sourceMapFilePath,
48
+ result.tokens,
49
+ dtsFormatOptions,
50
+ isExternalFile,
51
+ );
52
+ expect(dtsContent).toMatchInlineSnapshot(`
53
+ "declare const styles:
54
+ & Readonly<Pick<(typeof import("./3.css"))["default"], "d">>
55
+ & Readonly<Pick<(typeof import("./2.css"))["default"], "c">>
56
+ & Readonly<{ "a": string }>
57
+ & Readonly<{ "b": string }>
58
+ & Readonly<{ "b": string }>
59
+ ;
60
+ export default styles;
61
+ "
62
+ `);
63
+ const smc = await new SourceMapConsumer(sourceMap.toJSON());
64
+ expect(smc.originalPositionFor({ line: 4, column: 15 })).toMatchInlineSnapshot(`
65
+ {
66
+ "column": 0,
67
+ "line": 2,
68
+ "name": "a",
69
+ "source": "1.css",
70
+ }
71
+ `);
72
+ expect(smc.originalPositionFor({ line: 5, column: 15 })).toMatchInlineSnapshot(`
73
+ {
74
+ "column": 0,
75
+ "line": 3,
76
+ "name": "b",
77
+ "source": "1.css",
78
+ }
79
+ `);
80
+ expect(smc.originalPositionFor({ line: 6, column: 15 })).toMatchInlineSnapshot(`
81
+ {
82
+ "column": 0,
83
+ "line": 4,
84
+ "name": "b",
85
+ "source": "1.css",
86
+ }
87
+ `);
88
+ });
89
+ describe('format case', () => {
90
+ async function getResult(filePath: string) {
91
+ createFixtures({
92
+ '/test/1.css': dedent`
93
+ .foo-bar {}
94
+ .foo_bar {}
95
+ `,
96
+ });
97
+ return await loader.load(filePath);
98
+ }
99
+ test('undefined', async () => {
100
+ const result = await getResult(filePath);
101
+ const { dtsContent } = generateDtsContentWithSourceMap(
102
+ filePath,
103
+ dtsFilePath,
104
+ sourceMapFilePath,
105
+ result.tokens,
106
+ {
107
+ ...dtsFormatOptions,
108
+ localsConvention: undefined,
109
+ },
110
+ isExternalFile,
111
+ );
112
+ expect(dtsContent).toMatchInlineSnapshot(`
113
+ "declare const styles:
114
+ & Readonly<{ "foo-bar": string }>
115
+ & Readonly<{ "foo_bar": string }>
116
+ ;
117
+ export default styles;
118
+ "
119
+ `);
120
+ });
121
+ test('camelCaseOnly', async () => {
122
+ const result = await getResult(filePath);
123
+ const { dtsContent } = generateDtsContentWithSourceMap(
124
+ filePath,
125
+ dtsFilePath,
126
+ sourceMapFilePath,
127
+ result.tokens,
128
+ {
129
+ ...dtsFormatOptions,
130
+ localsConvention: 'camelCaseOnly',
131
+ },
132
+ isExternalFile,
133
+ );
134
+ expect(dtsContent).toMatchInlineSnapshot(`
135
+ "declare const styles:
136
+ & Readonly<{ "fooBar": string }>
137
+ & Readonly<{ "fooBar": string }>
138
+ ;
139
+ export default styles;
140
+ "
141
+ `);
142
+ });
143
+ test('camelCase', async () => {
144
+ const result = await getResult(filePath);
145
+ const { dtsContent } = generateDtsContentWithSourceMap(
146
+ filePath,
147
+ dtsFilePath,
148
+ sourceMapFilePath,
149
+ result.tokens,
150
+ {
151
+ ...dtsFormatOptions,
152
+ localsConvention: 'camelCase',
153
+ },
154
+ isExternalFile,
155
+ );
156
+ expect(dtsContent).toMatchInlineSnapshot(`
157
+ "declare const styles:
158
+ & Readonly<{ "foo-bar": string }>
159
+ & Readonly<{ "fooBar": string }>
160
+ & Readonly<{ "foo_bar": string }>
161
+ & Readonly<{ "fooBar": string }>
162
+ ;
163
+ export default styles;
164
+ "
165
+ `);
166
+ });
167
+ test('dashesOnly', async () => {
168
+ const result = await getResult(filePath);
169
+ const { dtsContent } = generateDtsContentWithSourceMap(
170
+ filePath,
171
+ dtsFilePath,
172
+ sourceMapFilePath,
173
+ result.tokens,
174
+ {
175
+ ...dtsFormatOptions,
176
+ localsConvention: 'dashesOnly',
177
+ },
178
+ isExternalFile,
179
+ );
180
+ expect(dtsContent).toMatchInlineSnapshot(`
181
+ "declare const styles:
182
+ & Readonly<{ "fooBar": string }>
183
+ & Readonly<{ "foo_bar": string }>
184
+ ;
185
+ export default styles;
186
+ "
187
+ `);
188
+ });
189
+ test('dashes', async () => {
190
+ const result = await getResult(filePath);
191
+ const { dtsContent } = generateDtsContentWithSourceMap(
192
+ filePath,
193
+ dtsFilePath,
194
+ sourceMapFilePath,
195
+ result.tokens,
196
+ {
197
+ ...dtsFormatOptions,
198
+ localsConvention: 'dashes',
199
+ },
200
+ isExternalFile,
201
+ );
202
+ expect(dtsContent).toMatchInlineSnapshot(`
203
+ "declare const styles:
204
+ & Readonly<{ "foo-bar": string }>
205
+ & Readonly<{ "fooBar": string }>
206
+ & Readonly<{ "foo_bar": string }>
207
+ & Readonly<{ "foo_bar": string }>
208
+ ;
209
+ export default styles;
210
+ "
211
+ `);
212
+ });
213
+ });
214
+ test('emit other directory', async () => {
215
+ createFixtures({
216
+ '/test/1.css': `.a {}`,
217
+ });
218
+ const result = await loader.load(filePath);
219
+ const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
220
+ getFixturePath('/test/src/1.css'),
221
+ getFixturePath('/test/dist/1.css.d.ts'),
222
+ getFixturePath('/test/dist/1.css.d.ts.map'),
223
+ result.tokens,
224
+ dtsFormatOptions,
225
+ isExternalFile,
226
+ );
227
+ expect(dtsContent).toMatchInlineSnapshot(`
228
+ "declare const styles:
229
+ & Readonly<Pick<(typeof import("../1.css"))["default"], "a">>
230
+ ;
231
+ export default styles;
232
+ "
233
+ `);
234
+ expect(sourceMap.toJSON().sources).toStrictEqual(['../src/1.css']);
235
+ expect(sourceMap.toJSON().file).toStrictEqual('1.css.d.ts');
236
+ });
237
+ test('treats imported tokens from external files the same as local tokens', async () => {
238
+ createFixtures({
239
+ '/test/1.css': dedent`
240
+ @import './2.css';
241
+ @import './3.css';
242
+ .a {}
243
+ `,
244
+ '/test/2.css': `.b {}`,
245
+ '/test/3.css': `.c {}`,
246
+ });
247
+ const result = await loader.load(filePath);
248
+ const { dtsContent } = generateDtsContentWithSourceMap(
249
+ filePath,
250
+ dtsFilePath,
251
+ sourceMapFilePath,
252
+ result.tokens,
253
+ dtsFormatOptions,
254
+ (filePath) => filePath.endsWith('3.css'),
255
+ );
256
+ expect(dtsContent).toMatchInlineSnapshot(`
257
+ "declare const styles:
258
+ & Readonly<Pick<(typeof import("./2.css"))["default"], "b">>
259
+ & Readonly<{ "c": string }>
260
+ & Readonly<{ "a": string }>
261
+ ;
262
+ export default styles;
263
+ "
264
+ `);
265
+ });
266
+ });
@@ -0,0 +1,134 @@
1
+ import { EOL } from 'os';
2
+ import { join, relative, basename } from 'path';
3
+ import camelcase from 'camelcase';
4
+ import { SourceNode, type CodeWithSourceMap } from '../library/source-map/index.js';
5
+ import { type Token } from '../loader/index.js';
6
+ import { type LocalsConvention } from '../runner.js';
7
+ import { type DistOptions, getRelativePath, isSubDirectoryFile, type DtsFormatOptions } from './index.js';
8
+
9
+ /**
10
+ * Get .d.ts file path.
11
+ * @param filePath The path to the source file (i.e. `/dir/foo.css`). It is absolute.
12
+ * @param distOptions The distribution option.
13
+ * @returns The path to the .d.ts file. It is absolute.
14
+ */
15
+ export function getDtsFilePath(filePath: string, distOptions: DistOptions | undefined): string {
16
+ if (distOptions) {
17
+ if (!isSubDirectoryFile(distOptions.rootDir, filePath))
18
+ throw new Error(`The filePath(${filePath}) is not a subdirectory of rootDir(${distOptions.rootDir}).`);
19
+ if (!isSubDirectoryFile(distOptions.rootDir, distOptions.outDir))
20
+ throw new Error(`The outDir(${distOptions.outDir}) is not a subdirectory of rootDir(${distOptions.rootDir}).`);
21
+ return join(distOptions.outDir, relative(distOptions.rootDir, filePath) + '.d.ts');
22
+ } else {
23
+ return filePath + '.d.ts';
24
+ }
25
+ }
26
+
27
+ function dashesCamelCase(str: string): string {
28
+ return str.replace(/-+(\w)/g, function (match, firstLetter) {
29
+ return firstLetter.toUpperCase();
30
+ });
31
+ }
32
+
33
+ function formatTokens(tokens: Token[], localsConvention: LocalsConvention): Token[] {
34
+ const result: Token[] = [];
35
+ for (const token of tokens) {
36
+ if (localsConvention === 'camelCaseOnly') {
37
+ result.push({ ...token, name: camelcase(token.name) });
38
+ } else if (localsConvention === 'camelCase') {
39
+ result.push(token);
40
+ result.push({ ...token, name: camelcase(token.name) });
41
+ } else if (localsConvention === 'dashesOnly') {
42
+ result.push({ ...token, name: dashesCamelCase(token.name) });
43
+ } else if (localsConvention === 'dashes') {
44
+ result.push(token);
45
+ result.push({ ...token, name: dashesCamelCase(token.name) });
46
+ } else {
47
+ result.push(token); // asIs
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+
53
+ function generateTokenDeclarations(
54
+ filePath: string,
55
+ sourceMapFilePath: string,
56
+ tokens: Token[],
57
+ dtsFormatOptions: DtsFormatOptions | undefined,
58
+ isExternalFile: (filePath: string) => boolean,
59
+ ): typeof SourceNode[] {
60
+ const formattedTokens = formatTokens(tokens, dtsFormatOptions?.localsConvention);
61
+ const result: typeof SourceNode[] = [];
62
+
63
+ for (const token of formattedTokens) {
64
+ // Only one original position can be associated with one generated position.
65
+ // This is due to the sourcemap specification. Therefore, we output multiple type definitions
66
+ // with the same name and assign a separate original position to each.
67
+
68
+ for (const originalLocation of token.originalLocations) {
69
+ result.push(
70
+ originalLocation.filePath === filePath || isExternalFile(originalLocation.filePath)
71
+ ? new SourceNode(null, null, null, [
72
+ '& Readonly<{ ',
73
+ new SourceNode(
74
+ originalLocation.start.line ?? null,
75
+ // The SourceNode's column is 0-based, but the originalLocation's column is 1-based.
76
+ originalLocation.start.column - 1 ?? null,
77
+ getRelativePath(sourceMapFilePath, originalLocation.filePath),
78
+ `"${token.name}"`,
79
+ token.name,
80
+ ),
81
+ ': string }>',
82
+ ])
83
+ : // Imported tokens in non-external files are typed by dynamic import.
84
+ // See https://github.com/mizdra/happy-css-modules/issues/106.
85
+ new SourceNode(null, null, null, [
86
+ '& Readonly<Pick<(typeof import(',
87
+ `"${getRelativePath(filePath, originalLocation.filePath)}"`,
88
+ '))["default"], ',
89
+ `"${token.name}"`,
90
+ '>>',
91
+ ]),
92
+ );
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+
98
+ export function generateDtsContentWithSourceMap(
99
+ filePath: string,
100
+ dtsFilePath: string,
101
+ sourceMapFilePath: string,
102
+ tokens: Token[],
103
+ dtsFormatOptions: DtsFormatOptions | undefined,
104
+ isExternalFile: (filePath: string) => boolean,
105
+ ): { dtsContent: CodeWithSourceMap['code']; sourceMap: CodeWithSourceMap['map'] } {
106
+ const tokenDeclarations = generateTokenDeclarations(
107
+ filePath,
108
+ sourceMapFilePath,
109
+ tokens,
110
+ dtsFormatOptions,
111
+ isExternalFile,
112
+ );
113
+
114
+ let sourceNode: typeof SourceNode;
115
+ if (!tokenDeclarations || !tokenDeclarations.length) {
116
+ sourceNode = new SourceNode(null, null, null, '');
117
+ } else {
118
+ sourceNode = new SourceNode(1, 0, getRelativePath(sourceMapFilePath, filePath), [
119
+ 'declare const styles:' + EOL,
120
+ ...tokenDeclarations.map((tokenDeclaration) => [' ', tokenDeclaration, EOL]),
121
+ ';' + EOL,
122
+ 'export default styles;' + EOL,
123
+ ]);
124
+ }
125
+ const codeWithSourceMap = sourceNode.toStringWithSourceMap({
126
+ // Since sourcemap and type definitions are in the same directory, they can be referenced by relative paths.
127
+ file: basename(dtsFilePath),
128
+ sourceRoot: '',
129
+ });
130
+ return {
131
+ dtsContent: codeWithSourceMap.code,
132
+ sourceMap: codeWithSourceMap.map,
133
+ };
134
+ }
@@ -0,0 +1,36 @@
1
+ import { readFile, rm, stat } from 'fs/promises';
2
+ import { createFixtures, getFixturePath } from '../test/util.js';
3
+ import { writeFileIfChanged } from './file-system.js';
4
+
5
+ const TEST_FILE_PATH = getFixturePath('/test.txt');
6
+ const TEST_NESTED_FILE_PATH = getFixturePath('/dir/test.txt');
7
+ const OLD_CONTENT = 'old';
8
+ const NEW_CONTENT = 'new';
9
+
10
+ beforeEach(async () => {
11
+ createFixtures({});
12
+ await writeFileIfChanged(TEST_FILE_PATH, OLD_CONTENT);
13
+ });
14
+
15
+ describe('writeFileIfChanged', () => {
16
+ test('should write file if changed', async () => {
17
+ await writeFileIfChanged(TEST_FILE_PATH, NEW_CONTENT);
18
+ expect(await readFile(TEST_FILE_PATH, 'utf8')).toBe(NEW_CONTENT);
19
+ });
20
+ test('should not write file if not changed', async () => {
21
+ const oldStat = await stat(TEST_FILE_PATH);
22
+ await writeFileIfChanged(TEST_FILE_PATH, OLD_CONTENT);
23
+ const newStat = await stat(TEST_FILE_PATH);
24
+ expect(oldStat.mtimeMs).toBe(newStat.mtimeMs);
25
+ });
26
+ test("should create new file if it doesn't exist", async () => {
27
+ await rm(TEST_FILE_PATH);
28
+ await writeFileIfChanged(TEST_FILE_PATH, NEW_CONTENT);
29
+ expect(await readFile(TEST_FILE_PATH, 'utf8')).toBe(NEW_CONTENT);
30
+ });
31
+ test('should write file in nested directories', async () => {
32
+ await writeFileIfChanged(TEST_NESTED_FILE_PATH, NEW_CONTENT);
33
+ expect(await readFile(TEST_NESTED_FILE_PATH, 'utf8')).toBe(NEW_CONTENT);
34
+ await writeFileIfChanged(TEST_NESTED_FILE_PATH, NEW_CONTENT);
35
+ });
36
+ });
@@ -0,0 +1,24 @@
1
+ import { mkdir, readFile, writeFile } from 'fs/promises';
2
+ import { dirname } from 'path';
3
+ import { isSystemError } from '../util.js';
4
+
5
+ /**
6
+ * Write a file if it doesn't exist or is changed.
7
+ * @param filePath The file path.
8
+ * @param newContent The new content of the file.
9
+ */
10
+ export async function writeFileIfChanged(filePath: string, newContent: string): Promise<void> {
11
+ try {
12
+ const content = await readFile(filePath, 'utf8');
13
+ if (content !== newContent) {
14
+ await writeFile(filePath, newContent, 'utf8');
15
+ }
16
+ } catch (e) {
17
+ if (isSystemError(e) && e.code === 'ENOENT') {
18
+ await mkdir(dirname(filePath), { recursive: true }); // if directory doesn't exist, create it
19
+ await writeFile(filePath, newContent, 'utf8');
20
+ } else {
21
+ throw e;
22
+ }
23
+ }
24
+ }