happy-css-modules 0.3.0 → 0.4.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 (42) hide show
  1. package/README.md +15 -18
  2. package/dist/cli.js +15 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.test.js +10 -0
  5. package/dist/cli.test.js.map +1 -1
  6. package/dist/integration-test/go-to-definition.test.js +9 -8
  7. package/dist/integration-test/go-to-definition.test.js.map +1 -1
  8. package/dist/resolver/index.d.ts +3 -1
  9. package/dist/resolver/index.js +17 -14
  10. package/dist/resolver/index.js.map +1 -1
  11. package/dist/resolver/webpack-resolver.d.ts +13 -1
  12. package/dist/resolver/webpack-resolver.js +73 -59
  13. package/dist/resolver/webpack-resolver.js.map +1 -1
  14. package/dist/resolver/webpack-resolver.test.js +34 -7
  15. package/dist/resolver/webpack-resolver.test.js.map +1 -1
  16. package/dist/runner.d.ts +12 -0
  17. package/dist/runner.js +16 -10
  18. package/dist/runner.js.map +1 -1
  19. package/dist/runner.test.js +33 -9
  20. package/dist/runner.test.js.map +1 -1
  21. package/dist/test/tsserver.d.ts +10 -6
  22. package/dist/test/tsserver.js +94 -85
  23. package/dist/test/tsserver.js.map +1 -1
  24. package/dist/transformer/index.js +11 -9
  25. package/dist/transformer/index.js.map +1 -1
  26. package/dist/transformer/less-transformer.js +1 -2
  27. package/dist/transformer/less-transformer.js.map +1 -1
  28. package/dist/transformer/scss-transformer.js +0 -47
  29. package/dist/transformer/scss-transformer.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/cli.test.ts +14 -0
  32. package/src/cli.ts +15 -4
  33. package/src/integration-test/go-to-definition.test.ts +10 -8
  34. package/src/resolver/index.ts +21 -12
  35. package/src/resolver/webpack-resolver.test.ts +44 -8
  36. package/src/resolver/webpack-resolver.ts +90 -57
  37. package/src/runner.test.ts +35 -9
  38. package/src/runner.ts +29 -10
  39. package/src/test/tsserver.ts +106 -129
  40. package/src/transformer/index.ts +10 -8
  41. package/src/transformer/less-transformer.ts +1 -2
  42. package/src/transformer/scss-transformer.ts +0 -51
@@ -1,11 +1,21 @@
1
1
  import { readFileSync } from 'fs';
2
+ import { mkdir, writeFile as nativeWriteFile } from 'fs/promises';
3
+ import { dirname } from 'path';
2
4
  import { fileURLToPath } from 'url';
5
+ import { promisify } from 'util';
3
6
  import serverHarness from '@typescript/server-harness';
7
+ import _glob from 'glob';
4
8
  import { resolve } from 'import-meta-resolve';
5
9
  import lineColumn from 'line-column';
10
+ import type { UpdateOpenRequest, DefinitionResponse, DefinitionRequest } from 'typescript/lib/protocol.js';
6
11
  import { getFixturePath } from './util.js';
7
12
 
8
- // TODO: refactor this
13
+ const glob = promisify(_glob);
14
+
15
+ async function writeFile(path: string, content: string): Promise<void> {
16
+ await mkdir(dirname(path), { recursive: true });
17
+ return nativeWriteFile(path, content, 'utf8');
18
+ }
9
19
 
10
20
  type Definition = {
11
21
  /** The path of the destination file */
@@ -28,42 +38,7 @@ type Definition = {
28
38
  };
29
39
  };
