happy-css-modules 1.0.0 → 2.0.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 (87) hide show
  1. package/README.md +35 -24
  2. package/bin/hcm.js +1 -0
  3. package/dist/cli.js +5 -5
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.test.js +4 -3
  6. package/dist/cli.test.js.map +1 -1
  7. package/dist/emitter/dts.test.js +1 -1
  8. package/dist/emitter/dts.test.js.map +1 -1
  9. package/dist/emitter/file-system.test.js +1 -1
  10. package/dist/emitter/file-system.test.js.map +1 -1
  11. package/dist/emitter/index.test.js +1 -1
  12. package/dist/emitter/index.test.js.map +1 -1
  13. package/dist/integration-test/go-to-definition.test.js +3 -3
  14. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  15. package/dist/locator/index.test.js +1 -1
  16. package/dist/locator/index.test.js.map +1 -1
  17. package/dist/locator/postcss.test.js +1 -1
  18. package/dist/locator/postcss.test.js.map +1 -1
  19. package/dist/logger.d.ts +9 -0
  20. package/dist/logger.js +28 -0
  21. package/dist/logger.js.map +1 -0
  22. package/dist/regression-test/issue-168.test.d.ts +1 -0
  23. package/dist/regression-test/issue-168.test.js +30 -0
  24. package/dist/regression-test/issue-168.test.js.map +1 -0
  25. package/dist/resolver/index.test.js +1 -1
  26. package/dist/resolver/index.test.js.map +1 -1
  27. package/dist/resolver/node-resolver.test.js +1 -1
  28. package/dist/resolver/node-resolver.test.js.map +1 -1
  29. package/dist/resolver/relative-resolver.test.js +1 -1
  30. package/dist/resolver/relative-resolver.test.js.map +1 -1
  31. package/dist/resolver/webpack-resolver.test.js +1 -1
  32. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  33. package/dist/runner.d.ts +3 -3
  34. package/dist/runner.js +48 -55
  35. package/dist/runner.js.map +1 -1
  36. package/dist/runner.test.js +83 -28
  37. package/dist/runner.test.js.map +1 -1
  38. package/dist/test-util/jest/resolver.cjs.map +1 -0
  39. package/dist/test-util/tsserver.js.map +1 -0
  40. package/dist/test-util/util.js.map +1 -0
  41. package/dist/transformer/index.js +2 -2
  42. package/dist/transformer/index.js.map +1 -1
  43. package/dist/transformer/index.test.js +1 -1
  44. package/dist/transformer/index.test.js.map +1 -1
  45. package/dist/transformer/less-transformer.test.js +1 -1
  46. package/dist/transformer/less-transformer.test.js.map +1 -1
  47. package/dist/transformer/postcss-transformer.test.js +1 -1
  48. package/dist/transformer/postcss-transformer.test.js.map +1 -1
  49. package/dist/transformer/scss-transformer.test.js +1 -1
  50. package/dist/transformer/scss-transformer.test.js.map +1 -1
  51. package/dist/util.test.js +1 -1
  52. package/dist/util.test.js.map +1 -1
  53. package/package.json +7 -7
  54. package/src/cli.test.ts +4 -3
  55. package/src/cli.ts +5 -5
  56. package/src/emitter/dts.test.ts +1 -1
  57. package/src/emitter/file-system.test.ts +1 -1
  58. package/src/emitter/index.test.ts +1 -1
  59. package/src/integration-test/go-to-definition.test.ts +5 -4
  60. package/src/locator/index.test.ts +1 -1
  61. package/src/locator/postcss.test.ts +1 -1
  62. package/src/logger.ts +31 -0
  63. package/src/regression-test/issue-168.test.ts +34 -0
  64. package/src/resolver/index.test.ts +1 -1
  65. package/src/resolver/node-resolver.test.ts +1 -1
  66. package/src/resolver/relative-resolver.test.ts +1 -1
  67. package/src/resolver/webpack-resolver.test.ts +1 -1
  68. package/src/runner.test.ts +125 -31
  69. package/src/runner.ts +56 -50
  70. package/src/transformer/index.test.ts +1 -1
  71. package/src/transformer/index.ts +2 -2
  72. package/src/transformer/less-transformer.test.ts +1 -1
  73. package/src/transformer/postcss-transformer.test.ts +1 -1
  74. package/src/transformer/scss-transformer.test.ts +1 -1
  75. package/src/util.test.ts +1 -1
  76. package/dist/test/jest/resolver.cjs.map +0 -1
  77. package/dist/test/tsserver.js.map +0 -1
  78. package/dist/test/util.js.map +0 -1
  79. /package/dist/{test → test-util}/jest/resolver.cjs +0 -0
  80. /package/dist/{test → test-util}/jest/resolver.d.cts +0 -0
  81. /package/dist/{test → test-util}/tsserver.d.ts +0 -0
  82. /package/dist/{test → test-util}/tsserver.js +0 -0
  83. /package/dist/{test → test-util}/util.d.ts +0 -0
  84. /package/dist/{test → test-util}/util.js +0 -0
  85. /package/src/{test → test-util}/jest/resolver.cjs +0 -0
  86. /package/src/{test → test-util}/tsserver.ts +0 -0
  87. /package/src/{test → test-util}/util.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happy-css-modules",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Creates .d.ts files from CSS Modules .css files",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,14 +30,14 @@
