happy-css-modules 0.5.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 (85) hide show
  1. package/README.md +78 -9
  2. package/dist/cli.js +17 -10
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +9 -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 +4 -14
  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 +1 -0
  22. package/dist/index.js +1 -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 +25 -17
  31. package/dist/locator/index.test.js.map +1 -0
  32. package/dist/{loader → locator}/postcss.d.ts +0 -0
  33. package/dist/{loader → locator}/postcss.js +0 -0
  34. package/dist/locator/postcss.js.map +1 -0
  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/runner.d.ts +10 -1
  39. package/dist/runner.js +45 -18
  40. package/dist/runner.js.map +1 -1
  41. package/dist/runner.test.js +55 -3
  42. package/dist/runner.test.js.map +1 -1
  43. package/dist/test/util.d.ts +1 -1
  44. package/dist/test/util.js +1 -1
  45. package/dist/test/util.js.map +1 -1
  46. package/dist/transformer/index.d.ts +1 -1
  47. package/dist/transformer/index.test.js +8 -8
  48. package/dist/transformer/index.test.js.map +1 -1
  49. package/dist/transformer/less-transformer.test.js +8 -8
  50. package/dist/transformer/less-transformer.test.js.map +1 -1
  51. package/dist/transformer/postcss-transformer.test.js +13 -13
  52. package/dist/transformer/postcss-transformer.test.js.map +1 -1
  53. package/dist/transformer/scss-transformer.test.js +8 -8
  54. package/dist/transformer/scss-transformer.test.js.map +1 -1
  55. package/dist/util.d.ts +2 -0
  56. package/dist/util.js +19 -2
  57. package/dist/util.js.map +1 -1
  58. package/package.json +5 -7
  59. package/src/cli.test.ts +9 -4
  60. package/src/cli.ts +17 -11
  61. package/src/emitter/dts.test.ts +7 -12
  62. package/src/emitter/dts.ts +5 -14
  63. package/src/emitter/index.test.ts +0 -52
  64. package/src/emitter/index.ts +22 -29
  65. package/src/emitter/source-map.test.ts +1 -6
  66. package/src/emitter/source-map.ts +3 -4
  67. package/src/index.ts +1 -0
  68. package/src/integration-test/go-to-definition.test.ts +1 -0
  69. package/src/{loader → locator}/index.test.ts +26 -17
  70. package/src/{loader → locator}/index.ts +16 -8
  71. package/src/{loader → locator}/postcss.test.ts +0 -0
  72. package/src/{loader → locator}/postcss.ts +0 -0
  73. package/src/runner.test.ts +65 -3
  74. package/src/runner.ts +53 -19
  75. package/src/test/util.ts +1 -1
  76. package/src/transformer/index.test.ts +8 -8
  77. package/src/transformer/index.ts +1 -1
  78. package/src/transformer/less-transformer.test.ts +8 -8
  79. package/src/transformer/postcss-transformer.test.ts +13 -13
  80. package/src/transformer/scss-transformer.test.ts +8 -8
  81. package/src/util.ts +21 -2
  82. package/dist/loader/index.js.map +0 -1
  83. package/dist/loader/index.test.js.map +0 -1
  84. package/dist/loader/postcss.js.map +0 -1
  85. package/dist/loader/postcss.test.js.map +0 -1
