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
@@ -0,0 +1,188 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { jest } from '@jest/globals';
4
+ import dedent from 'dedent';
5
+ import { Loader } from '../loader/index.js';
6
+ import { createFixtures, getFixturePath } from '../test/util.js';
7
+ import { createPostcssTransformer } from './postcss-transformer.js';
8
+
9
+ const cwd = getFixturePath('/');
10
+ const require = createRequire(import.meta.url);
11
+
12
+ // NOTE: postcss-load-config caches the configuration file using the path as a key.
13
+ // Therefore, change the path for each test case so that a new configuration file is always used.
14
+
15
+ test('handles postcss features', async () => {
16
+ const uuid = randomUUID();
17
+ const loader = new Loader({
18
+ transformer: createPostcssTransformer({
19
+ cwd,
20
+ postcssConfig: `${uuid}/postcss.config.js`,
21
+ }),
22
+ });
23
+ createFixtures({
24
+ [`/${uuid}/postcss.config.js`]: dedent`
25
+ module.exports = {
26
+ plugins: [
27
+ require('${require.resolve('postcss-simple-vars')}'),
28
+ ],
29
+ };
30
+ `,
31
+ '/test/1.css': dedent`
32
+ $prefix: foo;
33
+ .$(prefix)_bar {}
34
+ `,
35
+ });
36
+ const result = await loader.load(getFixturePath('/test/1.css'));
37
+
38
+ expect(result).toMatchInlineSnapshot(`
39
+ {
40
+ dependencies: [],
41
+ tokens: [
42
+ {
43
+ name: "foo_bar",
44
+ originalLocations: [
45
+ { filePath: "<fixtures>/test/1.css", start: { line: 2, column: 1 }, end: { line: 2, column: 8 } },
46
+ ],
47
+ },
48
+ ],
49
+ }
50
+ `);
51
+ });
52
+
53
+ test('tracks dependencies that have been pre-bundled by postcss compiler', async () => {
54
+ const uuid = randomUUID();
55
+ const loader = new Loader({
56
+ transformer: createPostcssTransformer({
57
+ cwd,
58
+ postcssConfig: `${uuid}/postcss.config.js`,
59
+ }),
60
+ });
61
+ const loadSpy = jest.spyOn(loader, 'load');
62
+ createFixtures({
63
+ [`/${uuid}/postcss.config.js`]: dedent`
64
+ module.exports = {
65
+ plugins: [
66
+ require('${require.resolve('postcss-import')}'),
67
+ ],
68
+ };
69
+ `,
70
+ '/test/1.css': dedent`
71
+ @import './2.css';
72
+ @import './3.css';
73
+ `,
74
+ '/test/2.css': ``,
75
+ '/test/3.css': `@import './4.css'`,
76
+ '/test/4.css': ``,
77
+ });
78
+ const result = await loader.load(getFixturePath('/test/1.css'));
79
+
80
+ // The files imported using @import are pre-bundled by the compiler.
81
+ // Therefore, `Loader#load` is not called to process other files.
82
+ expect(loadSpy).toBeCalledTimes(1);
83
+ expect(loadSpy).toHaveBeenNthCalledWith(1, getFixturePath('/test/1.css'));
84
+
85
+ // The files pre-bundled by the compiler are also included in `result.dependencies`
86
+ expect(result.dependencies).toStrictEqual(['/test/2.css', '/test/3.css', '/test/4.css'].map(getFixturePath));
87
+ });
88
+
89
+ test('resolves specifier using resolver', async () => {
90
+ const uuid = randomUUID();
91
+ const loader = new Loader({
92
+ transformer: createPostcssTransformer({
93
+ cwd,
94
+ postcssConfig: `${uuid}/postcss.config.js`,
95
+ }),
96
+ });
97
+ createFixtures({
98
+ [`/${uuid}/postcss.config.js`]: dedent`
99
+ module.exports = {
100
+ plugins: [
101
+ // When using postcss-import, the resolver of happy-css-modules is ignored.
102
+ // Therefore, we test here without postcss-import.
103
+ // require('${require.resolve('postcss-import')}'),
104
+ ],
105
+ };
106
+ `,
107
+ '/test/1.css': dedent`
108
+ @import 'package';
109
+ `,
110
+ '/node_modules/package/index.css': `.a {}`,
111
+ });
112
+ const result = await loader.load(getFixturePath('/test/1.css'));
113
+ expect(result.dependencies).toStrictEqual(['/node_modules/package/index.css'].map(getFixturePath));
114
+ });
115
+
116
+ test('ignores http(s) protocol file', async () => {
117
+ const uuid = randomUUID();
118
+ const loader = new Loader({
119
+ transformer: createPostcssTransformer({
120
+ cwd,
121
+ postcssConfig: `${uuid}/postcss.config.js`,
122
+ }),
123
+ });
124
+ createFixtures({
125
+ [`/${uuid}/postcss.config.js`]: dedent`
126
+ module.exports = {
127
+ plugins: [
128
+ // When using postcss-import, the resolver of happy-css-modules is ignored.
129
+ // Therefore, we test here without postcss-import.
130
+ // require('${require.resolve('postcss-import')}'),
131
+ ],
132
+ };
133
+ `,
134
+ '/test/1.css': dedent`
135
+ @import 'http://example.com/path/http.css';
136
+ @import 'https://example.com/path/https.css';
137
+ `,
138
+ });
139
+ const result = await loader.load(getFixturePath('/test/1.css'));
140
+ expect(result.dependencies).toStrictEqual([]);
141
+ });
142
+
143
+ test('returns false if postcssrc is not found', async () => {
144
+ const uuid = randomUUID();
145
+ const transformer = createPostcssTransformer({
146
+ cwd,
147
+ postcssConfig: `${uuid}/postcss.config.js`,
148
+ });
149
+ createFixtures({
150
+ '/test/1.css': dedent`
151
+ @import 'http://example.com/path/http.css';
152
+ @import 'https://example.com/path/https.css';
153
+ `,
154
+ });
155
+ expect(
156
+ await transformer('', {
157
+ from: getFixturePath('/test/1.css'),
158
+ isIgnoredSpecifier: () => false,
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ resolver: jest.fn() as any,
161
+ }),
162
+ ).toBe(false);
163
+ });
164
+
165
+ test('searches config from cwd if postcssConfig option is missing', async () => {
166
+ const uuid = randomUUID();
167
+ const cwd = `/${uuid}`;
168
+ const loader = new Loader({
169
+ transformer: createPostcssTransformer({
170
+ cwd: getFixturePath(cwd),
171
+ }),
172
+ });
173
+ createFixtures({
174
+ [`/${uuid}/postcss.config.js`]: dedent`
175
+ module.exports = {
176
+ plugins: [
177
+ require('${require.resolve('postcss-simple-vars')}'),
178
+ ],
179
+ };
180
+ `,
181
+ '/test/1.css': dedent`
182
+ $prefix: foo;
183
+ .$(prefix)_bar {}
184
+ `,
185
+ });
186
+ const result = await loader.load(getFixturePath('/test/1.css'));
187
+ expect(result.tokens.map((token) => token.name)).toStrictEqual(['foo_bar']);
188
+ });
@@ -0,0 +1,57 @@
1
+ import { createRequire } from 'node:module';
2
+ import { resolve } from 'node:path';
3
+ import { default as postcss, type Message } from 'postcss';
4
+ import type { Result } from 'postcss-load-config';
5
+ import type { Transformer } from '../index.js';
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ const postcssrc: typeof import('postcss-load-config') = require('postcss-load-config');
10
+
11
+ //ref: https://github.com/postcss/postcss-import#dependency-message-support
12
+ interface DependencyMessage extends Message {
13
+ type: 'dependency';
14
+ file: string;
15
+ parent: string;
16
+ }
17
+ function isDependencyMessage(message: Message): message is DependencyMessage {
18
+ return message.type === 'dependency';
19
+ }
20
+
21
+ export type PostcssTransformerOptions = {
22
+ cwd?: string | undefined;
23
+ /**
24
+ * The option compatible with postcss's `--config`. It is a relative or absolute path.
25
+ * @example '.'
26
+ * @example 'postcss.config.js'
27
+ * @example '/home/user/repository/src'
28
+ */
29
+ postcssConfig?: string | undefined;
30
+ };
31
+
32
+ export const createPostcssTransformer: (postcssTransformerOptions?: PostcssTransformerOptions) => Transformer = (
33
+ postcssTransformerOptions,
34
+ ) => {
35
+ const cwd = postcssTransformerOptions?.cwd ?? process.cwd();
36
+ const configSearchPath = postcssTransformerOptions?.postcssConfig
37
+ ? resolve(cwd, postcssTransformerOptions?.postcssConfig)
38
+ : cwd;
39
+ return async (source, options) => {
40
+ // NOTE: postcss-load-config cache the configuration file so is is not reloaded.
41
+ const postcssConfig: Result | undefined = await postcssrc({ cwd }, configSearchPath).catch((e) => {
42
+ if (e instanceof Error && e.message.includes('No PostCSS Config found')) return undefined;
43
+ throw e;
44
+ });
45
+ if (postcssConfig === undefined) return false;
46
+
47
+ const result = await postcss(postcssConfig.plugins).process(source, {
48
+ ...postcssConfig.options,
49
+ from: options.from,
50
+ map: { inline: false, absolute: true },
51
+ });
52
+
53
+ const dependencies = result.messages.filter(isDependencyMessage).map((message) => message.file);
54
+
55
+ return { css: result.css, map: result.map, dependencies };
56
+ };
57
+ };
@@ -51,44 +51,44 @@ test('handles sass features', async () => {
51
51
  {
52
52
  name: "b_1",
53
53
  originalLocations: [
54
- { filePath: "<fixtures>/test/2.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 3 } },
54
+ { filePath: "<fixtures>/test/2.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 4 } },
55
55
  ],
56
56
  },
57
57
  {
58
58
  name: "c",
59
59
  originalLocations: [
60
- { filePath: "<fixtures>/test/3.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
60
+ { filePath: "<fixtures>/test/3.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
61
61
  ],
62
62
  },
63
63
  {
64
64
  name: "a_1",
65
65
  originalLocations: [
66
- { filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 1 }, end: { line: 3, column: 3 } },
66
+ { filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 1 }, end: { line: 3, column: 4 } },
67
67
  ],
68
68
  },
69
69
  {
70
70
  name: "a_2",
71
71
  originalLocations: [
72
- { filePath: "<fixtures>/test/1.scss", start: { line: 4, column: 1 }, end: { line: 4, column: 3 } },
73
- { filePath: "<fixtures>/test/1.scss", start: { line: 7, column: 3 }, end: { line: 7, column: 5 } },
72
+ { filePath: "<fixtures>/test/1.scss", start: { line: 4, column: 1 }, end: { line: 4, column: 4 } },
73
+ { filePath: "<fixtures>/test/1.scss", start: { line: 7, column: 3 }, end: { line: 7, column: 6 } },
74
74
  ],
75
75
  },
76
76
  {
77
77
  name: "a_2_1",
78
78
  originalLocations: [
79
- { filePath: "<fixtures>/test/1.scss", start: { line: 7, column: 3 }, end: { line: 7, column: 7 } },
79
+ { filePath: "<fixtures>/test/1.scss", start: { line: 7, column: 3 }, end: { line: 7, column: 8 } },
80
80
  ],
81
81
  },
82
82
  {
83
83
  name: "a_2_2",
84
84
  originalLocations: [
85
- { filePath: "<fixtures>/test/1.scss", start: { line: 8, column: 3 }, end: { line: 8, column: 7 } },
85
+ { filePath: "<fixtures>/test/1.scss", start: { line: 8, column: 3 }, end: { line: 8, column: 8 } },
86
86
  ],
87
87
  },
88
88
  {
89
89
  name: "d",
90
90
  originalLocations: [
91
- { filePath: "<fixtures>/test/4.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
91
+ { filePath: "<fixtures>/test/4.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 2 } },
92
92
  ],
93
93
  },
94
94
  ],
@@ -3,41 +3,39 @@
3
3
  // Therefore, the workaround is now disabled. See
4
4
  // https://github.com/mizdra/happy-css-modules/issues/65#issuecomment-1229471950 for more information.
5
5
 
6
- import type { LegacyResult } from 'sass';
7
- import type { Transformer, TransformerOptions } from './index.js';
6
+ import type { LegacyOptions, LegacyResult } from 'sass';
7
+ import type { Transformer } from './index.js';
8
8
  import { handleImportError } from './index.js';
9
9
 
10
- async function renderSass(sass: typeof import('sass'), source: string, options: TransformerOptions) {
11
- return new Promise<LegacyResult>((resolve, reject) => {
12
- sass.render(
13
- {
14
- data: source,
15
- file: options.from,
16
- outFile: 'DUMMY', // Required for sourcemap output.
17
- sourceMap: true,
18
- importer: (url, prev, done) => {
19
- options
20
- .resolver(url, { request: prev })
21
- .then((resolved) => done({ file: resolved }))
22
- .catch((e) => done(e));
23
- },
24
- },
25
- (exception, result) => {
26
- if (exception) {
27
- reject(exception);
28
- } else {
29
- resolve(result!);
30
- }
31
- },
32
- );
33
- });
10
+ // For some reason, `util.promisify` does not work. Therefore, use the original promisify.
11
+ function promisifySassRender(sass: typeof import('sass')) {
12
+ return async (options: LegacyOptions<'async'>) => {
13
+ return new Promise<LegacyResult>((resolve, reject) => {
14
+ sass.render(options, (exception, result) => {
15
+ if (exception) reject(exception);
16
+ else resolve(result!);
17
+ });
18
+ });
19
+ };
34
20
  }
35
21
 
36
22
  export const createScssTransformer: () => Transformer = () => {
37
23
  let sass: typeof import('sass');
38
24
  return async (source, options) => {
39
25
  sass ??= (await import('sass').catch(handleImportError('sass'))).default;
40
- const result = await renderSass(sass, source, options);
26
+ const render = promisifySassRender(sass);
27
+ const result = await render({
28
+ data: source,
29
+ file: options.from,
30
+ outFile: 'DUMMY', // Required for sourcemap output.
31
+ sourceMap: true,
32
+ importer: (url, prev, done) => {
33
+ options
34
+ .resolver(url, { request: prev })
35
+ .then((resolved) => done({ file: resolved }))
36
+ .catch((e) => done(e));
37
+ },
38
+ });
41
39
  return { css: result.css.toString(), map: result.map!.toString(), dependencies: result.stats.includedFiles };
42
40
  };
43
41
  };