30
30
  "@file-cache/core": "^1.1.3",
31
31
  "@file-cache/npm": "^1.1.3",
32
32
  "await-lock": "^2.2.2",
33
- "camelcase": "^7.0.0",
33
+ "camelcase": "^7.0.1",
34
34
  "chalk": "^5.0.1",
35
35
  "chokidar": "^3.5.3",
36
36
  "enhanced-resolve": "^5.10.0",
37
37
  "glob": "^8.0.3",
38
38
  "import-meta-resolve": "^2.1.0",
39
- "minimatch": "^5.1.0",
40
- "postcss": "^8.4.17",
39
+ "minimatch": "^5.1.2",
40
+ "postcss": "^8.4.18",
41
41
  "postcss-load-config": "^4.0.1",
42
42
  "postcss-modules": "^4.3.1",
43
43
  "postcss-selector-parser": "^6.0.10",
@@ -50,8 +50,8 @@
50
50
  "@jest/types": "^29.0.3",
51
51
  "@mizdra/eslint-config-mizdra": "^1.2.0",
52
52
  "@mizdra/prettier-config-mizdra": "^1.0.0",
53
- "@swc/core": "^1.3.8",
54
- "@swc/jest": "^0.2.23",
53
+ "@swc/core": "^1.3.24",
54
+ "@swc/jest": "^0.2.24",
55
55
  "@types/dedent": "^0.7.0",
56
56
  "@types/glob": "^7.2.0",
57
57
  "@types/jest": "^29.0.0",
@@ -72,7 +72,7 @@
72
72
  "line-column": "^1.0.2",
73
73
  "npm-run-all": "^4.1.5",
74
74
  "postcss-import": "^15.0.0",
75
- "postcss-simple-vars": "^7.0.0",
75
+ "postcss-simple-vars": "^7.0.1",
76
76
  "prettier": "~2.7.1",
77
77
  "sass": "^1.54.3",
78
78
  "tsc-watch": "^5.0.3",
package/src/cli.test.ts CHANGED
@@ -65,8 +65,9 @@ describe('parseArgv', () => {
65
65
  expect(parseArgv([...baseArgs, '1.css']).cacheStrategy).toBe('content');
66
66
  expect(parseArgv([...baseArgs, '1.css', '--cacheStrategy', 'metadata']).cacheStrategy).toBe('metadata');
67
67
  });
68
- test('--silent', () => {
69
- expect(parseArgv([...baseArgs, '1.css', '--silent']).silent).toBe(true);
70
- expect(parseArgv([...baseArgs, '1.css', '--no-silent']).silent).toBe(false);
68
+ test('--logLevel', () => {
69
+ expect(parseArgv([...baseArgs, '1.css']).logLevel).toBe('info');
70
+ expect(parseArgv([...baseArgs, '1.css', '--logLevel', 'debug']).logLevel).toBe('debug');
71
+ expect(parseArgv([...baseArgs, '1.css', '--logLevel', 'silent']).logLevel).toBe('silent');
71
72
  });
72
73
  });
