happy-css-modules 0.4.0 → 0.6.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 (102) hide show
  1. package/README.md +172 -53
  2. package/dist/cli.js +45 -11
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +20 -4
  5. package/dist/cli.test.js.map +1 -1
  6. package/dist/emitter/dts.d.ts +3 -4
  7. package/dist/emitter/dts.js +13 -15
  8. package/dist/emitter/dts.js.map +1 -1
  9. package/dist/emitter/dts.test.js +7 -10
  10. package/dist/emitter/dts.test.js.map +1 -1
  11. package/dist/emitter/index.d.ts +6 -15
  12. package/dist/emitter/index.js +18 -13
  13. package/dist/emitter/index.js.map +1 -1
  14. package/dist/emitter/index.test.js +0 -50
  15. package/dist/emitter/index.test.js.map +1 -1
  16. package/dist/emitter/source-map.d.ts +1 -3
  17. package/dist/emitter/source-map.js +2 -3
  18. package/dist/emitter/source-map.js.map +1 -1
  19. package/dist/emitter/source-map.test.js +1 -4
  20. package/dist/emitter/source-map.test.js.map +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.js +3 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/integration-test/go-to-definition.test.js +1 -0
  25. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  26. package/dist/{loader → locator}/index.d.ts +6 -4
  27. package/dist/{loader → locator}/index.js +13 -5
  28. package/dist/locator/index.js.map +1 -0
  29. package/dist/{loader → locator}/index.test.d.ts +0 -0
  30. package/dist/{loader → locator}/index.test.js +100 -14
  31. package/dist/locator/index.test.js.map +1 -0
  32. package/dist/{loader → locator}/postcss.d.ts +5 -1
  33. package/dist/{loader → locator}/postcss.js +26 -30
  34. package/dist/{loader → locator}/postcss.js.map +1 -1
  35. package/dist/{loader → locator}/postcss.test.d.ts +0 -0
  36. package/dist/{loader → locator}/postcss.test.js +0 -0
  37. package/dist/locator/postcss.test.js.map +1 -0
  38. package/dist/resolver/webpack-resolver.d.ts +12 -2
  39. package/dist/resolver/webpack-resolver.js +12 -3
  40. package/dist/resolver/webpack-resolver.js.map +1 -1
  41. package/dist/resolver/webpack-resolver.test.js +43 -7
  42. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  43. package/dist/runner.d.ts +23 -1
  44. package/dist/runner.js +62 -23
  45. package/dist/runner.js.map +1 -1
  46. package/dist/runner.test.js +90 -11
  47. package/dist/runner.test.js.map +1 -1
  48. package/dist/test/util.d.ts +2 -2
  49. package/dist/test/util.js +16 -9
  50. package/dist/test/util.js.map +1 -1
  51. package/dist/transformer/index.d.ts +4 -2
  52. package/dist/transformer/index.js +7 -3
  53. package/dist/transformer/index.js.map +1 -1
  54. package/dist/transformer/index.test.d.ts +1 -0
  55. package/dist/transformer/index.test.js +66 -0
  56. package/dist/transformer/index.test.js.map +1 -0
  57. package/dist/transformer/less-transformer.test.js +14 -14
  58. package/dist/transformer/less-transformer.test.js.map +1 -1
  59. package/dist/transformer/postcss-transformer.d.ts +12 -0
  60. package/dist/transformer/postcss-transformer.js +32 -0
  61. package/dist/transformer/postcss-transformer.js.map +1 -0
  62. package/dist/transformer/postcss-transformer.test.d.ts +1 -0
  63. package/dist/transformer/postcss-transformer.test.js +176 -0
  64. package/dist/transformer/postcss-transformer.test.js.map +1 -0
  65. package/dist/transformer/scss-transformer.js +19 -17
  66. package/dist/transformer/scss-transformer.js.map +1 -1
  67. package/dist/transformer/scss-transformer.test.js +16 -16
  68. package/dist/transformer/scss-transformer.test.js.map +1 -1
  69. package/dist/util.d.ts +2 -0
  70. package/dist/util.js +19 -2
  71. package/dist/util.js.map +1 -1
  72. package/package.json +10 -9
  73. package/src/cli.test.ts +24 -4
  74. package/src/cli.ts +44 -12
  75. package/src/emitter/dts.test.ts +7 -12
  76. package/src/emitter/dts.ts +15 -15
  77. package/src/emitter/index.test.ts +0 -52
  78. package/src/emitter/index.ts +22 -29
  79. package/src/emitter/source-map.test.ts +1 -6
  80. package/src/emitter/source-map.ts +3 -4
  81. package/src/index.ts +9 -2
  82. package/src/integration-test/go-to-definition.test.ts +1 -0
  83. package/src/{loader → locator}/index.test.ts +101 -14
  84. package/src/{loader → locator}/index.ts +16 -8
  85. package/src/{loader → locator}/postcss.test.ts +0 -0
  86. package/src/{loader → locator}/postcss.ts +42 -40
  87. package/src/resolver/webpack-resolver.test.ts +63 -7
  88. package/src/resolver/webpack-resolver.ts +26 -5
  89. package/src/runner.test.ts +103 -11
  90. package/src/runner.ts +83 -25
  91. package/src/test/util.ts +16 -10
  92. package/src/transformer/index.test.ts +71 -0
  93. package/src/transformer/index.ts +12 -4
  94. package/src/transformer/less-transformer.test.ts +14 -14
  95. package/src/transformer/postcss-transformer.test.ts +188 -0
  96. package/src/transformer/postcss-transformer.ts +57 -0
  97. package/src/transformer/scss-transformer.test.ts +16 -16
  98. package/src/transformer/scss-transformer.ts +25 -27
  99. package/src/util.ts +21 -2
  100. package/dist/loader/index.js.map +0 -1
  101. package/dist/loader/index.test.js.map +0 -1
  102. package/dist/loader/postcss.test.js.map +0 -1
