happy-css-modules 0.4.0 → 0.6.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 (102) hide show
  1. package/README.md +172 -53
  2. package/dist/cli.js +45 -11
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +20 -4
  5. package/dist/cli.test.js.map +1 -1
  6. package/dist/emitter/dts.d.ts +3 -4
  7. package/dist/emitter/dts.js +13 -15
  8. package/dist/emitter/dts.js.map +1 -1
  9. package/dist/emitter/dts.test.js +7 -10
  10. package/dist/emitter/dts.test.js.map +1 -1
  11. package/dist/emitter/index.d.ts +6 -15
  12. package/dist/emitter/index.js +18 -13
  13. package/dist/emitter/index.js.map +1 -1
  14. package/dist/emitter/index.test.js +0 -50
  15. package/dist/emitter/index.test.js.map +1 -1
  16. package/dist/emitter/source-map.d.ts +1 -3
  17. package/dist/emitter/source-map.js +2 -3
  18. package/dist/emitter/source-map.js.map +1 -1
  19. package/dist/emitter/source-map.test.js +1 -4
  20. package/dist/emitter/source-map.test.js.map +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.js +3 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/integration-test/go-to-definition.test.js +1 -0
  25. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  26. package/dist/{loader → locator}/index.d.ts +6 -4
  27. package/dist/{loader → locator}/index.js +13 -5
  28. package/dist/locator/index.js.map +1 -0
  29. package/dist/{loader → locator}/index.test.d.ts +0 -0
  30. package/dist/{loader → locator}/index.test.js +100 -14
  31. package/dist/locator/index.test.js.map +1 -0
  32. package/dist/{loader → locator}/postcss.d.ts +5 -1
  33. package/dist/{loader → locator}/postcss.js +26 -30
  34. package/dist/{loader → locator}/postcss.js.map +1 -1
  35. package/dist/{loader → locator}/postcss.test.d.ts +0 -0
  36. package/dist/{loader → locator}/postcss.test.js +0 -0
  37. package/dist/locator/postcss.test.js.map +1 -0
  38. package/dist/resolver/webpack-resolver.d.ts +12 -2
  39. package/dist/resolver/webpack-resolver.js +12 -3
  40. package/dist/resolver/webpack-resolver.js.map +1 -1
  41. package/dist/resolver/webpack-resolver.test.js +43 -7
  42. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  43. package/dist/runner.d.ts +23 -1
  44. package/dist/runner.js +62 -23
  45. package/dist/runner.js.map +1 -1
  46. package/dist/runner.test.js +90 -11
  47. package/dist/runner.test.js.map +1 -1
  48. package/dist/test/util.d.ts +2 -2
  49. package/dist/test/util.js +16 -9
  50. package/dist/test/util.js.map +1 -1
  51. package/dist/transformer/index.d.ts +4 -2
  52. package/dist/transformer/index.js +7 -3
  53. package/dist/transformer/index.js.map +1 -1
  54. package/dist/transformer/index.test.d.ts +1 -0
  55. package/dist/transformer/index.test.js +66 -0
  56. package/dist/transformer/index.test.js.map +1 -0
  57. package/dist/transformer/less-transformer.test.js +14 -14
  58. package/dist/transformer/less-transformer.test.js.map +1 -1
  59. package/dist/transformer/postcss-transformer.d.ts +12 -0
  60. package/dist/transformer/postcss-transformer.js +32 -0
  61. package/dist/transformer/postcss-transformer.js.map +1 -0
  62. package/dist/transformer/postcss-transformer.test.d.ts +1 -0
  63. package/dist/transformer/postcss-transformer.test.js +176 -0
  64. package/dist/transformer/postcss-transformer.test.js.map +1 -0
  65. package/dist/transformer/scss-transformer.js +19 -17
  66. package/dist/transformer/scss-transformer.js.map +1 -1
  67. package/dist/transformer/scss-transformer.test.js +16 -16
  68. package/dist/transformer/scss-transformer.test.js.map +1 -1
  69. package/dist/util.d.ts +2 -0
  70. package/dist/util.js +19 -2
  71. package/dist/util.js.map +1 -1
  72. package/package.json +10 -9
  73. package/src/cli.test.ts +24 -4
  74. package/src/cli.ts +44 -12
  75. package/src/emitter/dts.test.ts +7 -12
  76. package/src/emitter/dts.ts +15 -15
  77. package/src/emitter/index.test.ts +0 -52
  78. package/src/emitter/index.ts +22 -29
  79. package/src/emitter/source-map.test.ts +1 -6
  80. package/src/emitter/source-map.ts +3 -4
  81. package/src/index.ts +9 -2
  82. package/src/integration-test/go-to-definition.test.ts +1 -0
  83. package/src/{loader → locator}/index.test.ts +101 -14
  84. package/src/{loader → locator}/index.ts +16 -8
  85. package/src/{loader → locator}/postcss.test.ts +0 -0
  86. package/src/{loader → locator}/postcss.ts +42 -40
  87. package/src/resolver/webpack-resolver.test.ts +63 -7
  88. package/src/resolver/webpack-resolver.ts +26 -5
  89. package/src/runner.test.ts +103 -11
  90. package/src/runner.ts +83 -25
  91. package/src/test/util.ts +16 -10
  92. package/src/transformer/index.test.ts +71 -0
  93. package/src/transformer/index.ts +12 -4
  94. package/src/transformer/less-transformer.test.ts +14 -14
  95. package/src/transformer/postcss-transformer.test.ts +188 -0
  96. package/src/transformer/postcss-transformer.ts +57 -0
  97. package/src/transformer/scss-transformer.test.ts +16 -16
  98. package/src/transformer/scss-transformer.ts +25 -27
  99. package/src/util.ts +21 -2
  100. package/dist/loader/index.js.map +0 -1
  101. package/dist/loader/index.test.js.map +0 -1
  102. package/dist/loader/postcss.test.js.map +0 -1