package/src/cli.ts CHANGED
@@ -67,10 +67,10 @@ export function parseArgv(argv: string[]): RunnerOptions {
67
67
  default: 'content' as RunnerOptions['cacheStrategy'],
68
68
  describe: 'Strategy for the cache to use for detecting changed files.',
69
69
  })
70
- .option('silent', {
71
- type: 'boolean',
72
- default: false,
73
- describe: 'Silent output. Do not show "files written" messages',
70
+ .option('logLevel', {
71
+ choices: ['debug', 'info', 'silent'] as const,
72
+ default: 'info' as RunnerOptions['logLevel'],
73
+ describe: 'What level of logs to report.',
74
74
  })
75
75
  .alias('h', 'help')
76
76
  .alias('v', 'version')
@@ -108,6 +108,6 @@ export function parseArgv(argv: string[]): RunnerOptions {
108
108
  postcssConfig: parsedArgv.postcssConfig,
109
109
  cache: parsedArgv.cache,
110
110
  cacheStrategy: parsedArgv.cacheStrategy,
111
- silent: parsedArgv.silent,
111
+ logLevel: parsedArgv.logLevel,
112
112
  };
113
113
  }
@@ -1,7 +1,7 @@
1
1
  import dedent from 'dedent';
2
2
  import { SourceMapConsumer } from 'source-map';
3
3
  import { Locator } from '../locator/index.js';
4
- import { getFixturePath, createFixtures } from '../test/util.js';
4
+ import { getFixturePath, createFixtures } from '../test-util/util.js';
5
5
  import { generateDtsContentWithSourceMap, getDtsFilePath } from './dts.js';
6
6
  import { type DtsFormatOptions } from './index.js';
7
7
 
@@ -1,5 +1,5 @@
1
1
  import { readFile, rm, stat } from 'fs/promises';
2
- import { createFixtures, getFixturePath } from '../test/util.js';
2
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
3
3
  import { writeFileIfChanged } from './file-system.js';
4
4
 
5
5
  const TEST_FILE_PATH = getFixturePath('/test.txt');
@@ -1,6 +1,6 @@
1
1
  import { readFile, stat } from 'fs/promises';
2
2
  import { jest } from '@jest/globals';
3
- import { createFixtures, exists, fakeToken, getFixturePath, waitForAsyncTask } from '../test/util.js';
3
+ import { createFixtures, exists, fakeToken, getFixturePath, waitForAsyncTask } from '../test-util/util.js';
4
4
  import { emitGeneratedFiles, getRelativePath, isSubDirectoryFile } from './index.js';
5
5
 
6
6
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -1,13 +1,14 @@
1
1
  import dedent from 'dedent';
2
+ import type { RunnerOptions } from '../runner.js';
2
3
  import { run } from '../runner.js';
3
- import { createTSServer } from '../test/tsserver.js';
4
- import { createFixtures, getFixturePath } from '../test/util.js';
4
+ import { createTSServer } from '../test-util/tsserver.js';
5
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
5
6
 
6
7
  const server = await createTSServer();
7
8
 
8
- const defaultOptions = {
9
+ const defaultOptions: RunnerOptions = {
9
10
  pattern: 'test/**/*.{css,scss}',
10
- silent: true,
11
+ logLevel: 'silent',
11
12
  declarationMap: true,
12
13
  cwd: getFixturePath('/'),
13
14
  cache: false,
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { jest } from '@jest/globals';
4
4
  import dedent from 'dedent';
5
5
  import { createDefaultTransformer } from '../index.js';
6
- import { createFixtures, FIXTURE_DIR_PATH, getFixturePath } from '../test/util.js';
6
+ import { createFixtures, FIXTURE_DIR_PATH, getFixturePath } from '../test-util/util.js';
7
7
  import { sleepSync } from '../util.js';
8
8
 
9
9
  const readFileSpy = jest.spyOn(fs, 'readFile');
@@ -5,7 +5,7 @@ import {
5
5
  createAtImports,
6
6
  createComposesDeclarations,
7
7
  createFixtures,
8
- } from '../test/util.js';
8
+ } from '../test-util/util.js';
9
9
  import {
10
10
  generateLocalTokenNames,
11
11
  getOriginalLocation,
package/src/logger.ts ADDED
@@ -0,0 +1,31 @@
1
+ import chalk from 'chalk';
2
+ type LogLevelLabel = 'debug' | 'info' | 'silent';
3
+ type LogLevel = 2 | 1 | 0;
4
+ const LOG_LEVEL: Record<LogLevelLabel, LogLevel> = {
5
+ debug: 2,
6
+ info: 1,
7
+ silent: 0,
8
+ };
9
+
10
+ export class Logger {
11
+ private logLevel: LogLevel;
12
+ constructor(logLevelLabel: LogLevelLabel) {
13
+ this.logLevel = LOG_LEVEL[logLevelLabel];
14
+ }
15
+ debug(message: unknown) {
16
+ if (this.logLevel >= LOG_LEVEL['debug']) {
17
+ // eslint-disable-next-line no-console
18
+ console.log('[debug]', message);
19
+ }
20
+ }
21
+ info(message: unknown) {
22
+ if (this.logLevel >= LOG_LEVEL['info']) {
23
+ // eslint-disable-next-line no-console
24
+ console.log(chalk.blue('[info]'), message);
25
+ }
26
+ }
27
+ error(message: unknown) {
28
+ // eslint-disable-next-line no-console
29
+ console.error(chalk.red('[error]'), message);
30
+ }
31
+ }
@@ -0,0 +1,34 @@
1
+ import { symlink } from 'fs/promises';
2
+ import { jest } from '@jest/globals';
3
+ import type { RunnerOptions, Watcher } from '../runner.js';
4
+ import { run } from '../runner.js';
5
+ import { createFixtures, getFixturePath, waitForAsyncTask } from '../test-util/util.js';
6
+
7
+ const defaultOptions: RunnerOptions = {
8
+ pattern: 'test/**/*.css',
9
+ cwd: getFixturePath('/'),
10
+ cache: false,
11
+ logLevel: 'silent',
12
+ };
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
15
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
16
+
17
+ // Exit the watcher even if the test fails
18
+ let watcher: Watcher | undefined;
19
+ afterEach(async () => {
20
+ if (watcher) {
21
+ await watcher.close();
22
+ }
23
+ });
24
+
25
+ it('issue-168', async () => {
26
+ createFixtures({
27
+ '/test/css-file.css': '.a {}',
28
+ '/test/non-css-file.txt': 'text file',
29
+ });
30
+ await symlink(getFixturePath('/test/non-css-file.txt'), getFixturePath('/test/symlink.txt'));
31
+ watcher = await run({ ...defaultOptions, watch: true });
32
+ await waitForAsyncTask(300); // Wait for initial code generation to complete
33
+ expect(consoleErrorSpy).not.toBeCalled(); // If an error is output, then failed
34
+ });
@@ -1,4 +1,4 @@
1
- import { createFixtures, getFixturePath } from '../test/util.js';
1
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
2
2
  import { createDefaultResolver } from './index.js';
3
3
 
4
4
  const defaultResolver = createDefaultResolver();
@@ -1,4 +1,4 @@
1
- import { createFixtures, getFixturePath } from '../test/util.js';
1
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
2
2
  import { createNodeResolver } from './node-resolver.js';
3
3
 
4
4
  const nodeResolver = createNodeResolver();
@@ -1,4 +1,4 @@
1
- import { getFixturePath } from '../test/util.js';
1
+ import { getFixturePath } from '../test-util/util.js';
2
2
  import { createRelativeResolver } from './relative-resolver.js';
3
3
 
4
4
  const relativeResolver = createRelativeResolver();
@@ -1,4 +1,4 @@
1
- import { createFixtures, getFixturePath } from '../test/util.js';
1
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
2
2
  import { createWebpackResolver } from './webpack-resolver.js';
3
3
 
4
4
  test('resolves specifier with css-loader mechanism', async () => {
@@ -1,14 +1,13 @@
1
- import { readFile, rm, writeFile } from 'fs/promises';
1
+ import { readFile, rm, symlink, writeFile } from 'fs/promises';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, join, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import * as fileCacheNpm from '@file-cache/npm';
7
7
  import { jest } from '@jest/globals';
8
- import chalk from 'chalk';
9
8
  import dedent from 'dedent';
10
- import type { Watcher } from './runner.js';
11
- import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test/util.js';
9
+ import type { RunnerOptions, Watcher } from './runner.js';
10
+ import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test-util/util.js';
12
11
 
13
12
  const require = createRequire(import.meta.url);
14
13
 
@@ -24,10 +23,10 @@ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
24
23
  // eslint-disable-next-line @typescript-eslint/no-empty-function
25
24
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
26
25
 
27
- const defaultOptions = {
26
+ const defaultOptions: RunnerOptions = {
28
27
  pattern: 'test/**/*.{css,scss}',
29
28
  declarationMap: true,
30
- silent: true,
29
+ logLevel: 'silent',
31
30
  cwd: getFixturePath('/'),
32
31
  cache: false,
33
32
  };
@@ -60,47 +59,119 @@ test('generates .d.ts and .d.ts.map', async () => {
60
59
  expect(await readFile(getFixturePath('/test/2.css.d.ts.map'), 'utf8')).toMatchSnapshot();
61
60
  });
62
61
 
63
- test('uses cache', async () => {
62
+ test('uses cache in non-watch mode', async () => {
64
63
  createFixtures({
65
64
  '/test/1.css': '.a {}',
66
65
  });
67
- await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
68
- expect(consoleLogSpy).toBeCalledTimes(1);
69
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
66
+ await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
67
+ expect(consoleLogSpy).toBeCalledTimes(2);
68
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
69
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
70
70
  consoleLogSpy.mockClear();
71
71
 
72
72
  // Skip generation
73
- await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
74
- expect(consoleLogSpy).toBeCalledTimes(1);
75
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('skipped'));
73
+ await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
74
+ expect(consoleLogSpy).toBeCalledTimes(2);
75
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
76
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('skipped'));
76
77
  consoleLogSpy.mockClear();
77
78
 
78
79
  // Generates if generated files are missing
79
80
  await rm(getFixturePath('/test/1.css.d.ts'));
80
- await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
81
- expect(consoleLogSpy).toBeCalledTimes(1);
82
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
81
+ await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
82
+ expect(consoleLogSpy).toBeCalledTimes(2);
83
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
84
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
83
85
  consoleLogSpy.mockClear();
84
86
 
85
87
  // Generates if options are changed
86
- await run({ ...defaultOptions, declarationMap: false, silent: false, cache: true });
88
+ await run({ ...defaultOptions, declarationMap: false, logLevel: 'debug', cache: true });
89
+ expect(consoleLogSpy).toBeCalledTimes(2);
90
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
91
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
92
+ consoleLogSpy.mockClear();
93
+ });
94
+
95
+ test('uses cache in watch mode', async () => {
96
+ createFixtures({
97
+ '/test/1.css': '.a-1 {}',
98
+ '/test/2.css': '.b-1 {}',
99
+ });
100
+
101
+ // At first, process all files
102
+ watcher = await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true, watch: true });
103
+ await waitForAsyncTask(1000); // Wait until the watcher is ready
104
+ expect(consoleLogSpy).toBeCalledTimes(3);
105
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
106
+ 1,
107
+ expect.anything(),
108
+ expect.stringContaining('Watch test/**/*.{css,scss}...'),
109
+ );
110
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
111
+ 2,
112
+ expect.anything(),
113
+ expect.stringContaining('test/1.css (generated)'),
114
+ );
115
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
116
+ 3,
117
+ expect.anything(),
118
+ expect.stringContaining('test/2.css (generated)'),
119
+ );
120
+ consoleLogSpy.mockClear();
121
+
122
+ // Updating 1.css, it will only be processed
123
+ await writeFile(getFixturePath('/test/1.css'), '.a-2 {}');
124
+ await waitForAsyncTask(500); // Wait until the file is written
87
125
  expect(consoleLogSpy).toBeCalledTimes(1);
88
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
126
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
127
+ 1,
128
+ expect.anything(),
129
+ expect.stringContaining('test/1.css (generated)'),
130
+ );
131
+
132
+ // Close the watcher
133
+ await watcher.close();
89
134
  consoleLogSpy.mockClear();
135
+
136
+ // Update 1.css
137
+ await writeFile(getFixturePath('/test/1.css'), '.a-1 {}');
138
+ await waitForAsyncTask(500); // Wait until the file is written
139
+
140
+ // The updated 1.css will be processed, and the non-updated 2.css will be skipped.
141
+ watcher = await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true, watch: true });
142
+ await waitForAsyncTask(1000); // Wait until the watcher is ready
143
+ expect(consoleLogSpy).toBeCalledTimes(3);
144
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
145
+ 1,
146
+ expect.anything(),
147
+ expect.stringContaining('Watch test/**/*.{css,scss}...'),
148
+ );
149
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
150
+ 2,
151
+ expect.anything(),
152
+ expect.stringContaining('test/1.css (generated)'),
153
+ );
154
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(3, expect.anything(), expect.stringContaining('test/2.css (skipped)'));
90
155
  });
