Skip to content

Commit 9b75888

Browse files
feat: reuse AST from other loaders (#468)
1 parent 5e4a77b commit 9b75888

File tree

8 files changed

+279
-17
lines changed

8 files changed

+279
-17
lines changed

package-lock.json

+69-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
"dependencies": {
4646
"cosmiconfig": "^7.0.0",
4747
"loader-utils": "^2.0.0",
48-
"schema-utils": "^2.7.1"
48+
"schema-utils": "^2.7.1",
49+
"semver": "^7.3.2"
4950
},
5051
"devDependencies": {
5152
"@babel/cli": "^7.11.6",

src/index.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { getOptions } from 'loader-utils';
22
import validateOptions from 'schema-utils';
33

44
import postcss from 'postcss';
5+
import { satisfies } from 'semver';
6+
import postcssPackage from 'postcss/package.json';
57

68
import Warning from './Warning';
79
import SyntaxError from './Error';
@@ -23,11 +25,12 @@ import {
2325
*
2426
* @param {String} content Source
2527
* @param {Object} sourceMap Source Map
28+
* @param {Object} meta Meta
2629
*
2730
* @return {callback} callback Result
2831
*/
2932

30-
export default async function loader(content, sourceMap) {
33+
export default async function loader(content, sourceMap, meta) {
3134
const options = getOptions(this);
3235

3336
validateOptions(schema, options, {
@@ -65,11 +68,6 @@ export default async function loader(content, sourceMap) {
6568
options.postcssOptions
6669
);
6770

68-
if (options.execute) {
69-
// eslint-disable-next-line no-param-reassign
70-
content = exec(content, this);
71-
}
72-
7371
if (useSourceMap) {
7472
processOptions.map = { inline: false, annotation: false };
7573

@@ -84,10 +82,27 @@ export default async function loader(content, sourceMap) {
8482
processOptions.map.prev = sourceMap;
8583
}
8684

85+
let root;
86+
87+
// Reuse PostCSS AST from other loaders
88+
if (
89+
meta &&
90+
meta.ast &&
91+
meta.ast.type === 'postcss' &&
92+
satisfies(meta.ast.version, `^${postcssPackage.version}`)
93+
) {
94+
({ root } = meta.ast);
95+
}
96+
97+
if (!root && options.execute) {
98+
// eslint-disable-next-line no-param-reassign
99+
content = exec(content, this);
100+
}
101+
87102
let result;
88103

89104
try {
90-
result = await postcss(plugins).process(content, processOptions);
105+
result = await postcss(plugins).process(root || content, processOptions);
91106
} catch (error) {
92107
if (error.file) {
93108
this.addDependency(error.file);

test/__snapshots__/execute.test.js.snap

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`"execute" option should reuse PostCSS AST with JS styles: css 1`] = `
4+
"a {
5+
color: green
6+
}"
7+
`;
8+
9+
exports[`"execute" option should reuse PostCSS AST with JS styles: errors 1`] = `Array []`;
10+
11+
exports[`"execute" option should reuse PostCSS AST with JS styles: warnings 1`] = `Array []`;
12+
313
exports[`"execute" option should work with "Boolean" value: css 1`] = `
414
"a {
515
color: green

test/__snapshots__/loader.test.js.snap

+51
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,57 @@ Warning
106106
]
107107
`;
108108
109+
exports[`loader should reuse PostCSS AST: css 1`] = `
110+
"a {
111+
color: black;
112+
}
113+
114+
a {
115+
color: red;
116+
}
117+
118+
a {
119+
color: green;
120+
}
121+
122+
a {
123+
color: blue;
124+
}
125+
126+
.class {
127+
-x-border-color: blue blue *;
128+
-x-color: * #fafafa;
129+
}
130+
131+
.class-foo {
132+
-z-border-color: blue blue *;
133+
-z-color: * #fafafa;
134+
}
135+
136+
.phone {
137+
&_title {
138+
width: 500px;
139+
140+
@media (max-width: 500px) {
141+
width: auto;
142+
}
143+
144+
body.is_dark & {
145+
color: white;
146+
}
147+
}
148+
149+
img {
150+
display: block;
151+
}
152+
}
153+
"
154+
`;
155+
156+
exports[`loader should reuse PostCSS AST: errors 1`] = `Array []`;
157+
158+
exports[`loader should reuse PostCSS AST: warnings 1`] = `Array []`;
159+
109160
exports[`loader should throw an error on invalid syntax: errors 1`] = `
110161
Array [
111162
"ModuleBuildError: Module build failed (from \`replaced original path\`):

test/execute.test.js

+40-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ describe('"execute" option', () => {
3636
}
3737
);
3838
const stats = await compile(compiler);
39-
4039
const codeFromBundle = getCodeFromBundle('style.exec.js', stats);
4140

4241
expect(codeFromBundle.css).toMatchSnapshot('css');
@@ -73,13 +72,51 @@ describe('"execute" option', () => {
7372
},
7473
}
7574
);
76-
7775
const stats = await compile(compiler);
78-
7976
const codeFromBundle = getCodeFromBundle('style.js', stats);
8077

8178
expect(codeFromBundle.css).toMatchSnapshot('css');
8279
expect(getWarnings(stats)).toMatchSnapshot('warnings');
8380
expect(getErrors(stats)).toMatchSnapshot('errors');
8481
});
82+
83+
it('should reuse PostCSS AST with JS styles', async () => {
84+
const spy = jest.fn();
85+
const compiler = getCompiler(
86+
'./jss/exec/index.js',
87+
{},
88+
{
89+
module: {
90+
rules: [
91+
{
92+
test: /style\.(exec\.js|js)$/i,
93+
use: [
94+
{
95+
loader: require.resolve('./helpers/testLoader'),
96+
options: {},
97+
},
98+
{
99+
loader: path.resolve(__dirname, '../src'),
100+
options: {
101+
execute: true,
102+
},
103+
},
104+
{
105+
loader: require.resolve('./helpers/astLoader'),
106+
options: { spy, execute: true },
107+
},
108+
],
109+
},
110+
],
111+
},
112+
}
113+
);
114+
const stats = await compile(compiler);
115+
const codeFromBundle = getCodeFromBundle('style.exec.js', stats);
116+
117+
expect(spy).toHaveBeenCalledTimes(1);
118+
expect(codeFromBundle.css).toMatchSnapshot('css');
119+
expect(getWarnings(stats)).toMatchSnapshot('warnings');
120+
expect(getErrors(stats)).toMatchSnapshot('errors');
121+
});
85122
});

test/helpers/astLoader.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Module from 'module';
2+
3+
const postcss = require('postcss');
4+
5+
const parentModule = module;
6+
7+
function exec(code, loaderContext) {
8+
const { resource, context } = loaderContext;
9+
10+
const module = new Module(resource, parentModule);
11+
12+
// eslint-disable-next-line no-underscore-dangle
13+
module.paths = Module._nodeModulePaths(context);
14+
module.filename = resource;
15+
16+
// eslint-disable-next-line no-underscore-dangle
17+
module._compile(code, resource);
18+
19+
return module.exports;
20+
}
21+
22+
module.exports = function astLoader(content) {
23+
const callback = this.async();
24+
const { spy = jest.fn(), execute } = this.query;
25+
26+
if (execute) {
27+
// eslint-disable-next-line no-param-reassign
28+
content = exec(content, this);
29+
}
30+
31+
postcss()
32+
.process(content)
33+
.then((result) => {
34+
const ast = {
35+
type: 'postcss',
36+
version: result.processor.version,
37+
root: result.root,
38+
};
39+
40+
Object.defineProperty(ast, 'root', {
41+
get: spy.mockReturnValue(result.root),
42+
});
43+
44+
callback(null, result.css, result.map, { ast });
45+
});
46+
};

0 commit comments

Comments
 (0)