happy-css-modules 4.0.0 → 5.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 (114) hide show
  1. package/LICENSE.txt +23 -0
  2. package/README.md +307 -2
  3. package/bin/hcm.js +1 -1
  4. package/dist/cli.d.ts +0 -4
  5. package/dist/cli.js +111 -118
  6. package/dist/cli.js.map +1 -1
  7. package/dist/emitter/dts.js +3 -4
  8. package/dist/emitter/dts.js.map +1 -1
  9. package/dist/emitter/dts.test.js +12 -12
  10. package/dist/emitter/file-system.js +2 -2
  11. package/dist/emitter/file-system.js.map +1 -1
  12. package/dist/emitter/file-system.test.js +1 -1
  13. package/dist/emitter/file-system.test.js.map +1 -1
  14. package/dist/emitter/index.js +1 -1
  15. package/dist/emitter/index.js.map +1 -1
  16. package/dist/emitter/index.test.js +1 -2
  17. package/dist/emitter/index.test.js.map +1 -1
  18. package/dist/emitter/source-map.js +1 -1
  19. package/dist/emitter/source-map.js.map +1 -1
  20. package/dist/emitter/source-map.test.js +1 -1
  21. package/dist/emitter/source-map.test.js.map +1 -1
  22. package/dist/integration-test/go-to-definition.test.js +403 -105
  23. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  24. package/dist/locator/index.js +10 -8
  25. package/dist/locator/index.js.map +1 -1
  26. package/dist/locator/index.test.js +303 -169
  27. package/dist/locator/index.test.js.map +1 -1
  28. package/dist/locator/postcss.js +2 -3
  29. package/dist/locator/postcss.js.map +1 -1
  30. package/dist/locator/postcss.test.js +317 -73
  31. package/dist/locator/postcss.test.js.map +1 -1
  32. package/dist/logger.js +7 -6
  33. package/dist/logger.js.map +1 -1
  34. package/dist/regression-test/issue-168.test.js +2 -3
  35. package/dist/regression-test/issue-168.test.js.map +1 -1
  36. package/dist/resolver/index.d.ts +1 -1
  37. package/dist/resolver/index.js +3 -3
  38. package/dist/resolver/index.js.map +1 -1
  39. package/dist/resolver/node-resolver.js +1 -1
  40. package/dist/resolver/node-resolver.js.map +1 -1
  41. package/dist/resolver/relative-resolver.js +1 -1
  42. package/dist/resolver/relative-resolver.js.map +1 -1
  43. package/dist/resolver/webpack-resolver.d.ts +1 -1
  44. package/dist/resolver/webpack-resolver.js +4 -4
  45. package/dist/resolver/webpack-resolver.js.map +1 -1
  46. package/dist/runner.js +50 -18
  47. package/dist/runner.js.map +1 -1
  48. package/dist/runner.test.js +47 -32
  49. package/dist/runner.test.js.map +1 -1
  50. package/dist/test-util/line-column.d.ts +9 -0
  51. package/dist/test-util/line-column.js +16 -0
  52. package/dist/test-util/line-column.js.map +1 -0
  53. package/dist/test-util/line-column.test.d.ts +1 -0
  54. package/dist/test-util/line-column.test.js +21 -0
  55. package/dist/test-util/line-column.test.js.map +1 -0
  56. package/dist/test-util/tsserver.js +11 -12
  57. package/dist/test-util/tsserver.js.map +1 -1
  58. package/dist/test-util/util.d.ts +6 -0
  59. package/dist/test-util/util.js +22 -5
  60. package/dist/test-util/util.js.map +1 -1
  61. package/dist/transformer/index.js +1 -1
  62. package/dist/transformer/index.js.map +1 -1
  63. package/dist/transformer/index.test.js +17 -17
  64. package/dist/transformer/index.test.js.map +1 -1
  65. package/dist/transformer/less-transformer.js +4 -4
  66. package/dist/transformer/less-transformer.js.map +1 -1
  67. package/dist/transformer/less-transformer.test.js +75 -49
  68. package/dist/transformer/less-transformer.test.js.map +1 -1
  69. package/dist/transformer/postcss-transformer.test.js +56 -50
  70. package/dist/transformer/postcss-transformer.test.js.map +1 -1
  71. package/dist/transformer/scss-transformer.js +0 -1
  72. package/dist/transformer/scss-transformer.js.map +1 -1
  73. package/dist/transformer/scss-transformer.test.js +105 -52
  74. package/dist/transformer/scss-transformer.test.js.map +1 -1
  75. package/dist/util.js +6 -8
  76. package/dist/util.js.map +1 -1
  77. package/dist/util.test.js +2 -2
  78. package/dist/util.test.js.map +1 -1
  79. package/package.json +58 -34
  80. package/src/cli.ts +119 -117
  81. package/src/emitter/dts.test.ts +12 -12
  82. package/src/emitter/dts.ts +25 -26
  83. package/src/emitter/file-system.test.ts +1 -1
  84. package/src/emitter/file-system.ts +2 -2
  85. package/src/emitter/index.test.ts +1 -2
  86. package/src/emitter/index.ts +1 -1
  87. package/src/emitter/source-map.test.ts +1 -1
  88. package/src/emitter/source-map.ts +1 -1
  89. package/src/integration-test/go-to-definition.test.ts +405 -105
  90. package/src/locator/index.test.ts +303 -169
  91. package/src/locator/index.ts +6 -6
  92. package/src/locator/postcss.test.ts +317 -73
  93. package/src/locator/postcss.ts +2 -3
  94. package/src/logger.ts +6 -6
  95. package/src/regression-test/issue-168.test.ts +2 -3
  96. package/src/resolver/index.ts +4 -4
  97. package/src/resolver/node-resolver.ts +1 -1
  98. package/src/resolver/relative-resolver.ts +1 -1
  99. package/src/resolver/webpack-resolver.ts +5 -5
  100. package/src/runner.test.ts +54 -32
  101. package/src/runner.ts +51 -19
  102. package/src/test-util/line-column.test.ts +21 -0
  103. package/src/test-util/line-column.ts +15 -0
  104. package/src/test-util/tsserver.ts +11 -12
  105. package/src/test-util/util.ts +23 -5
  106. package/src/transformer/index.test.ts +17 -17
  107. package/src/transformer/index.ts +1 -1
  108. package/src/transformer/less-transformer.test.ts +73 -45
  109. package/src/transformer/less-transformer.ts +1 -3
  110. package/src/transformer/postcss-transformer.test.ts +56 -50
  111. package/src/transformer/scss-transformer.test.ts +107 -52
  112. package/src/transformer/scss-transformer.ts +0 -1
  113. package/src/util.test.ts +2 -2
  114. package/src/util.ts +6 -8