91
156
 
92
157
  test('outputs logs', async () => {
93
158
  createFixtures({
94
159
  '/test/1.css': '.a {}',
95
160
  });
96
- await run({ ...defaultOptions, silent: false, cache: true });
97
- expect(consoleLogSpy).toBeCalledTimes(1);
98
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `${chalk.green('test/1.css')} (generated)`);
161
+ await run({ ...defaultOptions, logLevel: 'debug', cache: true });
162
+ expect(consoleLogSpy).toBeCalledTimes(2);
163
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
164
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(
165
+ 2,
166
+ expect.anything(),
167
+ expect.stringContaining('test/1.css (generated)'),
168
+ );
99
169
  consoleLogSpy.mockClear();
100
170
 
101
- await run({ ...defaultOptions, silent: false, cache: true });
102
- expect(consoleLogSpy).toBeCalledTimes(1);
103
- expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `${chalk.gray('test/1.css (skipped)')}`);
171
+ await run({ ...defaultOptions, logLevel: 'debug', cache: true });
172
+ expect(consoleLogSpy).toBeCalledTimes(2);
173
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
174
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('test/1.css (skipped)'));
104
175
  });
105
176
 
106
177
  test.todo('changes dts format with localsConvention options');
@@ -156,13 +227,6 @@ test('returns an error if the file fails to process in non-watch mode', async ()
156
227
  expect(error.errors[0]).toMatchInlineSnapshot(`<fixtures>/test/2.css:1:1: Unknown word`);
157
228
  expect(error.errors[1]).toMatchInlineSnapshot(`<fixtures>/test/3.css:1:1: Unknown word`);
158
229
 
159
- // The error is logged to console.error.
160
- expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
161
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
162
- expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]!.stack));
163
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
164
- expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1].stack));
165
-
166
230
  // The valid files are emitted.
