happy-css-modules 3.2.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 (120) 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 +7 -8
  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 +2 -4
  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 +304 -171
  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 +3 -5
  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 +52 -21
  47. package/dist/runner.js.map +1 -1
  48. package/dist/runner.test.js +59 -43
  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 +23 -6
  60. package/dist/test-util/util.js.map +1 -1
  61. package/dist/transformer/index.d.ts +0 -1
  62. package/dist/transformer/index.js +1 -1
  63. package/dist/transformer/index.js.map +1 -1
  64. package/dist/transformer/index.test.js +17 -17
  65. package/dist/transformer/index.test.js.map +1 -1
  66. package/dist/transformer/less-transformer.js +4 -4
  67. package/dist/transformer/less-transformer.js.map +1 -1
  68. package/dist/transformer/less-transformer.test.js +76 -51
  69. package/dist/transformer/less-transformer.test.js.map +1 -1
  70. package/dist/transformer/postcss-transformer.test.js +58 -54
  71. package/dist/transformer/postcss-transformer.test.js.map +1 -1
  72. package/dist/transformer/scss-transformer.js +11 -30
  73. package/dist/transformer/scss-transformer.js.map +1 -1
  74. package/dist/transformer/scss-transformer.test.js +106 -54
  75. package/dist/transformer/scss-transformer.test.js.map +1 -1
  76. package/dist/util.js +6 -8
  77. package/dist/util.js.map +1 -1
  78. package/dist/util.test.js +2 -2
  79. package/dist/util.test.js.map +1 -1
  80. package/package.json +59 -35
  81. package/src/__snapshots__/runner.test.ts.snap +1 -1
  82. package/src/cli.ts +119 -117
  83. package/src/emitter/dts.test.ts +12 -12
  84. package/src/emitter/dts.ts +27 -28
  85. package/src/emitter/file-system.test.ts +1 -1
  86. package/src/emitter/file-system.ts +2 -2
  87. package/src/emitter/index.test.ts +2 -4
  88. package/src/emitter/index.ts +1 -1
  89. package/src/emitter/source-map.test.ts +1 -1
  90. package/src/emitter/source-map.ts +1 -1
  91. package/src/integration-test/go-to-definition.test.ts +405 -105
  92. package/src/locator/index.test.ts +304 -171
  93. package/src/locator/index.ts +6 -6
  94. package/src/locator/postcss.test.ts +317 -73
  95. package/src/locator/postcss.ts +2 -3
  96. package/src/logger.ts +6 -6
  97. package/src/regression-test/issue-168.test.ts +3 -5
  98. package/src/resolver/index.ts +4 -4
  99. package/src/resolver/node-resolver.ts +1 -1
  100. package/src/resolver/relative-resolver.ts +1 -1
  101. package/src/resolver/webpack-resolver.ts +5 -5
  102. package/src/runner.test.ts +66 -43
  103. package/src/runner.ts +53 -22
  104. package/src/test-util/line-column.test.ts +21 -0
  105. package/src/test-util/line-column.ts +15 -0
  106. package/src/test-util/tsserver.ts +11 -12
  107. package/src/test-util/util.ts +24 -6
  108. package/src/transformer/index.test.ts +17 -17
  109. package/src/transformer/index.ts +1 -1
  110. package/src/transformer/less-transformer.test.ts +74 -47
  111. package/src/transformer/less-transformer.ts +1 -3
  112. package/src/transformer/postcss-transformer.test.ts +58 -54
  113. package/src/transformer/scss-transformer.test.ts +108 -54
  114. package/src/transformer/scss-transformer.ts +13 -30
  115. package/src/util.test.ts +2 -2
  116. package/src/util.ts +6 -8
  117. package/dist/test-util/jest/resolver.cjs +0 -31
  118. package/dist/test-util/jest/resolver.cjs.map +0 -1
  119. package/dist/test-util/jest/resolver.d.cts +0 -16
  120. package/src/test-util/jest/resolver.cjs +0 -31
@@ -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,32 +1,52 @@
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';
6
- import * as fileCacheCore from '@file-cache/core';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
7
6
  import type { CreateCacheOptions } from '@file-cache/core';
8
- import { jest } from '@jest/globals';
9
7
  import dedent from 'dedent';
10
8
  import type { RunnerOptions, Watcher } from './runner.js';
