happy-css-modules 0.3.0 → 0.5.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/README.md +95 -48
- package/dist/cli.js +42 -4
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +21 -0
- package/dist/cli.test.js.map +1 -1
- package/dist/emitter/dts.js +9 -1
- package/dist/emitter/dts.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-test/go-to-definition.test.js +9 -8
- package/dist/integration-test/go-to-definition.test.js.map +1 -1
- package/dist/loader/index.test.js +79 -1
- package/dist/loader/index.test.js.map +1 -1
- package/dist/loader/postcss.d.ts +5 -1
- package/dist/loader/postcss.js +26 -30
- package/dist/loader/postcss.js.map +1 -1
- package/dist/resolver/index.d.ts +3 -1
- package/dist/resolver/index.js +17 -14
- package/dist/resolver/index.js.map +1 -1
- package/dist/resolver/webpack-resolver.d.ts +23 -1
- package/dist/resolver/webpack-resolver.js +82 -59
- package/dist/resolver/webpack-resolver.js.map +1 -1
- package/dist/resolver/webpack-resolver.test.js +70 -7
- package/dist/resolver/webpack-resolver.test.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +30 -12
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +62 -11
- package/dist/runner.test.js.map +1 -1
- package/dist/test/tsserver.d.ts +10 -6
- package/dist/test/tsserver.js +94 -85
- package/dist/test/tsserver.js.map +1 -1
- package/dist/test/util.d.ts +1 -1
- package/dist/test/util.js +15 -8
- package/dist/test/util.js.map +1 -1
- package/dist/transformer/index.d.ts +3 -1
- package/dist/transformer/index.js +15 -9
- package/dist/transformer/index.js.map +1 -1
- package/dist/transformer/index.test.d.ts +1 -0
- package/dist/transformer/index.test.js +66 -0
- package/dist/transformer/index.test.js.map +1 -0
- package/dist/transformer/less-transformer.js +1 -2
- package/dist/transformer/less-transformer.js.map +1 -1
- package/dist/transformer/less-transformer.test.js +6 -6
- package/dist/transformer/postcss-transformer.d.ts +12 -0
- package/dist/transformer/postcss-transformer.js +32 -0
- package/dist/transformer/postcss-transformer.js.map +1 -0
- package/dist/transformer/postcss-transformer.test.d.ts +1 -0
- package/dist/transformer/postcss-transformer.test.js +176 -0
- package/dist/transformer/postcss-transformer.test.js.map +1 -0
- package/dist/transformer/scss-transformer.js +19 -64
- package/dist/transformer/scss-transformer.js.map +1 -1
- package/dist/transformer/scss-transformer.test.js +8 -8
- package/package.json +6 -3
- package/src/cli.test.ts +29 -0
- package/src/cli.ts +41 -4
- package/src/emitter/dts.ts +10 -1
- package/src/index.ts +8 -2
- package/src/integration-test/go-to-definition.test.ts +10 -8
- package/src/loader/index.test.ts +79 -1
- package/src/loader/postcss.ts +42 -40
- package/src/resolver/index.ts +21 -12
- package/src/resolver/webpack-resolver.test.ts +100 -8
- package/src/resolver/webpack-resolver.ts +111 -57
- package/src/runner.test.ts +67 -11
- package/src/runner.ts +56 -13
- package/src/test/tsserver.ts +106 -129
- package/src/test/util.ts +15 -9
- package/src/transformer/index.test.ts +71 -0
- package/src/transformer/index.ts +18 -8
- package/src/transformer/less-transformer.test.ts +6 -6
- package/src/transformer/less-transformer.ts +1 -2
- package/src/transformer/postcss-transformer.test.ts +188 -0
- package/src/transformer/postcss-transformer.ts +57 -0
- package/src/transformer/scss-transformer.test.ts +8 -8
- package/src/transformer/scss-transformer.ts +25 -78
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
1
|
import dedent from 'dedent';
|
|
3
2
|
import { run } from '../runner.js';
|
|
4
|
-
import {
|
|
3
|
+
import { createTSServer } from '../test/tsserver.js';
|
|
5
4
|
import { createFixtures, getFixturePath } from '../test/util.js';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
jest.setTimeout(60 * 1000); // 60s
|
|
6
|
+
const server = await createTSServer();
|
|
9
7
|
|
|
10
8
|
const defaultOptions = {
|
|
11
9
|
pattern: 'test/**/*.{css,scss}',
|
|
@@ -14,6 +12,10 @@ const defaultOptions = {
|
|
|
14
12
|
cwd: getFixturePath('/'),
|
|
15
13
|
};
|
|
16
14
|
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await server.exit();
|
|
17
|
+
});
|
|
18
|
+
|
|
17
19
|
test('basic', async () => {
|
|
18
20
|
createFixtures({
|
|
19
21
|
'/test/1.css': dedent`
|
|
@@ -44,7 +46,7 @@ test('basic', async () => {
|
|
|
44
46
|
`,
|
|
45
47
|
});
|
|
46
48
|
await run({ ...defaultOptions });
|
|
47
|
-
const results = await getMultipleIdentifierDefinitions(getFixturePath(`/test/1.css`), [
|
|
49
|
+
const results = await server.getMultipleIdentifierDefinitions(getFixturePath(`/test/1.css`), [
|
|
48
50
|
'basic',
|
|
49
51
|
'cascading',
|
|
50
52
|
'pseudo_class_1',
|
|
@@ -223,7 +225,7 @@ test('basic', async () => {
|
|
|
223
225
|
},
|
|
224
226
|
]
|
|
225
227
|
`);
|
|
226
|
-
const moduleDefinitions = await getModuleDefinitions(getFixturePath('/test/1.css'));
|
|
228
|
+
const moduleDefinitions = await server.getModuleDefinitions(getFixturePath('/test/1.css'));
|
|
227
229
|
expect(moduleDefinitions).toMatchInlineSnapshot(`
|
|
228
230
|
[
|
|
229
231
|
{ file: "<fixtures>/test/1.css", text: "", start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 } },
|
|
@@ -248,7 +250,7 @@ test('imported tokens', async () => {
|
|
|
248
250
|
`,
|
|
249
251
|
});
|
|
250
252
|
await run({ ...defaultOptions });
|
|
251
|
-
const results = await getMultipleIdentifierDefinitions(getFixturePath(`/test/1.css`), ['a', 'b', 'c', 'd']);
|
|
253
|
+
const results = await server.getMultipleIdentifierDefinitions(getFixturePath(`/test/1.css`), ['a', 'b', 'c', 'd']);
|
|
252
254
|
expect(results).toMatchInlineSnapshot(`
|
|
253
255
|
[
|
|
254
256
|
{
|
|
@@ -307,7 +309,7 @@ test('with transformer', async () => {
|
|
|
307
309
|
`,
|
|
308
310
|
});
|
|
309
311
|
await run({ ...defaultOptions });
|
|
310
|
-
const results = await getMultipleIdentifierDefinitions(getFixturePath(`/test/1.scss`), [
|
|
312
|
+
const results = await server.getMultipleIdentifierDefinitions(getFixturePath(`/test/1.scss`), [
|
|
311
313
|
'basic',
|
|
312
314
|
'nesting',
|
|
313
315
|
'nesting_1',
|
package/src/loader/index.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs, { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { jest } from '@jest/globals';
|
|
3
4
|
import dedent from 'dedent';
|
|
5
|
+
import { createDefaultTransformer } from '../index.js';
|
|
4
6
|
import { createFixtures, FIXTURE_DIR_PATH, getFixturePath } from '../test/util.js';
|
|
5
7
|
import { sleepSync } from '../util.js';
|
|
6
8
|
|
|
@@ -311,7 +313,83 @@ test('throws error the composition of non-existent file', async () => {
|
|
|
311
313
|
}).rejects.toThrowError(`Could not resolve './2.css' in '<fixtures>/test/1.css'`);
|
|
312
314
|
});
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
describe('supports sourcemap', () => {
|
|
317
|
+
test('restores original locations from sourcemap', async () => {
|
|
318
|
+
const transformer = createDefaultTransformer();
|
|
319
|
+
const loader = new Loader({ transformer });
|
|
320
|
+
createFixtures({
|
|
321
|
+
'/test/1.scss': dedent`
|
|
322
|
+
.nesting {
|
|
323
|
+
dummy: '';
|
|
324
|
+
.nesting_child {
|
|
325
|
+
dummy: '';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
`,
|
|
329
|
+
});
|
|
330
|
+
const result = await loader.load(getFixturePath('/test/1.scss'));
|
|
331
|
+
expect(result).toMatchInlineSnapshot(`
|
|
332
|
+
{
|
|
333
|
+
dependencies: [],
|
|
334
|
+
tokens: [
|
|
335
|
+
{
|
|
336
|
+
name: "nesting",
|
|
337
|
+
originalLocations: [
|
|
338
|
+
{ filePath: "<fixtures>/test/1.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 8 } },
|
|
339
|
+
{ filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 3 }, end: { line: 3, column: 10 } },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "nesting_child",
|
|
344
|
+
originalLocations: [
|
|
345
|
+
{ filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 3 }, end: { line: 3, column: 16 } },
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
}
|
|
350
|
+
`);
|
|
351
|
+
});
|
|
352
|
+
test('treats originalLocation as empty if sourcemap is broken', async () => {
|
|
353
|
+
const uuid = randomUUID();
|
|
354
|
+
createFixtures({
|
|
355
|
+
[`/${uuid}/postcss.config.js`]: dedent`
|
|
356
|
+
module.exports = {
|
|
357
|
+
plugins: [],
|
|
358
|
+
};
|
|
359
|
+
`,
|
|
360
|
+
'/test/1.css': dedent`
|
|
361
|
+
.selector_list_a_1, .selector_list_a_2 {}
|
|
362
|
+
/* In postcss, including newlines in the selector list breaks the sourcemap. */
|
|
363
|
+
.selector_list_b_1,
|
|
364
|
+
.selector_list_b_2 {}
|
|
365
|
+
`,
|
|
366
|
+
});
|
|
367
|
+
const transformer = createDefaultTransformer({ postcssConfig: getFixturePath(`/${uuid}/postcss.config.js`) });
|
|
368
|
+
const loader = new Loader({ transformer });
|
|
369
|
+
const result = await loader.load(getFixturePath('/test/1.css'));
|
|
370
|
+
expect(result).toMatchInlineSnapshot(`
|
|
371
|
+
{
|
|
372
|
+
dependencies: [],
|
|
373
|
+
tokens: [
|
|
374
|
+
{
|
|
375
|
+
name: "selector_list_a_1",
|
|
376
|
+
originalLocations: [
|
|
377
|
+
{ filePath: "<fixtures>/test/1.css", start: { line: 1, column: 1 }, end: { line: 1, column: 18 } },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "selector_list_a_2",
|
|
382
|
+
originalLocations: [
|
|
383
|
+
{ filePath: "<fixtures>/test/1.css", start: { line: 1, column: 1 }, end: { line: 1, column: 18 } },
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
{ name: "selector_list_b_1", originalLocations: [{}] },
|
|
387
|
+
{ name: "selector_list_b_2", originalLocations: [{}] },
|
|
388
|
+
],
|
|
389
|
+
}
|
|
390
|
+
`);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
315
393
|
|
|
316
394
|
test('ignores http(s) protocol file', async () => {
|
|
317
395
|
createFixtures({
|
package/src/loader/postcss.ts
CHANGED
|
@@ -11,14 +11,20 @@ export type Position = {
|
|
|
11
11
|
column: number;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
/** The location of class selector. */
|
|
15
|
-
export type Location =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
/** The original location of class selector. If the original location is not found, all fields are `undefined`. */
|
|
15
|
+
export type Location =
|
|
16
|
+
| {
|
|
17
|
+
filePath: string;
|
|
18
|
+
/** The inclusive starting position of the node's source (compatible with postcss). */
|
|
19
|
+
start: Position;
|
|
20
|
+
/** The inclusive ending position of the node's source (compatible with postcss). */
|
|
21
|
+
end: Position;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
filePath: undefined;
|
|
25
|
+
start: undefined;
|
|
26
|
+
end: undefined;
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
function removeDependenciesPlugin(): Plugin {
|
|
24
30
|
return {
|
|
@@ -80,49 +86,45 @@ export function getOriginalLocation(rule: Rule, classSelector: ClassName): Locat
|
|
|
80
86
|
if (rule.source.end === undefined || classSelector.source.end === undefined) throw new Error('Node#end is undefined');
|
|
81
87
|
if (rule.source.input.file === undefined) throw new Error('Node#input.file is undefined');
|
|
82
88
|
|
|
83
|
-
const
|
|
89
|
+
const classSelectorStartPosition = {
|
|
84
90
|
// The line is 1-based.
|
|
85
91
|
line: rule.source.start.line + (classSelector.source.start.line - 1),
|
|
86
92
|
// The column is 1-based.
|
|
87
93
|
column: rule.source.start.column + (classSelector.source.start.column - 1),
|
|
88
94
|
};
|
|
89
|
-
const
|
|
90
|
-
line:
|
|
95
|
+
const classSelectorEndPosition = {
|
|
96
|
+
line: classSelectorStartPosition.line,
|
|
91
97
|
// The column is inclusive.
|
|
92
|
-
column:
|
|
98
|
+
column: classSelectorStartPosition.column + classSelector.value.length,
|
|
93
99
|
};
|
|
94
|
-
|
|
100
|
+
const classSelectorLocation = {
|
|
95
101
|
filePath: rule.source.input.file,
|
|
96
|
-
start,
|
|
97
|
-
end,
|
|
102
|
+
start: classSelectorStartPosition,
|
|
103
|
+
end: classSelectorEndPosition,
|
|
98
104
|
};
|
|
99
105
|
|
|
100
|
-
if (rule.source.input.map)
|
|
101
|
-
const origin = rule.source.input.origin(
|
|
102
|
-
location.start.line,
|
|
103
|
-
// The column of `Input#origin` is 0-based. This behavior is undocumented and probably a postcss's bug.
|
|
104
|
-
// TODO: Open PR to postcss/postcss
|
|
105
|
-
location.start.column - 1,
|
|
106
|
-
);
|
|
107
|
-
if (origin === false) throw new Error('`Input#origin` returned false');
|
|
108
|
-
if (origin.file === undefined) throw new Error('`FilePosition#file` is undefined');
|
|
109
|
-
|
|
110
|
-
location = {
|
|
111
|
-
filePath: origin.file,
|
|
112
|
-
start: {
|
|
113
|
-
line: origin.line,
|
|
114
|
-
// The column of `Input#origin` is 0-based.
|
|
115
|
-
column: origin.column + 1,
|
|
116
|
-
},
|
|
117
|
-
end: {
|
|
118
|
-
line: origin.line,
|
|
119
|
-
// The column of `Input#origin` is 0-based. Also, the column of happy-css-modules is inclusive.
|
|
120
|
-
column: origin.column + 1 + (classSelector.value.length - 1),
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
106
|
+
if (!rule.source.input.map) return classSelectorLocation;
|
|
124
107
|
|
|
125
|
-
|
|
108
|
+
const classSelectorOrigin = rule.source.input.origin(
|
|
109
|
+
classSelectorLocation.start.line,
|
|
110
|
+
// The column of `Input#origin` is 0-based. This behavior is undocumented and probably a postcss's bug.
|
|
111
|
+
// TODO: Open PR to postcss/postcss
|
|
112
|
+
classSelectorLocation.start.column - 1,
|
|
113
|
+
);
|
|
114
|
+
if (classSelectorOrigin === false || classSelectorOrigin.file === undefined) {
|
|
115
|
+
return { filePath: undefined, start: undefined, end: undefined };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
filePath: classSelectorOrigin.file,
|
|
119
|
+
start: {
|
|
120
|
+
line: classSelectorOrigin.line,
|
|
121
|
+
column: classSelectorOrigin.column + 1,
|
|
122
|
+
},
|
|
123
|
+
end: {
|
|
124
|
+
line: classSelectorOrigin.line,
|
|
125
|
+
column: classSelectorOrigin.column + classSelector.value.length + 1,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
function isAtRuleNode(node: Node): node is AtRule {
|
package/src/resolver/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { exists } from '../util.js';
|
|
2
2
|
import { createNodeResolver } from './node-resolver.js';
|
|
3
3
|
import { createRelativeResolver } from './relative-resolver.js';
|
|
4
|
+
import type { WebpackResolverOptions } from './webpack-resolver.js';
|
|
4
5
|
import { createWebpackResolver } from './webpack-resolver.js';
|
|
5
6
|
|
|
6
7
|
export type ResolverOptions = {
|
|
@@ -13,6 +14,8 @@ export type ResolverOptions = {
|
|
|
13
14
|
* */
|
|
14
15
|
export type Resolver = (specifier: string, options: ResolverOptions) => string | false | Promise<string | false>;
|
|
15
16
|
|
|
17
|
+
export type DefaultResolverOptions = WebpackResolverOptions;
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* The Default resolver.
|
|
18
21
|
*
|
|
@@ -24,25 +27,31 @@ export type Resolver = (specifier: string, options: ResolverOptions) => string |
|
|
|
24
27
|
* @param options The options to resolve
|
|
25
28
|
* @returns The resolved path (absolute). `false` means to skip resolving.
|
|
26
29
|
*/
|
|
27
|
-
export const createDefaultResolver: () => Resolver = (
|
|
30
|
+
export const createDefaultResolver: (defaultResolverOptions?: DefaultResolverOptions | undefined) => Resolver = (
|
|
31
|
+
defaultResolverOptions,
|
|
32
|
+
) => {
|
|
28
33
|
const relativeResolver = createRelativeResolver();
|
|
29
34
|
const nodeResolver = createNodeResolver();
|
|
30
|
-
const webpackResolver = createWebpackResolver();
|
|
35
|
+
const webpackResolver = createWebpackResolver(defaultResolverOptions);
|
|
31
36
|
|
|
32
37
|
// In less-loader, `relativeResolver` has priority over `webpackResolver`.
|
|
33
38
|
// happy-css-modules follows suit.
|
|
39
|
+
// ref: https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L588-L596
|
|
34
40
|
// ref: https://github.com/webpack-contrib/less-loader/tree/454e187f58046356c3d383d67fda763db8bfc528#webpack-resolver
|
|
35
41
|
const resolvers = [relativeResolver, nodeResolver, webpackResolver];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
+
|
|
43
|
+
return async (specifier, options) => {
|
|
44
|
+
for (const resolver of resolvers) {
|
|
45
|
+
try {
|
|
46
|
+
const resolved = await resolver(specifier, options);
|
|
47
|
+
if (resolved !== false) {
|
|
48
|
+
const isExists = await exists(resolved);
|
|
49
|
+
if (isExists) return resolved;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// noop
|
|
42
53
|
}
|
|
43
|
-
} catch (e) {
|
|
44
|
-
// noop
|
|
45
54
|
}
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
48
57
|
};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createFixtures, getFixturePath } from '../test/util.js';
|
|
2
2
|
import { createWebpackResolver } from './webpack-resolver.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
test('resolves specifier with css-loader mechanism', async () => {
|
|
5
|
+
const webpackResolver = createWebpackResolver({
|
|
6
|
+
cwd: getFixturePath('/'),
|
|
7
|
+
webpackResolveAlias: {
|
|
8
|
+
'@relative': 'test/alias-relative',
|
|
9
|
+
'@absolute': getFixturePath('/test/alias-absolute'),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
const request = getFixturePath('/test/1.css');
|
|
8
13
|
createFixtures({
|
|
9
14
|
'/node_modules/package-1/index.css': `.a {}`,
|
|
10
15
|
'/node_modules/package-2/index.css': `.a {}`,
|
|
@@ -13,8 +18,8 @@ test('resolves specifier with webpack mechanism', async () => {
|
|
|
13
18
|
'/node_modules/package-4/style.css': `.a {}`,
|
|
14
19
|
'/node_modules/@scoped/package-5/index.css': `.a {}`,
|
|
15
20
|
'/node_modules/package-6/index.css': `.a {}`,
|
|
16
|
-
'/
|
|
17
|
-
'/
|
|
21
|
+
'/test/alias-relative/alias.css': `.a {}`,
|
|
22
|
+
'/test/alias-absolute/alias.css': `.a {}`,
|
|
18
23
|
});
|
|
19
24
|
expect(await webpackResolver('~package-1/index.css', { request })).toBe(
|
|
20
25
|
getFixturePath('/node_modules/package-1/index.css'),
|
|
@@ -28,6 +33,93 @@ test('resolves specifier with webpack mechanism', async () => {
|
|
|
28
33
|
expect(await webpackResolver('package-6/index.css', { request })).toBe(
|
|
29
34
|
getFixturePath('/node_modules/package-6/index.css'),
|
|
30
35
|
);
|
|
31
|
-
expect(await webpackResolver('
|
|
32
|
-
|
|
36
|
+
expect(await webpackResolver('@relative/alias.css', { request })).toBe(
|
|
37
|
+
getFixturePath('/test/alias-relative/alias.css'),
|
|
38
|
+
);
|
|
39
|
+
expect(await webpackResolver('@absolute/alias.css', { request })).toBe(
|
|
40
|
+
getFixturePath('/test/alias-absolute/alias.css'),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('resolves specifier with sass-loader mechanism', async () => {
|
|
45
|
+
const webpackResolver = createWebpackResolver({
|
|
46
|
+
cwd: getFixturePath('/'),
|
|
47
|
+
sassLoadPaths: ['test/load-paths-relative', getFixturePath('/test/load-paths-absolute')],
|
|
48
|
+
webpackResolveAlias: {
|
|
49
|
+
'@relative': 'test/alias-relative',
|
|
50
|
+
'@absolute': getFixturePath('/test/alias-absolute'),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const request = getFixturePath('/test/1.scss');
|
|
54
|
+
createFixtures({
|
|
55
|
+
'/node_modules/package-1/index.scss': `.a {}`,
|
|
56
|
+
'/test/load-paths-relative/load-paths-relative.scss': `.a {}`,
|
|
57
|
+
'/test/load-paths-absolute/load-paths-absolute.scss': `.a {}`,
|
|
58
|
+
'/test/_partial-import.scss': `.a {}`,
|
|
59
|
+
'/test/alias-relative/alias.scss': `.a {}`,
|
|
60
|
+
'/test/alias-absolute/alias.scss': `.a {}`,
|
|
61
|
+
});
|
|
62
|
+
expect(await webpackResolver('~package-1/index.scss', { request })).toBe(
|
|
63
|
+
getFixturePath('/node_modules/package-1/index.scss'),
|
|
64
|
+
);
|
|
65
|
+
expect(await webpackResolver('~package-1', { request })).toBe(getFixturePath('/node_modules/package-1/index.scss'));
|
|
66
|
+
// ref: https://github.com/webpack-contrib/sass-loader/blob/bed9fb5799a90020d43f705ea405f85b368621d7/test/scss/import-include-paths.scss#L1
|
|
67
|
+
expect(await webpackResolver('load-paths-relative', { request })).toBe(
|
|
68
|
+
getFixturePath('/test/load-paths-relative/load-paths-relative.scss'),
|
|
69
|
+
);
|
|
70
|
+
expect(await webpackResolver('load-paths-absolute', { request })).toBe(
|
|
71
|
+
getFixturePath('/test/load-paths-absolute/load-paths-absolute.scss'),
|
|
72
|
+
);
|
|
73
|
+
// https://sass-lang.com/documentation/at-rules/import#partials
|
|
74
|
+
// https://github.com/webpack-contrib/sass-loader/blob/0e9494074f69a6b6d47efea6c083a02a31a5ae84/test/sass/import-with-underscore.sass
|
|
75
|
+
expect(await webpackResolver('partial-import', { request: getFixturePath('/test/1.scss') })).toBe(
|
|
76
|
+
getFixturePath('/test/_partial-import.scss'),
|
|
77
|
+
);
|
|
78
|
+
expect(await webpackResolver('test/partial-import', { request: getFixturePath('/test') })).toBe(
|
|
79
|
+
getFixturePath('/test/_partial-import.scss'),
|
|
80
|
+
);
|
|
81
|
+
expect(await webpackResolver('@relative/alias.scss', { request })).toBe(
|
|
82
|
+
getFixturePath('/test/alias-relative/alias.scss'),
|
|
83
|
+
);
|
|
84
|
+
expect(await webpackResolver('@absolute/alias.scss', { request })).toBe(
|
|
85
|
+
getFixturePath('/test/alias-absolute/alias.scss'),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('resolves specifier with less-loader mechanism', async () => {
|
|
90
|
+
const webpackResolver = createWebpackResolver({
|
|
91
|
+
cwd: getFixturePath('/'),
|
|
92
|
+
lessIncludePaths: ['test/include-paths-relative', getFixturePath('/test/include-paths-absolute')],
|
|
93
|
+
webpackResolveAlias: {
|
|
94
|
+
'@relative': 'test/alias-relative',
|
|
95
|
+
'@absolute': getFixturePath('/test/alias-absolute'),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const request = getFixturePath('/test/1.less');
|
|
99
|
+
createFixtures({
|
|
100
|
+
'/node_modules/package-1/index.less': `.a {}`,
|
|
101
|
+
'/test/include-paths-relative/include-paths-relative.less': `.a {}`,
|
|
102
|
+
'/test/include-paths-absolute/include-paths-absolute.less': `.a {}`,
|
|
103
|
+
'/test/alias-relative/alias.less': `.a {}`,
|
|
104
|
+
'/test/alias-absolute/alias.less': `.a {}`,
|
|
105
|
+
});
|
|
106
|
+
expect(await webpackResolver('~package-1/index.less', { request })).toBe(
|
|
107
|
+
getFixturePath('/node_modules/package-1/index.less'),
|
|
108
|
+
);
|
|
109
|
+
expect(await webpackResolver('~package-1', { request })).toBe(getFixturePath('/node_modules/package-1/index.less'));
|
|
110
|
+
// ref: https://github.com/webpack-contrib/less-loader/blob/81a0d27eb6d18e5dc550a60fc1007fdc77305b78/test/loader.test.js#L248-L253
|
|
111
|
+
// ref: https://github.com/webpack-contrib/less-loader/blob/393147064672ace986ec84aca21f69f0ab819a9c/test/fixtures/import-paths.less#L1
|
|
112
|
+
// ref: https://github.com/webpack-contrib/less-loader/blob/99d80bd290dae50375db6e17c5f56ec33754e258/test/helpers/getCodeFromLess.js#L47-L54
|
|
113
|
+
expect(await webpackResolver('include-paths-relative', { request })).toBe(
|
|
114
|
+
getFixturePath('/test/include-paths-relative/include-paths-relative.less'),
|
|
115
|
+
);
|
|
116
|
+
expect(await webpackResolver('include-paths-absolute', { request })).toBe(
|
|
117
|
+
getFixturePath('/test/include-paths-absolute/include-paths-absolute.less'),
|
|
118
|
+
);
|
|
119
|
+
expect(await webpackResolver('@relative/alias.less', { request })).toBe(
|
|
120
|
+
getFixturePath('/test/alias-relative/alias.less'),
|
|
121
|
+
);
|
|
122
|
+
expect(await webpackResolver('@absolute/alias.less', { request })).toBe(
|
|
123
|
+
getFixturePath('/test/alias-absolute/alias.less'),
|
|
124
|
+
);
|
|
33
125
|
});
|
|
@@ -1,71 +1,125 @@
|
|
|
1
|
-
import { dirname } from 'path';
|
|
1
|
+
import { basename, dirname, join, resolve } from 'path';
|
|
2
2
|
import enhancedResolve from 'enhanced-resolve';
|
|
3
3
|
import { exists } from '../util.js';
|
|
4
4
|
import type { Resolver } from './index.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
export type WebpackResolverOptions = {
|
|
7
|
+
/** Working directory path. */
|
|
8
|
+
cwd?: string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The option compatible with sass's `--load-path`. It is an array of relative or absolute paths.
|
|
11
|
+
* @example ['src/styles']
|
|
12
|
+
* @example ['/home/user/repository/src/styles']
|
|
13
|
+
*/
|
|
14
|
+
sassLoadPaths?: string[] | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The option compatible with less's `--include-path`. It is an array of relative or absolute paths.
|
|
17
|
+
* @example ['src/styles']
|
|
18
|
+
* @example ['/home/user/repository/src/styles']
|
|
19
|
+
*/
|
|
20
|
+
lessIncludePaths?: string[] | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The option compatible with webpack's `resolve.alias`. It is an object consisting of a pair of alias names and relative or absolute paths.
|
|
23
|
+
* @example { style: 'src/styles', '@': 'src' }
|
|
24
|
+
* @example { style: '/home/user/repository/src/styles', '@': '/home/user/repository/src' }
|
|
25
|
+
*/
|
|
26
|
+
webpackResolveAlias?: Record<string, string> | undefined;
|
|
27
|
+
};
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// TODO: Support `resolve.alias` for Node.js API
|
|
30
|
+
export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOptions | undefined) => Resolver = (
|
|
31
|
+
webpackResolverOptions,
|
|
32
|
+
) => {
|
|
33
|
+
const cwd = webpackResolverOptions?.cwd ?? process.cwd();
|
|
34
|
+
const sassLoadPaths = webpackResolverOptions?.sassLoadPaths?.map((path) => resolve(cwd, path));
|
|
35
|
+
const lessIncludePaths = webpackResolverOptions?.lessIncludePaths?.map((path) => resolve(cwd, path));
|
|
36
|
+
const webpackResolveAlias = webpackResolverOptions?.webpackResolveAlias
|
|
37
|
+
? Object.fromEntries(
|
|
38
|
+
Object.entries(webpackResolverOptions?.webpackResolveAlias).map(([key, value]) => [key, resolve(cwd, value)]),
|
|
39
|
+
)
|
|
40
|
+
: undefined;
|
|
41
|
+
/**
|
|
42
|
+
* A resolver compatible with css-loader.
|
|
43
|
+
*
|
|
44
|
+
* @see https://github.com/webpack-contrib/css-loader/blob/897e7dd250ccdb0d31e6c66d4cd0d009f2022a85/src/plugins/postcss-import-parser.js#L228-L235
|
|
45
|
+
*/
|
|
46
|
+
const cssLoaderResolver = enhancedResolve.create.sync({
|
|
47
|
+
dependencyType: 'css',
|
|
48
|
+
conditionNames: ['style'],
|
|
49
|
+
// We are not sure how "..." affects behavior...
|
|
50
|
+
mainFields: ['css', 'style', 'main', '...'],
|
|
51
|
+
mainFiles: ['index', '...'],
|
|
52
|
+
extensions: ['.css', '...'],
|
|
53
|
+
preferRelative: true,
|
|
54
|
+
alias: webpackResolveAlias,
|
|
55
|
+
});
|
|
35
56
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
/**
|
|
58
|
+
* A resolver compatible with sass-loader.
|
|
59
|
+
*
|
|
60
|
+
* @see https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L531-L539
|
|
61
|
+
*/
|
|
62
|
+
const sassLoaderResolver = enhancedResolve.create.sync({
|
|
63
|
+
dependencyType: 'sass',
|
|
64
|
+
conditionNames: ['sass', 'style'],
|
|
65
|
+
mainFields: ['sass', 'style', 'main', '...'],
|
|
66
|
+
mainFiles: ['_index', 'index', '...'],
|
|
67
|
+
extensions: ['.sass', '.scss', '.css'],
|
|
68
|
+
restrictions: [/\.((sa|sc|c)ss)$/i],
|
|
69
|
+
preferRelative: true,
|
|
70
|
+
alias: webpackResolveAlias,
|
|
71
|
+
modules: ['node_modules', ...(sassLoadPaths ?? [])],
|
|
72
|
+
});
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
/**
|
|
75
|
+
* A resolver compatible with less-loader.
|
|
76
|
+
*
|
|
77
|
+
* @see https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L35-L42
|
|
78
|
+
*/
|
|
79
|
+
const lessLoaderResolver = enhancedResolve.create.sync({
|
|
80
|
+
dependencyType: 'less',
|
|
81
|
+
conditionNames: ['less', 'style'],
|
|
82
|
+
mainFields: ['less', 'style', 'main', '...'],
|
|
83
|
+
mainFiles: ['index', '...'],
|
|
84
|
+
extensions: ['.less', '.css'],
|
|
85
|
+
preferRelative: true,
|
|
86
|
+
alias: webpackResolveAlias,
|
|
87
|
+
modules: ['node_modules', ...(lessIncludePaths ?? [])],
|
|
88
|
+
});
|
|
55
89
|
|
|
56
90
|
// NOTE: In theory, `sassLoaderResolver` should only be used when the resolver is called from `sassTransformer`.
|
|
57
91
|
// However, we do not implement such behavior because it is cumbersome. If someone wants it, we will implement it.
|
|
58
92
|
const resolvers = [cssLoaderResolver, sassLoaderResolver, lessLoaderResolver];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
|
|
94
|
+
return async (specifier, options) => {
|
|
95
|
+
// `~` prefix is optional.
|
|
96
|
+
// ref: https://github.com/webpack-contrib/css-loader/blob/5e6cf91fd3f0c8b5fb4b91197b98dc56abdef4bf/src/utils.js#L92-L95
|
|
97
|
+
// ref: https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L368-L370
|
|
98
|
+
// ref: https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L72-L75
|
|
99
|
+
if (specifier.startsWith('~')) specifier = specifier.slice(1);
|
|
100
|
+
|
|
101
|
+
for (const resolver of resolvers) {
|
|
102
|
+
const specifierVariants =
|
|
103
|
+
resolver === sassLoaderResolver
|
|
104
|
+
? // Support partial import for sass
|
|
105
|
+
// https://sass-lang.com/documentation/at-rules/import#partials
|
|
106
|
+
// https://github.com/webpack-contrib/sass-loader/blob/0e9494074f69a6b6d47efea6c083a02a31a5ae84/test/sass/import-with-underscore.sass
|
|
107
|
+
[join(dirname(specifier), '_' + basename(specifier)), specifier]
|
|
108
|
+
: [specifier];
|
|
109
|
+
|
|
110
|
+
for (const specifierVariant of specifierVariants) {
|
|
111
|
+
try {
|
|
112
|
+
const resolved = resolver(dirname(options.request), specifierVariant);
|
|
113
|
+
if (resolved !== false) {
|
|
114
|
+
const isExists = await exists(resolved);
|
|
115
|
+
if (isExists) return resolved;
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// noop
|
|
119
|
+
}
|
|
65
120
|
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
// noop
|
|
68
121
|
}
|
|
69
|
-
|
|
70
|
-
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
71
125
|
};
|