@@ -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 {
@@ -2,7 +2,13 @@ import { createFixtures, getFixturePath } from '../test/util.js';
2
2
  import { createWebpackResolver } from './webpack-resolver.js';
3
3
 
4
4
  test('resolves specifier with css-loader mechanism', async () => {
5
- const webpackResolver = createWebpackResolver();
5
+ const webpackResolver = createWebpackResolver({
6
+ cwd: getFixturePath('/'),
7
+ webpackResolveAlias: {
8
+ '@relative': 'test/alias-relative',
9
+ '@absolute': getFixturePath('/test/alias-absolute'),
10
+ },
11
+ });
6
12
  const request = getFixturePath('/test/1.css');
7
13
  createFixtures({
8
14
  '/node_modules/package-1/index.css': `.a {}`,
@@ -12,6 +18,8 @@ test('resolves specifier with css-loader mechanism', async () => {
12
18
  '/node_modules/package-4/style.css': `.a {}`,
13
19
  '/node_modules/@scoped/package-5/index.css': `.a {}`,
14
20
  '/node_modules/package-6/index.css': `.a {}`,
21
+ '/test/alias-relative/alias.css': `.a {}`,
22
+ '/test/alias-absolute/alias.css': `.a {}`,
15
23
  });
16
24
  expect(await webpackResolver('~package-1/index.css', { request })).toBe(
17
25
  getFixturePath('/node_modules/package-1/index.css'),
@@ -25,22 +33,43 @@ test('resolves specifier with css-loader mechanism', async () => {
25
33
  expect(await webpackResolver('package-6/index.css', { request })).toBe(
26
34
  getFixturePath('/node_modules/package-6/index.css'),
27
35
  );
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
+ );
28
42
  });
29
43
 
30
44
  test('resolves specifier with sass-loader mechanism', async () => {
31
- const webpackResolver = createWebpackResolver({ sassLoadPaths: [getFixturePath('/test/styles')] });
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
+ });
32
53
  const request = getFixturePath('/test/1.scss');
33
54
  createFixtures({
34
55
  '/node_modules/package-1/index.scss': `.a {}`,
35
- '/test/styles/load-paths.scss': `.a {}`,
56
+ '/test/load-paths-relative/load-paths-relative.scss': `.a {}`,
57
+ '/test/load-paths-absolute/load-paths-absolute.scss': `.a {}`,
36
58
  '/test/_partial-import.scss': `.a {}`,
59
+ '/test/alias-relative/alias.scss': `.a {}`,
60
+ '/test/alias-absolute/alias.scss': `.a {}`,
37
61
  });
38
62
  expect(await webpackResolver('~package-1/index.scss', { request })).toBe(
39
63
  getFixturePath('/node_modules/package-1/index.scss'),
40
64
  );
41
65
  expect(await webpackResolver('~package-1', { request })).toBe(getFixturePath('/node_modules/package-1/index.scss'));
42
66
  // 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'));
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
+ );
44
73
  // https://sass-lang.com/documentation/at-rules/import#partials
45
74
  // https://github.com/webpack-contrib/sass-loader/blob/0e9494074f69a6b6d47efea6c083a02a31a5ae84/test/sass/import-with-underscore.sass
46
75
  expect(await webpackResolver('partial-import', { request: getFixturePath('/test/1.scss') })).toBe(
@@ -49,14 +78,30 @@ test('resolves specifier with sass-loader mechanism', async () => {
49
78
  expect(await webpackResolver('test/partial-import', { request: getFixturePath('/test') })).toBe(
50
79
  getFixturePath('/test/_partial-import.scss'),
51
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
+ );
52
87
  });
53
88
 
54
89
  test('resolves specifier with less-loader mechanism', async () => {
55
- const webpackResolver = createWebpackResolver({ lessIncludePaths: [getFixturePath('/test/styles')] });
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
+ });
56
98
  const request = getFixturePath('/test/1.less');
57
99
  createFixtures({
58
100
  '/node_modules/package-1/index.less': `.a {}`,
59
- '/test/styles/include-paths.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 {}`,
60
105
  });
61
106
  expect(await webpackResolver('~package-1/index.less', { request })).toBe(
62
107
  getFixturePath('/node_modules/package-1/index.less'),
@@ -65,5 +110,16 @@ test('resolves specifier with less-loader mechanism', async () => {
65
110
  // ref: https://github.com/webpack-contrib/less-loader/blob/81a0d27eb6d18e5dc550a60fc1007fdc77305b78/test/loader.test.js#L248-L253
66
111
  // ref: https://github.com/webpack-contrib/less-loader/blob/393147064672ace986ec84aca21f69f0ab819a9c/test/fixtures/import-paths.less#L1
67
112
  // 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'));
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
+ );
69
125
  });
@@ -1,25 +1,43 @@
1
- import { basename, dirname, join } 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
6
  export type WebpackResolverOptions = {
7
+ /** Working directory path. */
8
+ cwd?: string | undefined;
7
9
  /**
8
- * The option compatible with sass's `--load-path`. It is an array of absolute paths.
10
+ * The option compatible with sass's `--load-path`. It is an array of relative or absolute paths.
11
+ * @example ['src/styles']
9
12
  * @example ['/home/user/repository/src/styles']
10
13
  */
11
14
  sassLoadPaths?: string[] | undefined;
12
15
  /**
13
- * The option compatible with less's `--include-path`. It is an array of absolute paths.
16
+ * The option compatible with less's `--include-path`. It is an array of relative or absolute paths.
17
+ * @example ['src/styles']
14
18
  * @example ['/home/user/repository/src/styles']
15
19
  */
16
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;
17
27
  };
18
28
 
19
29
  // TODO: Support `resolve.alias` for Node.js API
20
30
  export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOptions | undefined) => Resolver = (
21
31
  webpackResolverOptions,
22
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;
23
41
  /**
24
42
  * A resolver compatible with css-loader.
25
43
  *
@@ -33,6 +51,7 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
33
51
  mainFiles: ['index', '...'],
34
52
  extensions: ['.css', '...'],
35
53
  preferRelative: true,
54
+ alias: webpackResolveAlias,
36
55
  });
37
56
 
38
57
  /**
@@ -48,7 +67,8 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
48
67
  extensions: ['.sass', '.scss', '.css'],
49
68
  restrictions: [/\.((sa|sc|c)ss)$/i],
50
69
  preferRelative: true,
51
- modules: ['node_modules', ...(webpackResolverOptions?.sassLoadPaths ?? [])],
70
+ alias: webpackResolveAlias,
71
+ modules: ['node_modules', ...(sassLoadPaths ?? [])],
52
72
  });
53
73
 
54
74
  /**
@@ -63,7 +83,8 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
63
83
  mainFiles: ['index', '...'],
64
84
  extensions: ['.less', '.css'],
65
85
  preferRelative: true,
66
- modules: ['node_modules', ...(webpackResolverOptions?.lessIncludePaths ?? [])],
86
+ alias: webpackResolveAlias,
87
+ modules: ['node_modules', ...(lessIncludePaths ?? [])],
67
88
  });
68
89
 
69
90
  // NOTE: In theory, `sassLoaderResolver` should only be used when the resolver is called from `sassTransformer`.
@@ -1,12 +1,26 @@
1
- import { readFile, writeFile } from 'fs/promises';
1
+ import { readFile, rm, writeFile } from 'fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, resolve } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import * as fileCacheNpm from '@file-cache/npm';
2
7
  import { jest } from '@jest/globals';
3
8
  import chalk from 'chalk';
4
9
  import dedent from 'dedent';
5
- import AggregateError from 'es-aggregate-error';
6
10
  import type { Watcher } from './runner.js';
7
- import { run } from './runner.js';
8
11
  import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test/util.js';
9
12
 
13
+ const require = createRequire(import.meta.url);
14
+
15
+ jest.unstable_mockModule('@file-cache/npm', () => ({
16
+ ...fileCacheNpm, // Inherit native functions
17
+ createNpmPackageKey: () => 'mocked-key',
18
+ }));
19
+
20
+ const { run } = await import('./runner.js');
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
23
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
10
24
  // eslint-disable-next-line @typescript-eslint/no-empty-function
11
25
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
12
26
 
@@ -15,8 +29,17 @@ const defaultOptions = {
15
29
  declarationMap: true,
16
30
  silent: true,
17
31
  cwd: getFixturePath('/'),
32
+ cache: false,
18
33
  };
19
34
 
35
+ const dir = join(dirname(fileURLToPath(import.meta.url)));
36
+
37
+ beforeEach(async () => {
38
+ consoleLogSpy.mockClear();
39
+ consoleErrorSpy.mockClear();
40
+ await rm(resolve(dir, '../node_modules/.cache/happy-css-modules'), { recursive: true, force: true }); // clear cache
41
+ });
42
+
20
43
  // Exit the watcher even if the test fails
21
44
  let watcher: Watcher | undefined;
22
45
  afterEach(async () => {
@@ -37,6 +60,49 @@ test('generates .d.ts and .d.ts.map', async () => {
37
60
  expect(await readFile(getFixturePath('/test/2.css.d.ts.map'), 'utf8')).toMatchSnapshot();
38
61
  });
39
62
 
63
+ test('uses cache', async () => {
64
+ createFixtures({
65
+ '/test/1.css': '.a {}',
66
+ });
67
+ await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
68
+ expect(consoleLogSpy).toBeCalledTimes(1);
69
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
70
+ consoleLogSpy.mockClear();
71
+
72
+ // Skip generation
73
+ await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
74
+ expect(consoleLogSpy).toBeCalledTimes(1);
75
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('skipped'));
76
+ consoleLogSpy.mockClear();
77
+
78
+ // Generates if generated files are missing
79
+ await rm(getFixturePath('/test/1.css.d.ts'));
80
+ await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
81
+ expect(consoleLogSpy).toBeCalledTimes(1);
82
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
83
+ consoleLogSpy.mockClear();
84
+
85
+ // Generates if options are changed
86
+ await run({ ...defaultOptions, declarationMap: false, silent: false, cache: true });
87
+ expect(consoleLogSpy).toBeCalledTimes(1);
88
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
89
+ consoleLogSpy.mockClear();
90
+ });
91
+
92
+ test('outputs logs', async () => {
93
+ createFixtures({
94
+ '/test/1.css': '.a {}',
95
+ });
96
+ await run({ ...defaultOptions, silent: false, cache: true });
97
+ expect(consoleLogSpy).toBeCalledTimes(1);
98
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `${chalk.green('test/1.css')} (generated)`);
99
+ consoleLogSpy.mockClear();
100
+
101
+ await run({ ...defaultOptions, silent: false, cache: true });
102
+ expect(consoleLogSpy).toBeCalledTimes(1);
103
+ expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `${chalk.gray('test/1.css (skipped)')}`);
104
+ });
105
+
40
106
  test.todo('changes dts format with localsConvention options');
41
107
  test('does not emit declaration map if declarationMap is false', async () => {
42
108
  createFixtures({
@@ -87,9 +153,9 @@ test('returns an error if the file fails to process in non-watch mode', async ()
87
153
  // The error is logged to console.error.
88
154
  expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
89
155
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
90
- expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]));
156
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]!.stack));
91
157
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
92
- expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1]));
158
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1].stack));
93
159
 
94
160
  // The valid files are emitted.
95
161
  expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
@@ -129,27 +195,53 @@ describe('handles external files', () => {
129
195
  });
130
196
 
131
197
  test('sassLoadPaths', async () => {
132
- const sassLoadPaths = ['test/relative', getFixturePath('/test/absolute')];
198
+ const sassLoadPaths = ['test/relative'];
133
199
  createFixtures({
134
200
  '/test/1.scss': dedent`
135
201
  @import '2.scss';
136
- @import '3.scss';
137
202
  `,
138
203
  '/test/relative/2.scss': `.a { dummy: ''; }`,
139
- '/test/absolute/3.scss': `.b { dummy: ''; }`,
140
204
  });
141
205
  await run({ ...defaultOptions, sassLoadPaths }); // not throw
142
206
  });
143
207
 
144
208
  test('lessIncludePaths', async () => {
145
- const lessIncludePaths = ['test/relative', getFixturePath('/test/absolute')];
209
+ const lessIncludePaths = ['test/relative'];
146
210
  createFixtures({
147
211
  '/test/1.less': dedent`
148
212
  @import '2.less';
149
- @import '3.less';
150
213
  `,
151
214
  '/test/relative/2.less': `.a { dummy: ''; }`,
152
- '/test/absolute/3.less': `.b { dummy: ''; }`,
153
215
  });
154
216
  await run({ ...defaultOptions, lessIncludePaths }); // not throw
155
217
  });
218
+
219
+ test('webpackResolveAlias', async () => {
220
+ const webpackResolveAlias = { '@relative': 'test/relative' };
221
+ createFixtures({
222
+ '/test/1.less': dedent`
223
+ @import '@relative/2.less';
224
+ `,
225
+ '/test/relative/2.less': `.a { dummy: ''; }`,
226
+ });
227
+ await run({ ...defaultOptions, webpackResolveAlias }); // not throw
228
+ });
229
+
230
+ test('postcssConfig', async () => {
231
+ const uuid = randomUUID();
232
+ const postcssConfig = `${uuid}/postcss.config.js`;
233
+ createFixtures({
234
+ [`/${uuid}/postcss.config.js`]: dedent`
235
+ module.exports = {
236
+ plugins: [
237
+ require('${require.resolve('postcss-simple-vars')}'),
238
+ ],
239
+ };
240
+ `,
241
+ '/test/1.css': dedent`
242
+ $prefix: foo;
243
+ .$(prefix)_bar {}
244
+ `,
245
+ });
246
+ await run({ ...defaultOptions, postcssConfig }); // not throw
247
+ });
package/src/runner.ts CHANGED
@@ -1,16 +1,17 @@
1
- import { resolve } from 'path';
1
+ import { resolve, relative } from 'path';
2
2
  import * as process from 'process';
3
3
  import * as util from 'util';
4
+ import { createCache } from '@file-cache/core';
5
+ import { createNpmPackageKey } from '@file-cache/npm';
4
6
  import chalk from 'chalk';
5
7
  import * as chokidar from 'chokidar';
6
- import AggregateError from 'es-aggregate-error';
7
8
  import _glob from 'glob';
8
- import { emitGeneratedFiles } from './emitter/index.js';
9
- import { Loader } from './loader/index.js';
9
+ import { isGeneratedFilesExist, emitGeneratedFiles } from './emitter/index.js';
10
+ import { Locator } from './locator/index.js';
10
11
  import type { Resolver } from './resolver/index.js';
11
12
  import { createDefaultResolver } from './resolver/index.js';
12
- import { type Transformer } from './transformer/index.js';
13
- import { isMatchByGlob } from './util.js';
13
+ import { createDefaultTransformer, type Transformer } from './transformer/index.js';
14
+ import { getInstalledPeerDependencies, isMatchByGlob } from './util.js';
14
15
 
15
16
  const glob = util.promisify(_glob);
16
17
 
@@ -22,7 +23,6 @@ export type LocalsConvention = 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashe
22
23
 
23
24
  export interface RunnerOptions {
24
25
  pattern: string;
25
- outDir?: string | undefined;
26
26
  watch?: boolean | undefined;
27
27
  localsConvention?: LocalsConvention | undefined;
28
28
  declarationMap?: boolean | undefined;
@@ -40,6 +40,29 @@ export interface RunnerOptions {
40
40
  * @example ['/home/user/repository/src/styles']
41
41
  */
42
42
  lessIncludePaths?: string[] | undefined;
43
+ /**
44
+ * The option compatible with webpack's `resolve.alias`. It is an object consisting of a pair of alias names and relative or absolute paths.
45
+ * @example { style: 'src/styles', '@': 'src' }
46
+ * @example { style: '/home/user/repository/src/styles', '@': '/home/user/repository/src' }
47
+ */
48
+ webpackResolveAlias?: Record<string, string> | undefined;
49
+ /**
50
+ * The option compatible with postcss's `--config`. It is a relative or absolute path.
51
+ * @example '.'
52
+ * @example 'postcss.config.js'
53
+ * @example '/home/user/repository/src'
54
+ */
55
+ postcssConfig?: string | undefined;
56
+ /**
57
+ * Only generate .d.ts and .d.ts.map for changed files.
58
+ * @default true
59
+ */
60
+ cache?: boolean | undefined;
61
+ /**
62
+ * Strategy for the cache to use for detecting changed files.
63
+ * @default 'content'
64
+ */
65
+ cacheStrategy?: 'content' | 'metadata' | undefined;
43
66
  /**
44
67
  * Silent output. Do not show "files written" messages.
45
68
  * @default false
@@ -61,43 +84,64 @@ export async function run(options: RunnerOptions): Promise<void>;
61
84
  export async function run(options: RunnerOptions): Promise<Watcher | void> {
62
85
  const cwd = options.cwd ?? process.cwd();
63
86
  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 });
67
- const distOptions = options.outDir
68
- ? {
69
- rootDir: cwd, // TODO: support `--rootDir` option
70
- outDir: options.outDir,
71
- }
72
- : undefined;
87
+ const resolver =
88
+ options.resolver ??
89
+ createDefaultResolver({
90
+ cwd,
91
+ sassLoadPaths: options.sassLoadPaths,
92
+ lessIncludePaths: options.lessIncludePaths,
93
+ webpackResolveAlias: options.webpackResolveAlias,
94
+ });
95
+ const transformer = options.transformer ?? createDefaultTransformer({ cwd, postcssConfig: options.postcssConfig });
73
96
 
74
- const loader = new Loader({ transformer: options.transformer, resolver });
97
+ const installedPeerDependencies = await getInstalledPeerDependencies();
98
+ const cache = await createCache({
99
+ mode: options.cacheStrategy ?? 'content',
100
+ keys: [
101
+ () => createNpmPackageKey(['happy-css-modules', ...installedPeerDependencies]),
102
+ () => {
103
+ return JSON.stringify(options);
104
+ },
105
+ ],
106
+ noCache: !(options.cache ?? true),
107
+ });
108
+
109
+ const locator = new Locator({ transformer, resolver });
75
110
  const isExternalFile = (filePath: string) => {
76
111
  return !isMatchByGlob(filePath, options.pattern, { cwd });
77
112
  };
78
113
 
79
114
  async function processFile(filePath: string) {
80
115
  try {
81
- const result = await loader.load(filePath);
116
+ const result = await locator.load(filePath);
82
117
  await emitGeneratedFiles({
83
118
  filePath,
84
119
  tokens: result.tokens,
85
- distOptions,
86
120
  emitDeclarationMap: options.declarationMap,
87
121
  dtsFormatOptions: {
88
122
  localsConvention: options.localsConvention,
89
123
  },
90
- silent,
91
- cwd,
92
124
  isExternalFile,
93
125
  });
126
+ if (!silent) console.log(`${chalk.green(relative(cwd, filePath))} (generated)`);
94
127
  } catch (error) {
95
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
96
- console.error(chalk.red('[Error] ' + error));
128
+ if (error instanceof Error) {
129
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
130
+ console.error(chalk.red('[Error] ' + error.stack));
131
+ } else {
132
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
133
+ console.error(chalk.red('[Error] ' + error));
134
+ }
97
135
  throw error;
98
136
  }
99
137
  }
100
138
 
139
+ async function isChangedFile(filePath: string) {
140
+ const result = await cache.getAndUpdateCache(filePath);
141
+ if (result.error) throw result.error;
142
+ return result.changed;
143
+ }
144
+
101
145
  if (options.watch) {
102
146
  if (!silent) console.log('Watch ' + options.pattern + '...');
103
147
  const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], { cwd });
@@ -114,11 +158,25 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
114
158
  // convert relative path to absolute path
115
159
  .map((file) => resolve(cwd, file));
116
160
 
117
- // TODO: Use `@file-cache/core` to process only files that have changed
118
161
  const errors: unknown[] = [];
119
162
  for (const filePath of filePaths) {
120
- await processFile(filePath).catch((e: unknown) => errors.push(e));
163
+ try {
164
+ const _isGeneratedFilesExist = await isGeneratedFilesExist(filePath, options.declarationMap);
165
+ const _isChangedFile = await isChangedFile(filePath);
166
+ // Generate .d.ts and .d.ts.map only when the file has been updated.
167
+ // However, if .d.ts or .d.ts.map has not yet been generated, always generate.
168
+ if (!_isGeneratedFilesExist || _isChangedFile) {
169
+ await processFile(filePath);
170
+ } else {
171
+ if (!silent) console.log(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
172
+ }
173
+ } catch (e: unknown) {
174
+ errors.push(e);
175
+ }
121
176
  }
122
177
  if (errors.length > 0) throw new AggregateError(errors, 'Failed to process files');
123
178
  }
179
+
180
+ // Write cache state to file for persistence
181
+ await cache.reconcile();
124
182
  }