happy-css-modules 0.4.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 (63) hide show
  1. package/README.md +95 -45
  2. package/dist/cli.js +28 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +11 -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/loader/index.test.js +79 -1
  12. package/dist/loader/index.test.js.map +1 -1
  13. package/dist/loader/postcss.d.ts +5 -1
  14. package/dist/loader/postcss.js +26 -30
  15. package/dist/loader/postcss.js.map +1 -1
  16. package/dist/resolver/webpack-resolver.d.ts +12 -2
  17. package/dist/resolver/webpack-resolver.js +12 -3
  18. package/dist/resolver/webpack-resolver.js.map +1 -1
  19. package/dist/resolver/webpack-resolver.test.js +43 -7
  20. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  21. package/dist/runner.d.ts +13 -0
  22. package/dist/runner.js +18 -6
  23. package/dist/runner.js.map +1 -1
  24. package/dist/runner.test.js +35 -8
  25. package/dist/runner.test.js.map +1 -1
  26. package/dist/test/util.d.ts +1 -1
  27. package/dist/test/util.js +15 -8
  28. package/dist/test/util.js.map +1 -1
  29. package/dist/transformer/index.d.ts +3 -1
  30. package/dist/transformer/index.js +7 -3
  31. package/dist/transformer/index.js.map +1 -1
  32. package/dist/transformer/index.test.d.ts +1 -0
  33. package/dist/transformer/index.test.js +66 -0
  34. package/dist/transformer/index.test.js.map +1 -0
  35. package/dist/transformer/less-transformer.test.js +6 -6
  36. package/dist/transformer/postcss-transformer.d.ts +12 -0
  37. package/dist/transformer/postcss-transformer.js +32 -0
  38. package/dist/transformer/postcss-transformer.js.map +1 -0
  39. package/dist/transformer/postcss-transformer.test.d.ts +1 -0
  40. package/dist/transformer/postcss-transformer.test.js +176 -0
  41. package/dist/transformer/postcss-transformer.test.js.map +1 -0
  42. package/dist/transformer/scss-transformer.js +19 -17
  43. package/dist/transformer/scss-transformer.js.map +1 -1
  44. package/dist/transformer/scss-transformer.test.js +8 -8
  45. package/package.json +6 -3
  46. package/src/cli.test.ts +15 -0
  47. package/src/cli.ts +27 -1
  48. package/src/emitter/dts.ts +10 -1
  49. package/src/index.ts +8 -2
  50. package/src/loader/index.test.ts +79 -1
  51. package/src/loader/postcss.ts +42 -40
  52. package/src/resolver/webpack-resolver.test.ts +63 -7
  53. package/src/resolver/webpack-resolver.ts +26 -5
  54. package/src/runner.test.ts +38 -8
  55. package/src/runner.ts +31 -7
  56. package/src/test/util.ts +15 -9
  57. package/src/transformer/index.test.ts +71 -0
  58. package/src/transformer/index.ts +11 -3
  59. package/src/transformer/less-transformer.test.ts +6 -6
  60. package/src/transformer/postcss-transformer.test.ts +188 -0
  61. package/src/transformer/postcss-transformer.ts +57 -0
  62. package/src/transformer/scss-transformer.test.ts +8 -8
  63. package/src/transformer/scss-transformer.ts +25 -27
@@ -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,4 +1,6 @@
1
1
  import { readFile, writeFile } from 'fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
2
4
  import { jest } from '@jest/globals';
3
5
  import chalk from 'chalk';
4
6
  import dedent from 'dedent';
@@ -7,6 +9,8 @@ import type { Watcher } from './runner.js';
7
9
  import { run } from './runner.js';
8
10
  import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test/util.js';
9
11
 
12
+ const require = createRequire(import.meta.url);
13
+
10
14
  // eslint-disable-next-line @typescript-eslint/no-empty-function
11
15
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
12
16
 
@@ -87,9 +91,9 @@ test('returns an error if the file fails to process in non-watch mode', async ()
87
91
  // The error is logged to console.error.
88
92
  expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
89
93
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
90
- expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]));
94
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]!.stack));
91
95
  // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
92
- expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1]));
96
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1].stack));
93
97
 
94
98
  // The valid files are emitted.
95
99
  expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
@@ -129,27 +133,53 @@ describe('handles external files', () => {
129
133
  });
130
134
 