30
40
 
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[] }[]> {
41
+ export async function createTSServer() {
67
42
  const server = serverHarness.launchServer(
68
43
  fileURLToPath(await resolve('typescript/lib/tsserver.js', import.meta.url)),
69
44
  [
@@ -72,105 +47,107 @@ export async function getMultipleIdentifierDefinitions(
72
47
  ],
73
48
  );
74
49
 
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
- ],
50
+ return {
51
+ async getIdentifierDefinitions(filePath: string, identifier: string): Promise<Definition[]> {
52
+ const results = await this.getMultipleIdentifierDefinitions(filePath, [identifier]);
53
+ return results[0]!.definitions;
95
54
  },
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' });
55
+ async getMultipleIdentifierDefinitions(
56
+ filePath: string,
57
+ identifiers: string[],
58
+ ): Promise<{ identifier: string; definitions: Definition[] }[]> {
59
+ const tmpFilePath = getFixturePath('/server-harness/tmp.ts');
60
+ const tmpFileContent = [
61
+ `import styles from '${filePath}';`,
62
+ ...identifiers.map((identifier) => `styles.${identifier};`),
63
+ ].join('\n');
64
+ await writeFile(tmpFilePath, tmpFileContent);
65
+
66
+ await this.refreshCache();
67
+
68
+ const results: { identifier: string; definitions: Definition[] }[] = [];
69
+
70
+ for (let i = 0; i < identifiers.length; i++) {
71
+ const response: DefinitionResponse = await server.message({
72
+ seq: 0,
73
+ type: 'request',
74
+ command: 'definition',
75
+ arguments: {
76
+ file: tmpFilePath,
77
+ line: i + 2, // line, 1-based
78
+ offset: 8, // column, 1-based
79
+ },
80
+ } as DefinitionRequest);
81
+ const definitions: Definition[] = response.body!.map((definition) => {
82
+ const { file, start, end } = definition;
83
+ const fileContent = readFileSync(file, 'utf-8');
84
+ const startIndex = lineColumn(fileContent).toIndex(start.line, start.offset);
85
+ const endIndex = lineColumn(fileContent).toIndex(end.line, end.offset);
86
+ const text = fileContent.slice(startIndex, endIndex);
87
+ return { file, text, start, end };
88
+ });
89
+ results.push({ identifier: identifiers[i]!, definitions });
90
+ }
91
+ return results;
92
+ },
93
+ async getModuleDefinitions(filePath: string): Promise<Definition[]> {
94
+ await this.refreshCache();
122
95
 
123
- return results;
124
- }
96
+ const tmpFilePath = getFixturePath('/server-harness/tmp.ts');
97
+ const tmpFileContent = `import styles from '${filePath}';`;
125
98
 
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
- );
99
+ await writeFile(tmpFilePath, tmpFileContent);
134
100
 
135
- const tmpFilePath = getFixturePath('/server-harness/tmp.ts');
136
- const tmpFileContent = `import styles from '${filePath}';`;
101
+ await this.refreshCache();
137
102
 
138
- await server.message({
139
- type: 'request',
140
- command: 'updateOpen',
141
- arguments: {
142
- changedFiles: [],
143
- closedFiles: [],
144
- openFiles: [
145
- {
103
+ const response: DefinitionResponse = await server.message({
104
+ seq: 0,
105
+ type: 'request',
106
+ command: 'definition',
107
+ arguments: {
146
108
  file: tmpFilePath,
147
- fileContent: tmpFileContent,
148
- projectRootPath: getFixturePath('/server-harness'),
149
- scriptKindName: 'TS', // It's easy to get this wrong when copy-pasting
109
+ line: 1, // line, 1-based
110
+ offset: 20, // column, 1-based
150
111
  },
151
- ],
112
+ } as DefinitionRequest);
113
+ const definitions: Definition[] = response.body!.map((definition) => {
114
+ const { file, start, end } = definition;
115
+ const fileContent = readFileSync(file, 'utf-8');
116
+ const startIndex = lineColumn(fileContent).toIndex(start.line, start.offset);
117
+ const endIndex = lineColumn(fileContent).toIndex(end.line, end.offset);
118
+ const text = fileContent.slice(startIndex, endIndex);
119
+ return { file, text, start, end };
120
+ });
121
+ return definitions;
152
122
  },
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
123
+ async refreshCache() {
124
+ // tsserver caches the contents of opened files.
125
+ // When a file is updated, its cache remains with the old content.
126
+ // Therefore we need to overwrite the cache with the latest content.
127
+
128
+ const fixtureFilePaths = await glob(getFixturePath('/**/*.ts'), { dot: true });
129
+ // latest contents
130
+ const openFiles: UpdateOpenRequest['arguments']['openFiles'] = fixtureFilePaths.map((filePath) => ({
131
+ file: filePath,
132
+ fileContent: readFileSync(filePath, 'utf-8'),
133
+ projectRootPath: getFixturePath('/server-harness'),
134
+ scriptKindName: 'TS', // It's easy to get this wrong when copy-pasting
135
+ }));
136
+
137
+ // override the cache
138
+ await server.message({
139
+ seq: 0,
140
+ type: 'request',
141
+ command: 'updateOpen',
142
+ arguments: {
143
+ changedFiles: [],
144
+ closedFiles: [],
145
+ openFiles,
146
+ },
147
+ } as UpdateOpenRequest);
162
148
  },
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;
149
+ exit: async () => {
150
+ await server.message({ command: 'exit' });
151
+ },
152
+ };
176
153
  }
@@ -36,14 +36,16 @@ export const handleImportError = (packageName: string) => (e: unknown) => {
36
36
  throw e;
37
37
  };
38
38
 
39
- export const createDefaultTransformer: () => Transformer = () => async (source, options) => {
39
+ export const createDefaultTransformer: () => Transformer = () => {
40
40
  const scssTransformer = createScssTransformer();
41
41
  const lessTransformer = createLessTransformer();
42
- if (options.from.endsWith('.scss')) {
43
- return scssTransformer(source, options);
44
- } else if (options.from.endsWith('.less')) {
45
- return lessTransformer(source, options);
46
- }
47
- // TODO: support postcss
48
- return false;
42
+ return async (source, options) => {
43
+ if (options.from.endsWith('.scss')) {
44
+ return scssTransformer(source, options);
45
+ } else if (options.from.endsWith('.less')) {
46
+ return lessTransformer(source, options);
47
+ }
48
+ // TODO: support postcss
49
+ return false;
50
+ };
49
51
  };
@@ -2,7 +2,7 @@ import type { Transformer } from '../index.js';
2
2
  import type { TransformerOptions } from './index.js';
3
3
  import { handleImportError } from './index.js';
4
4
 
5
- // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
5
+ // eslint-disable-next-line @typescript-eslint/naming-convention
6
6
  function createLessPluginResolver(Less: typeof import('less'), options: TransformerOptions): Less.Plugin {
7
7
  class ResolverFileManager extends Less.FileManager {
8
8
  options: TransformerOptions;
@@ -42,7 +42,6 @@ function createLessPluginResolver(Less: typeof import('less'), options: Transfor
42
42
  }
43
43
 
44
44
  export const createLessTransformer: () => Transformer = () => {
45
- // eslint-disable-next-line @typescript-eslint/consistent-type-imports
46
45
  let less: typeof import('less');
47
46
  return async (source, options) => {
48
47
  less ??= (await import('less').catch(handleImportError('less'))).default;
@@ -7,48 +7,6 @@ import type { LegacyResult } from 'sass';
7
7
  import type { Transformer, TransformerOptions } from './index.js';
8
8
  import { handleImportError } from './index.js';
9
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
10
  async function renderSass(sass: typeof import('sass'), source: string, options: TransformerOptions) {
53
11
  return new Promise<LegacyResult>((resolve, reject) => {
54
12
  sass.render(
@@ -76,19 +34,10 @@ async function renderSass(sass: typeof import('sass'), source: string, options:
76
34
  }
77
35
 
78
36
  export const createScssTransformer: () => Transformer = () => {
79
- // eslint-disable-next-line @typescript-eslint/consistent-type-imports
80
37
  let sass: typeof import('sass');
81
38
  return async (source, options) => {
82
39
  sass ??= (await import('sass').catch(handleImportError('sass'))).default;
83
40
  const result = await renderSass(sass, source, options);
84
41
  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
42
  };
94
43
  };