167
231
  expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
168
232
  expect(await exists(getFixturePath('/test/1.css.d.ts.map'))).toBe(true);
@@ -251,3 +315,33 @@ test('postcssConfig', async () => {
251
315
  });
252
316
  await run({ ...defaultOptions, postcssConfig }); // not throw
253
317
  });
318
+
319
+ test('support symlink', async () => {
320
+ createFixtures({
321
+ '/external/1.css': '.a {}',
322
+ '/external/2.css': '.b {}',
323
+ '/test': {}, // empty directory
324
+ });
325
+ await symlink(getFixturePath('/external/1.css'), getFixturePath('/test/1.css'));
326
+ await symlink(getFixturePath('/external/2.css'), getFixturePath('/test/2.txt'));
327
+
328
+ await run({ ...defaultOptions, watch: false });
329
+
330
+ // Symlinks that do not match the pattern are not processed.
331
+ expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
332
+ expect(await exists(getFixturePath('/test/2.css.d.ts'))).toBe(false);
333
+ expect(await exists(getFixturePath('/test/2.txt.d.ts'))).toBe(false);
334
+
335
+ // The path referred to by sourceMappingURL or sources field is the path before symlink resolution.
336
+ expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatchInlineSnapshot(`
337
+ "declare const styles:
338
+ & Readonly<{ "a": string }>
339
+ ;
340
+ export default styles;
341
+ //# sourceMappingURL=./1.css.d.ts.map
342
+ "
343
+ `);
344
+ expect(await readFile(getFixturePath('/test/1.css.d.ts.map'), 'utf8')).toMatchInlineSnapshot(
345
+ `"{"version":3,"sources":["./1.css"],"names":["a"],"mappings":"AAAA;AAAA,E,aAAAA,G,WAAA;AAAA;AAAA","file":"1.css.d.ts","sourceRoot":""}"`,
346
+ );
347
+ });
package/src/runner.ts CHANGED
@@ -9,6 +9,7 @@ import * as chokidar from 'chokidar';
9
9
  import _glob from 'glob';
