happy-css-modules 0.2.1 → 0.4.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 (58) hide show
  1. package/README.md +15 -18
  2. package/dist/cli.js +15 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +10 -0
  5. package/dist/cli.test.js.map +1 -1
  6. package/dist/emitter/dts.js +3 -4
  7. package/dist/emitter/dts.js.map +1 -1
  8. package/dist/integration-test/go-to-definition.test.js +9 -8
  9. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  10. package/dist/loader/index.js +6 -10
  11. package/dist/loader/index.js.map +1 -1
  12. package/dist/loader/index.test.js +22 -3
  13. package/dist/loader/index.test.js.map +1 -1
  14. package/dist/resolver/index.d.ts +3 -1
  15. package/dist/resolver/index.js +17 -14
  16. package/dist/resolver/index.js.map +1 -1
  17. package/dist/resolver/webpack-resolver.d.ts +13 -1
  18. package/dist/resolver/webpack-resolver.js +73 -59
  19. package/dist/resolver/webpack-resolver.js.map +1 -1
  20. package/dist/resolver/webpack-resolver.test.js +34 -7
  21. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  22. package/dist/runner.d.ts +12 -0
  23. package/dist/runner.js +16 -14
  24. package/dist/runner.js.map +1 -1
  25. package/dist/runner.test.js +41 -11
  26. package/dist/runner.test.js.map +1 -1
  27. package/dist/test/tsserver.d.ts +10 -6
  28. package/dist/test/tsserver.js +94 -85
  29. package/dist/test/tsserver.js.map +1 -1
  30. package/dist/test/util.js +9 -12
  31. package/dist/test/util.js.map +1 -1
  32. package/dist/transformer/index.js +11 -9
  33. package/dist/transformer/index.js.map +1 -1
  34. package/dist/transformer/less-transformer.js +7 -5
  35. package/dist/transformer/less-transformer.js.map +1 -1
  36. package/dist/transformer/less-transformer.test.js +11 -6
  37. package/dist/transformer/less-transformer.test.js.map +1 -1
  38. package/dist/transformer/scss-transformer.js +1 -48
  39. package/dist/transformer/scss-transformer.js.map +1 -1
  40. package/dist/transformer/scss-transformer.test.js +11 -5
  41. package/dist/transformer/scss-transformer.test.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/cli.test.ts +14 -0
  44. package/src/cli.ts +15 -4
  45. package/src/integration-test/go-to-definition.test.ts +10 -8
  46. package/src/loader/index.test.ts +22 -3
  47. package/src/loader/index.ts +3 -4
  48. package/src/resolver/index.ts +21 -12
  49. package/src/resolver/webpack-resolver.test.ts +44 -8
  50. package/src/resolver/webpack-resolver.ts +90 -57
  51. package/src/runner.test.ts +45 -12
  52. package/src/runner.ts +29 -10
  53. package/src/test/tsserver.ts +106 -129
  54. package/src/transformer/index.ts +10 -8
  55. package/src/transformer/less-transformer.test.ts +12 -6
  56. package/src/transformer/less-transformer.ts +6 -4
  57. package/src/transformer/scss-transformer.test.ts +12 -5
  58. package/src/transformer/scss-transformer.ts +0 -51
