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.
- package/LICENSE.txt +23 -0
- package/README.md +307 -2
- package/bin/hcm.js +1 -1
- package/dist/cli.d.ts +0 -4
- package/dist/cli.js +111 -118
- package/dist/cli.js.map +1 -1
- package/dist/emitter/dts.js +7 -8
- package/dist/emitter/dts.js.map +1 -1
- package/dist/emitter/dts.test.js +12 -12
- package/dist/emitter/file-system.js +2 -2
- package/dist/emitter/file-system.js.map +1 -1
- package/dist/emitter/file-system.test.js +1 -1
- package/dist/emitter/file-system.test.js.map +1 -1
- package/dist/emitter/index.js +1 -1
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/index.test.js +2 -4
- package/dist/emitter/index.test.js.map +1 -1
- package/dist/emitter/source-map.js +1 -1
- package/dist/emitter/source-map.js.map +1 -1
- package/dist/emitter/source-map.test.js +1 -1
- package/dist/emitter/source-map.test.js.map +1 -1
- package/dist/integration-test/go-to-definition.test.js +403 -105
- package/dist/integration-test/go-to-definition.test.js.map +1 -1
- package/dist/locator/index.js +10 -8
- package/dist/locator/index.js.map +1 -1
- package/dist/locator/index.test.js +304 -171
- package/dist/locator/index.test.js.map +1 -1
- package/dist/locator/postcss.js +2 -3
- package/dist/locator/postcss.js.map +1 -1
- package/dist/locator/postcss.test.js +317 -73
- package/dist/locator/postcss.test.js.map +1 -1
- package/dist/logger.js +7 -6
- package/dist/logger.js.map +1 -1
- package/dist/regression-test/issue-168.test.js +3 -5
- package/dist/regression-test/issue-168.test.js.map +1 -1
- package/dist/resolver/index.d.ts +1 -1
- package/dist/resolver/index.js +3 -3
- package/dist/resolver/index.js.map +1 -1
- package/dist/resolver/node-resolver.js +1 -1
- package/dist/resolver/node-resolver.js.map +1 -1
- package/dist/resolver/relative-resolver.js +1 -1
- package/dist/resolver/relative-resolver.js.map +1 -1
- package/dist/resolver/webpack-resolver.d.ts +1 -1
- package/dist/resolver/webpack-resolver.js +4 -4
- package/dist/resolver/webpack-resolver.js.map +1 -1
- package/dist/runner.js +52 -21
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +59 -43
- package/dist/runner.test.js.map +1 -1
- package/dist/test-util/line-column.d.ts +9 -0
- package/dist/test-util/line-column.js +16 -0
- package/dist/test-util/line-column.js.map +1 -0
- package/dist/test-util/line-column.test.d.ts +1 -0
- package/dist/test-util/line-column.test.js +21 -0
- package/dist/test-util/line-column.test.js.map +1 -0
- package/dist/test-util/tsserver.js +11 -12
- package/dist/test-util/tsserver.js.map +1 -1
- package/dist/test-util/util.d.ts +6 -0
- package/dist/test-util/util.js +23 -6
- package/dist/test-util/util.js.map +1 -1
- package/dist/transformer/index.d.ts +0 -1
- package/dist/transformer/index.js +1 -1
- package/dist/transformer/index.js.map +1 -1
- package/dist/transformer/index.test.js +17 -17
- package/dist/transformer/index.test.js.map +1 -1
- package/dist/transformer/less-transformer.js +4 -4
- package/dist/transformer/less-transformer.js.map +1 -1
- package/dist/transformer/less-transformer.test.js +76 -51
- package/dist/transformer/less-transformer.test.js.map +1 -1
- package/dist/transformer/postcss-transformer.test.js +58 -54
- package/dist/transformer/postcss-transformer.test.js.map +1 -1
- package/dist/transformer/scss-transformer.js +11 -30
- package/dist/transformer/scss-transformer.js.map +1 -1
- package/dist/transformer/scss-transformer.test.js +106 -54
- package/dist/transformer/scss-transformer.test.js.map +1 -1
- package/dist/util.js +6 -8
- package/dist/util.js.map +1 -1
- package/dist/util.test.js +2 -2
- package/dist/util.test.js.map +1 -1
- package/package.json +59 -35
- package/src/__snapshots__/runner.test.ts.snap +1 -1
- package/src/cli.ts +119 -117
- package/src/emitter/dts.test.ts +12 -12
- package/src/emitter/dts.ts +27 -28
- package/src/emitter/file-system.test.ts +1 -1
- package/src/emitter/file-system.ts +2 -2
- package/src/emitter/index.test.ts +2 -4
- package/src/emitter/index.ts +1 -1
- package/src/emitter/source-map.test.ts +1 -1
- package/src/emitter/source-map.ts +1 -1
- package/src/integration-test/go-to-definition.test.ts +405 -105
- package/src/locator/index.test.ts +304 -171
- package/src/locator/index.ts +6 -6
- package/src/locator/postcss.test.ts +317 -73
- package/src/locator/postcss.ts +2 -3
- package/src/logger.ts +6 -6
- package/src/regression-test/issue-168.test.ts +3 -5
- package/src/resolver/index.ts +4 -4
- package/src/resolver/node-resolver.ts +1 -1
- package/src/resolver/relative-resolver.ts +1 -1
- package/src/resolver/webpack-resolver.ts +5 -5
- package/src/runner.test.ts +66 -43
- package/src/runner.ts +53 -22
- package/src/test-util/line-column.test.ts +21 -0
- package/src/test-util/line-column.ts +15 -0
- package/src/test-util/tsserver.ts +11 -12
- package/src/test-util/util.ts +24 -6
- package/src/transformer/index.test.ts +17 -17
- package/src/transformer/index.ts +1 -1
- package/src/transformer/less-transformer.test.ts +74 -47
- package/src/transformer/less-transformer.ts +1 -3
- package/src/transformer/postcss-transformer.test.ts +58 -54
- package/src/transformer/scss-transformer.test.ts +108 -54
- package/src/transformer/scss-transformer.ts +13 -30
- package/src/util.test.ts +2 -2
- package/src/util.ts +6 -8
- package/dist/test-util/jest/resolver.cjs +0 -31
- package/dist/test-util/jest/resolver.cjs.map +0 -1
- package/dist/test-util/jest/resolver.d.cts +0 -16
- package/src/test-util/jest/resolver.cjs +0 -31
package/src/resolver/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
46
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
47
47
|
const resolved = await resolver(specifier, options);
|
|
48
48
|
if (resolved !== false) {
|
|
49
|
-
//
|
|
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
|
|
53
|
+
} catch {
|
|
54
54
|
// noop
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
116
|
+
} catch {
|
|
117
117
|
// noop
|
|
118
118
|
}
|
|
119
119
|
}
|
package/src/runner.test.ts
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
const
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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).
|
|
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(
|
|
234
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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 {
|
|
2
|
-
import
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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(
|
|
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(
|
|
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, {
|
|
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
|
-
//
|
|
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(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
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 =
|
|
80
|
-
const endIndex =
|
|
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 =
|
|
112
|
-
const endIndex =
|
|
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')
|
|
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) => ({
|
package/src/test-util/util.ts
CHANGED
|
@@ -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['
|
|
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
|
|
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
|
+
}
|