happy-css-modules 0.2.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 (154) hide show
  1. package/LICENSE.txt +23 -0
  2. package/README.md +124 -0
  3. package/bin/hcm.js +9 -0
  4. package/dist/cli.d.ts +6 -0
  5. package/dist/cli.js +69 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cli.test.d.ts +1 -0
  8. package/dist/cli.test.js +34 -0
  9. package/dist/cli.test.js.map +1 -0
  10. package/dist/emitter/dts.d.ts +14 -0
  11. package/dist/emitter/dts.js +106 -0
  12. package/dist/emitter/dts.js.map +1 -0
  13. package/dist/emitter/dts.test.d.ts +1 -0
  14. package/dist/emitter/dts.test.js +205 -0
  15. package/dist/emitter/dts.test.js.map +1 -0
  16. package/dist/emitter/file-system.d.ts +6 -0
  17. package/dist/emitter/file-system.js +26 -0
  18. package/dist/emitter/file-system.js.map +1 -0
  19. package/dist/emitter/file-system.test.d.ts +1 -0
  20. package/dist/emitter/file-system.test.js +34 -0
  21. package/dist/emitter/file-system.test.js.map +1 -0
  22. package/dist/emitter/index.d.ts +34 -0
  23. package/dist/emitter/index.js +42 -0
  24. package/dist/emitter/index.js.map +1 -0
  25. package/dist/emitter/index.test.d.ts +1 -0
  26. package/dist/emitter/index.test.js +118 -0
  27. package/dist/emitter/index.test.js.map +1 -0
  28. package/dist/emitter/source-map.d.ts +9 -0
  29. package/dist/emitter/source-map.js +16 -0
  30. package/dist/emitter/source-map.js.map +1 -0
  31. package/dist/emitter/source-map.test.d.ts +1 -0
  32. package/dist/emitter/source-map.test.js +13 -0
  33. package/dist/emitter/source-map.test.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +3 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/integration-test/go-to-definition.test.d.ts +1 -0
  38. package/dist/integration-test/go-to-definition.test.js +367 -0
  39. package/dist/integration-test/go-to-definition.test.js.map +1 -0
  40. package/dist/library/source-map/index.d.ts +8 -0
  41. package/dist/library/source-map/index.js +5 -0
  42. package/dist/library/source-map/index.js.map +1 -0
  43. package/dist/loader/index.d.ts +42 -0
  44. package/dist/loader/index.js +145 -0
  45. package/dist/loader/index.js.map +1 -0
  46. package/dist/loader/index.test.d.ts +1 -0
  47. package/dist/loader/index.test.js +290 -0
  48. package/dist/loader/index.test.js.map +1 -0
  49. package/dist/loader/postcss.d.ts +60 -0
  50. package/dist/loader/postcss.js +209 -0
  51. package/dist/loader/postcss.js.map +1 -0
  52. package/dist/loader/postcss.test.d.ts +1 -0
  53. package/dist/loader/postcss.test.js +236 -0
  54. package/dist/loader/postcss.test.js.map +1 -0
  55. package/dist/resolver/index.d.ts +20 -0
  56. package/dist/resolver/index.js +39 -0
  57. package/dist/resolver/index.js.map +1 -0
  58. package/dist/resolver/index.test.d.ts +1 -0
  59. package/dist/resolver/index.test.js +16 -0
  60. package/dist/resolver/index.test.js.map +1 -0
  61. package/dist/resolver/node-resolver.d.ts +2 -0
  62. package/dist/resolver/node-resolver.js +6 -0
  63. package/dist/resolver/node-resolver.js.map +1 -0
  64. package/dist/resolver/node-resolver.test.d.ts +1 -0
  65. package/dist/resolver/node-resolver.test.js +25 -0
  66. package/dist/resolver/node-resolver.test.js.map +1 -0
  67. package/dist/resolver/relative-resolver.d.ts +2 -0
  68. package/dist/resolver/relative-resolver.js +5 -0
  69. package/dist/resolver/relative-resolver.js.map +1 -0
  70. package/dist/resolver/relative-resolver.test.d.ts +1 -0
  71. package/dist/resolver/relative-resolver.test.js +12 -0
  72. package/dist/resolver/relative-resolver.test.js.map +1 -0
  73. package/dist/resolver/webpack-resolver.d.ts +2 -0
  74. package/dist/resolver/webpack-resolver.js +69 -0
  75. package/dist/resolver/webpack-resolver.js.map +1 -0
  76. package/dist/resolver/webpack-resolver.test.d.ts +1 -0
  77. package/dist/resolver/webpack-resolver.test.js +26 -0
  78. package/dist/resolver/webpack-resolver.test.js.map +1 -0
  79. package/dist/runner.d.ts +33 -0
  80. package/dist/runner.js +75 -0
  81. package/dist/runner.js.map +1 -0
  82. package/dist/runner.test.d.ts +1 -0
  83. package/dist/runner.test.js +113 -0
  84. package/dist/runner.test.js.map +1 -0
  85. package/dist/test/jest/resolver.cjs +30 -0
  86. package/dist/test/jest/resolver.cjs.map +1 -0
  87. package/dist/test/jest/resolver.d.cts +30 -0
  88. package/dist/test/tsserver.d.ts +27 -0
  89. package/dist/test/tsserver.js +104 -0
  90. package/dist/test/tsserver.js.map +1 -0
  91. package/dist/test/util.d.ts +29 -0
  92. package/dist/test/util.js +78 -0
  93. package/dist/test/util.js.map +1 -0
  94. package/dist/transformer/index.d.ts +23 -0
  95. package/dist/transformer/index.js +19 -0
  96. package/dist/transformer/index.js.map +1 -0
  97. package/dist/transformer/less-transformer.d.ts +2 -0
  98. package/dist/transformer/less-transformer.js +43 -0
  99. package/dist/transformer/less-transformer.js.map +1 -0
  100. package/dist/transformer/less-transformer.test.d.ts +1 -0
  101. package/dist/transformer/less-transformer.test.js +126 -0
  102. package/dist/transformer/less-transformer.test.js.map +1 -0
  103. package/dist/transformer/scss-transformer.d.ts +2 -0
  104. package/dist/transformer/scss-transformer.js +84 -0
  105. package/dist/transformer/scss-transformer.js.map +1 -0
  106. package/dist/transformer/scss-transformer.test.d.ts +1 -0
  107. package/dist/transformer/scss-transformer.test.js +132 -0
  108. package/dist/transformer/scss-transformer.test.js.map +1 -0
  109. package/dist/util.d.ts +19 -0
  110. package/dist/util.js +52 -0
  111. package/dist/util.js.map +1 -0
  112. package/dist/util.test.d.ts +1 -0
  113. package/dist/util.test.js +75 -0
  114. package/dist/util.test.js.map +1 -0
  115. package/package.json +106 -0
  116. package/src/__snapshots__/runner.test.ts.snap +34 -0
  117. package/src/cli.test.ts +38 -0
  118. package/src/cli.ts +70 -0
  119. package/src/emitter/dts.test.ts +266 -0
  120. package/src/emitter/dts.ts +134 -0
  121. package/src/emitter/file-system.test.ts +36 -0
  122. package/src/emitter/file-system.ts +24 -0
  123. package/src/emitter/index.test.ts +130 -0
  124. package/src/emitter/index.ts +92 -0
  125. package/src/emitter/source-map.test.ts +20 -0
  126. package/src/emitter/source-map.ts +17 -0
  127. package/src/index.ts +3 -0
  128. package/src/integration-test/go-to-definition.test.ts +371 -0
  129. package/src/library/README.md +3 -0
  130. package/src/library/source-map/index.ts +26 -0
  131. package/src/loader/index.test.ts +306 -0
  132. package/src/loader/index.ts +199 -0
  133. package/src/loader/postcss.test.ts +336 -0
  134. package/src/loader/postcss.ts +239 -0
  135. package/src/resolver/index.test.ts +17 -0
  136. package/src/resolver/index.ts +48 -0
  137. package/src/resolver/node-resolver.test.ts +26 -0
  138. package/src/resolver/node-resolver.ts +7 -0
  139. package/src/resolver/relative-resolver.test.ts +13 -0
  140. package/src/resolver/relative-resolver.ts +6 -0
  141. package/src/resolver/webpack-resolver.test.ts +33 -0
  142. package/src/resolver/webpack-resolver.ts +71 -0
  143. package/src/runner.test.ts +122 -0
  144. package/src/runner.ts +105 -0
  145. package/src/test/jest/resolver.cjs +30 -0
  146. package/src/test/tsserver.ts +176 -0
  147. package/src/test/util.ts +100 -0
  148. package/src/transformer/index.ts +44 -0
  149. package/src/transformer/less-transformer.test.ts +135 -0
  150. package/src/transformer/less-transformer.ts +55 -0
  151. package/src/transformer/scss-transformer.test.ts +142 -0
  152. package/src/transformer/scss-transformer.ts +94 -0
  153. package/src/util.test.ts +89 -0
  154. package/src/util.ts +67 -0
