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.
Files changed (77) hide show
  1. package/README.md +95 -48
  2. package/dist/cli.js +42 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +21 -0
  5. package/dist/cli.test.js.map +1 -1
  6. package/dist/emitter/dts.js +9 -1
  7. package/dist/emitter/dts.js.map +1 -1
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.js +2 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/integration-test/go-to-definition.test.js +9 -8
  12. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  13. package/dist/loader/index.test.js +79 -1
  14. package/dist/loader/index.test.js.map +1 -1
  15. package/dist/loader/postcss.d.ts +5 -1
  16. package/dist/loader/postcss.js +26 -30
  17. package/dist/loader/postcss.js.map +1 -1
  18. package/dist/resolver/index.d.ts +3 -1
  19. package/dist/resolver/index.js +17 -14
  20. package/dist/resolver/index.js.map +1 -1
  21. package/dist/resolver/webpack-resolver.d.ts +23 -1
  22. package/dist/resolver/webpack-resolver.js +82 -59
  23. package/dist/resolver/webpack-resolver.js.map +1 -1
  24. package/dist/resolver/webpack-resolver.test.js +70 -7
  25. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  26. package/dist/runner.d.ts +25 -0
  27. package/dist/runner.js +30 -12
  28. package/dist/runner.js.map +1 -1
  29. package/dist/runner.test.js +62 -11
  30. package/dist/runner.test.js.map +1 -1
  31. package/dist/test/tsserver.d.ts +10 -6
  32. package/dist/test/tsserver.js +94 -85
  33. package/dist/test/tsserver.js.map +1 -1
  34. package/dist/test/util.d.ts +1 -1
  35. package/dist/test/util.js +15 -8
  36. package/dist/test/util.js.map +1 -1
  37. package/dist/transformer/index.d.ts +3 -1
  38. package/dist/transformer/index.js +15 -9
  39. package/dist/transformer/index.js.map +1 -1
  40. package/dist/transformer/index.test.d.ts +1 -0
  41. package/dist/transformer/index.test.js +66 -0
  42. package/dist/transformer/index.test.js.map +1 -0
  43. package/dist/transformer/less-transformer.js +1 -2
  44. package/dist/transformer/less-transformer.js.map +1 -1
  45. package/dist/transformer/less-transformer.test.js +6 -6
  46. package/dist/transformer/postcss-transformer.d.ts +12 -0
  47. package/dist/transformer/postcss-transformer.js +32 -0
  48. package/dist/transformer/postcss-transformer.js.map +1 -0
  49. package/dist/transformer/postcss-transformer.test.d.ts +1 -0
  50. package/dist/transformer/postcss-transformer.test.js +176 -0
  51. package/dist/transformer/postcss-transformer.test.js.map +1 -0
  52. package/dist/transformer/scss-transformer.js +19 -64
  53. package/dist/transformer/scss-transformer.js.map +1 -1
  54. package/dist/transformer/scss-transformer.test.js +8 -8
  55. package/package.json +6 -3
  56. package/src/cli.test.ts +29 -0
  57. package/src/cli.ts +41 -4
  58. package/src/emitter/dts.ts +10 -1
  59. package/src/index.ts +8 -2
  60. package/src/integration-test/go-to-definition.test.ts +10 -8
  61. package/src/loader/index.test.ts +79 -1
  62. package/src/loader/postcss.ts +42 -40
  63. package/src/resolver/index.ts +21 -12
  64. package/src/resolver/webpack-resolver.test.ts +100 -8
  65. package/src/resolver/webpack-resolver.ts +111 -57
  66. package/src/runner.test.ts +67 -11
  67. package/src/runner.ts +56 -13
  68. package/src/test/tsserver.ts +106 -129
  69. package/src/test/util.ts +15 -9
  70. package/src/transformer/index.test.ts +71 -0
  71. package/src/transformer/index.ts +18 -8
  72. package/src/transformer/less-transformer.test.ts +6 -6
  73. package/src/transformer/less-transformer.ts +1 -2
  74. package/src/transformer/postcss-transformer.test.ts +188 -0
  75. package/src/transformer/postcss-transformer.ts +57 -0
  76. package/src/transformer/scss-transformer.test.ts +8 -8
  77. 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 { getModuleDefinitions, getMultipleIdentifierDefinitions } from '../test/tsserver.js';
