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,13 @@
1
+ import { getFixturePath } from '../test/util.js';
2
+ import { createRelativeResolver } from './relative-resolver.js';
3
+
4
+ const relativeResolver = createRelativeResolver();
5
+ const request = getFixturePath('/test/1.css');
6
+
7
+ test('resolves specifier with relative mechanism', async () => {
8
+ expect(await relativeResolver('2.css', { request })).toBe(getFixturePath('/test/2.css'));
9
+ expect(await relativeResolver('./3.css', { request })).toBe(getFixturePath('/test/3.css'));
10
+ expect(await relativeResolver('dir/4.css', { request })).toBe(getFixturePath('/test/dir/4.css'));
11
+ expect(await relativeResolver('../5.css', { request })).toBe(getFixturePath('/5.css'));
12
+ expect(await relativeResolver(getFixturePath('/test/6.css'), { request })).toBe(getFixturePath('/test/6.css'));
13
+ });
@@ -0,0 +1,6 @@
1
+ import { dirname, resolve } from 'path';
2
+ import type { Resolver } from './index.js';
3
+
4
+ export const createRelativeResolver: () => Resolver = () => (specifier, options) => {
5
+ return resolve(dirname(options.request), specifier);
6
+ };
@@ -0,0 +1,33 @@
1
+ import { createFixtures, getFixturePath } from '../test/util.js';
2
+ import { createWebpackResolver } from './webpack-resolver.js';
3
+
4
+ const webpackResolver = createWebpackResolver();
5
+ const request = getFixturePath('/test/1.css');
6
+
7
+ test('resolves specifier with webpack mechanism', async () => {
8
+ createFixtures({
9
+ '/node_modules/package-1/index.css': `.a {}`,
10
+ '/node_modules/package-2/index.css': `.a {}`,
11
+ '/node_modules/package-3/index.css': `.a {}`,
12
+ '/node_modules/package-4/package.json': `{ "style": "./style.css" }`,
13
+ '/node_modules/package-4/style.css': `.a {}`,
14
+ '/node_modules/@scoped/package-5/index.css': `.a {}`,
15
+ '/node_modules/package-6/index.css': `.a {}`,
16
+ '/node_modules/package-7/index.scss': `.a { dummy: ''; }`,
17
+ '/node_modules/package-8/index.less': `.a { dummy: ''; }`,
18
+ });
19
+ expect(await webpackResolver('~package-1/index.css', { request })).toBe(
20
+ getFixturePath('/node_modules/package-1/index.css'),
21
+ );
22
+ expect(await webpackResolver('~package-2', { request })).toBe(getFixturePath('/node_modules/package-2/index.css'));
23
+ expect(await webpackResolver('~package-3/', { request })).toBe(getFixturePath('/node_modules/package-3/index.css'));
24
+ expect(await webpackResolver('~package-4', { request })).toBe(getFixturePath('/node_modules/package-4/style.css'));
25
+ expect(await webpackResolver('~@scoped/package-5/index.css', { request })).toBe(
26
+ getFixturePath('/node_modules/@scoped/package-5/index.css'),
27
+ );
28
+ expect(await webpackResolver('package-6/index.css', { request })).toBe(
29
+ getFixturePath('/node_modules/package-6/index.css'),
30
+ );
31
+ expect(await webpackResolver('~package-7', { request })).toBe(getFixturePath('/node_modules/package-7/index.scss'));
32
+ expect(await webpackResolver('~package-8', { request })).toBe(getFixturePath('/node_modules/package-8/index.less'));
33
+ });
@@ -0,0 +1,71 @@
1
+ import { dirname } from 'path';
2
+ import enhancedResolve from 'enhanced-resolve';
3
+ import { exists } from '../util.js';
4
+ import type { Resolver } from './index.js';
5
+
6
+ /**
7
+ * A resolver compatible with css-loader.
8
+ *
9
+ * @see https://github.com/webpack-contrib/css-loader/blob/897e7dd250ccdb0d31e6c66d4cd0d009f2022a85/src/plugins/postcss-import-parser.js#L228-L235
10
+ */
11
+ const cssLoaderResolver = enhancedResolve.create.sync({
12
+ dependencyType: 'css',
13
+ conditionNames: ['style'],
14
+ // We are not sure how "..." affects behavior...
15
+ mainFields: ['css', 'style', 'main', '...'],
16
+ mainFiles: ['index', '...'],
17
+ extensions: ['.css', '...'],
18
+ preferRelative: true,
19
+ });
20
+
21
+ /**
22
+ * A resolver compatible with sass-loader.
23
+ *
24
+ * @see https://github.com/webpack-contrib/sass-loader/blob/49a578a218574ddc92a597c7e365b6c21960717e/src/utils.js#L531-L539
25
+ */
26
+ const sassLoaderResolver = enhancedResolve.create.sync({
27
+ dependencyType: 'sass',
28
+ conditionNames: ['sass', 'style'],
29
+ mainFields: ['sass', 'style', 'main', '...'],
30
+ mainFiles: ['_index', 'index', '...'],
31
+ extensions: ['.sass', '.scss', '.css'],
32
+ restrictions: [/\.((sa|sc|c)ss)$/i],
33
+ preferRelative: true,
34
+ });
35
+
36
+ /**
37
+ * A resolver compatible with less-loader.
38
+ *
39
+ * @see https://github.com/webpack-contrib/less-loader/blob/d74f740c100c4006b00dfb3e02c6d5aaf8713519/src/utils.js#L35-L42
40
+ */
41
+ const lessLoaderResolver = enhancedResolve.create.sync({
42
+ dependencyType: 'less',
43
+ conditionNames: ['less', 'style'],
44
+ mainFields: ['less', 'style', 'main', '...'],
45
+ mainFiles: ['index', '...'],
46
+ extensions: ['.less', '.css'],
47
+ preferRelative: true,
48
+ });
49
+
50
+ // TODO: Support `resolve.alias` for Node.js API
51
+ export const createWebpackResolver: () => Resolver = () => async (specifier, options) => {
52
+ // `~` prefix is optional.
53
+ // ref: https://github.com/webpack-contrib/less-loader/tree/454e187f58046356c3d383d67fda763db8bfc528#webpack-resolver
54
+ if (specifier.startsWith('~')) specifier = specifier.slice(1);
55
+
56
+ // NOTE: In theory, `sassLoaderResolver` should only be used when the resolver is called from `sassTransformer`.
57
+ // However, we do not implement such behavior because it is cumbersome. If someone wants it, we will implement it.
58
+ const resolvers = [cssLoaderResolver, sassLoaderResolver, lessLoaderResolver];
59
+ for (const resolver of resolvers) {
60
+ try {
61
+ const resolved = resolver(dirname(options.request), specifier);
62
+ if (resolved !== false) {
63
+ const isExists = await exists(resolved);
64
+ if (isExists) return resolved;
65
+ }
66
+ } catch (e) {
67
+ // noop
68
+ }
69
+ }
70
+ return false;
71
+ };
@@ -0,0 +1,122 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ import { jest } from '@jest/globals';
3
+ import chalk from 'chalk';
4
+ import dedent from 'dedent';
5
+ import AggregateError from 'es-aggregate-error';
6
+ import { run } from './runner.js';
7
+ import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test/util.js';
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
10
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
11
+
12
+ const defaultOptions = {
13
+ pattern: 'test/**/*.{css,scss}',
14
+ declarationMap: true,
15
+ silent: true,
16
+ cwd: getFixturePath('/'),
17
+ };
18
+
19
+ test('generates .d.ts and .d.ts.map', async () => {
20
+ createFixtures({
21
+ '/test/1.css': '.a {}',
22
+ '/test/2.css': '.b {}',
23
+ });
24
+ await run({ ...defaultOptions });
25
+ expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatchSnapshot();
26
+ expect(await readFile(getFixturePath('/test/1.css.d.ts.map'), 'utf8')).toMatchSnapshot();
27
+ expect(await readFile(getFixturePath('/test/2.css.d.ts'), 'utf8')).toMatchSnapshot();
28
+ expect(await readFile(getFixturePath('/test/2.css.d.ts.map'), 'utf8')).toMatchSnapshot();
29
+ });
30
+
31
+ test.todo('changes dts format with localsConvention options');
32
+ test('does not emit declaration map if declarationMap is false', async () => {
33
+ createFixtures({
34
+ '/test/1.css': '.a {}',
35
+ });
36
+ await run({ ...defaultOptions, declarationMap: false });
37
+ await expect(readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).resolves.not.toThrow();
38
+ await expect(readFile(getFixturePath('/test/1.css.d.ts.map'), 'utf8')).rejects.toThrow(/ENOENT/);
39
+ });
40
+ test('supports transformer', async () => {
41
+ createFixtures({
42
+ '/test/1.scss': `.a { dummy: ''; }`,
43
+ });
44
+ await run({ ...defaultOptions });
45
+ expect(await readFile(getFixturePath('/test/1.scss.d.ts'), 'utf8')).toMatchSnapshot();
46
+ expect(await readFile(getFixturePath('/test/1.scss.d.ts.map'), 'utf8')).toMatchSnapshot();
47
+ });
48
+ test('watches for changes in files', async () => {
49
+ createFixtures({
50
+ '/test': {}, // empty directory
51
+ });
52
+ const watcher = await run({ ...defaultOptions, watch: true });
53
+
54
+ await writeFile(getFixturePath('/test/1.css'), '.a-1 {}');
55
+ await waitForAsyncTask(500); // Wait until the file is written
56
+ expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatch(/a-1/);
57
+
58
+ await writeFile(getFixturePath('/test/1.css'), '.a-2 {}');
59
+ await waitForAsyncTask(500); // Wait until the file is written
60
+ expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatch(/a-2/);
61
+
62
+ await watcher.close();
63
+ });
64
+ test('returns an error if the file fails to process in non-watch mode', async () => {
65
+ createFixtures({
66
+ '/test/1.css': '.a {}',
67
+ '/test/2.css': 'INVALID SYNTAX',
68
+ '/test/3.css': 'INVALID SYNTAX',
69
+ });
70
+ const maybeError = await run({ ...defaultOptions, watch: false }).catch((e) => e);
71
+
72
+ // The errors are aggregated into AggregateError.
73
+ expect(maybeError).toBeInstanceOf(AggregateError);
74
+ const error = maybeError as AggregateError;
75
+ expect(error.message).toMatchInlineSnapshot(`"Failed to process files"`);
76
+ expect(error.errors).toHaveLength(2);
77
+ expect(error.errors[0]).toMatchInlineSnapshot(`<fixtures>/test/2.css:1:1: Unknown word`);
78
+ expect(error.errors[1]).toMatchInlineSnapshot(`<fixtures>/test/3.css:1:1: Unknown word`);
79
+
80
+ // The error is logged to console.error.
81
+ expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
82
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
83
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]));
84
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
85
+ expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1]));
86
+
87
+ // The valid files are emitted.
88
+ expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
89
+ expect(await exists(getFixturePath('/test/1.css.d.ts.map'))).toBe(true);
90
+ });
91
+ describe('handles external files', () => {
92
+ beforeEach(() => {
93
+ createFixtures({
94
+ '/test/1.css': dedent`
95
+ @import './2.css';
96
+ @import 'external-library';
97
+ .a {}
98
+ `,
99
+ '/test/2.css': `.b {}`,
100
+ '/node_modules/external-library/index.css': `.c {}`,
101
+ });
102
+ });
103
+ test('do not emit .dts for external files', async () => {
104
+ await run({ ...defaultOptions });
105
+ expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
106
+ expect(await exists(getFixturePath('/test/2.css.d.ts'))).toBe(true);
107
+ expect(await exists(getFixturePath('/node_modules/external-library/index.css.d.ts'))).toBe(false);
108
+ });
109
+ test('treats imported tokens from external files the same as local tokens', async () => {
110
+ await run({ ...defaultOptions });
111
+ expect(await readFile(getFixturePath('/test/1.css.d.ts'), 'utf8')).toMatchInlineSnapshot(`
112
+ "declare const styles:
113
+ & Readonly<Pick<(typeof import("./2.css"))["default"], "b">>
114
+ & Readonly<{ "c": string }>
115
+ & Readonly<{ "a": string }>
116
+ ;
117
+ export default styles;
118
+ //# sourceMappingURL=./1.css.d.ts.map
119
+ "
120
+ `);
121
+ });
122
+ });
package/src/runner.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { resolve } from 'path';
2
+ import * as process from 'process';
3
+ import * as util from 'util';
4
+ import chalk from 'chalk';
5
+ import * as chokidar from 'chokidar';
6
+ import AggregateError from 'es-aggregate-error';
7
+ import _glob from 'glob';
8
+ import { emitGeneratedFiles } from './emitter/index.js';
9
+ import { Loader } from './loader/index.js';
10
+ import type { Resolver } from './resolver/index.js';
11
+ import { type Transformer } from './transformer/index.js';
12
+ import { isMatchByGlob } from './util.js';
13
+
14
+ const glob = util.promisify(_glob);
15
+
16
+ export type Watcher = {
17
+ close: () => Promise<void>;
18
+ };
19
+
20
+ export type LocalsConvention = 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashesOnly' | undefined;
21
+
22
+ export interface RunnerOptions {
23
+ pattern: string;
24
+ outDir?: string | undefined;
25
+ watch?: boolean | undefined;
26
+ localsConvention?: LocalsConvention | undefined;
27
+ declarationMap?: boolean | undefined;
28
+ transformer?: Transformer | undefined;
29
+ resolver?: Resolver | undefined;
30
+ /**
31
+ * Silent output. Do not show "files written" messages.
32
+ * @default false
33
+ */
34
+ silent?: boolean | undefined;
35
+ /** Working directory path. */
36
+ cwd?: string | undefined;
37
+ }
38
+
39
+ type OverrideProp<T, K extends keyof T, V extends T[K]> = Omit<T, K> & { [P in K]: V };
40
+
41
+ /**
42
+ * Run typed-css-module.
43
+ * @param options Runner options.
44
+ * @returns Returns `Promise<Watcher>` if `options.watch` is `true`, `Promise<void>` if `false`.
45
+ */
46
+ export async function run(options: OverrideProp<RunnerOptions, 'watch', true>): Promise<Watcher>;
47
+ export async function run(options: RunnerOptions): Promise<void>;
48
+ export async function run(options: RunnerOptions): Promise<Watcher | void> {
49
+ const loader = new Loader({ transformer: options.transformer, resolver: options.resolver });
50
+ const distOptions = options.outDir
51
+ ? {
52
+ rootDir: process.cwd(), // TODO: support `--rootDir` option
53
+ outDir: options.outDir,
54
+ }
55
+ : undefined;
56
+ const isExternalFile = (filePath: string) => {
57
+ return !isMatchByGlob(filePath, options.pattern, { cwd: options.cwd ?? process.cwd() });
58
+ };
59
+
60
+ async function processFile(filePath: string) {
61
+ try {
62
+ const result = await loader.load(filePath);
63
+ await emitGeneratedFiles({
64
+ filePath,
65
+ tokens: result.tokens,
66
+ distOptions,
67
+ emitDeclarationMap: options.declarationMap,
68
+ dtsFormatOptions: {
69
+ localsConvention: options.localsConvention,
70
+ },
71
+ silent: options.silent ?? false,
72
+ cwd: options.cwd ?? process.cwd(),
73
+ isExternalFile,
74
+ });
75
+ } catch (error) {
76
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
77
+ console.error(chalk.red('[Error] ' + error));
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ if (options.watch) {
83
+ if (!options.silent) console.log('Watch ' + options.pattern + '...');
84
+ const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], options.cwd ? { cwd: options.cwd } : {});
85
+ watcher.on('all', (eventName, filePath) => {
86
+ if (eventName === 'add' || eventName === 'change') {
87
+ processFile(resolve(options.cwd ?? process.cwd(), filePath)).catch(() => {
88
+ // TODO: Emit a error by `Watcher#onerror`
89
+ });
90
+ }
91
+ });
92
+ return { close: async () => watcher.close() };
93
+ } else {
94
+ const filePaths = (await glob(options.pattern, { dot: true, cwd: options.cwd ?? process.cwd() }))
95
+ // convert relative path to absolute path
96
+ .map((file) => resolve(options.cwd ?? process.cwd(), file));
97
+
98
+ // TODO: Use `@file-cache/core` to process only files that have changed
99
+ const errors: unknown[] = [];
100
+ for (const filePath of filePaths) {
101
+ await processFile(filePath).catch((e: unknown) => errors.push(e));
102
+ }
103
+ if (errors.length > 0) throw new AggregateError(errors, 'Failed to process files');
104
+ }
105
+ }
@@ -0,0 +1,30 @@
1
+ const nativeModule = require('node:module');
2
+
3
+ // workaround for https://github.com/facebook/jest/issues/12270#issuecomment-1194746382
4
+
5
+ /**
6
+ * @typedef {{
7
+ * basedir: string;
8
+ * conditions?: Array<string>;
9
+ * defaultResolver: (path: string, options: ResolverOptions) => string;
10
+ * extensions?: Array<string>;
11
+ * moduleDirectory?: Array<string>;
12
+ * paths?: Array<string>;
13
+ * packageFilter?: (pkg: any, file: string, dir: string) => any;
14
+ * pathFilter?: (pkg: any, path: string, relativePath: string) => string;
15
+ * rootDir?: string;
16
+ * }} ResolverOptions
17
+ * */
18
+
19
+ /** @type {(path: string, options: ResolverOptions) => string} */
20
+ function resolver(module, options) {
21
+ const { basedir, defaultResolver } = options;
22
+ try {
23
+ return defaultResolver(module, options);
24
+ // eslint-disable-next-line no-unused-vars
25
+ } catch (error) {
26
+ return nativeModule.createRequire(basedir).resolve(module);
27
+ }
28
+ }
29
+
30
+ module.exports = resolver;
@@ -0,0 +1,176 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import serverHarness from '@typescript/server-harness';
4
+ import { resolve } from 'import-meta-resolve';
5
+ import lineColumn from 'line-column';
6
+ import { getFixturePath } from './util.js';
7
+
8
+ // TODO: refactor this
9
+
10
+ type Definition = {
11
+ /** The path of the destination file */
12
+ file: string;
13
+ /** The text at definition destination */
14
+ text: string;
15
+ /** inclusive */
16
+ start: {
17
+ /** line, 1-based */
18
+ line: number;
19
+ /** column, 1-based */
20
+ offset: number;
21
+ };
22
+ /** exclusive */
23
+ end: {
24
+ /** line, 1-based */
25
+ line: number;
26
+ /** column, 1-based */
27
+ offset: number;
28
+ };
29
+ };
30
+
31
+ type DefinitionResponse = {
32
+ seq: number;
33
+ type: 'response';
34
+ command: 'definition';
35
+ success: boolean;
36
+ body: [
37
+ {
38
+ /** The path of the destination file */
39
+ file: string;
40
+ /** inclusive */
41
+ start: {
42
+ /** line, 1-based */
43
+ line: number;
44
+ /** column, 1-based */
45
+ offset: number;
46
+ };
47
+ /** exclusive */
48
+ end: {
49
+ /** line, 1-based */
50
+ line: number;
51
+ /** column, 1-based */
52
+ offset: number;
53
+ };
54
+ },
55
+ ];
56
+ };
57
+
58
+ export async function getIdentifierDefinitions(filePath: string, identifier: string): Promise<Definition[]> {
59
+ const results = await getMultipleIdentifierDefinitions(filePath, [identifier]);
60
+ return results[0]!.definitions;
61
+ }
62
+
63
+ export async function getMultipleIdentifierDefinitions(
64
+ filePath: string,
65
+ identifiers: string[],
66
+ ): Promise<{ identifier: string; definitions: Definition[] }[]> {
67
+ const server = serverHarness.launchServer(
68
+ fileURLToPath(await resolve('typescript/lib/tsserver.js', import.meta.url)),
69
+ [
70
+ // ATA generates some extra network traffic and isn't usually relevant when profiling
71
+ '--disableAutomaticTypingAcquisition',
72
+ ],
73
+ );
74
+
75
+ const tmpFilePath = getFixturePath('/server-harness/tmp.ts');
76
+ const tmpFileContent = [
77
+ `import styles from '${filePath}';`,
78
+ ...identifiers.map((identifier) => `styles.${identifier};`),
79
+ ].join('\n');
80
+
81
+ await server.message({
82
+ type: 'request',
83
+ command: 'updateOpen',
84
+ arguments: {
85
+ changedFiles: [],
86
+ closedFiles: [],
87
+ openFiles: [
88
+ {
89
+ file: tmpFilePath,
90
+ fileContent: tmpFileContent,
91
+ projectRootPath: getFixturePath('/server-harness'),
92
+ scriptKindName: 'TS', // It's easy to get this wrong when copy-pasting
93
+ },
94
+ ],
95
+ },
96
+ });
97
+
98
+ const results: { identifier: string; definitions: Definition[] }[] = [];
99
+
100
+ for (let i = 0; i < identifiers.length; i++) {
101
+ const response: DefinitionResponse = await server.message({
102
+ type: 'request',
103
+ command: 'definition',
104
+ arguments: {
105
+ file: tmpFilePath,
106
+ line: i + 2, // line, 1-based
107
+ offset: 8, // column, 1-based
108
+ },
109
+ });
110
+ const definitions: Definition[] = response.body.map((definition) => {
111
+ const { file, start, end } = definition;
112
+ const fileContent = readFileSync(file, 'utf-8');
113
+ const startIndex = lineColumn(fileContent).toIndex(start.line, start.offset);
114
+ const endIndex = lineColumn(fileContent).toIndex(end.line, end.offset);
115
+ const text = fileContent.slice(startIndex, endIndex);
116
+ return { file, text, start, end };
117
+ });
118
+ results.push({ identifier: identifiers[i]!, definitions });
119
+ }
120
+
121
+ await server.message({ command: 'exit' });
122
+
123
+ return results;
124
+ }
125
+
126
+ export async function getModuleDefinitions(filePath: string): Promise<Definition[]> {
127
+ const server = serverHarness.launchServer(
128
+ fileURLToPath(await resolve('typescript/lib/tsserver.js', import.meta.url)),
129
+ [
130
+ // ATA generates some extra network traffic and isn't usually relevant when profiling
131
+ '--disableAutomaticTypingAcquisition',
132
+ ],
133
+ );
134
+
135
+ const tmpFilePath = getFixturePath('/server-harness/tmp.ts');
136
+ const tmpFileContent = `import styles from '${filePath}';`;
137
+
138
+ await server.message({
139
+ type: 'request',
140
+ command: 'updateOpen',
141
+ arguments: {
142
+ changedFiles: [],
143
+ closedFiles: [],
144
+ openFiles: [
145
+ {
146
+ file: tmpFilePath,
147
+ fileContent: tmpFileContent,
148
+ projectRootPath: getFixturePath('/server-harness'),
149
+ scriptKindName: 'TS', // It's easy to get this wrong when copy-pasting
150
+ },
151
+ ],
152
+ },
153
+ });
154
+
155
+ const response: DefinitionResponse = await server.message({
156
+ type: 'request',
157
+ command: 'definition',
158
+ arguments: {
159
+ file: tmpFilePath,
160
+ line: 1, // line, 1-based
161
+ offset: 20, // column, 1-based
162
+ },
163
+ });
164
+ const definitions: Definition[] = response.body.map((definition) => {
165
+ const { file, start, end } = definition;
166
+ const fileContent = readFileSync(file, 'utf-8');
167
+ const startIndex = lineColumn(fileContent).toIndex(start.line, start.offset);
168
+ const endIndex = lineColumn(fileContent).toIndex(end.line, end.offset);
169
+ const text = fileContent.slice(startIndex, endIndex);
170
+ return { file, text, start, end };
171
+ });
172
+
173
+ await server.message({ command: 'exit' });
174
+
175
+ return definitions;
176
+ }
@@ -0,0 +1,100 @@
1
+ import { constants, mkdirSync, realpathSync, rmSync, writeFileSync } from 'fs';
2
+ import { access } from 'fs/promises';
3
+ import { tmpdir } from 'os';
4
+ import { dirname, join, resolve } from 'path';
5
+ import postcss, { type Root, type Rule, type AtRule, type Declaration } from 'postcss';
6
+ import { type ClassName } from 'postcss-selector-parser';
7
+ import { type Token, collectNodes, type Location } from '../loader/index.js';
8
+ import { sleepSync } from '../util.js';
9
+
10
+ export const FIXTURE_DIR_PATH = resolve(
11
+ realpathSync(tmpdir()),
12
+ 'happy-css-modules/fixtures',
13
+ process.env['JEST_WORKER_ID']!,
14
+ );
15
+
16
+ export function createRoot(code: string, from?: string): Root {
17
+ return postcss.parse(code, { from: from || '/test/test.css' });
18
+ }
19
+
20
+ export function createAtImports(root: Root): AtRule[] {
21
+ return collectNodes(root).atImports;
22
+ }
23
+
24
+ export function createClassSelectors(root: Root): { rule: Rule; classSelector: ClassName }[] {
25
+ return collectNodes(root).classSelectors;
26
+ }
27
+
28
+ export function createComposesDeclarations(root: Root): Declaration[] {
29
+ return collectNodes(root).composesDeclarations;
30
+ }
31
+
32
+ export function fakeToken(args: {
33
+ name: Token['name'];
34
+ originalLocations: { filePath?: Location['filePath']; start: Location['start'] }[];
35
+ }): Token {
36
+ return {
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
+ })),
46
+ };
47
+ }
48
+
49
+ export async function waitForAsyncTask(ms?: number): Promise<void> {
50
+ await new Promise((resolve) => setTimeout(resolve, ms ?? 0));
51
+ }
52
+
53
+ export async function exists(path: string): Promise<boolean> {
54
+ try {
55
+ await access(path, constants.F_OK);
56
+ return true;
57
+ } catch (e) {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ type File = string;
63
+
64
+ type DirectoryItem = File | DirectoryItems;
65
+
66
+ type DirectoryItems = {
67
+ [name: string]: DirectoryItem;
68
+ };
69
+
70
+ function isFile(item: DirectoryItem): item is File {
71
+ return typeof item === 'string';
72
+ }
73
+
74
+ export function createFixtures(items: DirectoryItems): void {
75
+ function createFixturesImpl(items: DirectoryItems, baseDir: string): void {
76
+ for (const [name, item] of Object.entries(items)) {
77
+ const path = join(baseDir, name);
78
+ if (isFile(item)) {
79
+ mkdirSync(dirname(path), { recursive: true });
80
+ if (typeof item === 'string') {
81
+ writeFileSync(path, item);
82
+ }
83
+ } else {
84
+ mkdirSync(path, { recursive: true });
85
+ createFixturesImpl(item, path);
86
+ }
87
+ }
88
+ }
89
+ removeFixtures();
90
+ sleepSync(2); // Wait 2 ms for mtime to change from the previous fixture.
91
+ createFixturesImpl(items, FIXTURE_DIR_PATH);
92
+ }
93
+
94
+ export function removeFixtures(): void {
95
+ rmSync(FIXTURE_DIR_PATH, { recursive: true, force: true });
96
+ }
97
+
98
+ export function getFixturePath(path: string): string {
99
+ return join(FIXTURE_DIR_PATH, path);
100
+ }