happy-css-modules 1.0.1 → 2.1.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 (66) hide show
  1. package/README.md +2 -1
  2. package/bin/hcm.js +1 -0
  3. package/dist/cli.js +12 -5
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.test.js +9 -3
  6. package/dist/cli.test.js.map +1 -1
  7. package/dist/config.d.ts +1 -0
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/emitter/dts.d.ts +2 -1
  11. package/dist/emitter/dts.js +10 -3
  12. package/dist/emitter/dts.js.map +1 -1
  13. package/dist/emitter/dts.test.js +4 -1
  14. package/dist/emitter/dts.test.js.map +1 -1
  15. package/dist/emitter/index.d.ts +12 -3
  16. package/dist/emitter/index.js +7 -5
  17. package/dist/emitter/index.js.map +1 -1
  18. package/dist/emitter/source-map.d.ts +2 -1
  19. package/dist/emitter/source-map.js +3 -2
  20. package/dist/emitter/source-map.js.map +1 -1
  21. package/dist/emitter/source-map.test.js +4 -1
  22. package/dist/emitter/source-map.test.js.map +1 -1
  23. package/dist/integration-test/go-to-definition.test.js +1 -1
  24. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  25. package/dist/library/source-map/index.d.ts +1 -1
  26. package/dist/locator/index.d.ts +4 -4
  27. package/dist/locator/postcss.d.ts +3 -3
  28. package/dist/logger.d.ts +9 -0
  29. package/dist/logger.js +28 -0
  30. package/dist/logger.js.map +1 -0
  31. package/dist/regression-test/issue-168.test.js +1 -1
  32. package/dist/regression-test/issue-168.test.js.map +1 -1
  33. package/dist/resolver/index.d.ts +3 -3
  34. package/dist/resolver/webpack-resolver.d.ts +1 -1
  35. package/dist/runner.d.ts +15 -6
  36. package/dist/runner.js +43 -53
  37. package/dist/runner.js.map +1 -1
  38. package/dist/runner.test.js +57 -26
  39. package/dist/runner.test.js.map +1 -1
  40. package/dist/test-util/jest/resolver.d.cts +1 -15
  41. package/dist/test-util/tsserver.d.ts +1 -1
  42. package/dist/test-util/tsserver.js.map +1 -1
  43. package/dist/test-util/util.d.ts +3 -3
  44. package/dist/transformer/index.d.ts +4 -4
  45. package/dist/transformer/index.js +2 -2
  46. package/dist/transformer/index.js.map +1 -1
  47. package/dist/transformer/postcss-transformer.d.ts +1 -1
  48. package/dist/transformer/postcss-transformer.js +2 -2
  49. package/dist/transformer/postcss-transformer.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/cli.test.ts +9 -3
  52. package/src/cli.ts +12 -5
  53. package/src/config.ts +1 -0
  54. package/src/emitter/dts.test.ts +4 -1
  55. package/src/emitter/dts.ts +9 -3
  56. package/src/emitter/index.ts +16 -4
  57. package/src/emitter/source-map.test.ts +4 -1
  58. package/src/emitter/source-map.ts +3 -2
  59. package/src/integration-test/go-to-definition.test.ts +3 -2
  60. package/src/logger.ts +31 -0
  61. package/src/regression-test/issue-168.test.ts +1 -1
  62. package/src/runner.test.ts +93 -29
  63. package/src/runner.ts +63 -48
  64. package/src/test-util/tsserver.ts +14 -13
  65. package/src/transformer/index.ts +2 -2
  66. package/src/transformer/postcss-transformer.ts +2 -2
@@ -9,7 +9,10 @@ const locator = new Locator();
9
9
  const isExternalFile = () => false;
10
10
 
11
11
  test('getDtsFilePath', () => {
12
- expect(getDtsFilePath('/app/src/dir/1.css')).toBe('/app/src/dir/1.css.d.ts');
12
+ expect(getDtsFilePath('/app/src/dir/1.css', false)).toBe('/app/src/dir/1.css.d.ts');
13
+ expect(getDtsFilePath('/app/src/dir/1.scss', false)).toBe('/app/src/dir/1.scss.d.ts');
14
+ expect(getDtsFilePath('/app/src/dir/1.css', true)).toBe('/app/src/dir/1.d.css.ts');
15
+ expect(getDtsFilePath('/app/src/dir/1.scss', true)).toBe('/app/src/dir/1.d.scss.ts');
13
16
  });
14
17
 