3
+ import { createTSServer } from '../test/tsserver.js';
5
4
  import { createFixtures, getFixturePath } from '../test/util.js';
6
5
 
7
- // It is heavy test, so increase timeout.
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',
@@ -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
- test.todo('supports sourcemap file and inline sourcemap');
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({
@@ -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
- filePath: string;
17
- /** The inclusive starting position of the node's source (compatible with postcss). */
18
- start: Position;
19
- /** The inclusive ending position of the node's source (compatible with postcss). */
20
- end: Position;
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 start = {
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 end = {
90
- line: start.line,
95
+ const classSelectorEndPosition = {
96
+ line: classSelectorStartPosition.line,
91
97
  // The column is inclusive.
92
- column: start.column + classSelector.value.length,
98
+ column: classSelectorStartPosition.column + classSelector.value.length,
93
99
  };
94
- let location = {
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
- return location;
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 {
@@ -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 = () => async (specifier, options) => {
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
- for (const resolver of resolvers) {
37
- try {
38
- const resolved = await resolver(specifier, options);
39
- if (resolved !== false) {
40
- const isExists = await exists(resolved);
41
- if (isExists) return resolved;
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
- return false;
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
- const webpackResolver = createWebpackResolver();
5
- const request = getFixturePath('/test/1.css');
6
-
7
- test('resolves specifier with webpack mechanism', async () => {
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
- '/node_modules/package-7/index.scss': `.a { dummy: ''; }`,
17
- '/node_modules/package-8/index.less': `.a { dummy: ''; }`,
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('~package-7', { request })).toBe(getFixturePath('/node_modules/package-7/index.scss'));
32
- expect(await webpackResolver('~package-8', { request })).toBe(getFixturePath('/node_modules/package-8/index.less'));
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
- * A resolver compatible with css-loader.
8
- *
9
- * @see https://github.com/webpack-contrib/css-loader/blob/897e7dd250ccdb0d31e6c66d4cd0d009f2022a85/src/plugins/postcss-import-parser.js#L228-L235
10
- */
11
- const cssLoaderResolver = enhancedResolve.create.sync({
12
- dependencyType: 'css',
13
- conditionNames: ['style'],
14
- // We are not sure how "..." affects behavior...
15
- mainFields: ['css', 'style', 'main', '...'],
16
- mainFiles: ['index', '...'],
17
- extensions: ['.css', '...'],
18
- preferRelative: true,
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
- * A resolver compatible with sass-loader.
23
- *
24
- * @see https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L531-L539
25
- */
26
- const sassLoaderResolver = enhancedResolve.create.sync({
27
- dependencyType: 'sass',
28
- conditionNames: ['sass', 'style'],
29
- mainFields: ['sass', 'style', 'main', '...'],
30
- mainFiles: ['_index', 'index', '...'],
31
- extensions: ['.sass', '.scss', '.css'],
32
- restrictions: [/\.((sa|sc|c)ss)$/i],
33
- preferRelative: true,
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
- * A resolver compatible with less-loader.
38
- *
39
- * @see https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L35-L42
40
- */
41
- const lessLoaderResolver = enhancedResolve.create.sync({
42
- dependencyType: 'less',
43
- conditionNames: ['less', 'style'],
44
- mainFields: ['less', 'style', 'main', '...'],
45
- mainFiles: ['index', '...'],
46
- extensions: ['.less', '.css'],
47
- preferRelative: true,
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
- // TODO: Support `resolve.alias` for Node.js API
51
- export const createWebpackResolver: () => Resolver = () => async (specifier, options) => {
52
- // `~` prefix is optional.
53
- // ref: https://github.com/webpack-contrib/less-loader/tree/454e187f58046356c3d383d67fda763db8bfc528#webpack-resolver
54
- if (specifier.startsWith('~')) specifier = specifier.slice(1);
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
- for (const resolver of resolvers) {
60
- try {
61
- const resolved = resolver(dirname(options.request), specifier);
62
- if (resolved !== false) {
63
- const isExists = await exists(resolved);
64
- if (isExists) return resolved;
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
- return false;
122
+
123
+ return false;
124
+ };
71
125
  };