@@ -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',
@@ -57,6 +57,7 @@ test('tracks other files when `@import` is present', async () => {
57
57
  @import './2.css';
58
58
  @import '3.css';
59
59
  @import '${getFixturePath('/test/4.css')}';
60
+ @import './5.css';
60
61
  `,
61
62
  '/test/2.css': dedent`
62
63
  .a {}
@@ -67,11 +68,23 @@ test('tracks other files when `@import` is present', async () => {
67
68
  '/test/4.css': dedent`
68
69
  .c {}
69
70
  `,
71
+ '/test/5.css': dedent`
72
+ @import './5-recursive.css';
73
+ `,
74
+ '/test/5-recursive.css': dedent`
75
+ .d {}
76
+ `,
70
77
  });
71
78
  const result = await loader.load(getFixturePath('/test/1.css'));
72
79
  expect(result).toMatchInlineSnapshot(`
73
80
  {
74
- dependencies: ["<fixtures>/test/2.css", "<fixtures>/test/3.css", "<fixtures>/test/4.css"],
81
+ dependencies: [
82
+ "<fixtures>/test/2.css",
83
+ "<fixtures>/test/3.css",
84
+ "<fixtures>/test/4.css",
85
+ "<fixtures>/test/5.css",
86
+ "<fixtures>/test/5-recursive.css",
87
+ ],
75
88
  tokens: [
76
89
  {
77
90
  name: "a",
@@ -91,6 +104,12 @@ test('tracks other files when `@import` is present', async () => {
91
104
  { filePath: "<fixtures>/test/4.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
92
105
  ],
93
106
  },
107
+ {
108
+ name: "d",
109
+ originalLocations: [
110
+ { filePath: "<fixtures>/test/5-recursive.css", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
111
+ ],
112
+ },
94
113
  ],
95
114
  }
96
115
  `);
@@ -297,8 +316,8 @@ test.todo('supports sourcemap file and inline sourcemap');
297
316
  test('ignores http(s) protocol file', async () => {
298
317
  createFixtures({
299
318
  '/test/1.css': dedent`
300
- @import 'http://example.com/path/1.css';
301
- @import 'https://example.com/path/1.css';
319
+ @import 'http://example.com/path/http.css';
320
+ @import 'https://example.com/path/https.css';
302
321
  `,
303
322
  });
304
323
  const result = await loader.load(getFixturePath('/test/1.css'));
@@ -130,8 +130,7 @@ export class Loader {
130
130
  // less makes a remote module inline, so it may be included in dependencies.
131
131
  // However, the dependencies field of happy-css-modules is not yet designed to store http protocol URLs.
132
132
  // Therefore, we exclude them from the dependencies field for now.
133
- // TODO: Support to store http protocol URLs in the dependencies field.
134
- return !(dep.startsWith('http://') || dep.startsWith('https://'));
133
+ return !isIgnoredSpecifier(dep);
135
134
  }),
136
135
  };
137
136
  }
@@ -167,7 +166,7 @@ export class Loader {
167
166
  const from = await this.resolver(importedSheetPath, { request: filePath });
168
167
  const result = await this.load(from);
169
168
  const externalTokens = result.tokens;
170
- dependencies.push(from);
169
+ dependencies.push(from, ...result.dependencies);
171
170
  tokens.push(...externalTokens);
172
171
  }
173
172
 
@@ -193,7 +192,7 @@ export class Loader {
193
192
  const from = await this.resolver(declarationDetail.from, { request: filePath });
194
193
  const result = await this.load(from);
195
194
  const externalTokens = result.tokens.filter((token) => declarationDetail.tokenNames.includes(token.name));
196
- dependencies.push(from);
195
+ dependencies.push(from, ...result.dependencies);
197
196
  tokens.push(...externalTokens);
198
197
  }
199
198
 
@@ -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,9 @@
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
+ const request = getFixturePath('/test/1.css');
8
7
  createFixtures({
9
8
  '/node_modules/package-1/index.css': `.a {}`,
10
9
  '/node_modules/package-2/index.css': `.a {}`,
@@ -13,8 +12,6 @@ test('resolves specifier with webpack mechanism', async () => {
13
12
  '/node_modules/package-4/style.css': `.a {}`,
14
13
  '/node_modules/@scoped/package-5/index.css': `.a {}`,
15
14
  '/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: ''; }`,
18
15
  });
19
16
  expect(await webpackResolver('~package-1/index.css', { request })).toBe(
20
17
  getFixturePath('/node_modules/package-1/index.css'),
@@ -28,6 +25,45 @@ test('resolves specifier with webpack mechanism', async () => {
28
25
  expect(await webpackResolver('package-6/index.css', { request })).toBe(
29
26
  getFixturePath('/node_modules/package-6/index.css'),
30
27
  );
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'));
28
+ });
29
+
30
+ test('resolves specifier with sass-loader mechanism', async () => {
31
+ const webpackResolver = createWebpackResolver({ sassLoadPaths: [getFixturePath('/test/styles')] });
32
+ const request = getFixturePath('/test/1.scss');
33
+ createFixtures({
34
+ '/node_modules/package-1/index.scss': `.a {}`,
35
+ '/test/styles/load-paths.scss': `.a {}`,
36
+ '/test/_partial-import.scss': `.a {}`,
37
+ });
38
+ expect(await webpackResolver('~package-1/index.scss', { request })).toBe(
39
+ getFixturePath('/node_modules/package-1/index.scss'),
40
+ );
41
+ expect(await webpackResolver('~package-1', { request })).toBe(getFixturePath('/node_modules/package-1/index.scss'));
42
+ // ref: https://github.com/webpack-contrib/sass-loader/blob/bed9fb5799a90020d43f705ea405f85b368621d7/test/scss/import-include-paths.scss#L1
43
+ expect(await webpackResolver('load-paths', { request })).toBe(getFixturePath('/test/styles/load-paths.scss'));
44
+ // https://sass-lang.com/documentation/at-rules/import#partials
45
+ // https://github.com/webpack-contrib/sass-loader/blob/0e9494074f69a6b6d47efea6c083a02a31a5ae84/test/sass/import-with-underscore.sass
46
+ expect(await webpackResolver('partial-import', { request: getFixturePath('/test/1.scss') })).toBe(
47
+ getFixturePath('/test/_partial-import.scss'),
48
+ );
49
+ expect(await webpackResolver('test/partial-import', { request: getFixturePath('/test') })).toBe(
50
+ getFixturePath('/test/_partial-import.scss'),
51
+ );
52
+ });
53
+
54
+ test('resolves specifier with less-loader mechanism', async () => {
55
+ const webpackResolver = createWebpackResolver({ lessIncludePaths: [getFixturePath('/test/styles')] });
56
+ const request = getFixturePath('/test/1.less');
57
+ createFixtures({
58
+ '/node_modules/package-1/index.less': `.a {}`,
59
+ '/test/styles/include-paths.less': `.a {}`,
60
+ });
61
+ expect(await webpackResolver('~package-1/index.less', { request })).toBe(
62
+ getFixturePath('/node_modules/package-1/index.less'),
63
+ );
64
+ expect(await webpackResolver('~package-1', { request })).toBe(getFixturePath('/node_modules/package-1/index.less'));
65
+ // ref: https://github.com/webpack-contrib/less-loader/blob/81a0d27eb6d18e5dc550a60fc1007fdc77305b78/test/loader.test.js#L248-L253
66
+ // ref: https://github.com/webpack-contrib/less-loader/blob/393147064672ace986ec84aca21f69f0ab819a9c/test/fixtures/import-paths.less#L1
67
+ // ref: https://github.com/webpack-contrib/less-loader/blob/99d80bd290dae50375db6e17c5f56ec33754e258/test/helpers/getCodeFromLess.js#L47-L54
68
+ expect(await webpackResolver('include-paths', { request })).toBe(getFixturePath('/test/styles/include-paths.less'));
33
69
  });
@@ -1,71 +1,104 @@
1
- import { dirname } from 'path';
1
+ import { basename, dirname, join } 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
+ /**
8
+ * The option compatible with sass's `--load-path`. It is an array of absolute paths.
9
+ * @example ['/home/user/repository/src/styles']
10
+ */
11
+ sassLoadPaths?: string[] | undefined;
12
+ /**
13
+ * The option compatible with less's `--include-path`. It is an array of absolute paths.
14
+ * @example ['/home/user/repository/src/styles']
15
+ */
16
+ lessIncludePaths?: string[] | undefined;
17
+ };
20
18
 
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
- });
19
+ // TODO: Support `resolve.alias` for Node.js API
20
+ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOptions | undefined) => Resolver = (
21
+ webpackResolverOptions,
22
+ ) => {
23
+ /**
24
+ * A resolver compatible with css-loader.
25
+ *
26
+ * @see https://github.com/webpack-contrib/css-loader/blob/897e7dd250ccdb0d31e6c66d4cd0d009f2022a85/src/plugins/postcss-import-parser.js#L228-L235
27
+ */
28
+ const cssLoaderResolver = enhancedResolve.create.sync({
29
+ dependencyType: 'css',
30
+ conditionNames: ['style'],
31
+ // We are not sure how "..." affects behavior...
32
+ mainFields: ['css', 'style', 'main', '...'],
33
+ mainFiles: ['index', '...'],
34
+ extensions: ['.css', '...'],
35
+ preferRelative: true,
36
+ });
35
37
 
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
- });
38
+ /**
39
+ * A resolver compatible with sass-loader.
40
+ *
41
+ * @see https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L531-L539
42
+ */
43
+ const sassLoaderResolver = enhancedResolve.create.sync({
44
+ dependencyType: 'sass',
45
+ conditionNames: ['sass', 'style'],
46
+ mainFields: ['sass', 'style', 'main', '...'],
47
+ mainFiles: ['_index', 'index', '...'],
48
+ extensions: ['.sass', '.scss', '.css'],
49
+ restrictions: [/\.((sa|sc|c)ss)$/i],
50
+ preferRelative: true,
51
+ modules: ['node_modules', ...(webpackResolverOptions?.sassLoadPaths ?? [])],
52
+ });
49
53
 
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);
54
+ /**
55
+ * A resolver compatible with less-loader.
56
+ *
57
+ * @see https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L35-L42
58
+ */
59
+ const lessLoaderResolver = enhancedResolve.create.sync({
60
+ dependencyType: 'less',
61
+ conditionNames: ['less', 'style'],
62
+ mainFields: ['less', 'style', 'main', '...'],
63
+ mainFiles: ['index', '...'],
64
+ extensions: ['.less', '.css'],
65
+ preferRelative: true,
66
+ modules: ['node_modules', ...(webpackResolverOptions?.lessIncludePaths ?? [])],
67
+ });
55
68
 