package/src/cli.test.ts CHANGED
@@ -10,10 +10,6 @@ describe('parseArgv', () => {
10
10
  // TODO: Support multiple patterns
11
11
  // parseArgv([...baseArgs, 'foo', 'bar']);
12
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
13
  test('--watch', () => {
18
14
  expect(parseArgv([...baseArgs, '1.css', '--watch']).watch).toBe(true);
19
15
  expect(parseArgv([...baseArgs, '1.css', '--no-watch']).watch).toBe(false);
@@ -60,6 +56,15 @@ describe('parseArgv', () => {
60
56
  'postcss.config.js',
61
57
  );
62
58
  });
59
+ test('--cache', () => {
60
+ expect(parseArgv([...baseArgs, '1.css']).cache).toBe(true);
61
+ expect(parseArgv([...baseArgs, '1.css', '--cache']).cache).toBe(true);
62
+ expect(parseArgv([...baseArgs, '1.css', '--no-cache']).cache).toBe(false);
63
+ });
64
+ test('--cacheStrategy', () => {
65
+ expect(parseArgv([...baseArgs, '1.css']).cacheStrategy).toBe('content');
66
+ expect(parseArgv([...baseArgs, '1.css', '--cacheStrategy', 'metadata']).cacheStrategy).toBe('metadata');
67
+ });
63
68
  test('--silent', () => {
64
69
  expect(parseArgv([...baseArgs, '1.css', '--silent']).silent).toBe(true);
65
70
  expect(parseArgv([...baseArgs, '1.css', '--no-silent']).silent).toBe(false);
package/src/cli.ts CHANGED
@@ -1,21 +1,18 @@
1
- import { readFileSync } from 'fs';
2
- import { dirname, resolve } from 'path';
3
- import { fileURLToPath } from 'url';
4
1
  import yargs from 'yargs';
5
2
  import { hideBin } from 'yargs/helpers';
6
3
  import { type RunnerOptions } from './runner.js';
7
-
8
- const pkgJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf-8'));
4
+ import { getPackageJson } from './util.js';
9
5
 
10
6
  /**
11
7
  * Parse command line arguments.
12
8
  * @returns Runner options.
13
9
  */
14
10
  export function parseArgv(argv: string[]): RunnerOptions {
11
+ const pkgJson = getPackageJson();
15
12
  const parsedArgv = yargs(hideBin(argv))
16
13
  .wrap(Math.min(140, process.stdout.columns))
17
14
  .scriptName('hcm')
18
- .usage('Create .d.ts and .d.ts.map from CSS modules *.css files.\n\n$0 [options] <glob>')
15
+ .usage('Generate .d.ts and .d.ts.map for CSS modules.\n\n$0 [options] <glob>')
19
16
  .example("$0 'src/**/*.module.css'", 'Generate .d.ts and .d.ts.map.')
20
17
  .example("$0 'src/**/*.module.{css,scss,less}'", 'Also generate files for sass and less.')
21
18
  .example("$0 'src/**/*.module.css' --watch", 'Watch for changes and generate .d.ts and .d.ts.map.')
@@ -23,11 +20,8 @@ export function parseArgv(argv: string[]): RunnerOptions {
23
20
  .example("$0 'src/**/*.module.css' --sassLoadPaths=src/style", "Run with sass's `--load-path`.")
24
21
  .example("$0 'src/**/*.module.css' --lessIncludePaths=src/style", "Run with less's `--include-path`.")
25
22
  .example('$0 \'src/**/*.module.css\' --webpackResolveAlias=\'{"@": "src"}\'', "Run with webpack's `resolve.alias`.")
23
+ .example("$0 'src/**/*.module.css' --cache=false", 'Disable cache.')
26
24
  .detectLocale(false)
27
- .option('outDir', {
28
- type: 'string',
29
- describe: 'Output directory',
30
- })
31
25
  .option('watch', {
32
26
  type: 'boolean',
33
27
  alias: 'w',
@@ -62,6 +56,17 @@ export function parseArgv(argv: string[]): RunnerOptions {
62
56
  string: true,
63
57
  describe: "The option compatible with postcss's `--config`.",
64
58
  })
59
+ .option('cache', {
60
+ type: 'boolean',
61
+ default: true,
62
+ describe: 'Only generate .d.ts and .d.ts.map for changed files.',
63
+ })
64
+ .option('cacheStrategy', {
65
+ choices: ['content', 'metadata'] as const,
66
+ // NOTE: This is a workaround for `parsedArgv.cacheStrategy` type breaks.
67
+ default: 'content' as RunnerOptions['cacheStrategy'],
68
+ describe: 'Strategy for the cache to use for detecting changed files.',
69
+ })
65
70
  .option('silent', {
66
71
  type: 'boolean',
67
72
  default: false,
@@ -94,7 +99,6 @@ export function parseArgv(argv: string[]): RunnerOptions {
94
99
  const patterns: string[] = parsedArgv._.map((pattern) => pattern.toString());
95
100
  return {
96
101
  pattern: patterns[0]!,
97
- outDir: parsedArgv.outDir,
98
102
  watch: parsedArgv.watch,
99
103
  localsConvention: parsedArgv.localsConvention,
100
104
  declarationMap: parsedArgv.declarationMap,
@@ -102,6 +106,8 @@ export function parseArgv(argv: string[]): RunnerOptions {
102
106
  lessIncludePaths: parsedArgv.lessIncludePaths?.map((item) => item.toString()),
103
107
  webpackResolveAlias: parsedArgv.webpackResolveAlias ? JSON.parse(parsedArgv.webpackResolveAlias) : undefined,
104
108
  postcssConfig: parsedArgv.postcssConfig,
109
+ cache: parsedArgv.cache,
110
+ cacheStrategy: parsedArgv.cacheStrategy,
105
111
  silent: parsedArgv.silent,
106
112
  };
107
113
  }
@@ -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 {
@@ -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
@@ -7,3 +7,4 @@ export {
7
7
  createDefaultTransformer,
8
8
  } from './transformer/index.js';
9
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 () => {
@@ -15,10 +15,10 @@ jest.unstable_mockModule('fs/promises', () => ({
15
15
 
16
16
  // After the mock of fs/promises is complete, . /index.js after the mock of fs/promises is complete.
17
17
  // ref: https://www.coolcomputerclub.com/posts/jest-hoist-await/
18
- const { Loader } = await import('./index.js');
18
+ const { Locator } = await import('./index.js');
19
19
  // NOTE: ../test/util.js depends on . /index.js, so it must also be imported dynamically...
20
20
 
21
- const loader = new Loader();
21
+ const locator = new Locator();
22
22
 
23
23
  afterEach(() => {
24
24
  readFileSpy.mockClear();
@@ -31,7 +31,7 @@ test('basic', async () => {
31
31
  .b {}
32
32
  `,
33
33
  });
34
- const result = await loader.load(getFixturePath('/test/1.css'));
34
+ const result = await locator.load(getFixturePath('/test/1.css'));
35
35
  expect(result).toMatchInlineSnapshot(`
36
36
  {
37
37
  dependencies: [],
@@ -77,7 +77,7 @@ test('tracks other files when `@import` is present', async () => {
77
77
  .d {}
78
78
  `,
79
79
  });
80
- const result = await loader.load(getFixturePath('/test/1.css'));
80
+ const result = await locator.load(getFixturePath('/test/1.css'));
81
81
  expect(result).toMatchInlineSnapshot(`
82
82
  {
83
83
  dependencies: [
@@ -137,7 +137,7 @@ test('tracks other files when `composes` is present', async () => {
137
137
  .e {}
138
138
  `,
139
139
  });
140
- const result = await loader.load(getFixturePath('/test/1.css'));
140
+ const result = await locator.load(getFixturePath('/test/1.css'));
141
141
  expect(result).toMatchInlineSnapshot(`
142
142
  {
143
143
  dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css", "<fixtures>/test/4.css"],
@@ -201,7 +201,7 @@ test('normalizes tokens', async () => {
201
201
  .c {}
202
202
  `,
203
203
  });
204
- const result = await loader.load(getFixturePath('/test/1.css'));
204
+ const result = await locator.load(getFixturePath('/test/1.css'));
205
205
  expect(result).toMatchInlineSnapshot(`
206
206
  {
207
207
  dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css"],
@@ -250,7 +250,7 @@ test.failing('returns the result from the cache when the file has not been modif
250
250
  .d {}
251
251
  `,
252
252
  });
253
- await loader.load(getFixturePath('/test/1.css'));
253
+ await locator.load(getFixturePath('/test/1.css'));
254
254
  expect(readFileSpy).toHaveBeenCalledTimes(3);
255
255
  expect(readFileSpy).toHaveBeenNthCalledWith(1, '/test/1.css', 'utf-8');
256
256
  expect(readFileSpy).toHaveBeenNthCalledWith(2, '/test/2.css', 'utf-8');
@@ -262,17 +262,17 @@ test.failing('returns the result from the cache when the file has not been modif
262
262
  await writeFile(getFixturePath('/test/2.css'), await readFile(getFixturePath('/test/2.css'), 'utf-8'));
263
263
 
264
264
  // `3.css` is not updated, so the cache is used. Therefore, `readFile` is not called.
265
- await loader.load(getFixturePath('/test/3.css'));
265
+ await locator.load(getFixturePath('/test/3.css'));
266
266
  expect(readFileSpy).toHaveBeenCalledTimes(0);
267
267
 
268
268
  // `1.css` is not updated, but dependencies are updated, so the cache is used. Therefore, `readFile` is called.
269
- await loader.load(getFixturePath('/test/1.css'));
269
+ await locator.load(getFixturePath('/test/1.css'));
270
270
  expect(readFileSpy).toHaveBeenCalledTimes(2);
271
271
  expect(readFileSpy).toHaveBeenNthCalledWith(1, '/test/1.css', 'utf-8');
272
272
  expect(readFileSpy).toHaveBeenNthCalledWith(2, '/test/2.css', 'utf-8');
273
273
 
274
274
  // ``2.css` is updated, but the cache is already available because it was updated in the previous step. Therefore, `readFile` is not called.
275
- await loader.load(getFixturePath('/test/2.css'));
275
+ await locator.load(getFixturePath('/test/2.css'));
276
276
  expect(readFileSpy).toHaveBeenCalledTimes(2);
277
277
  });
278
278
 
@@ -291,7 +291,7 @@ test('ignores the composition of non-existent tokens', async () => {
291
291
  .b {}
292
292
  `,
293
293
  });
294
- const result = await loader.load(getFixturePath('/test/1.css'));
294
+ const result = await locator.load(getFixturePath('/test/1.css'));
295
295
  expect(result.tokens.map((t) => t.name)).toStrictEqual(['a', 'b']);
296
296
  });
297
297
 
@@ -306,7 +306,7 @@ test('throws error the composition of non-existent file', async () => {
306
306
  `,
307
307
  });
308
308
  await expect(async () => {
309
- await loader.load(getFixturePath('/test/1.css')).catch((e) => {
309
+ await locator.load(getFixturePath('/test/1.css')).catch((e) => {
310
310
  e.message = e.message.replace(FIXTURE_DIR_PATH, '<fixtures>');
311
311
  throw e;
312
312
  });
@@ -316,7 +316,7 @@ test('throws error the composition of non-existent file', async () => {
316
316
  describe('supports sourcemap', () => {
317
317
  test('restores original locations from sourcemap', async () => {
318
318
  const transformer = createDefaultTransformer();
319
- const loader = new Loader({ transformer });
319
+ const locator = new Locator({ transformer });
320
320
  createFixtures({
321
321
  '/test/1.scss': dedent`
322
322
  .nesting {
@@ -327,7 +327,7 @@ describe('supports sourcemap', () => {
327
327
  }
328
328
  `,
329
329
  });
330
- const result = await loader.load(getFixturePath('/test/1.scss'));
330
+ const result = await locator.load(getFixturePath('/test/1.scss'));
331
331
  expect(result).toMatchInlineSnapshot(`
332
332
  {
333
333
  dependencies: [],
@@ -365,8 +365,8 @@ describe('supports sourcemap', () => {
365
365
  `,
366
366
  });
367
367
  const transformer = createDefaultTransformer({ postcssConfig: getFixturePath(`/${uuid}/postcss.config.js`) });
368
- const loader = new Loader({ transformer });
369
- const result = await loader.load(getFixturePath('/test/1.css'));
368
+ const locator = new Locator({ transformer });
369
+ const result = await locator.load(getFixturePath('/test/1.css'));
370
370
  expect(result).toMatchInlineSnapshot(`
371
371
  {
372
372
  dependencies: [],
@@ -398,6 +398,15 @@ test('ignores http(s) protocol file', async () => {
398
398
  @import 'https://example.com/path/https.css';
399
399
  `,
400
400
  });
401
- const result = await loader.load(getFixturePath('/test/1.css'));
401
+ const result = await locator.load(getFixturePath('/test/1.css'));
402
402
  expect(result.dependencies).toStrictEqual([]);
403
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
File without changes