11
- import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test-util/util.js';
9
+ import { createFixtures, exists, getFixturePath, replaceFixtureDir, waitForAsyncTask } from './test-util/util.js';
12
10
 
13
11
  const require = createRequire(import.meta.url);
14
12
 
15
13
  const uuid = randomUUID();
16
- jest.unstable_mockModule('@file-cache/core', () => ({
17
- ...fileCacheCore, // Inherit native functions
18
- createCache: async (options: CreateCacheOptions) => {
19
- options.keys.push(() => uuid); // Add a random key to avoid cache collision
20
- return fileCacheCore.createCache(options);
21
- },
22
- }));
14
+ vi.mock(import('@file-cache/core'), async (importOriginal) => {
15
+ const fileCacheCore = await importOriginal();
16
+ return {
17
+ ...fileCacheCore, // Inherit native functions
18
+ createCache: async (options: CreateCacheOptions) => {
19
+ options.keys.push(() => uuid); // Add a random key to avoid cache collision
20
+ return fileCacheCore.createCache(options);
21
+ },
22
+ };
23
+ });
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
+ });
23
45
 
24
46
  const { run } = await import('./runner.js');
25
47
 
26
- // eslint-disable-next-line @typescript-eslint/no-empty-function
27
- const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
28
- // eslint-disable-next-line @typescript-eslint/no-empty-function
29
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
48
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
49
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
30
50
 
31
51
  const defaultOptions: RunnerOptions = {
32
52
  pattern: 'test/**/*.{css,scss}',
@@ -69,14 +89,14 @@ test('uses cache in non-watch mode', async () => {
69
89
  '/test/1.css': '.a {}',
70
90
  });
71
91
  await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
72
- expect(consoleLogSpy).toBeCalledTimes(2);
92
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
73
93
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
74
94
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
75
95
  consoleLogSpy.mockClear();
76
96
 
77
97
  // Skip generation
78
98
  await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
79
- expect(consoleLogSpy).toBeCalledTimes(2);
99
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
80
100
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
81
101
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('skipped'));
82
102
  consoleLogSpy.mockClear();
@@ -84,20 +104,20 @@ test('uses cache in non-watch mode', async () => {
84
104
  // Generates if generated files are missing
85
105
  await rm(getFixturePath('/test/1.css.d.ts'));
86
106
  await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true });
87
- expect(consoleLogSpy).toBeCalledTimes(2);
107
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
88
108
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
89
109
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
90
110
  consoleLogSpy.mockClear();
91
111
 
92
112
  // Generates if options are changed
93
113
  await run({ ...defaultOptions, declarationMap: false, logLevel: 'debug', cache: true });
94
- expect(consoleLogSpy).toBeCalledTimes(2);
114
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
95
115
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
96
116
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('generated'));
97
117
  consoleLogSpy.mockClear();
98
118
  });
99
119
 