10
10
  import { isGeneratedFilesExist, emitGeneratedFiles } from './emitter/index.js';
11
11
  import { Locator } from './locator/index.js';
12
+ import { Logger } from './logger.js';
12
13
  import type { Resolver } from './resolver/index.js';
13
14
  import { createDefaultResolver } from './resolver/index.js';
14
15
  import { createDefaultTransformer, type Transformer } from './transformer/index.js';
@@ -65,10 +66,10 @@ export interface RunnerOptions {
65
66
  */
66
67
  cacheStrategy?: 'content' | 'metadata' | undefined;
67
68
  /**
68
- * Silent output. Do not show "files written" messages.
69
- * @default false
69
+ * What level of logs to report.
70
+ * @default 'info'
70
71
  */
71
- silent?: boolean | undefined;
72
+ logLevel?: 'debug' | 'info' | 'silent' | undefined;
72
73
  /** Working directory path. */
73
74
  cwd?: string | undefined;
74
75
  }
@@ -84,9 +85,9 @@ export async function run(options: OverrideProp<RunnerOptions, 'watch', true>):
84
85
  export async function run(options: RunnerOptions): Promise<void>;
85
86
  export async function run(options: RunnerOptions): Promise<Watcher | void> {
86
87
  const lock = new AwaitLock.default();
88
+ const logger = new Logger(options.logLevel ?? 'info');
87
89
 
88
90
  const cwd = options.cwd ?? process.cwd();
89
- const silent = options.silent ?? false;
90
91
  const resolver =
91
92
  options.resolver ??
92
93
  createDefaultResolver({
@@ -115,9 +116,24 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
115
116
  };
116
117
 
117
118
  async function processFile(filePath: string) {
119
+ async function isChangedFile(filePath: string) {
120
+ const result = await cache.getAndUpdateCache(filePath);
121
+ if (result.error) throw result.error;
122
+ return result.changed;
123
+ }
124
+
125
+ // Locator#load cannot be called concurrently. Therefore, it takes a lock and waits.
126
+ await lock.acquireAsync();
127
+
118
128
  try {
119
- // Locator#load cannot be called concurrently. Therefore, it takes a lock and waits.
120
- await lock.acquireAsync();
129
+ const _isGeneratedFilesExist = await isGeneratedFilesExist(filePath, options.declarationMap);
130
+ const _isChangedFile = await isChangedFile(filePath);
131
+ // Generate .d.ts and .d.ts.map only when the file has been updated.
132
+ // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
133
+ if (_isGeneratedFilesExist && !_isChangedFile) {
134
+ logger.debug(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
135
+ return;
136
+ }
121
137
 
122
138
  const result = await locator.load(filePath);
123
139
  await emitGeneratedFiles({
@@ -129,62 +145,52 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
129
145
  },
130
146
  isExternalFile,
131
147
  });
132
- if (!silent) console.log(`${chalk.green(relative(cwd, filePath))} (generated)`);
133
- } catch (error) {
134
- if (error instanceof Error) {
135
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
136
- console.error(chalk.red('[Error] ' + error.stack));
137
- } else {
138
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
139
- console.error(chalk.red('[Error] ' + error));
140
- }
141
- throw error;
148
+ logger.info(chalk.green(`${relative(cwd, filePath)} (generated)`));
149
+
150
+ await cache.reconcile(); // Update cache for the file
142
151
  } finally {
143
152
  lock.release();
144
153
  }
145
154
  }
146
155
 
147
- async function isChangedFile(filePath: string) {
148
- const result = await cache.getAndUpdateCache(filePath);
149
- if (result.error) throw result.error;
150
- return result.changed;
151
- }
152
-
153
- if (options.watch) {
154
- if (!silent) console.log('Watch ' + options.pattern + '...');
155
- const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], { cwd });
156
- watcher.on('all', (eventName, filePath) => {
157
- if (eventName === 'add' || eventName === 'change') {
158
- processFile(resolve(cwd, filePath)).catch(() => {
159
- // TODO: Emit a error by `Watcher#onerror`
160
- });
161
- }
162
- });
163
- return { close: async () => watcher.close() };
164
- } else {
156
+ async function processAllFiles() {
165
157
  const filePaths = (await glob(options.pattern, { dot: true, cwd }))
166
158
  // convert relative path to absolute path
167
159
  .map((file) => resolve(cwd, file));
168
160
 
169
161
  const errors: unknown[] = [];
170
162
  for (const filePath of filePaths) {
171
- try {
172
- const _isGeneratedFilesExist = await isGeneratedFilesExist(filePath, options.declarationMap);
173
- const _isChangedFile = await isChangedFile(filePath);
174
- // Generate .d.ts and .d.ts.map only when the file has been updated.
175
- // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
176
- if (!_isGeneratedFilesExist || _isChangedFile) {
177
- await processFile(filePath);
178
- } else {
179
- if (!silent) console.log(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
180
- }
181
- } catch (e: unknown) {
182
- errors.push(e);
183
- }
163
+ await processFile(filePath).catch((e) => errors.push(e));
164
+ }
165
+
166
+ if (errors.length > 0) {
167
+ throw new AggregateError(errors, 'Failed to process files');
184
168
  }
185
- if (errors.length > 0) throw new AggregateError(errors, 'Failed to process files');
186
169
  }
187
170
 
188
- // Write cache state to file for persistence
189
- await cache.reconcile();
171
+ if (!options.watch) {
172
+ logger.info('Generate .d.ts for ' + options.pattern + '...');
173
+ await processAllFiles();
174
+ // Write cache state to file for persistence
175
+ } else {
176
+ // First, watch files.
177
+ logger.info('Watch ' + options.pattern + '...');
178
+ const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], { cwd });
179
+ watcher.on('all', (eventName, relativeFilePath) => {
180
+ const filePath = resolve(cwd, relativeFilePath);
181
+
182
+ // There is a bug in chokidar that matches symlinks that do not match the pattern.
183
+ // ref: https://github.com/paulmillr/chokidar/issues/967
184
+ if (isExternalFile(filePath)) return;
185
+
186
+ if (eventName !== 'add' && eventName !== 'change') return;
187
+
188
+ processFile(filePath).catch((e) => {
189
+ logger.error(e);
190
+ // TODO: Emit a error by `Watcher#onerror`
191
+ });
192
+ });
193
+
194
+ return { close: async () => watcher.close() };
195
+ }
190
196
  }
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { createRequire } from 'node:module';
3
3
  import dedent from 'dedent';