56
69
  // NOTE: In theory, `sassLoaderResolver` should only be used when the resolver is called from `sassTransformer`.
57
70
  // However, we do not implement such behavior because it is cumbersome. If someone wants it, we will implement it.
58
71
  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;
72
+
73
+ return async (specifier, options) => {
74
+ // `~` prefix is optional.
75
+ // ref: https://github.com/webpack-contrib/css-loader/blob/5e6cf91fd3f0c8b5fb4b91197b98dc56abdef4bf/src/utils.js#L92-L95
76
+ // ref: https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L368-L370
77
+ // ref: https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L72-L75
78
+ if (specifier.startsWith('~')) specifier = specifier.slice(1);
79
+
80
+ for (const resolver of resolvers) {
81
+ const specifierVariants =
82
+ resolver === sassLoaderResolver
83
+ ? // Support partial import for sass
84
+ // https://sass-lang.com/documentation/at-rules/import#partials
85
+ // https://github.com/webpack-contrib/sass-loader/blob/0e9494074f69a6b6d47efea6c083a02a31a5ae84/test/sass/import-with-underscore.sass
86
+ [join(dirname(specifier), '_' + basename(specifier)), specifier]
87
+ : [specifier];
88
+
89
+ for (const specifierVariant of specifierVariants) {
90
+ try {
91
+ const resolved = resolver(dirname(options.request), specifierVariant);
92
+ if (resolved !== false) {
93
+ const isExists = await exists(resolved);
94
+ if (isExists) return resolved;
95
+ }
96
+ } catch (e) {
97
+ // noop
98
+ }
65
99
  }
66
- } catch (e) {
67
- // noop
68
100
  }
69
- }
70
- return false;
101
+
102
+ return false;
103
+ };
71
104
  };