131
135
  test('sassLoadPaths', async () => {
132
- const sassLoadPaths = ['test/relative', getFixturePath('/test/absolute')];
136
+ const sassLoadPaths = ['test/relative'];
133
137
  createFixtures({
134
138
  '/test/1.scss': dedent`
135
139
  @import '2.scss';
136
- @import '3.scss';
137
140
  `,
138
141
  '/test/relative/2.scss': `.a { dummy: ''; }`,
139
- '/test/absolute/3.scss': `.b { dummy: ''; }`,
140
142
  });
141
143
  await run({ ...defaultOptions, sassLoadPaths }); // not throw
142
144
  });
143
145
 
144
146
  test('lessIncludePaths', async () => {
145
- const lessIncludePaths = ['test/relative', getFixturePath('/test/absolute')];
147
+ const lessIncludePaths = ['test/relative'];
146
148
  createFixtures({
147
149
  '/test/1.less': dedent`
148
150
  @import '2.less';
149
- @import '3.less';
150
151
  `,
151
152
  '/test/relative/2.less': `.a { dummy: ''; }`,
152
- '/test/absolute/3.less': `.b { dummy: ''; }`,
153
153
  });
154
154
  await run({ ...defaultOptions, lessIncludePaths }); // not throw
155
155
  });
156
+
157
+ test('webpackResolveAlias', async () => {
158
+ const webpackResolveAlias = { '@relative': 'test/relative' };
159
+ createFixtures({
160
+ '/test/1.less': dedent`
161
+ @import '@relative/2.less';
162
+ `,
163
+ '/test/relative/2.less': `.a { dummy: ''; }`,
164
+ });
165
+ await run({ ...defaultOptions, webpackResolveAlias }); // not throw
166
+ });
167
+
168
+ test('postcssConfig', async () => {
169
+ const uuid = randomUUID();
170
+ const postcssConfig = `${uuid}/postcss.config.js`;
171
+ createFixtures({
172
+ [`/${uuid}/postcss.config.js`]: dedent`
173
+ module.exports = {
174
+ plugins: [
175
+ require('${require.resolve('postcss-simple-vars')}'),
176
+ ],
177
+ };
178
+ `,
179
+ '/test/1.css': dedent`
180
+ $prefix: foo;
181
+ .$(prefix)_bar {}
182
+ `,
183
+ });
184
+ await run({ ...defaultOptions, postcssConfig }); // not throw
185
+ });
package/src/runner.ts CHANGED
@@ -9,7 +9,7 @@ import { emitGeneratedFiles } from './emitter/index.js';
9
9
  import { Loader } from './loader/index.js';
10
10
  import type { Resolver } from './resolver/index.js';
11
11
  import { createDefaultResolver } from './resolver/index.js';
12
- import { type Transformer } from './transformer/index.js';
12
+ import { createDefaultTransformer, type Transformer } from './transformer/index.js';
13
13
  import { isMatchByGlob } from './util.js';
14
14
 
15
15
  const glob = util.promisify(_glob);