15
18
  describe('generateDtsContentWithSourceMap', () => {
@@ -1,5 +1,5 @@
1
1
  import { EOL } from 'os';
2
- import { basename } from 'path';
2
+ import { basename, parse, join } from 'path';
3
3
  import camelcase from 'camelcase';
4
4
  import { SourceNode, type CodeWithSourceMap } from '../library/source-map/index.js';
5
5
  import { type Token } from '../locator/index.js';
@@ -9,10 +9,16 @@ import { getRelativePath, type DtsFormatOptions } from './index.js';
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 arbitraryExtensions Generate `.d.css.ts` instead of `.css.d.ts`.
12
13
  * @returns The path to the .d.ts file. It is absolute.
13
14
  */
14
- export function getDtsFilePath(filePath: string): string {
15
- return filePath + '.d.ts';
15
+ export function getDtsFilePath(filePath: string, arbitraryExtensions: boolean): string {
16
+ if (arbitraryExtensions) {
17
+ const { dir, name, ext } = parse(filePath);
18
+ return join(dir, name + '.d' + ext + '.ts');
19
+ } else {
20
+ return filePath + '.d.ts';
21
+ }
16
22
  }
17
23
 
18
24
  function dashesCamelCase(str: string): string {
@@ -1,4 +1,5 @@
1
1
  import { dirname, isAbsolute, relative } from 'path';
2
+ import { DEFAULT_ARBITRARY_EXTENSIONS } from '../config.js';
2
3
  import { type Token } from '../locator/index.js';
3
4
  import { type LocalsConvention } from '../runner.js';
4
5
  import { exists } from '../util.js';
@@ -20,7 +21,16 @@ export function isSubDirectoryFile(fromDirectory: string, toFilePath: string): b
20
21
  }
21
22
 
22
23
  export type DtsFormatOptions = {
24
+ /**
25
+ * Style of exported class names.
26
+ * @default undefined
27
+ */
23
28
  localsConvention?: LocalsConvention;
29
+ /**
30
+ * Generate `.d.css.ts` instead of `.css.d.ts`.
31
+ * @default false
32
+ */
33
+ arbitraryExtensions?: boolean | undefined;
24
34
  };
25
35
 
26
36
  /** The options for emitter. */
@@ -44,8 +54,9 @@ export async function emitGeneratedFiles({
44
54
  dtsFormatOptions,
45
55
  isExternalFile,
46
56
  }: EmitterOptions): Promise<void> {
47
- const dtsFilePath = getDtsFilePath(filePath);
48
- const sourceMapFilePath = getSourceMapFilePath(filePath);
57
+ const arbitraryExtensions = dtsFormatOptions?.arbitraryExtensions ?? DEFAULT_ARBITRARY_EXTENSIONS;
58
+ const dtsFilePath = getDtsFilePath(filePath, arbitraryExtensions);
59
+ const sourceMapFilePath = getSourceMapFilePath(filePath, arbitraryExtensions);
49
60
  const { dtsContent, sourceMap } = generateDtsContentWithSourceMap(
50
61
  filePath,
51
62
  dtsFilePath,
@@ -72,9 +83,10 @@ export async function emitGeneratedFiles({
72
83
  export async function isGeneratedFilesExist(
73
84
  filePath: string,
74
85
  emitDeclarationMap: boolean | undefined,
86
+ arbitraryExtensions: boolean,
75
87
  ): Promise<boolean> {
76
- const dtsFilePath = getDtsFilePath(filePath);
77
- const sourceMapFilePath = getSourceMapFilePath(filePath);
88
+ const dtsFilePath = getDtsFilePath(filePath, arbitraryExtensions);
89
+ const sourceMapFilePath = getSourceMapFilePath(filePath, arbitraryExtensions);
78
90
  if (emitDeclarationMap && !(await exists(sourceMapFilePath))) {
79
91
  return false;
80
92
  }
@@ -2,7 +2,10 @@ 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')).toBe('/app/src/dir/1.css.d.ts.map');
5
+ expect(getSourceMapFilePath('/app/src/dir/1.css', false)).toBe('/app/src/dir/1.css.d.ts.map');
6
+ expect(getSourceMapFilePath('/app/src/dir/1.scss', false)).toBe('/app/src/dir/1.scss.d.ts.map');
7
+ expect(getSourceMapFilePath('/app/src/dir/1.css', true)).toBe('/app/src/dir/1.d.css.ts.map');
8
+ expect(getSourceMapFilePath('/app/src/dir/1.scss', true)).toBe('/app/src/dir/1.d.scss.ts.map');
6
9
  });
7
10
 
8
11
  test('generateSourceMappingURLComment', () => {
@@ -5,10 +5,11 @@ import { getRelativePath } from './index.js';
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 arbitraryExtensions Generate `.d.css.ts` instead of `.css.d.ts`.
8
9
  * @returns The path to the .d.ts.map file. It is absolute.
9
10
  */
10
- export function getSourceMapFilePath(filePath: string): string {
11
- return getDtsFilePath(filePath) + '.map';
11
+ export function getSourceMapFilePath(filePath: string, arbitraryExtensions: boolean): string {
12
+ return getDtsFilePath(filePath, arbitraryExtensions) + '.map';
12
13
  }
13
14
 
14
15
  export function generateSourceMappingURLComment(dtsFilePath: string, sourceMapFilePath: string): string {
@@ -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
4
  import { createTSServer } from '../test-util/tsserver.js';
4
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,
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
+ }
@@ -8,7 +8,7 @@ const defaultOptions: RunnerOptions = {
8
8
  pattern: 'test/**/*.css',
9
9
  cwd: getFixturePath('/'),
10
10
  cache: false,
11
- silent: true,
11
+ logLevel: 'silent',
12
12
  };
13
13
 
14
14
  // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -5,9 +5,8 @@ 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';
9
+ import type { RunnerOptions, Watcher } from './runner.js';
11
10
  import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test-util/util.js';
12
11
 
13
12
  const require = createRequire(import.meta.url);
@@ -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);
package/src/runner.ts CHANGED
@@ -7,8 +7,10 @@ import AwaitLock from 'await-lock';
7
7
  import chalk from 'chalk';
8
8
  import * as chokidar from 'chokidar';
9
9
  import _glob from 'glob';
10
+ import { DEFAULT_ARBITRARY_EXTENSIONS } from './config.js';
10
11
  import { isGeneratedFilesExist, emitGeneratedFiles } from './emitter/index.js';
11
12
  import { Locator } from './locator/index.js';
13
+ import { Logger } from './logger.js';
12
14
  import type { Resolver } from './resolver/index.js';
13
15
  import { createDefaultResolver } from './resolver/index.js';
14
16
  import { createDefaultTransformer, type Transformer } from './transformer/index.js';
@@ -25,6 +27,10 @@ export type LocalsConvention = 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashe
25
27
  export interface RunnerOptions {
26
28
  pattern: string;
27
29
  watch?: boolean | undefined;
30
+ /**
31
+ * Style of exported class names.
32
+ * @default undefined
33
+ */
28
34
  localsConvention?: LocalsConvention | undefined;
29
35
  declarationMap?: boolean | undefined;
30
36
  transformer?: Transformer | undefined;
@@ -54,6 +60,11 @@ export interface RunnerOptions {
54
60
  * @example '/home/user/repository/src'
55
61
  */
56
62
  postcssConfig?: string | undefined;
63
+ /**
64
+ * Generate `.d.css.ts` instead of `.css.d.ts`.
65
+ * @default false
66
+ */
67
+ arbitraryExtensions?: boolean | undefined;
57
68
  /**
58
69
  * Only generate .d.ts and .d.ts.map for changed files.
59
70
  * @default true
@@ -65,10 +76,10 @@ export interface RunnerOptions {
65
76
  */
66
77
  cacheStrategy?: 'content' | 'metadata' | undefined;
67
78
  /**
68
- * Silent output. Do not show "files written" messages.
69
- * @default false
79
+ * What level of logs to report.
80
+ * @default 'info'
70
81
  */
71
- silent?: boolean | undefined;
82
+ logLevel?: 'debug' | 'info' | 'silent' | undefined;
72
83
  /** Working directory path. */
73
84
  cwd?: string | undefined;
74
85
  }
@@ -84,9 +95,9 @@ export async function run(options: OverrideProp<RunnerOptions, 'watch', true>):
84
95
  export async function run(options: RunnerOptions): Promise<void>;
85
96
  export async function run(options: RunnerOptions): Promise<Watcher | void> {
86
97
  const lock = new AwaitLock.default();
98
+ const logger = new Logger(options.logLevel ?? 'info');
87
99
 
88
100
  const cwd = options.cwd ?? process.cwd();
89
- const silent = options.silent ?? false;
90
101
  const resolver =
91
102
  options.resolver ??
92
103
  createDefaultResolver({
@@ -115,9 +126,28 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
115
126
  };
116
127
 
117
128
  async function processFile(filePath: string) {
129
+ async function isChangedFile(filePath: string) {
130
+ const result = await cache.getAndUpdateCache(filePath);
131
+ if (result.error) throw result.error;
132
+ return result.changed;
133
+ }
134
+
135
+ // Locator#load cannot be called concurrently. Therefore, it takes a lock and waits.
136
+ await lock.acquireAsync();
137
+
118
138
  try {
119
- // Locator#load cannot be called concurrently. Therefore, it takes a lock and waits.
120
- await lock.acquireAsync();
139
+ const _isGeneratedFilesExist = await isGeneratedFilesExist(
140
+ filePath,
141
+ options.declarationMap,
142
+ options.arbitraryExtensions ?? DEFAULT_ARBITRARY_EXTENSIONS,
143
+ );
144
+ const _isChangedFile = await isChangedFile(filePath);
145
+ // Generate .d.ts and .d.ts.map only when the file has been updated.
146
+ // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
147
+ if (_isGeneratedFilesExist && !_isChangedFile) {
148
+ logger.debug(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
149
+ return;
150
+ }
121
151
 
122
152
  const result = await locator.load(filePath);
123
153
  await emitGeneratedFiles({
@@ -126,32 +156,40 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
126
156
  emitDeclarationMap: options.declarationMap,
127
157
  dtsFormatOptions: {
128
158
  localsConvention: options.localsConvention,
159
+ arbitraryExtensions: options.arbitraryExtensions,
129
160
  },
130
161
  isExternalFile,
131
162
  });
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;
163
+ logger.info(chalk.green(`${relative(cwd, filePath)} (generated)`));
164
+
165
+ await cache.reconcile(); // Update cache for the file
142
166
  } finally {
143
167
  lock.release();
144
168
  }
145
169
  }
146
170
 
147
- async function isChangedFile(filePath: string) {
148
- const result = await cache.getAndUpdateCache(filePath);
149
- if (result.error) throw result.error;
150
- return result.changed;
171
+ async function processAllFiles() {
172
+ const filePaths = (await glob(options.pattern, { dot: true, cwd }))
173
+ // convert relative path to absolute path
174
+ .map((file) => resolve(cwd, file));
175
+
176
+ const errors: unknown[] = [];
177
+ for (const filePath of filePaths) {
178
+ await processFile(filePath).catch((e) => errors.push(e));
179
+ }
180
+
181
+ if (errors.length > 0) {
182
+ throw new AggregateError(errors, 'Failed to process files');
183
+ }
151
184
  }
152
185
 
153
- if (options.watch) {
154
- if (!silent) console.log('Watch ' + options.pattern + '...');
186
+ if (!options.watch) {
187
+ logger.info('Generate .d.ts for ' + options.pattern + '...');
188
+ await processAllFiles();
189
+ // Write cache state to file for persistence
190
+ } else {
191
+ // First, watch files.
192
+ logger.info('Watch ' + options.pattern + '...');
155
193
  const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], { cwd });
156
194
  watcher.on('all', (eventName, relativeFilePath) => {
157
195
  const filePath = resolve(cwd, relativeFilePath);
@@ -162,35 +200,12 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
162
200
 
163
201
  if (eventName !== 'add' && eventName !== 'change') return;
164
202
 
165
- processFile(filePath).catch(() => {
203
+ processFile(filePath).catch((e) => {
204
+ logger.error(e);
166
205
  // TODO: Emit a error by `Watcher#onerror`
167
206
  });
168
207
  });
169
- return { close: async () => watcher.close() };
170
- } else {
171
- const filePaths = (await glob(options.pattern, { dot: true, cwd }))
172
- // convert relative path to absolute path
173
- .map((file) => resolve(cwd, file));
174
208
 
175
- const errors: unknown[] = [];
176
- for (const filePath of filePaths) {
177
- try {
178
- const _isGeneratedFilesExist = await isGeneratedFilesExist(filePath, options.declarationMap);
179
- const _isChangedFile = await isChangedFile(filePath);
180
- // Generate .d.ts and .d.ts.map only when the file has been updated.
181
- // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
182
- if (!_isGeneratedFilesExist || _isChangedFile) {
183
- await processFile(filePath);
184
- } else {
185
- if (!silent) console.log(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
186
- }
187
- } catch (e: unknown) {
188
- errors.push(e);
189
- }
190
- }
191
- if (errors.length > 0) throw new AggregateError(errors, 'Failed to process files');
209
+ return { close: async () => watcher.close() };
192
210
  }
193
-
194
- // Write cache state to file for persistence
195
- await cache.reconcile();
196
211
  }
@@ -7,9 +7,8 @@ import serverHarness from '@typescript/server-harness';
7
7
  import _glob from 'glob';
8
8
  import { resolve } from 'import-meta-resolve';
9
9
  import lineColumn from 'line-column';
10
- import type { UpdateOpenRequest, DefinitionResponse, DefinitionRequest } from 'typescript/lib/protocol.js';
10
+ import type { server } from 'typescript/lib/tsserverlibrary.js';
11
11
  import { getFixturePath } from './util.js';
12
-
13
12
  const glob = promisify(_glob);
14
13
 
15
14
  async function writeFile(path: string, content: string): Promise<void> {
@@ -68,7 +67,7 @@ export async function createTSServer() {
68
67
  const results: { identifier: string; definitions: Definition[] }[] = [];
69
68
 
70
69
  for (let i = 0; i < identifiers.length; i++) {
71
- const response: DefinitionResponse = await server.message({
70
+ const response: server.protocol.DefinitionResponse = await server.message({
72
71
  seq: 0,
73
72
  type: 'request',
74
73
  command: 'definition',
@@ -77,7 +76,7 @@ export async function createTSServer() {
77
76
  line: i + 2, // line, 1-based
78
77
  offset: 8, // column, 1-based
79
78
  },
80
- } as DefinitionRequest);
79
+ } as server.protocol.DefinitionRequest);
81
80
  const definitions: Definition[] = response.body!.map((definition) => {
82
81
  const { file, start, end } = definition;
83
82
  const fileContent = readFileSync(file, 'utf-8');
@@ -100,7 +99,7 @@ export async function createTSServer() {
100
99
 
101
100
  await this.refreshCache();
102
101
 
103
- const response: DefinitionResponse = await server.message({
102
+ const response: server.protocol.DefinitionResponse = await server.message({
104
103
  seq: 0,
105
104
  type: 'request',
106
105
  command: 'definition',
@@ -109,7 +108,7 @@ export async function createTSServer() {
109
108
  line: 1, // line, 1-based
110
109
  offset: 20, // column, 1-based
111
110
  },
112
- } as DefinitionRequest);
111
+ } as server.protocol.DefinitionRequest);
113
112
  const definitions: Definition[] = response.body!.map((definition) => {
114
113
  const { file, start, end } = definition;
115
114
  const fileContent = readFileSync(file, 'utf-8');
@@ -127,12 +126,14 @@ export async function createTSServer() {
127
126
 
128
127
  const fixtureFilePaths = await glob(getFixturePath('/**/*.ts'), { dot: true });
129
128
  // latest contents
130
- const openFiles: UpdateOpenRequest['arguments']['openFiles'] = fixtureFilePaths.map((filePath) => ({
131
- file: filePath,
132
- fileContent: readFileSync(filePath, 'utf-8'),
133
- projectRootPath: getFixturePath('/server-harness'),
134
- scriptKindName: 'TS', // It's easy to get this wrong when copy-pasting
135
- }));
129
+ const openFiles: server.protocol.UpdateOpenRequest['arguments']['openFiles'] = fixtureFilePaths.map(
130
+ (filePath) => ({
131
+ file: filePath,
132
+ fileContent: readFileSync(filePath, 'utf-8'),
133
+ projectRootPath: getFixturePath('/server-harness'),
134
+ scriptKindName: 'TS', // It's easy to get this wrong when copy-pasting
135
+ }),
136
+ );
136
137
 
137
138
  // override the cache
138
139
  await server.message({
@@ -144,7 +145,7 @@ export async function createTSServer() {
144
145
  closedFiles: [],
145
146
  openFiles,
146
147
  },
147
- } as UpdateOpenRequest);
148
+ } as server.protocol.UpdateOpenRequest);
148
149
  },
149
150
  exit: async () => {
150
151
  await server.message({ command: 'exit' });
@@ -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,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { resolve } from 'node:path';
3
- import { default as postcss, type Message } from 'postcss';
3
+ import postcss, { type Message } from 'postcss';
4
4
  import type { Result } from 'postcss-load-config';
5
5
  import type { Transformer } from '../index.js';
6
6
 
@@ -44,7 +44,7 @@ export const createPostcssTransformer: (postcssTransformerOptions?: PostcssTrans
44
44
  });
45
45
  if (postcssConfig === undefined) return false;
46
46
 
47
- const result = await postcss(postcssConfig.plugins).process(source, {
47
+ const result = await postcss.default(postcssConfig.plugins).process(source, {
48
48
  ...postcssConfig.options,
49
49
  from: options.from,
50
50
  map: { inline: false, absolute: true },