Skip to content

Commit

Permalink
feat: reuse AST from other loaders (#468)
Browse files Browse the repository at this point in the history
  • Loading branch information
evilebottnawi committed Sep 7, 2020
1 parent 5e4a77b commit 9b75888
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 17 deletions.
73 changes: 69 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -45,7 +45,8 @@
"dependencies": {
"cosmiconfig": "^7.0.0",
"loader-utils": "^2.0.0",
"schema-utils": "^2.7.1"
"schema-utils": "^2.7.1",
"semver": "^7.3.2"
},
"devDependencies": {
"@babel/cli": "^7.11.6",
Expand Down
29 changes: 22 additions & 7 deletions src/index.js
Expand Up @@ -2,6 +2,8 @@ import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';

import postcss from 'postcss';
import { satisfies } from 'semver';
import postcssPackage from 'postcss/package.json';

import Warning from './Warning';
import SyntaxError from './Error';
Expand All @@ -23,11 +25,12 @@ import {
*
* @param {String} content Source
* @param {Object} sourceMap Source Map
* @param {Object} meta Meta
*
* @return {callback} callback Result
*/

export default async function loader(content, sourceMap) {
export default async function loader(content, sourceMap, meta) {
const options = getOptions(this);

validateOptions(schema, options, {
Expand Down Expand Up @@ -65,11 +68,6 @@ export default async function loader(content, sourceMap) {
options.postcssOptions
);

if (options.execute) {
// eslint-disable-next-line no-param-reassign
content = exec(content, this);
}

if (useSourceMap) {
processOptions.map = { inline: false, annotation: false };

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

let root;

// Reuse PostCSS AST from other loaders
if (
meta &&
meta.ast &&
meta.ast.type === 'postcss' &&
satisfies(meta.ast.version, `^${postcssPackage.version}`)
) {
({ root } = meta.ast);
}

if (!root && options.execute) {
// eslint-disable-next-line no-param-reassign
content = exec(content, this);
}

let result;

try {
result = await postcss(plugins).process(content, processOptions);
result = await postcss(plugins).process(root || content, processOptions);
} catch (error) {
if (error.file) {
this.addDependency(error.file);
Expand Down
10 changes: 10 additions & 0 deletions test/__snapshots__/execute.test.js.snap
@@ -1,5 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`"execute" option should reuse PostCSS AST with JS styles: css 1`] = `
"a {
color: green
}"
`;

exports[`"execute" option should reuse PostCSS AST with JS styles: errors 1`] = `Array []`;

exports[`"execute" option should reuse PostCSS AST with JS styles: warnings 1`] = `Array []`;

exports[`"execute" option should work with "Boolean" value: css 1`] = `
"a {
color: green
Expand Down
51 changes: 51 additions & 0 deletions test/__snapshots__/loader.test.js.snap
Expand Up @@ -106,6 +106,57 @@ Warning
]
`;
exports[`loader should reuse PostCSS AST: css 1`] = `
"a {
color: black;
}
a {
color: red;
}
a {
color: green;
}
a {
color: blue;
}
.class {
-x-border-color: blue blue *;
-x-color: * #fafafa;
}
.class-foo {
-z-border-color: blue blue *;
-z-color: * #fafafa;
}
.phone {
&_title {
width: 500px;
@media (max-width: 500px) {
width: auto;
}
body.is_dark & {
color: white;
}
}
img {
display: block;
}
}
"
`;
exports[`loader should reuse PostCSS AST: errors 1`] = `Array []`;
exports[`loader should reuse PostCSS AST: warnings 1`] = `Array []`;
exports[`loader should throw an error on invalid syntax: errors 1`] = `
Array [
"ModuleBuildError: Module build failed (from \`replaced original path\`):
Expand Down
43 changes: 40 additions & 3 deletions test/execute.test.js
Expand Up @@ -36,7 +36,6 @@ describe('"execute" option', () => {
}
);
const stats = await compile(compiler);

const codeFromBundle = getCodeFromBundle('style.exec.js', stats);

expect(codeFromBundle.css).toMatchSnapshot('css');
Expand Down Expand Up @@ -73,13 +72,51 @@ describe('"execute" option', () => {
},
}
);

const stats = await compile(compiler);

const codeFromBundle = getCodeFromBundle('style.js', stats);

expect(codeFromBundle.css).toMatchSnapshot('css');
expect(getWarnings(stats)).toMatchSnapshot('warnings');
expect(getErrors(stats)).toMatchSnapshot('errors');
});

it('should reuse PostCSS AST with JS styles', async () => {
const spy = jest.fn();
const compiler = getCompiler(
'./jss/exec/index.js',
{},
{
module: {
rules: [
{
test: /style\.(exec\.js|js)$/i,
use: [
{
loader: require.resolve('./helpers/testLoader'),
options: {},
},
{
loader: path.resolve(__dirname, '../src'),
options: {
execute: true,
},
},
{
loader: require.resolve('./helpers/astLoader'),
options: { spy, execute: true },
},
],
},
],
},
}
);
const stats = await compile(compiler);
const codeFromBundle = getCodeFromBundle('style.exec.js', stats);

expect(spy).toHaveBeenCalledTimes(1);
expect(codeFromBundle.css).toMatchSnapshot('css');
expect(getWarnings(stats)).toMatchSnapshot('warnings');
expect(getErrors(stats)).toMatchSnapshot('errors');
});
});
46 changes: 46 additions & 0 deletions test/helpers/astLoader.js
@@ -0,0 +1,46 @@
import Module from 'module';

const postcss = require('postcss');

const parentModule = module;

function exec(code, loaderContext) {
const { resource, context } = loaderContext;

const module = new Module(resource, parentModule);

// eslint-disable-next-line no-underscore-dangle
module.paths = Module._nodeModulePaths(context);
module.filename = resource;

// eslint-disable-next-line no-underscore-dangle
module._compile(code, resource);

return module.exports;
}

module.exports = function astLoader(content) {
const callback = this.async();
const { spy = jest.fn(), execute } = this.query;

if (execute) {
// eslint-disable-next-line no-param-reassign
content = exec(content, this);
}

postcss()
.process(content)
.then((result) => {
const ast = {
type: 'postcss',
version: result.processor.version,
root: result.root,
};

Object.defineProperty(ast, 'root', {
get: spy.mockReturnValue(result.root),
});

callback(null, result.css, result.map, { ast });
});
};

0 comments on commit 9b75888

Please sign in to comment.