@@ -40,6 +40,19 @@ 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;
43
56
  /**
44
57
  * Silent output. Do not show "files written" messages.
45
58
  * @default false
@@ -61,9 +74,15 @@ export async function run(options: RunnerOptions): Promise<void>;
61
74
  export async function run(options: RunnerOptions): Promise<Watcher | void> {
62
75
  const cwd = options.cwd ?? process.cwd();
63
76
  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 });
77
+ const resolver =
78
+ options.resolver ??
79
+ createDefaultResolver({
80
+ cwd,
81
+ sassLoadPaths: options.sassLoadPaths,
82
+ lessIncludePaths: options.lessIncludePaths,
83
+ webpackResolveAlias: options.webpackResolveAlias,
84
+ });
85
+ const transformer = options.transformer ?? createDefaultTransformer({ cwd, postcssConfig: options.postcssConfig });
67
86
  const distOptions = options.outDir
68
87
  ? {
69
88
  rootDir: cwd, // TODO: support `--rootDir` option
@@ -71,7 +90,7 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
71
90
  }
72
91
  : undefined;
73
92
 
74
- const loader = new Loader({ transformer: options.transformer, resolver });
93
+ const loader = new Loader({ transformer, resolver });
75
94
  const isExternalFile = (filePath: string) => {
76
95
  return !isMatchByGlob(filePath, options.pattern, { cwd });
77
96
  };
@@ -92,8 +111,13 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
92
111
  isExternalFile,
93
112
  });
94
113
  } catch (error) {
95
- // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
96
- console.error(chalk.red('[Error] ' + error));
114
+ if (error instanceof Error) {
115
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
116
+ console.error(chalk.red('[Error] ' + error.stack));
117
+ } else {
118
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
119
+ console.error(chalk.red('[Error] ' + error));
120
+ }
97
121
  throw error;
98
122
  }
99
123
  }
package/src/test/util.ts CHANGED
@@ -31,18 +31,24 @@ export function createComposesDeclarations(root: Root): Declaration[] {
31
31
 
32
32
  export function fakeToken(args: {
33
33
  name: Token['name'];
34
- originalLocations: { filePath?: Location['filePath']; start: Location['start'] }[];
34
+ originalLocations: { filePath?: Location['filePath']; start?: Location['start'] }[];
35
35
  }): Token {
36
36
  return {
37
37
  name: args.name,
38
- originalLocations: args.originalLocations.map((location) => ({
39
- filePath: location.filePath ?? getFixturePath('/test/1.css'),
40
- start: location.start,
41
- end: {
42
- line: location.start.line,
43
- column: location.start.column + args.name.length - 1,
44
- },
45
- })),
38
+ originalLocations: args.originalLocations.map((location) => {
39
+ if (location.filePath === undefined || location.start === undefined) {
40
+ return { filePath: undefined, start: undefined, end: undefined };
41
+ } else {
42
+ return {
43
+ filePath: location.filePath ?? getFixturePath('/test/1.css'),
44
+ start: location.start,
45
+ end: {
46
+ line: location.start.line,
47
+ column: location.start.column + args.name.length - 1,
48
+ },
49
+ };
50
+ }
51
+ }),
46
52
  };
47
53
  }
48
54
 
@@ -0,0 +1,71 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import dedent from 'dedent';
4
+ import { Loader } from '../loader/index.js';
5
+ import { createFixtures, getFixturePath } from '../test/util.js';
6
+ import { createDefaultTransformer } from './index.js';
7
+
8
+ const require = createRequire(import.meta.url);
9
+
10
+ const cwd = getFixturePath('/');
11
+ const loader = new Loader({ transformer: createDefaultTransformer({ cwd }) });
12
+
13
+ test('processes .scss with scss transformer', async () => {
14
+ createFixtures({
15
+ '/test/1.scss': dedent`
16
+ .a {
17
+ // scss feature test (nesting)
18
+ .a_1 { dummy: ''; }
19
+ }
20
+ `,
21
+ });
22
+ const result = await loader.load(getFixturePath('/test/1.scss'));
23
+ expect(result.tokens.map((token) => token.name)).toStrictEqual(['a', 'a_1']);
24
+ });
25
+
26
+ test('processes .less with less transformer', async () => {
27
+ createFixtures({
28
+ '/test/1.less': dedent`
29
+ .a {
30
+ // less feature test (nesting)
31
+ .a_1 { dummy: ''; }
32
+ }
33
+ `,
34
+ });
35
+ const result = await loader.load(getFixturePath('/test/1.less'));
36
+ expect(result.tokens.map((token) => token.name)).toStrictEqual(['a', 'a_1']);
37
+ });
38
+
39
+ test('processes .css with postcss transformer if postcssrc is found', async () => {
40
+ // if postcssrc is not found
41
+ const loader1 = new Loader({ transformer: createDefaultTransformer() });
42
+ createFixtures({
43
+ '/test/1.css': dedent`
44
+ $prefix: foo;
45
+ .$(prefix)_bar {}
46
+ `,
47
+ });
48
+ const result1 = await loader1.load(getFixturePath('/test/1.css'));
49
+ expect(result1.tokens.map((token) => token.name)).toStrictEqual(['$(prefix)']);
50
+
51
+ // if postcssrc is found
52
+ const uuid = randomUUID();
53
+ const loader2 = new Loader({
54
+ transformer: createDefaultTransformer({ cwd, postcssConfig: `${uuid}/postcss.config.js` }),
55
+ });
56
+ createFixtures({
57
+ [`/${uuid}/postcss.config.js`]: dedent`
58
+ module.exports = {
59
+ plugins: [
60
+ require('${require.resolve('postcss-simple-vars')}'),
61
+ ],
62
+ };
63
+ `,
64
+ '/test/1.css': dedent`
65
+ $prefix: foo;
66
+ .$(prefix)_bar {}
67
+ `,
68
+ });
69
+ const result2 = await loader2.load(getFixturePath('/test/1.css'));
70
+ expect(result2.tokens.map((token) => token.name)).toStrictEqual(['foo_bar']);
71
+ });
@@ -1,5 +1,7 @@
1
1
  import type { StrictlyResolver } from '../loader/index.js';
2
2
  import { createLessTransformer } from './less-transformer.js';
3
+ import type { PostcssTransformerOptions } from './postcss-transformer.js';
4
+ import { createPostcssTransformer } from './postcss-transformer.js';
3
5
  import { createScssTransformer } from './scss-transformer.js';
4
6
 
5
7
  /**
@@ -36,16 +38,22 @@ export const handleImportError = (packageName: string) => (e: unknown) => {
36
38
  throw e;
37
39
  };
38
40
 
39
- export const createDefaultTransformer: () => Transformer = () => {
41
+ export type DefaultTransformerOptions = PostcssTransformerOptions;
42
+
43
+ export const createDefaultTransformer: (defaultTransformerOptions?: DefaultTransformerOptions) => Transformer = (
44
+ defaultTransformerOptions,
45
+ ) => {
40
46
  const scssTransformer = createScssTransformer();
41
47
  const lessTransformer = createLessTransformer();
48
+ const postcssTransformer = createPostcssTransformer(defaultTransformerOptions);
42
49
  return async (source, options) => {
43
50
  if (options.from.endsWith('.scss')) {
44
51
  return scssTransformer(source, options);
45
52
  } else if (options.from.endsWith('.less')) {
46
53
  return lessTransformer(source, options);
54
+ } else {
55
+ // TODO: Support multi-stage transformations by sass and less.
56
+ return postcssTransformer(source, options);
47
57
  }
48
- // TODO: support postcss
49
- return false;
50
58
  };
51
59
  };
@@ -45,37 +45,37 @@ test('handles less features', async () => {
45
45
  {
46
46
  name: "b_1",
47
47
  originalLocations: [
48
- { filePath: "<fixtures>/test/2.less", start: { line: 1, column: 1 }, end: { line: 1, column: 3 } },
48
+ { filePath: "<fixtures>/test/2.less", start: { line: 1, column: 1 }, end: { line: 1, column: 4 } },
49
49
  ],
50
50
  },
51
51
  {
52
52
  name: "a_1",
53
53
  originalLocations: [
54
- { filePath: "<fixtures>/test/1.less", start: { line: 2, column: 1 }, end: { line: 2, column: 3 } },
54
+ { filePath: "<fixtures>/test/1.less", start: { line: 2, column: 1 }, end: { line: 2, column: 4 } },
55
55
  ],
56
56
  },
57
57
  {
58
58
  name: "a_2",
59
59
  originalLocations: [
60
- { filePath: "<fixtures>/test/1.less", start: { line: 3, column: 1 }, end: { line: 3, column: 3 } },
60
+ { filePath: "<fixtures>/test/1.less", start: { line: 3, column: 1 }, end: { line: 3, column: 4 } },
61
61
  ],
62
62
  },
63
63
  {
64
64
  name: "a_2_1",
65
65
  originalLocations: [
66
- { filePath: "<fixtures>/test/1.less", start: { line: 6, column: 3 }, end: { line: 6, column: 7 } },
66
+ { filePath: "<fixtures>/test/1.less", start: { line: 6, column: 3 }, end: { line: 6, column: 8 } },
67
67
  ],
68
68
  },
69
69
  {
70
70
  name: "a_2_2",
71
71
  originalLocations: [
72
- { filePath: "<fixtures>/test/1.less", start: { line: 7, column: 3 }, end: { line: 7, column: 7 } },
72
+ { filePath: "<fixtures>/test/1.less", start: { line: 7, column: 3 }, end: { line: 7, column: 8 } },
73
73
  ],
74
74
  },
75
75
  {
76
76
  name: "c",
77
77
  originalLocations: [
78
- { filePath: "<fixtures>/test/3.less", start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
78
+ { filePath: "<fixtures>/test/3.less", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
79
79
  ],
80
80
  },
81
81
  ],