@@ -1,20 +1,15 @@
1
1
  import dedent from 'dedent';
2
2
  import { SourceMapConsumer } from 'source-map';
3
- import { Loader } from '../loader/index.js';
3
+ import { Locator } from '../locator/index.js';
4
4
  import { getFixturePath, createFixtures } from '../test/util.js';
5
5
  import { generateDtsContentWithSourceMap, getDtsFilePath } from './dts.js';
6
6
  import { type DtsFormatOptions } from './index.js';
7
7
 
8
- const loader = new Loader();
8
+ const locator = new Locator();
9
9
  const isExternalFile = () => false;
10
10
 
11
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();
12
+ expect(getDtsFilePath('/app/src/dir/1.css')).toBe('/app/src/dir/1.css.d.ts');
18
13
  });
19
14
 
20
15
  describe('generateDtsContentWithSourceMap', () => {
@@ -40,7 +35,7 @@ describe('generateDtsContentWithSourceMap', () => {
40
35
  .d {}
41
36
  `,
42
37
  });
43
- const result = await loader.load(filePath);
38
+ const result = await locator.load(filePath);
44
39
  const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
45
40
  filePath,
46
41
  dtsFilePath,
@@ -94,7 +89,7 @@ describe('generateDtsContentWithSourceMap', () => {
94
89
  .foo_bar {}
95
90
  `,
96
91
  });
97
- return await loader.load(filePath);
92
+ return await locator.load(filePath);
98
93
  }
99
94
  test('undefined', async () => {
100
95
  const result = await getResult(filePath);
@@ -215,7 +210,7 @@ describe('generateDtsContentWithSourceMap', () => {
215
210
  createFixtures({
216
211
  '/test/1.css': `.a {}`,
217
212
  });
218
- const result = await loader.load(filePath);
213
+ const result = await locator.load(filePath);
219
214
  const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
220
215
  getFixturePath('/test/src/1.css'),
221
216
  getFixturePath('/test/dist/1.css.d.ts'),
@@ -244,7 +239,7 @@ describe('generateDtsContentWithSourceMap', () => {
244
239
  '/test/2.css': `.b {}`,
245
240
  '/test/3.css': `.c {}`,
246
241
  });
247
- const result = await loader.load(filePath);
242
+ const result = await locator.load(filePath);
248
243
  const { dtsContent } = generateDtsContentWithSourceMap(
249
244
  filePath,
250
245
  dtsFilePath,
@@ -1,27 +1,18 @@
1
1
  import { EOL } from 'os';
2
- import { join, relative, basename } from 'path';
2
+ import { basename } from 'path';
3
3
  import camelcase from 'camelcase';
4
4
  import { SourceNode, type CodeWithSourceMap } from '../library/source-map/index.js';
5
- import { type Token } from '../loader/index.js';
5
+ import { type Token } from '../locator/index.js';
6
6
  import { type LocalsConvention } from '../runner.js';
7
- import { type DistOptions, getRelativePath, isSubDirectoryFile, type DtsFormatOptions } from './index.js';
7
+ import { getRelativePath, type DtsFormatOptions } from './index.js';
8
8
 
9
9
  /**
10
10
  * Get .d.ts file path.
11
11
  * @param filePath The path to the source file (i.e. `/dir/foo.css`). It is absolute.
12
- * @param distOptions The distribution option.
13
12
  * @returns The path to the .d.ts file. It is absolute.
14
13
  */
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
- }
14
+ export function getDtsFilePath(filePath: string): string {
15
+ return filePath + '.d.ts';
25
16
  }
26
17
 
27
18
  function dashesCamelCase(str: string): string {
@@ -65,7 +56,16 @@ function generateTokenDeclarations(
65
56
  // This is due to the sourcemap specification. Therefore, we output multiple type definitions
66
57
  // with the same name and assign a separate original position to each.
67
58
 
68
- for (const originalLocation of token.originalLocations) {
59
+ for (let originalLocation of token.originalLocations) {
60
+ if (originalLocation.filePath === undefined) {
61
+ // If the original location is not specified, fallback to the source file.
62
+ originalLocation = {
63
+ filePath: filePath,
64
+ start: { line: 1, column: 1 },
65
+ end: { line: 1, column: 1 },
66
+ };
67
+ }
68
+
69
69
  result.push(
70
70
  originalLocation.filePath === filePath || isExternalFile(originalLocation.filePath)
71
71
  ? new SourceNode(null, null, null, [
@@ -1,6 +1,5 @@
1
1
  import { readFile, stat } from 'fs/promises';
2
2
  import { jest } from '@jest/globals';
3
- import chalk from 'chalk';
4
3
  import { createFixtures, exists, fakeToken, getFixturePath, waitForAsyncTask } from '../test/util.js';
5
4
  import { emitGeneratedFiles, getRelativePath, isSubDirectoryFile } from './index.js';
6
5
 
@@ -27,10 +26,8 @@ describe('emitGeneratedFiles', () => {
27
26
  const defaultArgs = {
28
27
  filePath: getFixturePath('/test/1.css'),
29
28
  tokens: [fakeToken({ name: 'foo', originalLocations: [{ start: { line: 1, column: 1 } }] })],
30
- distOptions: undefined,
31
29
  emitDeclarationMap: true,
32
30
  dtsFormatOptions: undefined,
33
- silent: true,
34
31
  cwd: getFixturePath('/test'),
35
32
  isExternalFile: () => false,
36
33
  };
@@ -78,53 +75,4 @@ describe('emitGeneratedFiles', () => {
78
75
  expect(mtimeForDts1).not.toEqual(mtimeForDts3); // not skipped
79
76
  expect(mtimeForSourceMap1).not.toEqual(mtimeForSourceMap3); // not skipped
80
77
  });
81
- test('outputs write log', async () => {
82
- await emitGeneratedFiles({
83
- ...defaultArgs,
84
- filePath: getFixturePath('/test/1.css'),
85
- emitDeclarationMap: true,
86
- silent: false,
87
- });
88
- expect(consoleLogSpy).toHaveBeenCalledTimes(2);
89
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `Wrote ${chalk.green('1.css.d.ts')}`);
90
- expect(consoleLogSpy).toHaveBeenNthCalledWith(2, `Wrote ${chalk.green('1.css.d.ts.map')}`);
91
- consoleLogSpy.mockClear();
92
-
93
- await emitGeneratedFiles({
94
- ...defaultArgs,
95
- filePath: getFixturePath('/test/2.css'),
96
- emitDeclarationMap: false,
97
- silent: false,
98
- });
99
- expect(consoleLogSpy).toHaveBeenCalledTimes(1);
100
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `Wrote ${chalk.green('2.css.d.ts')}`);
101
- consoleLogSpy.mockClear();
102
-
103
- await emitGeneratedFiles({
104
- ...defaultArgs,
105
- filePath: getFixturePath('/test/3.css'),
106
- emitDeclarationMap: false,
107
- silent: true,
108
- });
109
- expect(consoleLogSpy).toHaveBeenCalledTimes(0);
110
- });
111
- test('changes working directory by cwd', async () => {
112
- await emitGeneratedFiles({
113
- ...defaultArgs,
114
- filePath: getFixturePath('/test/1.css'),
115
- emitDeclarationMap: false,
116
- silent: false,
117
- cwd: getFixturePath('/test'),
118
- });
119
- await emitGeneratedFiles({
120
- ...defaultArgs,
121
- filePath: getFixturePath('/test/1.css'),
122
- emitDeclarationMap: false,
123
- silent: false,
124
- cwd: getFixturePath('/'),
125
- });
126
- expect(consoleLogSpy).toHaveBeenCalledTimes(2);
127
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `Wrote ${chalk.green('1.css.d.ts')}`);
128
- expect(consoleLogSpy).toHaveBeenNthCalledWith(2, `Wrote ${chalk.green('test/1.css.d.ts')}`);
129
- });
130
78
  });
@@ -1,7 +1,7 @@
1
1
  import { dirname, isAbsolute, relative } from 'path';
2
- import chalk from 'chalk';
3
- import { type Token } from '../loader/index.js';
2
+ import { type Token } from '../locator/index.js';
4
3
  import { type LocalsConvention } from '../runner.js';
4
+ import { exists } from '../util.js';
5
5
  import { generateDtsContentWithSourceMap, getDtsFilePath } from './dts.js';
6
6
  import { writeFileIfChanged } from './file-system.js';
7
7
  import { generateSourceMappingURLComment, getSourceMapFilePath } from './source-map.js';
@@ -19,18 +19,6 @@ export function isSubDirectoryFile(fromDirectory: string, toFilePath: string): b
19
19
  return isAbsolute(toFilePath) && toFilePath.startsWith(fromDirectory);
20
20
  }
21
21
 
22
- function outputWriteLog(cwd: string, filePath: string) {
23
- console.log('Wrote ' + chalk.green(relative(cwd, filePath)));
24
- }
25
-
26
- /** The distribution option. */
27
- export type DistOptions = {
28
- /** Root directory. It is absolute. */
29
- rootDir: string;
30
- /** The path to the output directory. It is absolute. */
31
- outDir: string;
32
- };
33
-
34
22
  export type DtsFormatOptions = {
35
23
  localsConvention?: LocalsConvention;
36
24
  };
@@ -41,16 +29,10 @@ export type EmitterOptions = {
41
29
  filePath: string;
42
30
  /** The tokens exported by the source file. */
43
31
  tokens: Token[];
44
- /** The distribution option. */
45
- distOptions: DistOptions | undefined;
46
32
  /** Whether to output declaration map (i.e. `/dir/foo.css.d.ts.map`) or not. */
47
33
  emitDeclarationMap: boolean | undefined;
48
34
  /** The options for formatting the type definition. */
49
35
  dtsFormatOptions: DtsFormatOptions | undefined;
50
- /** Silent output. Do not show "files written" messages */
51
- silent: boolean;
52
- /** Working directory path. */
53
- cwd: string;
54
36
  /** Whether the file is from an external library or not. */
55
37
  isExternalFile: (filePath: string) => boolean;
56
38
  };
@@ -58,15 +40,12 @@ export type EmitterOptions = {
58
40
  export async function emitGeneratedFiles({
59
41
  filePath,
60
42
  tokens,
61
- distOptions,
62
43
  emitDeclarationMap,
63
44
  dtsFormatOptions,
64
- silent,
65
- cwd,
66
45
  isExternalFile,
67
46
  }: EmitterOptions): Promise<void> {
68
- const dtsFilePath = getDtsFilePath(filePath, distOptions);
69
- const sourceMapFilePath = getSourceMapFilePath(filePath, distOptions);
47
+ const dtsFilePath = getDtsFilePath(filePath);
48
+ const sourceMapFilePath = getSourceMapFilePath(filePath);
70
49
  const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
71
50
  filePath,
72
51
  dtsFilePath,
@@ -79,14 +58,28 @@ export async function emitGeneratedFiles({
79
58
  if (emitDeclarationMap) {
80
59
  const sourceMappingURLComment = generateSourceMappingURLComment(dtsFilePath, sourceMapFilePath);
81
60
  await writeFileIfChanged(dtsFilePath, dtsContent + sourceMappingURLComment);
82
- if (!silent) outputWriteLog(cwd, dtsFilePath);
83
-
84
61
  // NOTE: tsserver does not support inline declaration maps. Therefore, sourcemap files must be output.
85
62
  // blocked by: https://github.com/microsoft/TypeScript/issues/38966
86
63
  await writeFileIfChanged(sourceMapFilePath, sourceMap.toString());
87
- if (!silent) outputWriteLog(cwd, sourceMapFilePath);
88
64
  } else {
89
65
  await writeFileIfChanged(dtsFilePath, dtsContent);
90
- if (!silent) outputWriteLog(cwd, dtsFilePath);
91
66
  }
92
67
  }
68
+
69
+ /**
70
+ * Returns true if .d.ts (and .d.ts.map) files are generated for the given file.
71
+ */
72
+ export async function isGeneratedFilesExist(
73
+ filePath: string,
74
+ emitDeclarationMap: boolean | undefined,
75
+ ): Promise<boolean> {
76
+ const dtsFilePath = getDtsFilePath(filePath);
77
+ const sourceMapFilePath = getSourceMapFilePath(filePath);
78
+ if (emitDeclarationMap && !(await exists(sourceMapFilePath))) {
79
+ return false;
80
+ }
81
+ if (!(await exists(dtsFilePath))) {
82
+ return false;
83
+ }
84
+ return true;
85
+ }
@@ -2,12 +2,7 @@ import { EOL } from 'os';
2
2
  import { getSourceMapFilePath, generateSourceMappingURLComment } from './source-map.js';
3
3
 
4
4
  test('getSourceMapFilePath', () => {
5
- expect(getSourceMapFilePath('/app/src/dir/1.css', undefined)).toBe('/app/src/dir/1.css.d.ts.map');
6
- expect(getSourceMapFilePath('/app/src/dir/1.css', { rootDir: '/app', outDir: '/app/dist' })).toBe(
7
- '/app/dist/src/dir/1.css.d.ts.map',
8
- );
9
- expect(() => getSourceMapFilePath('/tmp/src/dir/1.css', { rootDir: '/app', outDir: '/app/dist' })).toThrow();
10
- expect(() => getSourceMapFilePath('/app/src/dir/1.css', { rootDir: '/app', outDir: '/tmp/dist' })).toThrow();
5
+ expect(getSourceMapFilePath('/app/src/dir/1.css')).toBe('/app/src/dir/1.css.d.ts.map');
11
6
  });
12
7
 
13
8
  test('generateSourceMappingURLComment', () => {
@@ -1,15 +1,14 @@
1
1
  import { EOL } from 'os';
2
2
  import { getDtsFilePath } from './dts.js';
3
- import { type DistOptions, getRelativePath } from './index.js';
3
+ import { getRelativePath } from './index.js';
4
4
 
5
5
  /**
6
6
  * Get .d.ts.map file path.
7
7
  * @param filePath The path to the source file (i.e. `foo.css`). It is absolute.
8
- * @param distOptions The distribution option.
9
8
  * @returns The path to the .d.ts.map file. It is absolute.
10
9
  */
11
- export function getSourceMapFilePath(filePath: string, distOptions: DistOptions | undefined): string {
12
- return getDtsFilePath(filePath, distOptions) + '.map';
10
+ export function getSourceMapFilePath(filePath: string): string {
11
+ return getDtsFilePath(filePath) + '.map';
13
12
  }
14
13
 
15
14
  export function generateSourceMappingURLComment(dtsFilePath: string, sourceMapFilePath: string): string {
package/src/index.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  export { parseArgv } from './cli.js';
2
- export { run } from './runner.js';
3
- export { type Transformer } from './transformer/index.js';
2
+ export { run, type LocalsConvention } from './runner.js';
3
+ export {
4
+ type Transformer,
5
+ type TransformerOptions,
6
+ type TransformResult,
7
+ createDefaultTransformer,
8
+ } from './transformer/index.js';
9
+ export { type Resolver, type ResolverOptions, createDefaultResolver } from './resolver/index.js';
10
+ export { Locator, type LocatorOptions, type LoadResult, type Token, type Location } from './locator/index.js';
@@ -10,6 +10,7 @@ const defaultOptions = {
10
10
  silent: true,
11
11
  declarationMap: true,
12
12
  cwd: getFixturePath('/'),
13
+ cache: false,
13
14
  };
14
15
 
15
16
  afterAll(async () => {
@@ -1,6 +1,8 @@
1
1
  import fs, { readFile, writeFile } from 'fs/promises';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { jest } from '@jest/globals';
3
4
  import dedent from 'dedent';
5
+ import { createDefaultTransformer } from '../index.js';
4
6
  import { createFixtures, FIXTURE_DIR_PATH, getFixturePath } from '../test/util.js';
5
7
  import { sleepSync } from '../util.js';
6
8
 
@@ -13,10 +15,10 @@ jest.unstable_mockModule('fs/promises', () => ({
13
15
 
14
16
  // After the mock of fs/promises is complete, . /index.js after the mock of fs/promises is complete.
15
17
  // ref: https://www.coolcomputerclub.com/posts/jest-hoist-await/
16
- const { Loader } = await import('./index.js');
18
+ const { Locator } = await import('./index.js');
17
19
  // NOTE: ../test/util.js depends on . /index.js, so it must also be imported dynamically...
18
20
 
19
- const loader = new Loader();
21
+ const locator = new Locator();
20
22
 
21
23
  afterEach(() => {
22
24
  readFileSpy.mockClear();
@@ -29,7 +31,7 @@ test('basic', async () => {
29
31
  .b {}
30
32
  `,
31
33
  });
32
- const result = await loader.load(getFixturePath('/test/1.css'));
34
+ const result = await locator.load(getFixturePath('/test/1.css'));
33
35
  expect(result).toMatchInlineSnapshot(`
34
36
  {
35
37
  dependencies: [],
@@ -75,7 +77,7 @@ test('tracks other files when `@import` is present', async () => {
75
77
  .d {}
76
78
  `,
77
79
  });
78
- const result = await loader.load(getFixturePath('/test/1.css'));
80
+ const result = await locator.load(getFixturePath('/test/1.css'));
79
81
  expect(result).toMatchInlineSnapshot(`
80
82
  {
81
83
  dependencies: [
@@ -135,7 +137,7 @@ test('tracks other files when `composes` is present', async () => {
135
137
  .e {}
136
138
  `,
137
139
  });
138
- const result = await loader.load(getFixturePath('/test/1.css'));
140
+ const result = await locator.load(getFixturePath('/test/1.css'));
139
141
  expect(result).toMatchInlineSnapshot(`
140
142
  {
141
143
  dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css", "<fixtures>/test/4.css"],
@@ -199,7 +201,7 @@ test('normalizes tokens', async () => {
199
201
  .c {}
200
202
  `,
201
203
  });
202
- const result = await loader.load(getFixturePath('/test/1.css'));
204
+ const result = await locator.load(getFixturePath('/test/1.css'));
203
205
  expect(result).toMatchInlineSnapshot(`
204
206
  {
205
207
  dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css"],
@@ -248,7 +250,7 @@ test.failing('returns the result from the cache when the file has not been modif
248
250
  .d {}
249
251
  `,
250
252
  });
251
- await loader.load(getFixturePath('/test/1.css'));
253
+ await locator.load(getFixturePath('/test/1.css'));
252
254
  expect(readFileSpy).toHaveBeenCalledTimes(3);
253
255
  expect(readFileSpy).toHaveBeenNthCalledWith(1, '/test/1.css', 'utf-8');
254
256
  expect(readFileSpy).toHaveBeenNthCalledWith(2, '/test/2.css', 'utf-8');
@@ -260,17 +262,17 @@ test.failing('returns the result from the cache when the file has not been modif
260
262
  await writeFile(getFixturePath('/test/2.css'), await readFile(getFixturePath('/test/2.css'), 'utf-8'));
261
263
 
262
264
  // `3.css` is not updated, so the cache is used. Therefore, `readFile` is not called.
263
- await loader.load(getFixturePath('/test/3.css'));
265
+ await locator.load(getFixturePath('/test/3.css'));
264
266
  expect(readFileSpy).toHaveBeenCalledTimes(0);
265
267
 
266
268
  // `1.css` is not updated, but dependencies are updated, so the cache is used. Therefore, `readFile` is called.
267
- await loader.load(getFixturePath('/test/1.css'));
269
+ await locator.load(getFixturePath('/test/1.css'));
268
270
  expect(readFileSpy).toHaveBeenCalledTimes(2);
269
271
  expect(readFileSpy).toHaveBeenNthCalledWith(1, '/test/1.css', 'utf-8');
270
272
  expect(readFileSpy).toHaveBeenNthCalledWith(2, '/test/2.css', 'utf-8');
271
273
 
272
274
  // ``2.css` is updated, but the cache is already available because it was updated in the previous step. Therefore, `readFile` is not called.
273
- await loader.load(getFixturePath('/test/2.css'));
275
+ await locator.load(getFixturePath('/test/2.css'));
274
276
  expect(readFileSpy).toHaveBeenCalledTimes(2);
275
277
  });
276
278
 
@@ -289,7 +291,7 @@ test('ignores the composition of non-existent tokens', async () => {
289
291
  .b {}
290
292
  `,
291
293
  });
292
- const result = await loader.load(getFixturePath('/test/1.css'));
294
+ const result = await locator.load(getFixturePath('/test/1.css'));
293
295
  expect(result.tokens.map((t) => t.name)).toStrictEqual(['a', 'b']);
294
296
  });
295
297
 
@@ -304,14 +306,90 @@ test('throws error the composition of non-existent file', async () => {
304
306
  `,
305
307
  });
306
308
  await expect(async () => {
307
- await loader.load(getFixturePath('/test/1.css')).catch((e) => {
309
+ await locator.load(getFixturePath('/test/1.css')).catch((e) => {
308
310
  e.message = e.message.replace(FIXTURE_DIR_PATH, '<fixtures>');
309
311
  throw e;
310
312
  });
311
313
  }).rejects.toThrowError(`Could not resolve './2.css' in '<fixtures>/test/1.css'`);
312
314
  });
313
315
 
314
- test.todo('supports sourcemap file and inline sourcemap');
316
+ describe('supports sourcemap', () => {
317
+ test('restores original locations from sourcemap', async () => {
318
+ const transformer = createDefaultTransformer();
319
+ const locator = new Locator({ transformer });
320
+ createFixtures({
321
+ '/test/1.scss': dedent`
322
+ .nesting {
323
+ dummy: '';
324
+ .nesting_child {
325
+ dummy: '';
326
+ }
327
+ }
328
+ `,
329
+ });
330
+ const result = await locator.load(getFixturePath('/test/1.scss'));
331
+ expect(result).toMatchInlineSnapshot(`
332
+ {
333
+ dependencies: [],
334
+ tokens: [
335
+ {
336
+ name: "nesting",
337
+ originalLocations: [
338
+ { filePath: "<fixtures>/test/1.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 8 } },
339
+ { filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 3 }, end: { line: 3, column: 10 } },
340
+ ],
341
+ },
342
+ {
343
+ name: "nesting_child",
344
+ originalLocations: [
345
+ { filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 3 }, end: { line: 3, column: 16 } },
346
+ ],
347
+ },
348
+ ],
349
+ }
350
+ `);
351
+ });
352
+ test('treats originalLocation as empty if sourcemap is broken', async () => {
353
+ const uuid = randomUUID();
354
+ createFixtures({
355
+ [`/${uuid}/postcss.config.js`]: dedent`
356
+ module.exports = {
357
+ plugins: [],
358
+ };
359
+ `,
360
+ '/test/1.css': dedent`
361
+ .selector_list_a_1, .selector_list_a_2 {}
362
+ /* In postcss, including newlines in the selector list breaks the sourcemap. */
363
+ .selector_list_b_1,
364
+ .selector_list_b_2 {}
365
+ `,
366
+ });
367
+ const transformer = createDefaultTransformer({ postcssConfig: getFixturePath(`/${uuid}/postcss.config.js`) });
368
+ const locator = new Locator({ transformer });
369
+ const result = await locator.load(getFixturePath('/test/1.css'));
370
+ expect(result).toMatchInlineSnapshot(`
371
+ {
372
+ dependencies: [],
373
+ tokens: [
374
+ {
375
+ name: "selector_list_a_1",
376
+ originalLocations: [
377
+ { filePath: "<fixtures>/test/1.css", start: { line: 1, column: 1 }, end: { line: 1, column: 18 } },
378
+ ],
379
+ },
380
+ {
381
+ name: "selector_list_a_2",
382
+ originalLocations: [
383
+ { filePath: "<fixtures>/test/1.css", start: { line: 1, column: 1 }, end: { line: 1, column: 18 } },
384
+ ],
385
+ },
386
+ { name: "selector_list_b_1", originalLocations: [{}] },
387
+ { name: "selector_list_b_2", originalLocations: [{}] },
388
+ ],
389
+ }
390
+ `);
391
+ });
392
+ });
315
393
 
316
394
  test('ignores http(s) protocol file', async () => {
317
395
  createFixtures({
@@ -320,6 +398,15 @@ test('ignores http(s) protocol file', async () => {
320
398
  @import 'https://example.com/path/https.css';
321
399
  `,
322
400
  });
323
- const result = await loader.load(getFixturePath('/test/1.css'));
401
+ const result = await locator.load(getFixturePath('/test/1.css'));
324
402
  expect(result.dependencies).toStrictEqual([]);
325
403
  });
404
+
405
+ test('block concurrent calls to load method', async () => {
406
+ createFixtures({
407
+ '/test/1.css': `.a {}`,
408
+ });
409
+ await expect(async () => {
410
+ await Promise.all([locator.load(getFixturePath('/test/1.css')), locator.load(getFixturePath('/test/1.css'))]);
411
+ }).rejects.toThrowError('Cannot call `Locator#load` concurrently.');
412
+ });
@@ -36,7 +36,7 @@ type CacheEntry = {
36
36
  result: LoadResult;
37
37
  };
38
38
 
39
- /** The result of `Loader#load`. */
39
+ /** The result of `Locator#load`. */
40
40
  export type LoadResult = {
41
41
  /** The path of the file imported from the source file with `@import` or `composes`. */
42
42
  dependencies: string[];
@@ -60,7 +60,7 @@ function normalizeTokens(tokens: Token[]): Token[] {
60
60
  }));
61
61
  }
62
62
 
63
- export type LoaderOptions = {
63
+ export type LocatorOptions = {
64
64
  /** The function to transform source code. */
65
65
  transformer?: Transformer | undefined;
66
66
  /** The function to resolve the path of the imported file. */
@@ -71,12 +71,13 @@ export type LoaderOptions = {
71
71
  export type StrictlyResolver = (...args: Parameters<Resolver>) => Promise<string>;
72
72
 
73
73
  /** This class collects information on tokens exported from CSS Modules files. */
74
- export class Loader {
74
+ export class Locator {
75
75
  private readonly cache: Map<string, CacheEntry> = new Map();
76
76
  private readonly transformer: Transformer | undefined;
77
77
  private readonly resolver: StrictlyResolver;
78
+ private loading = false;
78
79
 
79
- constructor(options?: LoaderOptions) {
80
+ constructor(options?: LocatorOptions) {
80
81
  this.transformer = options?.transformer ?? createDefaultTransformer();
81
82
  this.resolver = async (specifier, resolverOptions) => {
82
83
  const resolver = options?.resolver ?? createDefaultResolver();
@@ -137,8 +138,15 @@ export class Loader {
137
138
 
138
139
  /** Returns information about the tokens exported from the CSS Modules file. */
139
140
  async load(filePath: string): Promise<LoadResult> {
140
- // NOTE: Loader does not support concurrent calls.
141
- // TODO: Throw an error if called concurrently.
141
+ if (this.loading) throw new Error('Cannot call `Locator#load` concurrently.');
142
+ this.loading = true;
143
+ const result = await this._load(filePath).finally(() => {
144
+ this.loading = false;
145
+ });
146
+ return result;
147
+ }
148
+
149
+ private async _load(filePath: string): Promise<LoadResult> {
142
150
  if (!(await this.isCacheOutdated(filePath))) {
143
151
  const cacheEntry = this.cache.get(filePath)!;
144
152
  return cacheEntry.result;
@@ -164,7 +172,7 @@ export class Loader {
164
172
  if (!importedSheetPath) continue;
165
173
  if (isIgnoredSpecifier(importedSheetPath)) continue;
166
174
  const from = await this.resolver(importedSheetPath, { request: filePath });
167
- const result = await this.load(from);
175
+ const result = await this._load(from);
168
176
  const externalTokens = result.tokens;
169
177
  dependencies.push(from, ...result.dependencies);
170
178
  tokens.push(...externalTokens);
@@ -190,7 +198,7 @@ export class Loader {
190
198
  if (!declarationDetail) continue;
191
199
  if (isIgnoredSpecifier(declarationDetail.from)) continue;
192
200
  const from = await this.resolver(declarationDetail.from, { request: filePath });
193
- const result = await this.load(from);
201
+ const result = await this._load(from);
194
202
  const externalTokens = result.tokens.filter((token) => declarationDetail.tokenNames.includes(token.name));
195
203
  dependencies.push(from, ...result.dependencies);
196
204
  tokens.push(...externalTokens);
File without changes