Skip to content

Commit 1e9505e

Browse files
feat: support ECMA modules
1 parent 55630b8 commit 1e9505e

File tree

6 files changed

+174
-20
lines changed

6 files changed

+174
-20
lines changed

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export default async function loader(content, sourceMap, meta) {
5959
? options.sourceMap
6060
: this.sourceMap;
6161

62-
const { plugins, processOptions } = getPostcssOptions(
62+
const { plugins, processOptions } = await getPostcssOptions(
6363
this,
6464
loadedConfig,
6565
options.postcssOptions

src/utils.js

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,26 @@ function pluginFactory() {
167167
};
168168
}
169169

170-
function getPostcssOptions(
170+
async function getImportEsm(module, requireError) {
171+
let importESM;
172+
173+
try {
174+
// eslint-disable-next-line no-new-func
175+
importESM = new Function("id", "return import(id);");
176+
} catch (e) {
177+
importESM = null;
178+
}
179+
180+
if (requireError.code === "ERR_REQUIRE_ESM" && importESM) {
181+
const exports = await importESM(module);
182+
183+
return exports.default;
184+
}
185+
186+
throw requireError;
187+
}
188+
189+
async function getPostcssOptions(
171190
loaderContext,
172191
loadedConfig = {},
173192
postcssOptions = {}
@@ -255,38 +274,59 @@ function getPostcssOptions(
255274
try {
256275
// eslint-disable-next-line import/no-dynamic-require, global-require
257276
processOptions.parser = require(processOptions.parser);
258-
} catch (error) {
259-
loaderContext.emitError(
260-
new Error(
261-
`Loading PostCSS "${processOptions.parser}" parser failed: ${error.message}\n\n(@${file})`
262-
)
263-
);
277+
} catch (requireError) {
278+
try {
279+
processOptions.parser = await getImportEsm(
280+
processOptions.parser,
281+
requireError
282+
);
283+
} catch (error) {
284+
loaderContext.emitError(
285+
new Error(
286+
`Loading PostCSS "${processOptions.parser}" parser failed: ${error.message}\n\n(@${file})`
287+
)
288+
);
289+
}
264290
}
265291
}
266292

267293
if (typeof processOptions.stringifier === "string") {
268294
try {
269295
// eslint-disable-next-line import/no-dynamic-require, global-require
270296
processOptions.stringifier = require(processOptions.stringifier);
271-
} catch (error) {
272-
loaderContext.emitError(
273-
new Error(
274-
`Loading PostCSS "${processOptions.stringifier}" stringifier failed: ${error.message}\n\n(@${file})`
275-
)
276-
);
297+
} catch (requireError) {
298+
try {
299+
processOptions.stringifier = await getImportEsm(
300+
processOptions.stringifier,
301+
requireError
302+
);
303+
} catch (error) {
304+
loaderContext.emitError(
305+
new Error(
306+
`Loading PostCSS "${processOptions.stringifier}" stringifier failed: ${error.message}\n\n(@${file})`
307+
)
308+
);
309+
}
277310
}
278311
}
279312

280313
if (typeof processOptions.syntax === "string") {
281314
try {
282315
// eslint-disable-next-line import/no-dynamic-require, global-require
283316
processOptions.syntax = require(processOptions.syntax);
284-
} catch (error) {
285-
loaderContext.emitError(
286-
new Error(
287-
`Loading PostCSS "${processOptions.syntax}" syntax failed: ${error.message}\n\n(@${file})`
288-
)
289-
);
317+
} catch (requireError) {
318+
try {
319+
processOptions.syntax = await getImportEsm(
320+
processOptions.syntax,
321+
requireError
322+
);
323+
} catch (error) {
324+
loaderContext.emitError(
325+
new Error(
326+
`Loading PostCSS "${processOptions.syntax}" syntax failed: ${error.message}\n\n(@${file})`
327+
)
328+
);
329+
}
290330
}
291331
}
292332

test/fixtures/esparser/index.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function stringify() {
2+
return "";
3+
}
4+
5+
function parse() {
6+
return "";
7+
}
8+
9+
export default { stringify, parse };

test/fixtures/esparser/package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "module",
3+
"exports": {
4+
".": {
5+
"require": "./index.js",
6+
"import": "./index.mjs"
7+
}
8+
},
9+
"name": "test",
10+
"version": "1.0.0"
11+
}

test/fixtures/esparser/runManual.mjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import webpack from 'webpack';
2+
import path from "path";
3+
4+
const __dirname = path.resolve();
5+
const rootDir = path.resolve(__dirname, "test/helpers");
6+
7+
const compiler = webpack({
8+
target: 'node',
9+
mode: "development",
10+
devtool: false,
11+
context: path.resolve(rootDir, "../fixtures"),
12+
entry: path.resolve(rootDir, "../fixtures", "./sss/index.js"),
13+
output: {
14+
path: path.resolve(rootDir, "../outputs"),
15+
filename: "[name].bundle.js",
16+
chunkFilename: "[name].chunk.js",
17+
publicPath: "/webpack/public/path/",
18+
},
19+
module: {
20+
rules: [
21+
{
22+
test: /\.(css|sss)$/i,
23+
use: [
24+
'css-loader',
25+
{
26+
loader: path.resolve(rootDir, "../../dist"),
27+
options: {
28+
postcssOptions: {
29+
parser: path.resolve(rootDir, "../fixtures/esparser/index.mjs"),
30+
stringifier: path.resolve(rootDir, "../fixtures/esparser/index.mjs"),
31+
syntax: path.resolve(rootDir, "../fixtures/esparser/index.mjs"),
32+
},
33+
},
34+
},
35+
],
36+
},
37+
],
38+
},
39+
plugins: [],
40+
});
41+
42+
compiler.run((error) => {
43+
if (error) {
44+
throw error;
45+
}
46+
});

test/postcssOptins.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,54 @@ describe('"postcssOptions" option', () => {
170170
expect(getErrors(stats)).toMatchSnapshot("errors");
171171
});
172172

173+
// TODO jest have not good support for ES modules for testing it, tested manually
174+
it.skip('should work with the "parser" option with "Object" value with ESM', async () => {
175+
const compiler = getCompiler("./sss/index.js", {
176+
postcssOptions: {
177+
parser: path.resolve(__dirname, "../fixtures/esparser/index.mjs"),
178+
},
179+
});
180+
const stats = await compile(compiler);
181+
182+
const codeFromBundle = getCodeFromBundle("style.sss", stats);
183+
184+
expect(codeFromBundle.css).toMatchSnapshot("css");
185+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
186+
expect(getErrors(stats)).toMatchSnapshot("errors");
187+
});
188+
189+
// TODO jest have not good support for ES modules for testing it, tested manually
190+
it.skip('should work with the "stringifier" option with "Object" value with ESM', async () => {
191+
const compiler = getCompiler("./sss/index.js", {
192+
postcssOptions: {
193+
stringifier: path.resolve(__dirname, "../fixtures/esparser/index.mjs"),
194+
},
195+
});
196+
const stats = await compile(compiler);
197+
198+
const codeFromBundle = getCodeFromBundle("style.sss", stats);
199+
200+
expect(codeFromBundle.css).toMatchSnapshot("css");
201+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
202+
expect(getErrors(stats)).toMatchSnapshot("errors");
203+
});
204+
205+
// TODO jest have not good support for ES modules for testing it, tested manually
206+
it.skip('should work with the "syntax" option with "Object" value with ESM', async () => {
207+
const compiler = getCompiler("./sss/index.js", {
208+
postcssOptions: {
209+
syntax: path.resolve(__dirname, "../fixtures/esparser/index.mjs"),
210+
},
211+
});
212+
const stats = await compile(compiler);
213+
214+
const codeFromBundle = getCodeFromBundle("style.sss", stats);
215+
216+
expect(codeFromBundle.css).toMatchSnapshot("css");
217+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
218+
expect(getErrors(stats)).toMatchSnapshot("errors");
219+
});
220+
173221
it('should work with the "parser" option with "Function" value', async () => {
174222
const compiler = getCompiler("./sss/index.js", {
175223
postcssOptions: {

0 commit comments

Comments
 (0)