happy-css-modules 0.4.0 → 0.6.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/README.md +172 -53
- package/dist/cli.js +45 -11
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.js +20 -4
- package/dist/cli.test.js.map +1 -1
- package/dist/emitter/dts.d.ts +3 -4
- package/dist/emitter/dts.js +13 -15
- package/dist/emitter/dts.js.map +1 -1
- package/dist/emitter/dts.test.js +7 -10
- package/dist/emitter/dts.test.js.map +1 -1
- package/dist/emitter/index.d.ts +6 -15
- package/dist/emitter/index.js +18 -13
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/index.test.js +0 -50
- package/dist/emitter/index.test.js.map +1 -1
- package/dist/emitter/source-map.d.ts +1 -3
- package/dist/emitter/source-map.js +2 -3
- package/dist/emitter/source-map.js.map +1 -1
- package/dist/emitter/source-map.test.js +1 -4
- package/dist/emitter/source-map.test.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-test/go-to-definition.test.js +1 -0
- package/dist/integration-test/go-to-definition.test.js.map +1 -1
- package/dist/{loader → locator}/index.d.ts +6 -4
- package/dist/{loader → locator}/index.js +13 -5
- package/dist/locator/index.js.map +1 -0
- package/dist/{loader → locator}/index.test.d.ts +0 -0
- package/dist/{loader → locator}/index.test.js +100 -14
- package/dist/locator/index.test.js.map +1 -0
- package/dist/{loader → locator}/postcss.d.ts +5 -1
- package/dist/{loader → locator}/postcss.js +26 -30
- package/dist/{loader → locator}/postcss.js.map +1 -1
- package/dist/{loader → locator}/postcss.test.d.ts +0 -0
- package/dist/{loader → locator}/postcss.test.js +0 -0
- package/dist/locator/postcss.test.js.map +1 -0
- package/dist/resolver/webpack-resolver.d.ts +12 -2
- package/dist/resolver/webpack-resolver.js +12 -3
- package/dist/resolver/webpack-resolver.js.map +1 -1
- package/dist/resolver/webpack-resolver.test.js +43 -7
- package/dist/resolver/webpack-resolver.test.js.map +1 -1
- package/dist/runner.d.ts +23 -1
- package/dist/runner.js +62 -23
- package/dist/runner.js.map +1 -1
- package/dist/runner.test.js +90 -11
- package/dist/runner.test.js.map +1 -1
- package/dist/test/util.d.ts +2 -2
- package/dist/test/util.js +16 -9
- package/dist/test/util.js.map +1 -1
- package/dist/transformer/index.d.ts +4 -2
- package/dist/transformer/index.js +7 -3
- package/dist/transformer/index.js.map +1 -1
- package/dist/transformer/index.test.d.ts +1 -0
- package/dist/transformer/index.test.js +66 -0
- package/dist/transformer/index.test.js.map +1 -0
- package/dist/transformer/less-transformer.test.js +14 -14
- package/dist/transformer/less-transformer.test.js.map +1 -1
- package/dist/transformer/postcss-transformer.d.ts +12 -0
- package/dist/transformer/postcss-transformer.js +32 -0
- package/dist/transformer/postcss-transformer.js.map +1 -0
- package/dist/transformer/postcss-transformer.test.d.ts +1 -0
- package/dist/transformer/postcss-transformer.test.js +176 -0
- package/dist/transformer/postcss-transformer.test.js.map +1 -0
- package/dist/transformer/scss-transformer.js +19 -17
- package/dist/transformer/scss-transformer.js.map +1 -1
- package/dist/transformer/scss-transformer.test.js +16 -16
- package/dist/transformer/scss-transformer.test.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +19 -2
- package/dist/util.js.map +1 -1
- package/package.json +10 -9
- package/src/cli.test.ts +24 -4
- package/src/cli.ts +44 -12
- package/src/emitter/dts.test.ts +7 -12
- package/src/emitter/dts.ts +15 -15
- package/src/emitter/index.test.ts +0 -52
- package/src/emitter/index.ts +22 -29
- package/src/emitter/source-map.test.ts +1 -6
- package/src/emitter/source-map.ts +3 -4
- package/src/index.ts +9 -2
- package/src/integration-test/go-to-definition.test.ts +1 -0
- package/src/{loader → locator}/index.test.ts +101 -14
- package/src/{loader → locator}/index.ts +16 -8
- package/src/{loader → locator}/postcss.test.ts +0 -0
- package/src/{loader → locator}/postcss.ts +42 -40
- package/src/resolver/webpack-resolver.test.ts +63 -7
- package/src/resolver/webpack-resolver.ts +26 -5
- package/src/runner.test.ts +103 -11
- package/src/runner.ts +83 -25
- package/src/test/util.ts +16 -10
- package/src/transformer/index.test.ts +71 -0
- package/src/transformer/index.ts +12 -4
- package/src/transformer/less-transformer.test.ts +14 -14
- package/src/transformer/postcss-transformer.test.ts +188 -0
- package/src/transformer/postcss-transformer.ts +57 -0
- package/src/transformer/scss-transformer.test.ts +16 -16
- package/src/transformer/scss-transformer.ts +25 -27
- package/src/util.ts +21 -2
- package/dist/loader/index.js.map +0 -1
- package/dist/loader/index.test.js.map +0 -1
- package/dist/loader/postcss.test.js.map +0 -1
|
@@ -11,14 +11,20 @@ export type Position = {
|
|
|
11
11
|
column: number;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
/** The location of class selector. */
|
|
15
|
-
export type Location =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
/** The original location of class selector. If the original location is not found, all fields are `undefined`. */
|
|
15
|
+
export type Location =
|
|
16
|
+
| {
|
|
17
|
+
filePath: string;
|
|
18
|
+
/** The inclusive starting position of the node's source (compatible with postcss). */
|
|
19
|
+
start: Position;
|
|
20
|
+
/** The inclusive ending position of the node's source (compatible with postcss). */
|
|
21
|
+
end: Position;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
filePath: undefined;
|
|
25
|
+
start: undefined;
|
|
26
|
+
end: undefined;
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
function removeDependenciesPlugin(): Plugin {
|
|
24
30
|
return {
|
|
@@ -80,49 +86,45 @@ export function getOriginalLocation(rule: Rule, classSelector: ClassName): Locat
|
|
|
80
86
|
if (rule.source.end === undefined || classSelector.source.end === undefined) throw new Error('Node#end is undefined');
|
|
81
87
|
if (rule.source.input.file === undefined) throw new Error('Node#input.file is undefined');
|
|
82
88
|
|
|
83
|
-
const
|
|
89
|
+
const classSelectorStartPosition = {
|
|
84
90
|
// The line is 1-based.
|
|
85
91
|
line: rule.source.start.line + (classSelector.source.start.line - 1),
|
|
86
92
|
// The column is 1-based.
|
|
87
93
|
column: rule.source.start.column + (classSelector.source.start.column - 1),
|
|
88
94
|
};
|
|
89
|
-
const
|
|
90
|
-
line:
|
|
95
|
+
const classSelectorEndPosition = {
|
|
96
|
+
line: classSelectorStartPosition.line,
|
|
91
97
|
// The column is inclusive.
|
|
92
|
-
column:
|
|
98
|
+
column: classSelectorStartPosition.column + classSelector.value.length,
|
|
93
99
|
};
|
|
94
|
-
|
|
100
|
+
const classSelectorLocation = {
|
|
95
101
|
filePath: rule.source.input.file,
|
|
96
|
-
start,
|
|
97
|
-
end,
|
|
102
|
+
start: classSelectorStartPosition,
|
|
103
|
+
end: classSelectorEndPosition,
|
|
98
104
|
};
|
|
99
105
|
|
|
100
|
-
if (rule.source.input.map)
|
|
101
|
-
const origin = rule.source.input.origin(
|
|
102
|
-
location.start.line,
|
|
103
|
-
// The column of `Input#origin` is 0-based. This behavior is undocumented and probably a postcss's bug.
|
|
104
|
-
// TODO: Open PR to postcss/postcss
|
|
105
|
-
location.start.column - 1,
|
|
106
|
-
);
|
|
107
|
-
if (origin === false) throw new Error('`Input#origin` returned false');
|
|
108
|
-
if (origin.file === undefined) throw new Error('`FilePosition#file` is undefined');
|
|
109
|
-
|
|
110
|
-
location = {
|
|
111
|
-
filePath: origin.file,
|
|
112
|
-
start: {
|
|
113
|
-
line: origin.line,
|
|
114
|
-
// The column of `Input#origin` is 0-based.
|
|
115
|
-
column: origin.column + 1,
|
|
116
|
-
},
|
|
117
|
-
end: {
|
|
118
|
-
line: origin.line,
|
|
119
|
-
// The column of `Input#origin` is 0-based. Also, the column of happy-css-modules is inclusive.
|
|
120
|
-
column: origin.column + 1 + (classSelector.value.length - 1),
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
106
|
+
if (!rule.source.input.map) return classSelectorLocation;
|
|
124
107
|
|
|
125
|
-
|
|
108
|
+
const classSelectorOrigin = rule.source.input.origin(
|
|
109
|
+
classSelectorLocation.start.line,
|
|
110
|
+
// The column of `Input#origin` is 0-based. This behavior is undocumented and probably a postcss's bug.
|
|
111
|
+
// TODO: Open PR to postcss/postcss
|
|
112
|
+
classSelectorLocation.start.column - 1,
|
|
113
|
+
);
|
|
114
|
+
if (classSelectorOrigin === false || classSelectorOrigin.file === undefined) {
|
|
115
|
+
return { filePath: undefined, start: undefined, end: undefined };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
filePath: classSelectorOrigin.file,
|
|
119
|
+
start: {
|
|
120
|
+
line: classSelectorOrigin.line,
|
|
121
|
+
column: classSelectorOrigin.column + 1,
|
|
122
|
+
},
|
|
123
|
+
end: {
|
|
124
|
+
line: classSelectorOrigin.line,
|
|
125
|
+
column: classSelectorOrigin.column + classSelector.value.length + 1,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
function isAtRuleNode(node: Node): node is AtRule {
|
|
@@ -2,7 +2,13 @@ import { createFixtures, getFixturePath } from '../test/util.js';
|
|
|
2
2
|
import { createWebpackResolver } from './webpack-resolver.js';
|
|
3
3
|
|
|
4
4
|
test('resolves specifier with css-loader mechanism', async () => {
|
|
5
|
-
const webpackResolver = createWebpackResolver(
|
|
5
|
+
const webpackResolver = createWebpackResolver({
|
|
6
|
+
cwd: getFixturePath('/'),
|
|
7
|
+
webpackResolveAlias: {
|
|
8
|
+
'@relative': 'test/alias-relative',
|
|
9
|
+
'@absolute': getFixturePath('/test/alias-absolute'),
|
|
10
|
+
},
|
|
11
|
+
});
|
|
6
12
|
const request = getFixturePath('/test/1.css');
|
|
7
13
|
createFixtures({
|
|
8
14
|
'/node_modules/package-1/index.css': `.a {}`,
|
|
@@ -12,6 +18,8 @@ test('resolves specifier with css-loader mechanism', async () => {
|
|
|
12
18
|
'/node_modules/package-4/style.css': `.a {}`,
|
|
13
19
|
'/node_modules/@scoped/package-5/index.css': `.a {}`,
|
|
14
20
|
'/node_modules/package-6/index.css': `.a {}`,
|
|
21
|
+
'/test/alias-relative/alias.css': `.a {}`,
|
|
22
|
+
'/test/alias-absolute/alias.css': `.a {}`,
|
|
15
23
|
});
|
|
16
24
|
expect(await webpackResolver('~package-1/index.css', { request })).toBe(
|
|
17
25
|
getFixturePath('/node_modules/package-1/index.css'),
|
|
@@ -25,22 +33,43 @@ test('resolves specifier with css-loader mechanism', async () => {
|
|
|
25
33
|
expect(await webpackResolver('package-6/index.css', { request })).toBe(
|
|
26
34
|
getFixturePath('/node_modules/package-6/index.css'),
|
|
27
35
|
);
|
|
36
|
+
expect(await webpackResolver('@relative/alias.css', { request })).toBe(
|
|
37
|
+
getFixturePath('/test/alias-relative/alias.css'),
|
|
38
|
+
);
|
|
39
|
+
expect(await webpackResolver('@absolute/alias.css', { request })).toBe(
|
|
40
|
+
getFixturePath('/test/alias-absolute/alias.css'),
|
|
41
|
+
);
|
|
28
42
|
});
|
|
29
43
|
|
|
30
44
|
test('resolves specifier with sass-loader mechanism', async () => {
|
|
31
|
-
const webpackResolver = createWebpackResolver({
|
|
45
|
+
const webpackResolver = createWebpackResolver({
|
|
46
|
+
cwd: getFixturePath('/'),
|
|
47
|
+
sassLoadPaths: ['test/load-paths-relative', getFixturePath('/test/load-paths-absolute')],
|
|
48
|
+
webpackResolveAlias: {
|
|
49
|
+
'@relative': 'test/alias-relative',
|
|
50
|
+
'@absolute': getFixturePath('/test/alias-absolute'),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
32
53
|
const request = getFixturePath('/test/1.scss');
|
|
33
54
|
createFixtures({
|
|
34
55
|
'/node_modules/package-1/index.scss': `.a {}`,
|
|
35
|
-
'/test/
|
|
56
|
+
'/test/load-paths-relative/load-paths-relative.scss': `.a {}`,
|
|
57
|
+
'/test/load-paths-absolute/load-paths-absolute.scss': `.a {}`,
|
|
36
58
|
'/test/_partial-import.scss': `.a {}`,
|
|
59
|
+
'/test/alias-relative/alias.scss': `.a {}`,
|
|
60
|
+
'/test/alias-absolute/alias.scss': `.a {}`,
|
|
37
61
|
});
|
|
38
62
|
expect(await webpackResolver('~package-1/index.scss', { request })).toBe(
|
|
39
63
|
getFixturePath('/node_modules/package-1/index.scss'),
|
|
40
64
|
);
|
|
41
65
|
expect(await webpackResolver('~package-1', { request })).toBe(getFixturePath('/node_modules/package-1/index.scss'));
|
|
42
66
|
// ref: https://github.com/webpack-contrib/sass-loader/blob/bed9fb5799a90020d43f705ea405f85b368621d7/test/scss/import-include-paths.scss#L1
|
|
43
|
-
expect(await webpackResolver('load-paths', { request })).toBe(
|
|
67
|
+
expect(await webpackResolver('load-paths-relative', { request })).toBe(
|
|
68
|
+
getFixturePath('/test/load-paths-relative/load-paths-relative.scss'),
|
|
69
|
+
);
|
|
70
|
+
expect(await webpackResolver('load-paths-absolute', { request })).toBe(
|
|
71
|
+
getFixturePath('/test/load-paths-absolute/load-paths-absolute.scss'),
|
|
72
|
+
);
|
|
44
73
|
// https://sass-lang.com/documentation/at-rules/import#partials
|
|
45
74
|
// https://github.com/webpack-contrib/sass-loader/blob/0e9494074f69a6b6d47efea6c083a02a31a5ae84/test/sass/import-with-underscore.sass
|
|
46
75
|
expect(await webpackResolver('partial-import', { request: getFixturePath('/test/1.scss') })).toBe(
|
|
@@ -49,14 +78,30 @@ test('resolves specifier with sass-loader mechanism', async () => {
|
|
|
49
78
|
expect(await webpackResolver('test/partial-import', { request: getFixturePath('/test') })).toBe(
|
|
50
79
|
getFixturePath('/test/_partial-import.scss'),
|
|
51
80
|
);
|
|
81
|
+
expect(await webpackResolver('@relative/alias.scss', { request })).toBe(
|
|
82
|
+
getFixturePath('/test/alias-relative/alias.scss'),
|
|
83
|
+
);
|
|
84
|
+
expect(await webpackResolver('@absolute/alias.scss', { request })).toBe(
|
|
85
|
+
getFixturePath('/test/alias-absolute/alias.scss'),
|
|
86
|
+
);
|
|
52
87
|
});
|
|
53
88
|
|
|
54
89
|
test('resolves specifier with less-loader mechanism', async () => {
|
|
55
|
-
const webpackResolver = createWebpackResolver({
|
|
90
|
+
const webpackResolver = createWebpackResolver({
|
|
91
|
+
cwd: getFixturePath('/'),
|
|
92
|
+
lessIncludePaths: ['test/include-paths-relative', getFixturePath('/test/include-paths-absolute')],
|
|
93
|
+
webpackResolveAlias: {
|
|
94
|
+
'@relative': 'test/alias-relative',
|
|
95
|
+
'@absolute': getFixturePath('/test/alias-absolute'),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
56
98
|
const request = getFixturePath('/test/1.less');
|
|
57
99
|
createFixtures({
|
|
58
100
|
'/node_modules/package-1/index.less': `.a {}`,
|
|
59
|
-
'/test/
|
|
101
|
+
'/test/include-paths-relative/include-paths-relative.less': `.a {}`,
|
|
102
|
+
'/test/include-paths-absolute/include-paths-absolute.less': `.a {}`,
|
|
103
|
+
'/test/alias-relative/alias.less': `.a {}`,
|
|
104
|
+
'/test/alias-absolute/alias.less': `.a {}`,
|
|
60
105
|
});
|
|
61
106
|
expect(await webpackResolver('~package-1/index.less', { request })).toBe(
|
|
62
107
|
getFixturePath('/node_modules/package-1/index.less'),
|
|
@@ -65,5 +110,16 @@ test('resolves specifier with less-loader mechanism', async () => {
|
|
|
65
110
|
// ref: https://github.com/webpack-contrib/less-loader/blob/81a0d27eb6d18e5dc550a60fc1007fdc77305b78/test/loader.test.js#L248-L253
|
|
66
111
|
// ref: https://github.com/webpack-contrib/less-loader/blob/393147064672ace986ec84aca21f69f0ab819a9c/test/fixtures/import-paths.less#L1
|
|
67
112
|
// ref: https://github.com/webpack-contrib/less-loader/blob/99d80bd290dae50375db6e17c5f56ec33754e258/test/helpers/getCodeFromLess.js#L47-L54
|
|
68
|
-
expect(await webpackResolver('include-paths', { request })).toBe(
|
|
113
|
+
expect(await webpackResolver('include-paths-relative', { request })).toBe(
|
|
114
|
+
getFixturePath('/test/include-paths-relative/include-paths-relative.less'),
|
|
115
|
+
);
|
|
116
|
+
expect(await webpackResolver('include-paths-absolute', { request })).toBe(
|
|
117
|
+
getFixturePath('/test/include-paths-absolute/include-paths-absolute.less'),
|
|
118
|
+
);
|
|
119
|
+
expect(await webpackResolver('@relative/alias.less', { request })).toBe(
|
|
120
|
+
getFixturePath('/test/alias-relative/alias.less'),
|
|
121
|
+
);
|
|
122
|
+
expect(await webpackResolver('@absolute/alias.less', { request })).toBe(
|
|
123
|
+
getFixturePath('/test/alias-absolute/alias.less'),
|
|
124
|
+
);
|
|
69
125
|
});
|
|
@@ -1,25 +1,43 @@
|
|
|
1
|
-
import { basename, dirname, join } from 'path';
|
|
1
|
+
import { basename, dirname, join, resolve } from 'path';
|
|
2
2
|
import enhancedResolve from 'enhanced-resolve';
|
|
3
3
|
import { exists } from '../util.js';
|
|
4
4
|
import type { Resolver } from './index.js';
|
|
5
5
|
|
|
6
6
|
export type WebpackResolverOptions = {
|
|
7
|
+
/** Working directory path. */
|
|
8
|
+
cwd?: string | undefined;
|
|
7
9
|
/**
|
|
8
|
-
* The option compatible with sass's `--load-path`. It is an array of absolute paths.
|
|
10
|
+
* The option compatible with sass's `--load-path`. It is an array of relative or absolute paths.
|
|
11
|
+
* @example ['src/styles']
|
|
9
12
|
* @example ['/home/user/repository/src/styles']
|
|
10
13
|
*/
|
|
11
14
|
sassLoadPaths?: string[] | undefined;
|
|
12
15
|
/**
|
|
13
|
-
* The option compatible with less's `--include-path`. It is an array of absolute paths.
|
|
16
|
+
* The option compatible with less's `--include-path`. It is an array of relative or absolute paths.
|
|
17
|
+
* @example ['src/styles']
|
|
14
18
|
* @example ['/home/user/repository/src/styles']
|
|
15
19
|
*/
|
|
16
20
|
lessIncludePaths?: string[] | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The option compatible with webpack's `resolve.alias`. It is an object consisting of a pair of alias names and relative or absolute paths.
|
|
23
|
+
* @example { style: 'src/styles', '@': 'src' }
|
|
24
|
+
* @example { style: '/home/user/repository/src/styles', '@': '/home/user/repository/src' }
|
|
25
|
+
*/
|
|
26
|
+
webpackResolveAlias?: Record<string, string> | undefined;
|
|
17
27
|
};
|
|
18
28
|
|
|
19
29
|
// TODO: Support `resolve.alias` for Node.js API
|
|
20
30
|
export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOptions | undefined) => Resolver = (
|
|
21
31
|
webpackResolverOptions,
|
|
22
32
|
) => {
|
|
33
|
+
const cwd = webpackResolverOptions?.cwd ?? process.cwd();
|
|
34
|
+
const sassLoadPaths = webpackResolverOptions?.sassLoadPaths?.map((path) => resolve(cwd, path));
|
|
35
|
+
const lessIncludePaths = webpackResolverOptions?.lessIncludePaths?.map((path) => resolve(cwd, path));
|
|
36
|
+
const webpackResolveAlias = webpackResolverOptions?.webpackResolveAlias
|
|
37
|
+
? Object.fromEntries(
|
|
38
|
+
Object.entries(webpackResolverOptions?.webpackResolveAlias).map(([key, value]) => [key, resolve(cwd, value)]),
|
|
39
|
+
)
|
|
40
|
+
: undefined;
|
|
23
41
|
/**
|
|
24
42
|
* A resolver compatible with css-loader.
|
|
25
43
|
*
|
|
@@ -33,6 +51,7 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
|
|
|
33
51
|
mainFiles: ['index', '...'],
|
|
34
52
|
extensions: ['.css', '...'],
|
|
35
53
|
preferRelative: true,
|
|
54
|
+
alias: webpackResolveAlias,
|
|
36
55
|
});
|
|
37
56
|
|
|
38
57
|
/**
|
|
@@ -48,7 +67,8 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
|
|
|
48
67
|
extensions: ['.sass', '.scss', '.css'],
|
|
49
68
|
restrictions: [/\.((sa|sc|c)ss)$/i],
|
|
50
69
|
preferRelative: true,
|
|
51
|
-
|
|
70
|
+
alias: webpackResolveAlias,
|
|
71
|
+
modules: ['node_modules', ...(sassLoadPaths ?? [])],
|
|
52
72
|
});
|
|
53
73
|
|
|
54
74
|
/**
|
|
@@ -63,7 +83,8 @@ export const createWebpackResolver: (webpackResolverOptions?: WebpackResolverOpt
|
|
|
63
83
|
mainFiles: ['index', '...'],
|
|
64
84
|
extensions: ['.less', '.css'],
|
|
65
85
|
preferRelative: true,
|
|
66
|
-
|
|
86
|
+
alias: webpackResolveAlias,
|
|
87
|
+
modules: ['node_modules', ...(lessIncludePaths ?? [])],
|
|
67
88
|
});
|
|
68
89
|
|
|
69
90
|
// NOTE: In theory, `sassLoaderResolver` should only be used when the resolver is called from `sassTransformer`.
|
package/src/runner.test.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'fs/promises';
|
|
1
|
+
import { readFile, rm, writeFile } from 'fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import * as fileCacheNpm from '@file-cache/npm';
|
|
2
7
|
import { jest } from '@jest/globals';
|
|
3
8
|
import chalk from 'chalk';
|
|
4
9
|
import dedent from 'dedent';
|
|
5
|
-
import AggregateError from 'es-aggregate-error';
|
|
6
10
|
import type { Watcher } from './runner.js';
|
|
7
|
-
import { run } from './runner.js';
|
|
8
11
|
import { createFixtures, exists, getFixturePath, waitForAsyncTask } from './test/util.js';
|
|
9
12
|
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
jest.unstable_mockModule('@file-cache/npm', () => ({
|
|
16
|
+
...fileCacheNpm, // Inherit native functions
|
|
17
|
+
createNpmPackageKey: () => 'mocked-key',
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const { run } = await import('./runner.js');
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
23
|
+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
10
24
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
11
25
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
12
26
|
|
|
@@ -15,8 +29,17 @@ const defaultOptions = {
|
|
|
15
29
|
declarationMap: true,
|
|
16
30
|
silent: true,
|
|
17
31
|
cwd: getFixturePath('/'),
|
|
32
|
+
cache: false,
|
|
18
33
|
};
|
|
19
34
|
|
|
35
|
+
const dir = join(dirname(fileURLToPath(import.meta.url)));
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
consoleLogSpy.mockClear();
|
|
39
|
+
consoleErrorSpy.mockClear();
|
|
40
|
+
await rm(resolve(dir, '../node_modules/.cache/happy-css-modules'), { recursive: true, force: true }); // clear cache
|
|
41
|
+
});
|
|
42
|
+
|
|
20
43
|
// Exit the watcher even if the test fails
|
|
21
44
|
let watcher: Watcher | undefined;
|
|
22
45
|
afterEach(async () => {
|
|
@@ -37,6 +60,49 @@ test('generates .d.ts and .d.ts.map', async () => {
|
|
|
37
60
|
expect(await readFile(getFixturePath('/test/2.css.d.ts.map'), 'utf8')).toMatchSnapshot();
|
|
38
61
|
});
|
|
39
62
|
|
|
63
|
+
test('uses cache', async () => {
|
|
64
|
+
createFixtures({
|
|
65
|
+
'/test/1.css': '.a {}',
|
|
66
|
+
});
|
|
67
|
+
await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
|
|
68
|
+
expect(consoleLogSpy).toBeCalledTimes(1);
|
|
69
|
+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
|
|
70
|
+
consoleLogSpy.mockClear();
|
|
71
|
+
|
|
72
|
+
// Skip generation
|
|
73
|
+
await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
|
|
74
|
+
expect(consoleLogSpy).toBeCalledTimes(1);
|
|
75
|
+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('skipped'));
|
|
76
|
+
consoleLogSpy.mockClear();
|
|
77
|
+
|
|
78
|
+
// Generates if generated files are missing
|
|
79
|
+
await rm(getFixturePath('/test/1.css.d.ts'));
|
|
80
|
+
await run({ ...defaultOptions, declarationMap: true, silent: false, cache: true });
|
|
81
|
+
expect(consoleLogSpy).toBeCalledTimes(1);
|
|
82
|
+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
|
|
83
|
+
consoleLogSpy.mockClear();
|
|
84
|
+
|
|
85
|
+
// Generates if options are changed
|
|
86
|
+
await run({ ...defaultOptions, declarationMap: false, silent: false, cache: true });
|
|
87
|
+
expect(consoleLogSpy).toBeCalledTimes(1);
|
|
88
|
+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, expect.stringContaining('generated'));
|
|
89
|
+
consoleLogSpy.mockClear();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('outputs logs', async () => {
|
|
93
|
+
createFixtures({
|
|
94
|
+
'/test/1.css': '.a {}',
|
|
95
|
+
});
|
|
96
|
+
await run({ ...defaultOptions, silent: false, cache: true });
|
|
97
|
+
expect(consoleLogSpy).toBeCalledTimes(1);
|
|
98
|
+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `${chalk.green('test/1.css')} (generated)`);
|
|
99
|
+
consoleLogSpy.mockClear();
|
|
100
|
+
|
|
101
|
+
await run({ ...defaultOptions, silent: false, cache: true });
|
|
102
|
+
expect(consoleLogSpy).toBeCalledTimes(1);
|
|
103
|
+
expect(consoleLogSpy).toHaveBeenNthCalledWith(1, `${chalk.gray('test/1.css (skipped)')}`);
|
|
104
|
+
});
|
|
105
|
+
|
|
40
106
|
test.todo('changes dts format with localsConvention options');
|
|
41
107
|
test('does not emit declaration map if declarationMap is false', async () => {
|
|
42
108
|
createFixtures({
|
|
@@ -87,9 +153,9 @@ test('returns an error if the file fails to process in non-watch mode', async ()
|
|
|
87
153
|
// The error is logged to console.error.
|
|
88
154
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
|
89
155
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
90
|
-
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]));
|
|
156
|
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, chalk.red('[Error] ' + error.errors[0]!.stack));
|
|
91
157
|
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
92
|
-
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1]));
|
|
158
|
+
expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, chalk.red('[Error] ' + error.errors[1].stack));
|
|
93
159
|
|
|
94
160
|
// The valid files are emitted.
|
|
95
161
|
expect(await exists(getFixturePath('/test/1.css.d.ts'))).toBe(true);
|
|
@@ -129,27 +195,53 @@ describe('handles external files', () => {
|
|
|
129
195
|
});
|
|
130
196
|
|
|
131
197
|
test('sassLoadPaths', async () => {
|
|
132
|
-
const sassLoadPaths = ['test/relative'
|
|
198
|
+
const sassLoadPaths = ['test/relative'];
|
|
133
199
|
createFixtures({
|
|
134
200
|
'/test/1.scss': dedent`
|
|
135
201
|
@import '2.scss';
|
|
136
|
-
@import '3.scss';
|
|
137
202
|
`,
|
|
138
203
|
'/test/relative/2.scss': `.a { dummy: ''; }`,
|
|
139
|
-
'/test/absolute/3.scss': `.b { dummy: ''; }`,
|
|
140
204
|
});
|
|
141
205
|
await run({ ...defaultOptions, sassLoadPaths }); // not throw
|
|
142
206
|
});
|
|
143
207
|
|
|
144
208
|
test('lessIncludePaths', async () => {
|
|
145
|
-
const lessIncludePaths = ['test/relative'
|
|
209
|
+
const lessIncludePaths = ['test/relative'];
|
|
146
210
|
createFixtures({
|
|
147
211
|
'/test/1.less': dedent`
|
|
148
212
|
@import '2.less';
|
|
149
|
-
@import '3.less';
|
|
150
213
|
`,
|
|
151
214
|
'/test/relative/2.less': `.a { dummy: ''; }`,
|
|
152
|
-
'/test/absolute/3.less': `.b { dummy: ''; }`,
|
|
153
215
|
});
|
|
154
216
|
await run({ ...defaultOptions, lessIncludePaths }); // not throw
|
|
155
217
|
});
|
|
218
|
+
|
|
219
|
+
test('webpackResolveAlias', async () => {
|
|
220
|
+
const webpackResolveAlias = { '@relative': 'test/relative' };
|
|
221
|
+
createFixtures({
|
|
222
|
+
'/test/1.less': dedent`
|
|
223
|
+
@import '@relative/2.less';
|
|
224
|
+
`,
|
|
225
|
+
'/test/relative/2.less': `.a { dummy: ''; }`,
|
|
226
|
+
});
|
|
227
|
+
await run({ ...defaultOptions, webpackResolveAlias }); // not throw
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('postcssConfig', async () => {
|
|
231
|
+
const uuid = randomUUID();
|
|
232
|
+
const postcssConfig = `${uuid}/postcss.config.js`;
|
|
233
|
+
createFixtures({
|
|
234
|
+
[`/${uuid}/postcss.config.js`]: dedent`
|
|
235
|
+
module.exports = {
|
|
236
|
+
plugins: [
|
|
237
|
+
require('${require.resolve('postcss-simple-vars')}'),
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
`,
|
|
241
|
+
'/test/1.css': dedent`
|
|
242
|
+
$prefix: foo;
|
|
243
|
+
.$(prefix)_bar {}
|
|
244
|
+
`,
|
|
245
|
+
});
|
|
246
|
+
await run({ ...defaultOptions, postcssConfig }); // not throw
|
|
247
|
+
});
|
package/src/runner.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
1
|
+
import { resolve, relative } from 'path';
|
|
2
2
|
import * as process from 'process';
|
|
3
3
|
import * as util from 'util';
|
|
4
|
+
import { createCache } from '@file-cache/core';
|
|
5
|
+
import { createNpmPackageKey } from '@file-cache/npm';
|
|
4
6
|
import chalk from 'chalk';
|
|
5
7
|
import * as chokidar from 'chokidar';
|
|
6
|
-
import AggregateError from 'es-aggregate-error';
|
|
7
8
|
import _glob from 'glob';
|
|
8
|
-
import { emitGeneratedFiles } from './emitter/index.js';
|
|
9
|
-
import {
|
|
9
|
+
import { isGeneratedFilesExist, emitGeneratedFiles } from './emitter/index.js';
|
|
10
|
+
import { Locator } from './locator/index.js';
|
|
10
11
|
import type { Resolver } from './resolver/index.js';
|
|
11
12
|
import { createDefaultResolver } from './resolver/index.js';
|
|
12
|
-
import { type Transformer } from './transformer/index.js';
|
|
13
|
-
import { isMatchByGlob } from './util.js';
|
|
13
|
+
import { createDefaultTransformer, type Transformer } from './transformer/index.js';
|
|
14
|
+
import { getInstalledPeerDependencies, isMatchByGlob } from './util.js';
|
|
14
15
|
|
|
15
16
|
const glob = util.promisify(_glob);
|
|
16
17
|
|
|
@@ -22,7 +23,6 @@ export type LocalsConvention = 'camelCase' | 'camelCaseOnly' | 'dashes' | 'dashe
|
|
|
22
23
|
|
|
23
24
|
export interface RunnerOptions {
|
|
24
25
|
pattern: string;
|
|
25
|
-
outDir?: string | undefined;
|
|
26
26
|
watch?: boolean | undefined;
|
|
27
27
|
localsConvention?: LocalsConvention | undefined;
|
|
28
28
|
declarationMap?: boolean | undefined;
|
|
@@ -40,6 +40,29 @@ export interface RunnerOptions {
|
|
|
40
40
|
* @example ['/home/user/repository/src/styles']
|
|
41
41
|
*/
|
|
42
42
|
lessIncludePaths?: string[] | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The option compatible with webpack's `resolve.alias`. It is an object consisting of a pair of alias names and relative or absolute paths.
|
|
45
|
+
* @example { style: 'src/styles', '@': 'src' }
|
|
46
|
+
* @example { style: '/home/user/repository/src/styles', '@': '/home/user/repository/src' }
|
|
47
|
+
*/
|
|
48
|
+
webpackResolveAlias?: Record<string, string> | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The option compatible with postcss's `--config`. It is a relative or absolute path.
|
|
51
|
+
* @example '.'
|
|
52
|
+
* @example 'postcss.config.js'
|
|
53
|
+
* @example '/home/user/repository/src'
|
|
54
|
+
*/
|
|
55
|
+
postcssConfig?: string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Only generate .d.ts and .d.ts.map for changed files.
|
|
58
|
+
* @default true
|
|
59
|
+
*/
|
|
60
|
+
cache?: boolean | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Strategy for the cache to use for detecting changed files.
|
|
63
|
+
* @default 'content'
|
|
64
|
+
*/
|
|
65
|
+
cacheStrategy?: 'content' | 'metadata' | undefined;
|
|
43
66
|
/**
|
|
44
67
|
* Silent output. Do not show "files written" messages.
|
|
45
68
|
* @default false
|
|
@@ -61,43 +84,64 @@ export async function run(options: RunnerOptions): Promise<void>;
|
|
|
61
84
|
export async function run(options: RunnerOptions): Promise<Watcher | void> {
|
|
62
85
|
const cwd = options.cwd ?? process.cwd();
|
|
63
86
|
const silent = options.silent ?? false;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
const resolver =
|
|
88
|
+
options.resolver ??
|
|
89
|
+
createDefaultResolver({
|
|
90
|
+
cwd,
|
|
91
|
+
sassLoadPaths: options.sassLoadPaths,
|
|
92
|
+
lessIncludePaths: options.lessIncludePaths,
|
|
93
|
+
webpackResolveAlias: options.webpackResolveAlias,
|
|
94
|
+
});
|
|
95
|
+
const transformer = options.transformer ?? createDefaultTransformer({ cwd, postcssConfig: options.postcssConfig });
|
|
73
96
|
|
|
74
|
-
const
|
|
97
|
+
const installedPeerDependencies = await getInstalledPeerDependencies();
|
|
98
|
+
const cache = await createCache({
|
|
99
|
+
mode: options.cacheStrategy ?? 'content',
|
|
100
|
+
keys: [
|
|
101
|
+
() => createNpmPackageKey(['happy-css-modules', ...installedPeerDependencies]),
|
|
102
|
+
() => {
|
|
103
|
+
return JSON.stringify(options);
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
noCache: !(options.cache ?? true),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const locator = new Locator({ transformer, resolver });
|
|
75
110
|
const isExternalFile = (filePath: string) => {
|
|
76
111
|
return !isMatchByGlob(filePath, options.pattern, { cwd });
|
|
77
112
|
};
|
|
78
113
|
|
|
79
114
|
async function processFile(filePath: string) {
|
|
80
115
|
try {
|
|
81
|
-
const result = await
|
|
116
|
+
const result = await locator.load(filePath);
|
|
82
117
|
await emitGeneratedFiles({
|
|
83
118
|
filePath,
|
|
84
119
|
tokens: result.tokens,
|
|
85
|
-
distOptions,
|
|
86
120
|
emitDeclarationMap: options.declarationMap,
|
|
87
121
|
dtsFormatOptions: {
|
|
88
122
|
localsConvention: options.localsConvention,
|
|
89
123
|
},
|
|
90
|
-
silent,
|
|
91
|
-
cwd,
|
|
92
124
|
isExternalFile,
|
|
93
125
|
});
|
|
126
|
+
if (!silent) console.log(`${chalk.green(relative(cwd, filePath))} (generated)`);
|
|
94
127
|
} catch (error) {
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
if (error instanceof Error) {
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
130
|
+
console.error(chalk.red('[Error] ' + error.stack));
|
|
131
|
+
} else {
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
|
133
|
+
console.error(chalk.red('[Error] ' + error));
|
|
134
|
+
}
|
|
97
135
|
throw error;
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
138
|
|
|
139
|
+
async function isChangedFile(filePath: string) {
|
|
140
|
+
const result = await cache.getAndUpdateCache(filePath);
|
|
141
|
+
if (result.error) throw result.error;
|
|
142
|
+
return result.changed;
|
|
143
|
+
}
|
|
144
|
+
|
|
101
145
|
if (options.watch) {
|
|
102
146
|
if (!silent) console.log('Watch ' + options.pattern + '...');
|
|
103
147
|
const watcher = chokidar.watch([options.pattern.replace(/\\/g, '/')], { cwd });
|
|
@@ -114,11 +158,25 @@ export async function run(options: RunnerOptions): Promise<Watcher | void> {
|
|
|
114
158
|
// convert relative path to absolute path
|
|
115
159
|
.map((file) => resolve(cwd, file));
|
|
116
160
|
|
|
117
|
-
// TODO: Use `@file-cache/core` to process only files that have changed
|
|
118
161
|
const errors: unknown[] = [];
|
|
119
162
|
for (const filePath of filePaths) {
|
|
120
|
-
|
|
163
|
+
try {
|
|
164
|
+
const _isGeneratedFilesExist = await isGeneratedFilesExist(filePath, options.declarationMap);
|
|
165
|
+
const _isChangedFile = await isChangedFile(filePath);
|
|
166
|
+
// Generate .d.ts and .d.ts.map only when the file has been updated.
|
|
167
|
+
// However, if .d.ts or .d.ts.map has not yet been generated, always generate.
|
|
168
|
+
if (!_isGeneratedFilesExist || _isChangedFile) {
|
|
169
|
+
await processFile(filePath);
|
|
170
|
+
} else {
|
|
171
|
+
if (!silent) console.log(chalk.gray(`${relative(cwd, filePath)} (skipped)`));
|
|
172
|
+
}
|
|
173
|
+
} catch (e: unknown) {
|
|
174
|
+
errors.push(e);
|
|
175
|
+
}
|
|
121
176
|
}
|
|
122
177
|
if (errors.length > 0) throw new AggregateError(errors, 'Failed to process files');
|
|
123
178
|
}
|
|
179
|
+
|
|
180
|
+
// Write cache state to file for persistence
|
|
181
|
+
await cache.reconcile();
|
|
124
182
|
}
|