@@ -0,0 +1,44 @@
1
+ import type { StrictlyResolver } from '../loader/index.js';
2
+ import { createLessTransformer } from './less-transformer.js';
3
+ import { createScssTransformer } from './scss-transformer.js';
4
+
5
+ /**
6
+ * The value returned from the transformer.
7
+ * `false` means to skip transpiling on that file.
8
+ * */
9
+ export type TransformResult =
10
+ | {
11
+ /** The transformed code. */
12
+ css: string;
13
+ /** The source map from the transformed code to the original code. */
14
+ map: string | object;
15
+ dependencies: (string | URL)[];
16
+ }
17
+ | false;
18
+
19
+ export type TransformerOptions = {
20
+ /** The path of the file to transform. */
21
+ from: string;
22
+ /** The function to resolve the path of the imported file. */
23
+ resolver: StrictlyResolver;
24
+ };
25
+
26
+ /** The function to transform source code. */
27
+ export type Transformer = (source: string, options: TransformerOptions) => TransformResult | Promise<TransformResult>;
28
+
29
+ export const handleImportError = (packageName: string) => (e: unknown) => {
30
+ console.error(`${packageName} import failed. Did you forget to \`npm install -D ${packageName}\`?`);
31
+ throw e;
32
+ };
33
+
34
+ export const createDefaultTransformer: () => Transformer = () => async (source, options) => {
35
+ const scssTransformer = createScssTransformer();
36
+ const lessTransformer = createLessTransformer();
37
+ if (options.from.endsWith('.scss')) {
38
+ return scssTransformer(source, options);
39
+ } else if (options.from.endsWith('.less')) {
40
+ return lessTransformer(source, options);
41
+ }
42
+ // TODO: support postcss
43
+ return false;
44
+ };
@@ -0,0 +1,135 @@
1
+ import { jest } from '@jest/globals';
2
+ import dedent from 'dedent';
3
+ import { Loader } from '../loader/index.js';
4
+ import { createFixtures, getFixturePath } from '../test/util.js';
5
+ import { createLessTransformer } from './less-transformer.js';
6
+
7
+ const loader = new Loader({ transformer: createLessTransformer() });
8
+ const loadSpy = jest.spyOn(loader, 'load');
9
+
10
+ afterEach(() => {
11
+ loadSpy.mockClear();
12
+ });
13
+
14
+ test('handles less features', async () => {
15
+ createFixtures({
16
+ '/test/1.less': dedent`
17
+ @import './2.less'; // less feature test (@use)
18
+ .a_1 { dummy: ''; }
19
+ .a_2 {
20
+ dummy: '';
21
+ // less feature test (nesting)
22
+ .a_2_1 { dummy: ''; }
23
+ &_2 { dummy: ''; }
24
+ .b_1();
25
+ .b_2();
26
+ composes: a_1; // css module feature test (composes)
27
+ composes: c from './3.less'; // css module feature test (composes from other file)
28
+ }
29
+ `,
30
+ '/test/2.less': dedent`
31
+ .b_1 { dummy: ''; }
32
+ .b_2() { dummy: ''; }
33
+ `,
34
+ '/test/3.less': dedent`
35
+ .c { dummy: ''; }
36
+ `,
37
+ });
38
+ const result = await loader.load(getFixturePath('/test/1.less'));
39
+
40
+ // FIXME: The end position of 'a_2_2' is incorrect.
41
+ expect(result).toMatchInlineSnapshot(`
42
+ {
43
+ dependencies: ["<fixtures>/test/2.less", "<fixtures>/test/3.less"],
44
+ tokens: [
45
+ {
46
+ name: "b_1",
47
+ originalLocations: [
48
+ { filePath: "<fixtures>/test/2.less", start: { line: 1, column: 1 }, end: { line: 1, column: 3 } },
49
+ ],
50
+ },
51
+ {
52
+ name: "a_1",
53
+ originalLocations: [
54
+ { filePath: "<fixtures>/test/1.less", start: { line: 2, column: 1 }, end: { line: 2, column: 3 } },
55
+ ],
56
+ },
57
+ {
58
+ name: "a_2",
59
+ originalLocations: [
60
+ { filePath: "<fixtures>/test/1.less", start: { line: 3, column: 1 }, end: { line: 3, column: 3 } },
61
+ ],
62
+ },
63
+ {
64
+ name: "a_2_1",
65
+ originalLocations: [
66
+ { filePath: "<fixtures>/test/1.less", start: { line: 6, column: 3 }, end: { line: 6, column: 7 } },
67
+ ],
68
+ },
69
+ {
70
+ name: "a_2_2",
71
+ originalLocations: [
72
+ { filePath: "<fixtures>/test/1.less", start: { line: 7, column: 3 }, end: { line: 7, column: 7 } },
73
+ ],
74
+ },
75
+ {
76
+ name: "c",
77
+ originalLocations: [
78
+ { filePath: "<fixtures>/test/3.less", start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
79
+ ],
80
+ },
81
+ ],
82
+ }
83
+ `);
84
+ });
85
+
86
+ test('tracks dependencies that have been pre-bundled by less compiler', async () => {
87
+ createFixtures({
88
+ '/test/1.less': dedent`
89
+ @import './2.less';
90
+ @import './3.less';
91
+ `,
92
+ '/test/2.less': dedent`
93
+ `,
94
+ '/test/3.less': dedent`
95
+ @import './4.less';
96
+ `,
97
+ '/test/4.less': dedent`
98
+ `,
99
+ });
100
+ const result = await loader.load(getFixturePath('/test/1.less'));
101
+
102
+ // The files imported using @import are pre-bundled by the compiler.
103
+ // Therefore, `Loader#load` is not called to process other files.
104
+ expect(loadSpy).toBeCalledTimes(1);
105
+ expect(loadSpy).toHaveBeenNthCalledWith(1, getFixturePath('/test/1.less'));
106
+
107
+ // The files pre-bundled by the compiler are also included in `result.dependencies`
108
+ // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
109
+ expect(result.dependencies.sort()).toStrictEqual(
110
+ // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
111
+ ['/test/2.less', '/test/3.less', '/test/4.less'].map(getFixturePath).sort(),
112
+ );
113
+ });
114
+
115
+ test('resolves specifier using resolver', async () => {
116
+ createFixtures({
117
+ '/test/1.less': dedent`
118
+ @import 'package-1';
119
+ @import 'package-2';
120
+ // NOTE: less does not resolve files that are http(s) protocol.
121
+ // Therefore, the resolver will not be called for those files,
122
+ // and they will not be included in result.dependencies.
123
+ @import 'http://example.com/path/1.css';
124
+ @import 'https://example.com/path/1.css';
125
+ `,
126
+ '/node_modules/package-1/index.css': `.a {}`,
127
+ '/node_modules/package-2/index.less': `.a {}`,
128
+ });
129
+ const result = await loader.load(getFixturePath('/test/1.less'));
130
+ // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
131
+ expect(result.dependencies.sort()).toStrictEqual(
132
+ // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
133
+ ['/node_modules/package-1/index.css', '/node_modules/package-2/index.less'].map(getFixturePath).sort(),
134
+ );
135
+ });
@@ -0,0 +1,55 @@
1
+ import type { Transformer } from '../index.js';
2
+ import type { TransformerOptions } from './index.js';
3
+ import { handleImportError } from './index.js';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
6
+ function createLessPluginResolver(Less: typeof import('less'), options: TransformerOptions): Less.Plugin {
7
+ class ResolverFileManager extends Less.FileManager {
8
+ options: TransformerOptions;
9
+ constructor(options: TransformerOptions) {
10
+ super();
11
+ this.options = options;
12
+ }
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ public override supports(filename: string): boolean {
15
+ return true;
16
+ }
17
+ public override async loadFile(
18
+ filename: string,
19
+ currentDirectory: string,
20
+ options: Less.LoadFileOptions,
21
+ environment: Less.Environment,
22
+ ): Promise<Less.FileLoadResult> {
23
+ const resolved = await this.options.resolver(filename, { request: currentDirectory });
24
+ return super.loadFile(resolved, currentDirectory, options, environment);
25
+ }
26
+ }
27
+
28
+ class LessPluginResolver implements Less.Plugin {
29
+ options: TransformerOptions;
30
+ constructor(options: TransformerOptions) {
31
+ this.options = options;
32
+ }
33
+ public install(less: LessStatic, pluginManager: Less.PluginManager): void {
34
+ pluginManager.addFileManager(new ResolverFileManager(this.options));
35
+ }
36
+ public minVersion: [number, number, number] = [2, 1, 1];
37
+ }
38
+
39
+ return new LessPluginResolver(options);
40
+ }
41
+
42
+ export const createLessTransformer: () => Transformer = () => {
43
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
44
+ let less: typeof import('less');
45
+ return async (source, options) => {
46
+ less ??= (await import('less').catch(handleImportError('less'))).default;
47
+ const result = await less.render(source, {
48
+ filename: options.from,
49
+ sourceMap: {},
50
+ plugins: [createLessPluginResolver(less, options)],
51
+ syncImport: false, // Don't use `Less.FileManager#loadFileSync`.
52
+ });
53
+ return { css: result.css, map: result.map, dependencies: result.imports };
54
+ };
55
+ };
@@ -0,0 +1,142 @@
1
+ import { jest } from '@jest/globals';
2
+ import dedent from 'dedent';
3
+ import { Loader } from '../loader/index.js';
4
+ import { createFixtures, getFixturePath } from '../test/util.js';
5
+ import { createScssTransformer } from './scss-transformer.js';
6
+
7
+ const loader = new Loader({ transformer: createScssTransformer() });
8
+ const loadSpy = jest.spyOn(loader, 'load');
9
+
10
+ afterEach(() => {
11
+ loadSpy.mockClear();
12
+ });
13
+
14
+ test('handles sass features', async () => {
15
+ createFixtures({
16
+ '/test/1.scss': dedent`
17
+ @use './2.scss' as two; // sass feature test (@use)
18
+ @import './3.scss'; // css feature test (@import)
19
+ .a_1 { dummy: ''; }
20
+ .a_2 {
21
+ dummy: '';
22
+ // sass feature test (nesting)
23
+ .a_2_1 { dummy: ''; }
24
+ &_2 { dummy: ''; }
25
+ composes: a_1; // css module feature test (composes)
26
+ composes: d from './4.scss'; // css module feature test (composes from other file)
27
+ }
28
+ `,
29
+ '/test/2.scss': dedent`
30
+ .b_1 { dummy: ''; }
31
+ @mixin b_2 { dummy: ''; }
32
+ `,
33
+ '/test/3.scss': dedent`
34
+ .c { dummy: ''; }
35
+ `,
36
+ '/test/4.scss': dedent`
37
+ .d { dummy: ''; }
38
+ `,
39
+ });
40
+ const result = await loader.load(getFixturePath('/test/1.scss'));
41
+
42
+ // NOTE: There should be only one originalLocations for 'a_2', but there are multiple.
43
+ // This is probably due to an incorrect sourcemap output by the sass compiler.
44
+ // FIXME: The sass compiler or Loader implementation needs to be fixed.
45
+
46
+ // FIXME: The end position of 'a_2_2' is incorrect.
47
+ expect(result).toMatchInlineSnapshot(`
48
+ {
49
+ dependencies: ["<fixtures>/test/2.scss", "<fixtures>/test/3.scss", "<fixtures>/test/4.scss"],
50
+ tokens: [
51
+ {
52
+ name: "b_1",
53
+ originalLocations: [
54
+ { filePath: "<fixtures>/test/2.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 3 } },
55
+ ],
56
+ },
57
+ {
58
+ name: "c",
59
+ originalLocations: [
60
+ { filePath: "<fixtures>/test/3.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
61
+ ],
62
+ },
63
+ {
64
+ name: "a_1",
65
+ originalLocations: [
66
+ { filePath: "<fixtures>/test/1.scss", start: { line: 3, column: 1 }, end: { line: 3, column: 3 } },
67
+ ],
68
+ },
69
+ {
70
+ name: "a_2",
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 } },
74
+ ],
75
+ },
76
+ {
77
+ name: "a_2_1",
78
+ originalLocations: [
79
+ { filePath: "<fixtures>/test/1.scss", start: { line: 7, column: 3 }, end: { line: 7, column: 7 } },
80
+ ],
81
+ },
82
+ {
83
+ name: "a_2_2",
84
+ originalLocations: [
85
+ { filePath: "<fixtures>/test/1.scss", start: { line: 8, column: 3 }, end: { line: 8, column: 7 } },
86
+ ],
87
+ },
88
+ {
89
+ name: "d",
90
+ originalLocations: [
91
+ { filePath: "<fixtures>/test/4.scss", start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },
92
+ ],
93
+ },
94
+ ],
95
+ }
96
+ `);
97
+ });
98
+
99
+ test('tracks dependencies that have been pre-bundled by sass compiler', async () => {
100
+ createFixtures({
101
+ '/test/1.scss': dedent`
102
+ @import './2.scss';
103
+ @import './3.scss';
104
+ `,
105
+ '/test/2.scss': dedent`
106
+ `,
107
+ '/test/3.scss': dedent`
108
+ @import './4.scss';
109
+ `,
110
+ '/test/4.scss': dedent`
111
+ `,
112
+ });
113
+ const result = await loader.load(getFixturePath('/test/1.scss'));
114
+
115
+ // The files imported using @import are pre-bundled by the compiler.
116
+ // Therefore, `Loader#load` is not called to process other files.
117
+ expect(loadSpy).toBeCalledTimes(1);
118
+ expect(loadSpy).toHaveBeenNthCalledWith(1, getFixturePath('/test/1.scss'));
119
+
120
+ // The files pre-bundled by the compiler are also included in `result.dependencies`
121
+ expect(result.dependencies).toStrictEqual(['/test/2.scss', '/test/3.scss', '/test/4.scss'].map(getFixturePath));
122
+ });
123
+
124
+ test('resolves specifier using resolver', async () => {
125
+ createFixtures({
126
+ '/test/1.scss': dedent`
127
+ @import 'package-1';
128
+ @import 'package-2';
129
+ // NOTE: sass does not resolve files that are http(s) protocol.
130
+ // Therefore, the resolver will not be called for those files,
131
+ // and they will not be included in result.dependencies.
132
+ @import 'http://example.com/path/1.css';
133
+ @import 'https://example.com/path/1.css';
134
+ `,
135
+ '/node_modules/package-1/index.css': `.a {}`,
136
+ '/node_modules/package-2/index.scss': `.a {}`,
137
+ });
138
+ const result = await loader.load(getFixturePath('/test/1.scss'));
139
+ expect(result.dependencies).toStrictEqual(
140
+ ['/node_modules/package-1/index.css', '/node_modules/package-2/index.scss'].map(getFixturePath),
141
+ );
142
+ });
@@ -0,0 +1,94 @@
1
+ // NOTE: The workaround for using sass's modern API. happy-css-modules used to use this API.
2
+ // However, due to the implementation of custom resolvers, we have switched to the legacy API.
3
+ // Therefore, the workaround is now disabled. See
4
+ // https://github.com/mizdra/happy-css-modules/issues/65#issuecomment-1229471950 for more information.
5
+
6
+ import type { LegacyResult } from 'sass';
7
+ import type { Transformer, TransformerOptions } from './index.js';
8
+ import { handleImportError } from './index.js';
9
+
10
+ // const IS_JEST_ENVIRONMENT = process.env.JEST_WORKER_ID !== undefined;
11
+
12
+ // function verifyJestEnvironment() {
13
+ // if (
14
+ // !(
15
+ // 'window' in global &&
16
+ // 'location' in global &&
17
+ // // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ // 'href' in (global as any).location &&
19
+ // // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ // typeof (global as any).location.href === 'string' &&
21
+ // // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ // (global as any).location.href.startsWith('http://')
23
+ // )
24
+ // ) {
25
+ // throw new Error(
26
+ // 'To use dart-sass with jest, dummy `global.window` and `global.location.href` must be set. See https://github.com/sass/dart-sass/issues/1692#issuecomment-1229219993 .',
27
+ // );
28
+ // }
29
+ // }
30
+
31
+ // const createImporterForJest: (from: string) => Importer<'async'> = (from) => ({
32
+ // canonicalize(url) {
33
+ // // NOTE: The format of `url` changes depending on the import source.
34
+ // //
35
+ // // - When `from === '/test/1.scss'` and `@import './2.scss'` in `/test/1.scss` is resolved, `url === '2.scss'`.
36
+ // // - When `from === '/test/1.scss'` and `@import './3.scss'` in `/test/2.scss` is resolved, `url === 'file:///test/3.scss'`.
37
+ // //
38
+ // // That is, the paths of @import statements written to the `from` file is passed through unresolved,
39
+ // // but paths written to other files is passed through resolved to absolute paths.
40
+ // return new URL(url, pathToFileURL(from));
41
+ // },
42
+ // async load(canonicalUrl) {
43
+ // return {
44
+ // contents: await readFile(fileURLToPath(canonicalUrl.href), 'utf8'),
45
+ // syntax: 'scss',
46
+ // sourceMapUrl: canonicalUrl,
47
+ // };
48
+ // },
49
+ // });
50
+
51
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
52
+ async function renderSass(sass: typeof import('sass'), source: string, options: TransformerOptions) {
53
+ return new Promise<LegacyResult>((resolve, reject) => {
54
+ sass.render(
55
+ {
56
+ data: source,
57
+ file: options.from,
58
+ outFile: 'DUMMY', // Required for sourcemap output.
59
+ sourceMap: true,
60
+ importer: (url, prev, done) => {
61
+ options
62
+ .resolver(url, { request: prev })
63
+ .then((resolved) => done({ file: resolved }))
64
+ .catch((e) => done(e));
65
+ },
66
+ },
67
+ (exception, result) => {
68
+ if (exception) {
69
+ reject(exception);
70
+ } else {
71
+ resolve(result!);
72
+ }
73
+ },
74
+ );
75
+ });
76
+ }
77
+
78
+ export const createScssTransformer: () => Transformer = () => {
79
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
80
+ let sass: typeof import('sass');
81
+ return async (source, options) => {
82
+ sass ??= (await import('sass').catch(handleImportError('sass'))).default;
83
+ const result = await renderSass(sass, source, options);
84
+ return { css: result.css.toString(), map: result.map!.toString(), dependencies: result.stats.includedFiles };
85
+
86
+ // if (IS_JEST_ENVIRONMENT) verifyJestEnvironment();
87
+ // const result = await sass.default.compileStringAsync(source, {
88
+ // url: pathToFileURL(from),
89
+ // sourceMap: true,
90
+ // importers: IS_JEST_ENVIRONMENT ? [createImporterForJest(from)] : [],
91
+ // });
92
+ // return { css: result.css, map: result.sourceMap!, dependencies: result.loadedUrls };
93
+ };
94
+ };
@@ -0,0 +1,89 @@
1
+ import { join } from 'path';
2
+ import { hasProp, isObject, isSystemError, unique, uniqueBy } from '../src/util.js';
3
+ import { createFixtures, getFixturePath } from './test/util.js';
4
+ import { isMatchByGlob, exists } from './util.js';
5
+
6
+ function fakeSystemError({ code }: { code: string }) {
7
+ const error = new Error();
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ (error as any).code = code;
10
+ return error;
11
+ }
12
+
13
+ test('isSystemError', () => {
14
+ expect(isSystemError(fakeSystemError({ code: 'ENOENT' }))).toBe(true);
15
+ expect(isSystemError(fakeSystemError({ code: 'EACCES' }))).toBe(true);
16
+ expect(isSystemError(new Error('ENOENT'))).toBe(false);
17
+ expect(isSystemError({ code: 'ENOENT' })).toBe(false);
18
+ });
19
+
20
+ test('isObject', () => {
21
+ expect(isObject({})).toBe(true);
22
+ expect(isObject({ a: '1' })).toBe(true);
23
+ expect(isObject([])).toBe(true);
24
+ expect(
25
+ isObject(() => {
26
+ /* noop */
27
+ }),
28
+ ).toBe(true);
29
+ expect(isObject(null)).toBe(false);
30
+ expect(isObject(undefined)).toBe(false);
31
+ expect(isObject(1)).toBe(false);
32
+ expect(isObject('1')).toBe(false);
33
+ expect(isObject(true)).toBe(false);
34
+ });
35
+
36
+ test('hasProp', () => {
37
+ expect(hasProp({ a: '1' }, 'a')).toBe(true);
38
+ expect(hasProp({ a: '1' }, 'b')).toBe(false);
39
+ // it can check prototype
40
+ expect(hasProp({}, 'toString')).toBe(true);
41
+ expect(hasProp([], 'length')).toBe(true);
42
+ });
43
+
44
+ test('unique', () => {
45
+ expect(unique([0, 1, 1, 2, 1])).toStrictEqual([0, 1, 2]);
46
+ });
47
+
48
+ test('uniqueBy', () => {
49
+ expect(uniqueBy([], () => 0)).toStrictEqual([]);
50
+ expect(
51
+ uniqueBy(
52
+ [
53
+ { key: 'a', value: 0 },
54
+ { key: 'a', value: 1 },
55
+ { key: 'b', value: 2 },
56
+ { key: 'b', value: 3 },
57
+ { key: 'c', value: 4 },
58
+ { key: 'c', value: 5 },
59
+ ],
60
+ (el) => el.key,
61
+ ),
62
+ ).toStrictEqual([
63
+ { key: 'a', value: 0 },
64
+ { key: 'b', value: 2 },
65
+ { key: 'c', value: 4 },
66
+ ]);
67
+ });
68
+
69
+ test('exists', async () => {
70
+ createFixtures({
71
+ '/test/1.css': `.a {}`,
72
+ });
73
+ expect(await exists(getFixturePath('/test/1.css'))).toBe(true);
74
+ expect(await exists(getFixturePath('/test/2.css'))).toBe(false);
75
+ });
76
+
77
+ test('isMatchByGlob', () => {
78
+ const cwd = process.cwd();
79
+ expect(isMatchByGlob(join(cwd, '1.css'), '*.css', { cwd })).toBe(true);
80
+ expect(isMatchByGlob(join(cwd, '1.scss'), '*.css', { cwd })).toBe(false);
81
+ expect(isMatchByGlob(join(cwd, 'dir/1.css'), '**/*.css', { cwd })).toBe(true);
82
+ expect(isMatchByGlob(join(cwd, 'dir/dir/1.css'), '**/*.css', { cwd })).toBe(true);
83
+ expect(isMatchByGlob(join(cwd, '1.css'), '*.{css,scss}', { cwd })).toBe(true);
84
+ expect(isMatchByGlob(join(cwd, '1.scss'), '*.{css,scss}', { cwd })).toBe(true);
85
+ expect(isMatchByGlob(join(cwd, '1.less'), '*.{css,scss}', { cwd })).toBe(false);
86
+ expect(isMatchByGlob(join(cwd, '1.css'), '!(*.css)', { cwd })).toBe(false);
87
+ expect(isMatchByGlob(join(cwd, '1.scss'), '!(*.css)', { cwd })).toBe(true);
88
+ expect(isMatchByGlob(join(cwd, '1.less'), '!(*.css)', { cwd })).toBe(true);
89
+ });
package/src/util.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { constants } from 'fs';
2
+ import { access } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import minimatch from 'minimatch';
5
+ /**
6
+ * The SystemError type of Node.js.
7
+ * @see https://nodejs.org/api/errors.html#class-systemerror
8
+ */
9
+ export interface SystemError {
10
+ code: string;
11
+ }
12
+
13
+ export function isSystemError(value: unknown): value is SystemError {
14
+ return (
15
+ isObject(value) &&
16
+ hasProp(value, 'constructor') &&
17
+ isObject(value.constructor) &&
18
+ hasProp(value.constructor, 'name') &&
19
+ value.constructor.name === 'Error' &&
20
+ hasProp(value, 'code') &&
21
+ typeof value.code === 'string'
22
+ );
23
+ }
24
+
25
+ export function isObject(value: unknown): value is object {
26
+ return (typeof value === 'object' || typeof value === 'function') && value !== null;
27
+ }
28
+
29
+ export function hasProp<T extends string>(obj: object, prop: T): obj is { [key in T]: unknown } {
30
+ return prop in obj;
31
+ }
32
+
33
+ export function unique<T>(array: T[]): T[] {
34
+ return [...new Set(array)];
35
+ }
36
+
37
+ export function uniqueBy<T, U>(arr: T[], fn: (el: T) => U): T[] {
38
+ const result: T[] = [];
39
+ const keys = new Set<U>();
40
+ for (const el of arr) {
41
+ const key = fn(el);
42
+ if (!keys.has(key)) {
43
+ keys.add(key);
44
+ result.push(el);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+
50
+ export function sleepSync(ms: number) {
51
+ const start = Date.now();
52
+ // eslint-disable-next-line no-empty
53
+ while (Date.now() - start < ms) {}
54
+ }
55
+
56
+ export async function exists(path: string): Promise<boolean> {
57
+ try {
58
+ await access(path, constants.F_OK);
59
+ return true;
60
+ } catch (e) {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ export function isMatchByGlob(filePath: string, pattern: string, options: { cwd: string }): boolean {
66
+ return minimatch(filePath, join(options.cwd, pattern));
67
+ }