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.
- package/LICENSE.txt +23 -0
- package/README.md +124 -0
- package/bin/hcm.js +9 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +34 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/emitter/dts.d.ts +14 -0
- package/dist/emitter/dts.js +106 -0
- package/dist/emitter/dts.js.map +1 -0
- package/dist/emitter/dts.test.d.ts +1 -0
- package/dist/emitter/dts.test.js +205 -0
- package/dist/emitter/dts.test.js.map +1 -0
- package/dist/emitter/file-system.d.ts +6 -0
- package/dist/emitter/file-system.js +26 -0
- package/dist/emitter/file-system.js.map +1 -0
- package/dist/emitter/file-system.test.d.ts +1 -0
- package/dist/emitter/file-system.test.js +34 -0
- package/dist/emitter/file-system.test.js.map +1 -0
- package/dist/emitter/index.d.ts +34 -0
- package/dist/emitter/index.js +42 -0
- package/dist/emitter/index.js.map +1 -0
- package/dist/emitter/index.test.d.ts +1 -0
- package/dist/emitter/index.test.js +118 -0
- package/dist/emitter/index.test.js.map +1 -0
- package/dist/emitter/source-map.d.ts +9 -0
- package/dist/emitter/source-map.js +16 -0
- package/dist/emitter/source-map.js.map +1 -0
- package/dist/emitter/source-map.test.d.ts +1 -0
- package/dist/emitter/source-map.test.js +13 -0
- package/dist/emitter/source-map.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/integration-test/go-to-definition.test.d.ts +1 -0
- package/dist/integration-test/go-to-definition.test.js +367 -0
- package/dist/integration-test/go-to-definition.test.js.map +1 -0
- package/dist/library/source-map/index.d.ts +8 -0
- package/dist/library/source-map/index.js +5 -0
- package/dist/library/source-map/index.js.map +1 -0
- package/dist/loader/index.d.ts +42 -0
- package/dist/loader/index.js +145 -0
- package/dist/loader/index.js.map +1 -0
- package/dist/loader/index.test.d.ts +1 -0
- package/dist/loader/index.test.js +290 -0
- package/dist/loader/index.test.js.map +1 -0
- package/dist/loader/postcss.d.ts +60 -0
- package/dist/loader/postcss.js +209 -0
- package/dist/loader/postcss.js.map +1 -0
- package/dist/loader/postcss.test.d.ts +1 -0
- package/dist/loader/postcss.test.js +236 -0
- package/dist/loader/postcss.test.js.map +1 -0
- package/dist/resolver/index.d.ts +20 -0
- package/dist/resolver/index.js +39 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/resolver/index.test.d.ts +1 -0
- package/dist/resolver/index.test.js +16 -0
- package/dist/resolver/index.test.js.map +1 -0
- package/dist/resolver/node-resolver.d.ts +2 -0
- package/dist/resolver/node-resolver.js +6 -0
- package/dist/resolver/node-resolver.js.map +1 -0
- package/dist/resolver/node-resolver.test.d.ts +1 -0
- package/dist/resolver/node-resolver.test.js +25 -0
- package/dist/resolver/node-resolver.test.js.map +1 -0
- package/dist/resolver/relative-resolver.d.ts +2 -0
- package/dist/resolver/relative-resolver.js +5 -0
- package/dist/resolver/relative-resolver.js.map +1 -0
- package/dist/resolver/relative-resolver.test.d.ts +1 -0
- package/dist/resolver/relative-resolver.test.js +12 -0
- package/dist/resolver/relative-resolver.test.js.map +1 -0
- package/dist/resolver/webpack-resolver.d.ts +2 -0
- package/dist/resolver/webpack-resolver.js +69 -0
- package/dist/resolver/webpack-resolver.js.map +1 -0
- package/dist/resolver/webpack-resolver.test.d.ts +1 -0
- package/dist/resolver/webpack-resolver.test.js +26 -0
- package/dist/resolver/webpack-resolver.test.js.map +1 -0
- package/dist/runner.d.ts +33 -0
- package/dist/runner.js +75 -0
- package/dist/runner.js.map +1 -0
- package/dist/runner.test.d.ts +1 -0
- package/dist/runner.test.js +113 -0
- package/dist/runner.test.js.map +1 -0
- package/dist/test/jest/resolver.cjs +30 -0
- package/dist/test/jest/resolver.cjs.map +1 -0
- package/dist/test/jest/resolver.d.cts +30 -0
- package/dist/test/tsserver.d.ts +27 -0
- package/dist/test/tsserver.js +104 -0
- package/dist/test/tsserver.js.map +1 -0
- package/dist/test/util.d.ts +29 -0
- package/dist/test/util.js +78 -0
- package/dist/test/util.js.map +1 -0
- package/dist/transformer/index.d.ts +23 -0
- package/dist/transformer/index.js +19 -0
- package/dist/transformer/index.js.map +1 -0
- package/dist/transformer/less-transformer.d.ts +2 -0
- package/dist/transformer/less-transformer.js +43 -0
- package/dist/transformer/less-transformer.js.map +1 -0
- package/dist/transformer/less-transformer.test.d.ts +1 -0
- package/dist/transformer/less-transformer.test.js +126 -0
- package/dist/transformer/less-transformer.test.js.map +1 -0
- package/dist/transformer/scss-transformer.d.ts +2 -0
- package/dist/transformer/scss-transformer.js +84 -0
- package/dist/transformer/scss-transformer.js.map +1 -0
- package/dist/transformer/scss-transformer.test.d.ts +1 -0
- package/dist/transformer/scss-transformer.test.js +132 -0
- package/dist/transformer/scss-transformer.test.js.map +1 -0
- package/dist/util.d.ts +19 -0
- package/dist/util.js +52 -0
- package/dist/util.js.map +1 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.js +75 -0
- package/dist/util.test.js.map +1 -0
- package/package.json +106 -0
- package/src/__snapshots__/runner.test.ts.snap +34 -0
- package/src/cli.test.ts +38 -0
- package/src/cli.ts +70 -0
- package/src/emitter/dts.test.ts +266 -0
- package/src/emitter/dts.ts +134 -0
- package/src/emitter/file-system.test.ts +36 -0
- package/src/emitter/file-system.ts +24 -0
- package/src/emitter/index.test.ts +130 -0
- package/src/emitter/index.ts +92 -0
- package/src/emitter/source-map.test.ts +20 -0
- package/src/emitter/source-map.ts +17 -0
- package/src/index.ts +3 -0
- package/src/integration-test/go-to-definition.test.ts +371 -0
- package/src/library/README.md +3 -0
- package/src/library/source-map/index.ts +26 -0
- package/src/loader/index.test.ts +306 -0
- package/src/loader/index.ts +199 -0
- package/src/loader/postcss.test.ts +336 -0
- package/src/loader/postcss.ts +239 -0
- package/src/resolver/index.test.ts +17 -0
- package/src/resolver/index.ts +48 -0
- package/src/resolver/node-resolver.test.ts +26 -0
- package/src/resolver/node-resolver.ts +7 -0
- package/src/resolver/relative-resolver.test.ts +13 -0
- package/src/resolver/relative-resolver.ts +6 -0
- package/src/resolver/webpack-resolver.test.ts +33 -0
- package/src/resolver/webpack-resolver.ts +71 -0
- package/src/runner.test.ts +122 -0
- package/src/runner.ts +105 -0
- package/src/test/jest/resolver.cjs +30 -0
- package/src/test/tsserver.ts +176 -0
- package/src/test/util.ts +100 -0
- package/src/transformer/index.ts +44 -0
- package/src/transformer/less-transformer.test.ts +135 -0
- package/src/transformer/less-transformer.ts +55 -0
- package/src/transformer/scss-transformer.test.ts +142 -0
- package/src/transformer/scss-transformer.ts +94 -0
- package/src/util.test.ts +89 -0
- 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,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
|
+
}
|
package/src/test/util.ts
ADDED
|
@@ -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
|
+
}
|