100
- test('uses cache in watch mode', async () => {
120
+ test('uses cache in watch mode', { retry: 5 }, async () => {
101
121
  createFixtures({
102
122
  '/test/1.css': '.a-1 {}',
103
123
  '/test/2.css': '.b-1 {}',
@@ -106,7 +126,7 @@ test('uses cache in watch mode', async () => {
106
126
  // At first, process all files
107
127
  watcher = await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true, watch: true });
108
128
  await waitForAsyncTask(1000); // Wait until the watcher is ready
109
- expect(consoleLogSpy).toBeCalledTimes(3);
129
+ expect(consoleLogSpy).toHaveBeenCalledTimes(3);
110
130
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
111
131
  1,
112
132
  expect.anything(),
@@ -127,7 +147,7 @@ test('uses cache in watch mode', async () => {
127
147
  // Updating 1.css, it will only be processed
128
148
  await writeFile(getFixturePath('/test/1.css'), '.a-2 {}');
129
149
  await waitForAsyncTask(500); // Wait until the file is written
130
- expect(consoleLogSpy).toBeCalledTimes(1);
150
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1);
131
151
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
132
152
  1,
133
153
  expect.anything(),
@@ -143,10 +163,9 @@ test('uses cache in watch mode', async () => {
143
163
  await waitForAsyncTask(500); // Wait until the file is written
144
164
 
145
165
  // The updated 1.css will be processed, and the non-updated 2.css will be skipped.
146
- // eslint-disable-next-line require-atomic-updates
147
166
  watcher = await run({ ...defaultOptions, declarationMap: true, logLevel: 'debug', cache: true, watch: true });
148
167
  await waitForAsyncTask(1000); // Wait until the watcher is ready
149
- expect(consoleLogSpy).toBeCalledTimes(3);
168
+ expect(consoleLogSpy).toHaveBeenCalledTimes(3);
150
169
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
151
170
  1,
152
171
  expect.anything(),
@@ -165,7 +184,7 @@ test('outputs logs', async () => {
165
184
  '/test/1.css': '.a {}',
166
185
  });
167
186
  await run({ ...defaultOptions, logLevel: 'debug', cache: true });
168
- expect(consoleLogSpy).toBeCalledTimes(2);
187
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
169
188
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
170
189
  expect(consoleLogSpy).toHaveBeenNthCalledWith(
171
190
  2,
@@ -175,7 +194,7 @@ test('outputs logs', async () => {
175
194
  consoleLogSpy.mockClear();
176
195
 
177
196
  await run({ ...defaultOptions, logLevel: 'debug', cache: true });
178
- expect(consoleLogSpy).toBeCalledTimes(2);
197
+ expect(consoleLogSpy).toHaveBeenCalledTimes(2);
179
198
  expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.anything(), expect.stringContaining('Generate .d.ts for'));
180
199
  expect(consoleLogSpy).toHaveBeenNthCalledWith(2, expect.anything(), expect.stringContaining('test/1.css (skipped)'));
181
200
  });
@@ -230,8 +249,12 @@ test('returns an error if the file fails to process in non-watch mode', async ()
230
249
  const error = maybeError as AggregateError;
231
250
  expect(error.message).toMatchInlineSnapshot(`"Failed to process files"`);
232
251
  expect(error.errors).toHaveLength(2);
233
- expect(error.errors[0]).toMatchInlineSnapshot(`<fixtures>/test/3.css:1:1: Unknown word`);
234
- 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
+ );
235
258
 
236
259
  // The valid files are emitted.
237
260
  expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
@@ -241,9 +264,9 @@ describe('handles external files', () => {
241
264
  beforeEach(() => {
242
265
  createFixtures({
243
266
  '/test/1.css': dedent`
244
- @import './2.css';
245
- @import 'external-library';
246
- .a {}
267
+ @import './2.css';
268
+ @import 'external-library';
269
+ .a {}
247
270
  `,
248
271
  '/test/2.css': `.b {}`,
249
272
  '/node_modules/external-library/index.css': `.c {}`,
@@ -274,7 +297,7 @@ test('sassLoadPaths', async () => {
274
297
  const sassLoadPaths = ['test/relative'];
275
298
  createFixtures({
276
299
  '/test/1.scss': dedent`
277
- @import '2.scss';
300
+ @import '2.scss';
278
301
  `,
279
302
  '/test/relative/2.scss': `.a { dummy: ''; }`,
280
303
  });
@@ -285,7 +308,7 @@ test('lessIncludePaths', async () => {
285
308
  const lessIncludePaths = ['test/relative'];
286
309
  createFixtures({
287
310
  '/test/1.less': dedent`
288
- @import '2.less';
311
+ @import '2.less';
289
312
  `,
290
313
  '/test/relative/2.less': `.a { dummy: ''; }`,
291
314
  });
@@ -296,7 +319,7 @@ test('webpackResolveAlias', async () => {
296
319
  const webpackResolveAlias = { '@relative': 'test/relative' };
297
320
  createFixtures({
298
321
  '/test/1.less': dedent`
299
- @import '@relative/2.less';
322
+ @import '@relative/2.less';
300
323
  `,
301
324
  '/test/relative/2.less': `.a { dummy: ''; }`,
302
325
  });
@@ -308,15 +331,15 @@ test('postcssConfig', async () => {
308
331
  const postcssConfig = `${uuid}/postcss.config.js`;
309
332
  createFixtures({
310
333
  [`/${uuid}/postcss.config.js`]: dedent`
311
- module.exports = {
312
- plugins: [
313
- require('${require.resolve('postcss-simple-vars')}'),
314
- ],
315
- };
334
+ module.exports = {
335
+ plugins: [
336
+ require('${require.resolve('postcss-simple-vars')}'),
337
+ ],
338
+ };
316
339
  `,
317
340
  '/test/1.css': dedent`
318
- $prefix: foo;
319
- .$(prefix)_bar {}
341
+ $prefix: foo;
342
+ .$(prefix)_bar {}
320
343
  `,
321
344
  });
322
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 AwaitLock from 'await-lock';
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
  };
@@ -93,8 +118,7 @@ type OverrideProp<T, K extends keyof T, V extends T[K]> = Omit<T, K> & { [P in K
93
118
  export async function run(options: OverrideProp<RunnerOptions, 'watch', true>): Promise<Watcher>;
94
119
  export async function run(options: RunnerOptions): Promise<void>;
95
120
  export async function run(options: RunnerOptions): Promise<Watcher | void> {
96
- // eslint-disable-next-line new-cap
97
- const lock = new AwaitLock.default();
121
+ const lock = new Mutex();
98
122
  const logger = new Logger(options.logLevel ?? 'info');
99
123
 
100
124
  const cwd = options.cwd ?? process.cwd();
@@ -129,13 +153,13 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
129
153
  async function processFile(filePath: string) {
130
154
  async function isChangedFile(filePath: string) {
131
155
  const result = await cache.getAndUpdateCache(filePath);
132
- // eslint-disable-next-line @typescript-eslint/no-throw-literal
156
+ // oxlint-disable-next-line typescript/only-throw-error
133
157
  if (result.error) throw result.error;
134
158
  return result.changed;
135
159
  }
136
160
 
137
161
  // Locator#load cannot be called concurrently. Therefore, it takes a lock and waits.
138
- await lock.acquireAsync();
162
+ await lock.acquire();
139
163
 
140
164
  try {
141
165
  const _isGeneratedFilesExist = await isGeneratedFilesExist(
@@ -149,7 +173,7 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
149
173
  // Generate .d.ts and .d.ts.map only when the file has been updated.
150
174
  // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
151
175
  if (_isGeneratedFilesExist && !_isChangedFile) {
152
- logger.debug(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
176
+ logger.debug(styleText('gray', `${relative(cwd, filePath)} (skipped)`));
153
177
  return;
154
178
  }
155
179
 
@@ -166,7 +190,7 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
166
190
  outDir: options.outDir,
167
191
  cwd,
168
192
  });
169
- logger.info(chalk.green(`${relative(cwd, filePath)} (generated)`));
193
+ logger.info(styleText('green', `${relative(cwd, filePath)} (generated)`));
170
194
 
171
195
  await cache.reconcile(); // Update cache for the file
172
196
  } finally {
@@ -175,13 +199,13 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
175
199
  }
176
200
 
177
201
  async function processAllFiles() {
178
- const filePaths = (await glob(options.pattern, { dot: true, cwd }))
202
+ const filePaths = (await Array.fromAsync(glob(options.pattern, { cwd })))
179
203
  // convert relative path to absolute path
180
204
  .map((file) => resolve(cwd, file));
181
205
 
182
206
  const errors: unknown[] = [];
183
207
  for (const filePath of filePaths) {
184
- // eslint-disable-next-line no-await-in-loop
208
+ // oxlint-disable-next-line no-await-in-loop
185
209
  await processFile(filePath).catch((e) => errors.push(e));
186
210
  }
187
211
 
@@ -197,14 +221,21 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
197
221
  } else {
198
222
  // First, watch files.
199
223
  logger.info(`Watch ${options.pattern}...`);
200
- const watcher = chokidar.watch([options.pattern.replace(/\\/gu, '/')], { cwd });
201
- watcher.on('all', (eventName, relativeFilePath) => {
202
- const filePath = resolve(cwd, relativeFilePath);
203
-
204
- // There is a bug in chokidar that matches symlinks that do not match the pattern.
205
- // ref: https://github.com/paulmillr/chokidar/issues/967
206
- if (isExternalFile(filePath)) return;
207
-
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) => {
208
239
  if (eventName !== 'add' && eventName !== 'change') return;
209
240
 
210
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';
@@ -10,7 +10,7 @@ import { sleepSync } from '../util.js';
10
10
  export const FIXTURE_DIR_PATH = resolve(
11
11
  realpathSync(tmpdir()),
12
12
  'happy-css-modules/fixtures',
13
- process.env['JEST_WORKER_ID']!,
13
+ process.env['VITEST_POOL_ID']!,
14
14
  );
15
15
 
16
16
  export function createRoot(code: string, from?: string): Root {
@@ -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
+ }