4
4
  import { Locator } from '../locator/index.js';
5
- import { createFixtures, getFixturePath } from '../test/util.js';
5
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
6
6
  import { createDefaultTransformer } from './index.js';
7
7
 
8
8
  const require = createRequire(import.meta.url);
@@ -33,9 +33,9 @@ export type TransformerOptions = {
33
33
  /** The function to transform source code. */
34
34
  export type Transformer = (source: string, options: TransformerOptions) => TransformResult | Promise<TransformResult>;
35
35
 
36
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: pass `e` to Error's cause option
36
37
  export const handleImportError = (packageName: string) => (e: unknown) => {
37
- console.error(`${packageName} import failed. Did you forget to \`npm install -D ${packageName}\`?`);
38
- throw e;
38
+ throw new Error(`${packageName} import failed. Did you forget to \`npm install -D ${packageName}\`?`);
39
39
  };
40
40
 
41
41
  export type DefaultTransformerOptions = PostcssTransformerOptions;
@@ -1,7 +1,7 @@
1
1
  import { jest } from '@jest/globals';
2
2
  import dedent from 'dedent';
3
3
  import { Locator } from '../locator/index.js';
4
- import { createFixtures, getFixturePath } from '../test/util.js';
4
+ import { createFixtures, getFixturePath } from '../test-util/util.js';
5
5
  import { createLessTransformer } from './less-transformer.js';
6
6
 
7
7
  const locator = new Locator({ transformer: createLessTransformer() });