@@ -3,6 +3,7 @@ import { jest } from '@jest/globals';
3
3
  import chalk from 'chalk';
4
4
  import dedent from 'dedent';
5
5
  import AggregateError from 'es-aggregate-error';
6
+ import type { Watcher } from './runner.js';
6
7
  import { run } from './runner.js';
7
8
  import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test/util.js';
8
9
 
@@ -16,6 +17,14 @@ const defaultOptions = {
16
17
  cwd: getFixturePath('/'),
17
18
  };
18
19
 
20
+ // Exit the watcher even if the test fails
21
+ let watcher: Watcher | undefined;
22
+ afterEach(async () => {
23
+ if (watcher) {
24
+ await watcher.close();
25
+ }
26
+ });
27
+
19
28
  test('generates .d.ts and .d.ts.map', async () => {
20
29
  createFixtures({
21
30
  '/test/1.css': '.a {}',
@@ -49,7 +58,7 @@ test('watches for changes in files', async () => {
49
58
  createFixtures({
50
59
  '/test': {}, // empty directory
51
60
  });
52
- const watcher = await run({ ...defaultOptions, watch: true });
61
+ watcher = await run({ ...defaultOptions, watch: true });
53
62
 
54
63
  await writeFile(getFixturePath('/test/1.css'), '.a-1 {}');
55
64
  await waitForAsyncTask(500); // Wait until the file is written
@@ -58,8 +67,6 @@ test('watches for changes in files', async () => {
58
67
  await writeFile(getFixturePath('/test/1.css'), '.a-2 {}');
59
68
  await waitForAsyncTask(500); // Wait until the file is written
60
69
  expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatch(/a-2/);
61
-
62
- await watcher.close();
63
70
  });
64
71
  test('returns an error if the file fails to process in non-watch mode', async () => {
65
72
  createFixtures({
@@ -109,14 +116,40 @@ describe('handles external files', () => {
109
116
  test('treats imported tokens from external files the same as local tokens', async () => {
110
117
  await run({ ...defaultOptions });
111
118
  expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatchInlineSnapshot(`
112
- "declare const styles:
113
- & Readonly<Pick<(typeof import("./2.css"))["default"], "b">>
114
- & Readonly<{ "c": string }>
115
- & Readonly<{ "a": string }>
116
- ;
117
- export default styles;
118
- //# sourceMappingURL=./1.css.d.ts.map
119
- "
120
- `);
119
+ "declare const styles:
120
+ & Readonly<Pick<(typeof import("./2.css"))["default"], "b">>
121
+ & Readonly<{ "c": string }>
122
+ & Readonly<{ "a": string }>
123
+ ;
124
+ export default styles;
125
+ //# sourceMappingURL=./1.css.d.ts.map
126
+ "
127
+ `);
128
+ });
129
+ });
130
+
131
+ test('sassLoadPaths', async () => {
132
+ const sassLoadPaths = ['test/relative', getFixturePath('/test/absolute')];
133
+ createFixtures({
134
+ '/test/1.scss': dedent`
135
+ @import '2.scss';
136
+ @import '3.scss';
137
+ `,
138
+ '/test/relative/2.scss': `.a { dummy: ''; }`,
139
+ '/test/absolute/3.scss': `.b { dummy: ''; }`,
140
+ });
141
+ await run({ ...defaultOptions, sassLoadPaths }); // not throw
142
+ });
143
+
144
+ test('lessIncludePaths', async () => {
145
+ const lessIncludePaths = ['test/relative', getFixturePath('/test/absolute')];
146
+ createFixtures({
147
+ '/test/1.less': dedent`
148
+ @import '2.less';
149
+ @import '3.less';
150
+ `,
151
+ '/test/relative/2.less': `.a { dummy: ''; }`,
152
+ '/test/absolute/3.less': `.b { dummy: ''; }`,
121
153
  });
154
+ await run({ ...defaultOptions, lessIncludePaths }); // not throw
122
155
  });
package/src/runner.ts CHANGED
@@ -8,6 +8,7 @@ import _glob from 'glob';
8
8
  import { emitGeneratedFiles } from './emitter/index.js';
9
9
  import { Loader } from './loader/index.js';
10
10
  import type { Resolver } from './resolver/index.js';
11
+ import { createDefaultResolver } from './resolver/index.js';
11
12
  import { type Transformer } from './transformer/index.js';
12
13
  import { isMatchByGlob } from './util.js';
13
14
 
@@ -27,6 +28,18 @@ export interface RunnerOptions {
27
28
  declarationMap?: boolean | undefined;
28
29
  transformer?: Transformer | undefined;
29
30
  resolver?: Resolver | undefined;
31
+ /**
32
+ * The option compatible with sass's `--load-path`. It is an array of relative or absolute paths.
33
+ * @example ['src/styles']
34
+ * @example ['/home/user/repository/src/styles']
35
+ */
36
+ sassLoadPaths?: string[] | undefined;
37
+ /**
38
+ * The option compatible with less's `--include-path`. It is an array of relative or absolute paths.
39
+ * @example ['src/styles']
40
+ * @example ['/home/user/repository/src/styles']
41
+ */
42
+ lessIncludePaths?: string[] | undefined;
30
43
  /**
31
44
  * Silent output. Do not show "files written" messages.
32
45
  * @default false
@@ -46,15 +59,21 @@ type OverrideProp<T, K extends keyof T, V extends T[K]> = Omit<T, K> & { [P in K
46
59
  export async function run(options: OverrideProp<RunnerOptions, 'watch', true>): Promise<Watcher>;
47
60
  export async function run(options: RunnerOptions): Promise<void>;
48
61
  export async function run(options: RunnerOptions): Promise<Watcher | void> {
49
- const loader = new Loader({ transformer: options.transformer, resolver: options.resolver });
62
+ const cwd = options.cwd ?? process.cwd();
63
+ const silent = options.silent ?? false;
64
+ const sassLoadPaths = options.sassLoadPaths?.map((path) => resolve(cwd, path));
65
+ const lessIncludePaths = options.lessIncludePaths?.map((path) => resolve(cwd, path));
66
+ const resolver = options.resolver ?? createDefaultResolver({ sassLoadPaths, lessIncludePaths });
50
67
  const distOptions = options.outDir
51
68
  ? {
52
- rootDir: process.cwd(), // TODO: support `--rootDir` option
69
+ rootDir: cwd, // TODO: support `--rootDir` option
53
70
  outDir: options.outDir,
54
71
  }
55
72
  : undefined;
73
+
74
+ const loader = new Loader({ transformer: options.transformer, resolver });
56
75
  const isExternalFile = (filePath: string) => {
57
- return !isMatchByGlob(filePath, options.pattern, { cwd: options.cwd ?? process.cwd() });
76
+ return !isMatchByGlob(filePath, options.pattern, { cwd });
58
77
  };
59
78
 
60
79
  async function processFile(filePath: string) {
@@ -68,8 +87,8 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
68
87
  dtsFormatOptions: {
69
88
  localsConvention: options.localsConvention,
70
89
  },
71
- silent: options.silent ?? false,
72
- cwd: options.cwd ?? process.cwd(),
90
+ silent,
91
+ cwd,
73
92
  isExternalFile,
74
93
  });
75
94
  } catch (error) {
@@ -80,20 +99,20 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
80
99
  }
81
100
 
82
101
  if (options.watch) {
83
- if (!options.silent) console.log('Watch ' + options.pattern + '...');
84
- const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], options.cwd ? { cwd: options.cwd } : {});
102
+ if (!silent) console.log('Watch ' + options.pattern + '...');
103
+ const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], { cwd });
85
104
  watcher.on('all', (eventName, filePath) => {
86
105
  if (eventName === 'add' || eventName === 'change') {
87
- processFile(resolve(options.cwd ?? process.cwd(), filePath)).catch(() => {
106
+ processFile(resolve(cwd, filePath)).catch(() => {
88
107
  // TODO: Emit a error by `Watcher#onerror`
89
108
  });
90
109
  }
91
110
  });
92
111
  return { close: async () => watcher.close() };
93
112
  } else {
94
- const filePaths = (await glob(options.pattern, { dot: true, cwd: options.cwd ?? process.cwd() }))
113
+ const filePaths = (await glob(options.pattern, { dot: true, cwd }))
95
114
  // convert relative path to absolute path
96
- .map((file) => resolve(options.cwd ?? process.cwd(), file));
115
+ .map((file) => resolve(cwd, file));
97
116
 
98
117
  // TODO: Use `@file-cache/core` to process only files that have changed
99
118
  const errors: unknown[] = [];