@@ -27,7 +27,7 @@ export type DefaultResolverOptions = WebpackResolverOptions;
27
27
  * @param options The options to resolve
28
28
  * @returns The resolved path (absolute). `false` means to skip resolving.
29
29
  */
30
- export const createDefaultResolver: (defaultResolverOptions?: DefaultResolverOptions | undefined) => Resolver = (
30
+ export const createDefaultResolver: (defaultResolverOptions?: DefaultResolverOptions) => Resolver = (
31
31
  defaultResolverOptions,
32
32
  ) => {
33
33
  const relativeResolver = createRelativeResolver();
@@ -43,14 +43,14 @@ export const createDefaultResolver: (defaultResolverOptions?: DefaultResolverOpt
43
43
  return async (specifier, options) => {
44
44
  for (const resolver of resolvers) {
45
45
  try {
46
- // eslint-disable-next-line no-await-in-loop
46
+ // oxlint-disable-next-line no-await-in-loop
47
47
  const resolved = await resolver(specifier, options);
48
48
  if (resolved !== false) {
49
- // eslint-disable-next-line no-await-in-loop
49
+ // oxlint-disable-next-line no-await-in-loop
50
50
  const isExists = await exists(resolved);
51
51
  if (isExists) return resolved;
52
52
  }
53
- } catch (e) {
53
+ } catch {
54
54
  // noop
55
55
  }
56
56
  }
@@ -1,4 +1,4 @@
1
- import { fileURLToPath, pathToFileURL } from 'url';
1
+ import { fileURLToPath, pathToFileURL } from 'node:url';
2
2
  import { resolve } from 'import-meta-resolve';
3
3
  import type { Resolver } from './index.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { dirname, resolve } from 'path';
1
+ import { dirname, resolve } from 'node:path';
2
2
  import type { Resolver } from './index.js';
3
3
 
4
4
  export const createRelativeResolver: () => Resolver = () => (specifier, options) => {
@@ -1,4 +1,4 @@
1
- import { basename, dirname, join, resolve } from 'path';
1
+ import { basename, dirname, join, resolve } from 'node:path';
2
2
  import enhancedResolve from 'enhanced-resolve';
3
3
  import { exists } from '../util.js';
4
4
  import type { Resolver } from './index.js';
@@ -27,7 +27,7 @@ export type WebpackResolverOptions = {
27
27
  };
28
28
 
29
29
  // TODO: Support `resolve.alias` for Node.js API
30
- export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOptions | undefined) => Resolver = (
30
+ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOptions) => Resolver = (
31
31
  webpackResolverOptions,
32
32
  ) => {
33
33
  const cwd = webpackResolverOptions?.cwd ?? process.cwd();
@@ -93,7 +93,7 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
93
93
  // ref: https://github.com/webpack-contrib/css-loader/blob/5e6cf91fd3f0c8b5fb4b91197b98dc56abdef4bf/src/utils.js#L92-L95
94
94
  // ref: https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L368-L370
95
95
  // ref: https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L72-L75
96
- // eslint-disable-next-line no-param-reassign
96
+ // oxlint-disable-next-line no-param-reassign
97
97
  if (specifier.startsWith('~')) specifier = specifier.slice(1);
98
98
 
99
99
  for (const resolver of resolvers) {
@@ -109,11 +109,11 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
109
109
  try {
110
110
  const resolved = resolver(dirname(options.request), specifierVariant);
111
111
  if (resolved !== false) {
112
- // eslint-disable-next-line no-await-in-loop
112
+ // oxlint-disable-next-line no-await-in-loop
113
113
  const isExists = await exists(resolved);
114
114
  if (isExists) return resolved;
115
115
  }
116
- } catch (e) {
116
+ } catch {
117
117
  // noop
118
118
  }
119
119
  }
@@ -1,12 +1,12 @@
1
- import { readFile, rm, symlink, writeFile } from 'fs/promises';
2
1
  import { randomUUID } from 'node:crypto';
2
+ import { readFile, rm, symlink, writeFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
- import { dirname, join, resolve } from 'path';
5
- import { fileURLToPath } from 'url';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
6
  import type { CreateCacheOptions } from '@file-cache/core';
7
7
  import dedent from 'dedent';
8
8
  import type { RunnerOptions, Watcher } from './runner.js';
9
- import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test-util/util.js';
9
+ import { createFixtures, exists, getFixturePath, replaceFixtureDir, waitForAsyncTask } from './test-util/util.js';
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
 
@@ -22,11 +22,30 @@ vi.mock(import('@file-cache/core'), async (importOriginal) => {
22
22
  };
23
23
  });
24
24
 
25
+ // `createNpmPackageKey` resolves the version of `happy-css-modules` from node_modules, but this
26
+ // repository is the `happy-css-modules` package itself and does not install itself into node_modules.
27
+ // Stub it so the cache key can be computed without the package being installed.
28
+ vi.mock(import('@file-cache/npm'), async (importOriginal) => {
29
+ const fileCacheNpm = await importOriginal();
30
+ return {
31
+ ...fileCacheNpm,
32
+ createNpmPackageKey: () => '__happy-css-modules',
33
+ };
34
+ });
35
+
36
+ vi.mock('sass', async (importOriginal) => {
37
+ const sass = await importOriginal<typeof import('sass')>();
38
+ return {
39
+ ...sass,
40
+ // Suppress the warning: `Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.`
41
+ compileStringAsync: async (source: string, options?: Parameters<typeof sass.compileStringAsync>[1]) =>
42
+ sass.compileStringAsync(source, { silenceDeprecations: ['import'], ...options }),
43
+ };
44
+ });
45
+
25
46
  const { run } = await import('./runner.js');
26
47
 
27
- // eslint-disable-next-line @typescript-eslint/no-empty-function
28
48
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
29
- // eslint-disable-next-line @typescript-eslint/no-empty-function
30
49
  const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
31
50
 
32
51
  const defaultOptions: RunnerOptions = {
@@ -70,14 +89,14 @@ test('uses cache in non-watch mode', async () => {
70
89
  '/test/1.css': '.a {}',
71
90
  });
72
91
  await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
73
- expect(consoleLogSpy).toBeCalledTimes(2);
92
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
74
93
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
75
94
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
76
95
  consoleLogSpy.mockClear();
77
96
 
78
97
  // Skip generation
79
98
  await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
80
- expect(consoleLogSpy).toBeCalledTimes(2);
99
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
81
100
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
82
101
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('skipped'));
83
102
  consoleLogSpy.mockClear();
@@ -85,20 +104,20 @@ test('uses cache in non-watch mode', async () => {
85
104
  // Generates if generated files are missing
86
105
  await rm(getFixturePath('/test/1.css.d.ts'));
87
106
  await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
88
- expect(consoleLogSpy).toBeCalledTimes(2);
107
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
89
108
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
90
109
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
91
110
  consoleLogSpy.mockClear();
92
111
 
93
112
  // Generates if options are changed
94
113
  await run({ ...defaultOptions, declarationMap: false, logLevel: 'debug', cache: true });
95
- expect(consoleLogSpy).toBeCalledTimes(2);
114
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
96
115
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
97
116
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
98
117
  consoleLogSpy.mockClear();
99
118
  });
100
119
 
101
- test('uses cache in watch mode', async () => {
120
+ test('uses cache in watch mode', { retry: 5 }, async () => {
102
121
  createFixtures({
103
122
  '/test/1.css': '.a-1 {}',
104
123
  '/test/2.css': '.b-1 {}',
@@ -107,7 +126,7 @@ test('uses cache in watch mode', async () => {
107
126
  // At first, process all files
108
127
  watcher = await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true, watch: true });
109
128
  await waitForAsyncTask(1000); // Wait until the watcher is ready
110
- expect(consoleLogSpy).toBeCalledTimes(3);
129
+ expect(consoleLogSpy).toHaveBeenCalledTimes(3);
111
130
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
112
131
  1,
113
132
  expect.anything(),
@@ -128,7 +147,7 @@ test('uses cache in watch mode', async () => {
128
147
  // Updating 1.css, it will only be processed
129
148
  await writeFile(getFixturePath('/test/1.css'), '.a-2 {}');
130
149
  await waitForAsyncTask(500); // Wait until the file is written
131
- expect(consoleLogSpy).toBeCalledTimes(1);
150
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1);
132
151
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
133
152
  1,
134
153
  expect.anything(),
@@ -144,10 +163,9 @@ test('uses cache in watch mode', async () => {
144
163
  await waitForAsyncTask(500); // Wait until the file is written
145
164
 
146
165
  // The updated 1.css will be processed, and the non-updated 2.css will be skipped.
147
- // eslint-disable-next-line require-atomic-updates
148
166
  watcher = await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true, watch: true });
149
167
  await waitForAsyncTask(1000); // Wait until the watcher is ready
150
- expect(consoleLogSpy).toBeCalledTimes(3);
168
+ expect(consoleLogSpy).toHaveBeenCalledTimes(3);
151
169
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
152
170
  1,
153
171
  expect.anything(),
@@ -166,7 +184,7 @@ test('outputs logs', async () => {
166
184
  '/test/1.css': '.a {}',
167
185
  });
168
186
  await run({ ...defaultOptions, logLevel: 'debug', cache: true });
169
- expect(consoleLogSpy).toBeCalledTimes(2);
187
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
170
188
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
171
189
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
172
190
  2,
@@ -176,7 +194,7 @@ test('outputs logs', async () => {
176
194
  consoleLogSpy.mockClear();
177
195
 
178
196
  await run({ ...defaultOptions, logLevel: 'debug', cache: true });
179
- expect(consoleLogSpy).toBeCalledTimes(2);
197
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
180
198
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
181
199
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('test/1.css (skipped)'));
182
200
  });
@@ -231,8 +249,12 @@ test('returns an error if the file fails to process in non-watch mode', async ()
231
249
  const error = maybeError as AggregateError;
232
250
  expect(error.message).toMatchInlineSnapshot(`"Failed to process files"`);
233
251
  expect(error.errors).toHaveLength(2);
234
- expect(error.errors[0]).toMatchInlineSnapshot(`<fixtures>/test/3.css:1:1: Unknown word`);
235
- expect(error.errors[1]).toMatchInlineSnapshot(`<fixtures>/test/2.css:1:1: Unknown word`);
252
+ expect(replaceFixtureDir((error.errors[0] as Error).message)).toMatchInlineSnapshot(
253
+ `"<fixtures>/test/2.css:1:1: Unknown word INVALID"`,
254
+ );
255
+ expect(replaceFixtureDir((error.errors[1] as Error).message)).toMatchInlineSnapshot(
256
+ `"<fixtures>/test/3.css:1:1: Unknown word INVALID"`,
257
+ );
236
258
 
237
259
  // The valid files are emitted.
238
260
  expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
@@ -242,9 +264,9 @@ describe('handles external files', () => {
242
264
  beforeEach(() => {
243
265
  createFixtures({
244
266
  '/test/1.css': dedent`
245
- @import './2.css';
246
- @import 'external-library';
247
- .a {}
267
+ @import './2.css';
268
+ @import 'external-library';
269
+ .a {}
248
270
  `,
249
271
  '/test/2.css': `.b {}`,
250
272
  '/node_modules/external-library/index.css': `.c {}`,
@@ -275,7 +297,7 @@ test('sassLoadPaths', async () => {
275
297
  const sassLoadPaths = ['test/relative'];
276
298
  createFixtures({
277
299
  '/test/1.scss': dedent`
278
- @import '2.scss';
300
+ @import '2.scss';
279
301
  `,
280
302
  '/test/relative/2.scss': `.a { dummy: ''; }`,
281
303
  });
@@ -286,7 +308,7 @@ test('lessIncludePaths', async () => {
286
308
  const lessIncludePaths = ['test/relative'];
287
309
  createFixtures({
288
310
  '/test/1.less': dedent`
289
- @import '2.less';
311
+ @import '2.less';
290
312
  `,
291
313
  '/test/relative/2.less': `.a { dummy: ''; }`,
292
314
  });
@@ -297,7 +319,7 @@ test('webpackResolveAlias', async () => {
297
319
  const webpackResolveAlias = { '@relative': 'test/relative' };
298
320
  createFixtures({
299
321
  '/test/1.less': dedent`
300
- @import '@relative/2.less';
322
+ @import '@relative/2.less';
301
323
  `,
302
324
  '/test/relative/2.less': `.a { dummy: ''; }`,
303
325
  });
@@ -309,15 +331,15 @@ test('postcssConfig', async () => {
309
331
  const postcssConfig = `${uuid}/postcss.config.js`;
310
332
  createFixtures({
311
333
  [`/${uuid}/postcss.config.js`]: dedent`
312
- module.exports = {
313
- plugins: [
314
- require('${require.resolve('postcss-simple-vars')}'),
315
- ],
316
- };
334
+ module.exports = {
335
+ plugins: [
336
+ require('${require.resolve('postcss-simple-vars')}'),
337
+ ],
338
+ };
317
339
  `,
318
340
  '/test/1.css': dedent`
319
- $prefix: foo;
320
- .$(prefix)_bar {}
341
+ $prefix: foo;
342
+ .$(prefix)_bar {}
321
343
  `,
322
344
  });
323
345
  await run({ ...defaultOptions, postcssConfig }); // not throw
package/src/runner.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { resolve, relative } from 'path';
2
- import * as process from 'process';
1
+ import type { Stats } from 'node:fs';
2
+ import { glob } from 'node:fs/promises';
3
+ import { resolve, relative, basename } from 'node:path';
4
+ import * as process from 'node:process';
5
+ import { styleText } from 'node:util';
3
6
  import { createCache } from '@file-cache/core';
4
7
  import { createNpmPackageKey } from '@file-cache/npm';
5
- import { Mutex } from 'async-mutex';
6
- import chalk from 'chalk';
7
- import * as chokidar from 'chokidar';
8
- import { glob } from 'glob';
8
+ import chokidar from 'chokidar';
9
9
  import { DEFAULT_ARBITRARY_EXTENSIONS } from './config.js';
10
10
  import { isGeneratedFilesExist, emitGeneratedFiles } from './emitter/index.js';
11
11
  import { Locator } from './locator/index.js';
@@ -15,6 +15,31 @@ import { createDefaultResolver } from './resolver/index.js';
15
15
  import { createDefaultTransformer, type Transformer } from './transformer/index.js';
16
16
  import { getInstalledPeerDependencies, isMatchByGlob } from './util.js';
17
17
 
18
+ class Mutex {
19
+ private _queue: (() => void)[] = [];
20
+ private _locked = false;
21
+
22
+ async acquire(): Promise<void> {
23
+ await new Promise<void>((resolve) => {
24
+ if (!this._locked) {
25
+ this._locked = true;
26
+ resolve();
27
+ } else {
28
+ this._queue.push(resolve);
29
+ }
30
+ });
31
+ }
32
+
33
+ release(): void {
34
+ const next = this._queue.shift();
35
+ if (next) {
36
+ next();
37
+ } else {
38
+ this._locked = false;
39
+ }
40
+ }
41
+ }
42
+
18
43
  export type Watcher = {
19
44
  close: () => Promise<void>;
20
45
  };
@@ -128,7 +153,7 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
128
153
  async function processFile(filePath: string) {
129
154
  async function isChangedFile(filePath: string) {
130
155
  const result = await cache.getAndUpdateCache(filePath);
131
- // eslint-disable-next-line @typescript-eslint/no-throw-literal
156
+ // oxlint-disable-next-line typescript/only-throw-error
132
157
  if (result.error) throw result.error;
133
158
  return result.changed;
134
159
  }
@@ -148,7 +173,7 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
148
173
  // Generate .d.ts and .d.ts.map only when the file has been updated.
149
174
  // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
150
175
  if (_isGeneratedFilesExist && !_isChangedFile) {
151
- logger.debug(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
176
+ logger.debug(styleText('gray', `${relative(cwd, filePath)} (skipped)`));
152
177
  return;
153
178
  }
154
179
 
@@ -165,7 +190,7 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
165
190
  outDir: options.outDir,
166
191
  cwd,
167
192
  });
168
- logger.info(chalk.green(`${relative(cwd, filePath)} (generated)`));
193
+ logger.info(styleText('green', `${relative(cwd, filePath)} (generated)`));
169
194
 
170
195
  await cache.reconcile(); // Update cache for the file
171
196
  } finally {
@@ -174,13 +199,13 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
174
199
  }
175
200
 
176
201
  async function processAllFiles() {
177
- const filePaths = (await glob(options.pattern, { dot: true, cwd }))
202
+ const filePaths = (await Array.fromAsync(glob(options.pattern, { cwd })))
178
203
  // convert relative path to absolute path
179
204
  .map((file) => resolve(cwd, file));
180
205
 
181
206
  const errors: unknown[] = [];
182
207
  for (const filePath of filePaths) {
183
- // eslint-disable-next-line no-await-in-loop
208
+ // oxlint-disable-next-line no-await-in-loop
184
209
  await processFile(filePath).catch((e) => errors.push(e));
185
210
  }
186
211
 
@@ -196,14 +221,21 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
196
221
  } else {
197
222
  // First, watch files.
198
223
  logger.info(`Watch ${options.pattern}...`);
199
- const watcher = chokidar.watch([options.pattern.replace(/\\/gu, '/')], { cwd });
200
- watcher.on('all', (eventName, relativeFilePath) => {
201
- const filePath = resolve(cwd, relativeFilePath);
202
-
203
- // There is a bug in chokidar that matches symlinks that do not match the pattern.
204
- // ref: https://github.com/paulmillr/chokidar/issues/967
205
- if (isExternalFile(filePath)) return;
206
-
224
+ const watcher = chokidar.watch(cwd, {
225
+ ignored: (filePath: string, stats?: Stats) => {
226
+ // The ignored function is called twice for the same path. The first time with stats undefined,
227
+ // and the second time with stats provided.
228
+ // In the first call, we can't determine if the path is a directory or file,
229
+ // so we include it considering it might be a directory.
230
+ if (!stats) return false;
231
+ if (stats.isDirectory()) {
232
+ const name = basename(filePath);
233
+ return name === 'node_modules' || name === '.git';
234
+ }
235
+ return isExternalFile(filePath);
236
+ },
237
+ });
238
+ watcher.on('all', (eventName, filePath) => {
207
239
  if (eventName !== 'add' && eventName !== 'change') return;
208
240
 
209
241
  processFile(filePath).catch((e) => {
@@ -0,0 +1,21 @@
1
+ import { getIndexFromLineColumn } from './line-column.js';
2
+
3
+ describe('getIndexFromLineColumn', () => {
4
+ test('returns 0 for the first character', () => {
5
+ expect(getIndexFromLineColumn('abc', 1, 1)).toBe(0);
6
+ });
7
+ test('returns the index of a column in the middle of the first line', () => {
8
+ expect(getIndexFromLineColumn('abc', 1, 3)).toBe(2);
9
+ });
10
+ test('returns the index of the head of the second line', () => {
11
+ expect(getIndexFromLineColumn('abc\ndef', 2, 1)).toBe(4);
12
+ });
13
+ test('returns the index of a column in the middle of the second line', () => {
14
+ expect(getIndexFromLineColumn('abc\ndef', 2, 3)).toBe(6);
15
+ });
16
+ test('handles empty lines', () => {
17
+ // 'abc\n\ndef' => a=0 b=1 c=2 \n=3 \n=4 d=5 e=6 f=7
18
+ expect(getIndexFromLineColumn('abc\n\ndef', 2, 1)).toBe(4);
19
+ expect(getIndexFromLineColumn('abc\n\ndef', 3, 1)).toBe(5);
20
+ });
21
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Convert a 1-based line and column into a 0-based index in a string.
3
+ *
4
+ * @param str - The string to search in.
5
+ * @param line - The line number, 1-based.
6
+ * @param column - The column number, 1-based.
7
+ * @returns The 0-based index.
8
+ */
9
+ export function getIndexFromLineColumn(str: string, line: number, column: number): number {
10
+ const offsetToLine = str
11
+ .split('\n')
12
+ .slice(0, line - 1)
13
+ .reduce((offset, precedingLine) => offset + precedingLine.length + 1 /* '\n'.length */, 0);
14
+ return offsetToLine + (column - 1);
15
+ }
@@ -1,12 +1,11 @@
1
- import { readFileSync } from 'fs';
2
- import { mkdir, writeFile as nativeWriteFile } from 'fs/promises';
3
- import { dirname } from 'path';
4
- import { fileURLToPath } from 'url';
1
+ import { readFileSync } from 'node:fs';
2
+ import { mkdir, writeFile as nativeWriteFile, glob } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
5
  import serverHarness from '@typescript/server-harness';
6
- import { glob } from 'glob';
7
6
  import { resolve } from 'import-meta-resolve';
8
- import lineColumn from 'line-column';
9
7
  import type { server } from 'typescript/lib/tsserverlibrary.js';
8
+ import { getIndexFromLineColumn } from './line-column.js';
10
9
  import { getFixturePath } from './util.js';
11
10
 
12
11
  async function writeFile(path: string, content: string): Promise<void> {
@@ -62,7 +61,7 @@ export function createTSServer() {
62
61
  const results: { identifier: string; definitions: Definition[] }[] = [];
63
62
 
64
63
  for (let i = 0; i < identifiers.length; i++) {
65
- // eslint-disable-next-line no-await-in-loop
64
+ // oxlint-disable-next-line no-await-in-loop
66
65
  const response: server.protocol.DefinitionResponse = await server.message({
67
66
  seq: 0,
68
67
  type: 'request',
@@ -76,8 +75,8 @@ export function createTSServer() {
76
75
  const definitions: Definition[] = response.body!.map((definition) => {
77
76
  const { file, start, end } = definition;
78
77
  const fileContent = readFileSync(file, 'utf-8');
79
- const startIndex = lineColumn(fileContent).toIndex(start.line, start.offset);
80
- const endIndex = lineColumn(fileContent).toIndex(end.line, end.offset);
78
+ const startIndex = getIndexFromLineColumn(fileContent, start.line, start.offset);
79
+ const endIndex = getIndexFromLineColumn(fileContent, end.line, end.offset);
81
80
  const text = fileContent.slice(startIndex, endIndex);
82
81
  return { file, text, start, end };
83
82
  });
@@ -108,8 +107,8 @@ export function createTSServer() {
108
107
  const definitions: Definition[] = response.body!.map((definition) => {
109
108
  const { file, start, end } = definition;
110
109
  const fileContent = readFileSync(file, 'utf-8');
111
- const startIndex = lineColumn(fileContent).toIndex(start.line, start.offset);
112
- const endIndex = lineColumn(fileContent).toIndex(end.line, end.offset);
110
+ const startIndex = getIndexFromLineColumn(fileContent, start.line, start.offset);
111
+ const endIndex = getIndexFromLineColumn(fileContent, end.line, end.offset);
113
112
  const text = fileContent.slice(startIndex, endIndex);
114
113
  return { file, text, start, end };
115
114
  });
@@ -120,7 +119,7 @@ export function createTSServer() {
120
119
  // When a file is updated, its cache remains with the old content.
121
120
  // Therefore we need to overwrite the cache with the latest content.
122
121
 
123
- const fixtureFilePaths = await glob(getFixturePath('/**/*.ts'), { dot: true });
122
+ const fixtureFilePaths = await Array.fromAsync(glob(getFixturePath('/**/*.ts')));
124
123
  // latest contents
125
124
  const openFiles: server.protocol.UpdateOpenRequest['arguments']['openFiles'] = fixtureFilePaths.map(
126
125
  (filePath) => ({
@@ -1,7 +1,7 @@
1
- import { constants, mkdirSync, realpathSync, rmSync, writeFileSync } from 'fs';
2
- import { access } from 'fs/promises';
3
- import { tmpdir } from 'os';
4
- import { dirname, join, resolve } from 'path';
1
+ import { constants, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { access } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join, resolve } from 'node:path';
5
5
  import postcss, { type Root, type Rule, type AtRule } from 'postcss';
6
6
  import { type ClassName } from 'postcss-selector-parser';
7
7
  import { type Token, collectNodes, type Location } from '../locator/index.js';
@@ -63,7 +63,7 @@ export async function exists(path: string): Promise<boolean> {
63
63
  try {
64
64
  await access(path, constants.F_OK);
65
65
  return true;
66
- } catch (e) {
66
+ } catch {
67
67
  return false;
68
68
  }
69
69
  }
@@ -107,3 +107,21 @@ export function removeFixtures(): void {
107
107
  export function getFixturePath(path: string): string {
108
108
  return join(FIXTURE_DIR_PATH, path);
109
109
  }
110
+
111
+ /**
112
+ * Deeply clone `value` and replace all occurrences of `FIXTURE_DIR_PATH` in strings with `<fixtures>`.
113
+ * `FIXTURE_DIR_PATH` varies for each test run, so it must be replaced with a fixed string to make snapshots deterministic.
114
+ * For errors, pass `error.message` instead of the error itself.
115
+ */
116
+ export function replaceFixtureDir<T>(value: T): T {
117
+ if (typeof value === 'string') {
118
+ return value.replaceAll(FIXTURE_DIR_PATH, '<fixtures>') as T;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ return value.map((item) => replaceFixtureDir(item)) as T;
122
+ }
123
+ if (value !== null && typeof value === 'object') {
124
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceFixtureDir(item)])) as T;
125
+ }
126
+ return value;
127
+ }
@@ -13,10 +13,10 @@ const locator = new Locator({ transformer: createDefaultTransformer({ cwd }) });
13
13
  test('processes .scss with scss transformer', async () => {
14
14
  createFixtures({
15
15
  '/test/1.scss': dedent`
16
- .a {
17
- // scss feature test (nesting)
18
- .a_1 { dummy: ''; }
19
- }
16
+ .a {
17
+ // scss feature test (nesting)
18
+ .a_1 { dummy: ''; }
19
+ }
20
20
  `,
21
21
  });
22
22
  const result = await locator.load(getFixturePath('/test/1.scss'));
@@ -26,10 +26,10 @@ test('processes .scss with scss transformer', async () => {
26
26
  test('processes .less with less transformer', async () => {
27
27
  createFixtures({
28
28
  '/test/1.less': dedent`
29
- .a {
30
- // less feature test (nesting)
31
- .a_1 { dummy: ''; }
32
- }
29
+ .a {
30
+ // less feature test (nesting)
31
+ .a_1 { dummy: ''; }
32
+ }
33
33
  `,
34
34
  });
35
35
  const result = await locator.load(getFixturePath('/test/1.less'));
@@ -41,8 +41,8 @@ test('processes .css with postcss transformer if postcssrc is found', async () =
41
41
  const locator1 = new Locator({ transformer: createDefaultTransformer() });
42
42
  createFixtures({
43
43
  '/test/1.css': dedent`
44
- $prefix: foo;
45
- .$(prefix)_bar {}
44
+ $prefix: foo;
45
+ .$(prefix)_bar {}
46
46
  `,
47
47
  });
48
48
  const result1 = await locator1.load(getFixturePath('/test/1.css'));
@@ -55,15 +55,15 @@ test('processes .css with postcss transformer if postcssrc is found', async () =
55
55
  });
56
56
  createFixtures({
57
57
  [`/${uuid}/postcss.config.js`]: dedent`
58
- module.exports = {
59
- plugins: [
60
- require('${require.resolve('postcss-simple-vars')}'),
61
- ],
62
- };
58
+ module.exports = {
59
+ plugins: [
60
+ require('${require.resolve('postcss-simple-vars')}'),
61
+ ],
62
+ };
63
63
  `,
64
64
  '/test/1.css': dedent`
65
- $prefix: foo;
66
- .$(prefix)_bar {}
65
+ $prefix: foo;
66
+ .$(prefix)_bar {}
67
67
  `,
68
68
  });
69
69
  const result2 = await locator2.load(getFixturePath('/test/1.css'));
@@ -33,7 +33,7 @@ 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
+ // oxlint-disable-next-line no-unused-vars -- TODO: pass `e` to Error's cause option
37
37
  export const handleImportError = (packageName: string) => (e: unknown) => {
38
38
  throw new Error(`${packageName} import failed. Did you forget to \`npm install -D ${packageName}\`?`);
39
39
  };