diff --git a/src/index.js b/src/index.js index f582df1e..907bc68e 100644 --- a/src/index.js +++ b/src/index.js @@ -59,7 +59,7 @@ export default async function loader(content, sourceMap, meta) { ? options.sourceMap : this.sourceMap; - const { plugins, processOptions } = getPostcssOptions( + const { plugins, processOptions } = await getPostcssOptions( this, loadedConfig, options.postcssOptions diff --git a/src/utils.js b/src/utils.js index e43becf0..4ca898ab 100644 --- a/src/utils.js +++ b/src/utils.js @@ -167,7 +167,35 @@ function pluginFactory() { }; } -function getPostcssOptions( +async function load(module) { + let exports; + + try { + // eslint-disable-next-line import/no-dynamic-require, global-require + exports = require(module); + + return exports; + } catch (requireError) { + let importESM; + + try { + // eslint-disable-next-line no-new-func + importESM = new Function("id", "return import(id);"); + } catch (e) { + importESM = null; + } + + if (requireError.code === "ERR_REQUIRE_ESM" && importESM) { + exports = await importESM(module); + + return exports.default; + } + + throw requireError; + } +} + +async function getPostcssOptions( loaderContext, loadedConfig = {}, postcssOptions = {} @@ -253,8 +281,7 @@ function getPostcssOptions( if (typeof processOptions.parser === "string") { try { - // eslint-disable-next-line import/no-dynamic-require, global-require - processOptions.parser = require(processOptions.parser); + processOptions.parser = await load(processOptions.parser); } catch (error) { loaderContext.emitError( new Error( @@ -266,8 +293,7 @@ function getPostcssOptions( if (typeof processOptions.stringifier === "string") { try { - // eslint-disable-next-line import/no-dynamic-require, global-require - processOptions.stringifier = require(processOptions.stringifier); + processOptions.stringifier = await load(processOptions.stringifier); } catch (error) { loaderContext.emitError( new Error( @@ -279,8 +305,7 @@ function getPostcssOptions( if (typeof processOptions.syntax === "string") { try { - // eslint-disable-next-line import/no-dynamic-require, global-require - processOptions.syntax = require(processOptions.syntax); + processOptions.syntax = await load(processOptions.syntax); } catch (error) { loaderContext.emitError( new Error( diff --git a/test/fixtures/esparser/index.mjs b/test/fixtures/esparser/index.mjs new file mode 100644 index 00000000..5905c023 --- /dev/null +++ b/test/fixtures/esparser/index.mjs @@ -0,0 +1,9 @@ +function stringify() { + return ""; +} + +function parse() { + return ""; +} + +export default { stringify, parse }; diff --git a/test/fixtures/esparser/package.json b/test/fixtures/esparser/package.json new file mode 100644 index 00000000..29a16e5d --- /dev/null +++ b/test/fixtures/esparser/package.json @@ -0,0 +1,11 @@ +{ + "type": "module", + "exports": { + ".": { + "require": "./index.js", + "import": "./index.mjs" + } + }, + "name": "test", + "version": "1.0.0" +} diff --git a/test/fixtures/esparser/runManual.mjs b/test/fixtures/esparser/runManual.mjs new file mode 100644 index 00000000..2fc433fd --- /dev/null +++ b/test/fixtures/esparser/runManual.mjs @@ -0,0 +1,46 @@ +import webpack from 'webpack'; +import path from "path"; + +const __dirname = path.resolve(); +const rootDir = path.resolve(__dirname, "test/helpers"); + +const compiler = webpack({ + target: 'node', + mode: "development", + devtool: false, + context: path.resolve(rootDir, "../fixtures"), + entry: path.resolve(rootDir, "../fixtures", "./sss/index.js"), + output: { + path: path.resolve(rootDir, "../outputs"), + filename: "[name].bundle.js", + chunkFilename: "[name].chunk.js", + publicPath: "/webpack/public/path/", + }, + module: { + rules: [ + { + test: /\.(css|sss)$/i, + use: [ + 'css-loader', + { + loader: path.resolve(rootDir, "../../dist"), + options: { + postcssOptions: { + parser: path.resolve(rootDir, "../fixtures/esparser/index.mjs"), + stringifier: path.resolve(rootDir, "../fixtures/esparser/index.mjs"), + syntax: path.resolve(rootDir, "../fixtures/esparser/index.mjs"), + }, + }, + }, + ], + }, + ], + }, + plugins: [], +}); + +compiler.run((error) => { + if (error) { + throw error; + } +}); diff --git a/test/postcssOptins.test.js b/test/postcssOptins.test.js index 17c01911..b1d76dcf 100644 --- a/test/postcssOptins.test.js +++ b/test/postcssOptins.test.js @@ -170,6 +170,54 @@ describe('"postcssOptions" option', () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); + // TODO jest have not good support for ES modules for testing it, tested manually + it.skip('should work with the "parser" option with "Object" value with ESM', async () => { + const compiler = getCompiler("./sss/index.js", { + postcssOptions: { + parser: path.resolve(__dirname, "../fixtures/esparser/index.mjs"), + }, + }); + const stats = await compile(compiler); + + const codeFromBundle = getCodeFromBundle("style.sss", stats); + + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + // TODO jest have not good support for ES modules for testing it, tested manually + it.skip('should work with the "stringifier" option with "Object" value with ESM', async () => { + const compiler = getCompiler("./sss/index.js", { + postcssOptions: { + stringifier: path.resolve(__dirname, "../fixtures/esparser/index.mjs"), + }, + }); + const stats = await compile(compiler); + + const codeFromBundle = getCodeFromBundle("style.sss", stats); + + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + + // TODO jest have not good support for ES modules for testing it, tested manually + it.skip('should work with the "syntax" option with "Object" value with ESM', async () => { + const compiler = getCompiler("./sss/index.js", { + postcssOptions: { + syntax: path.resolve(__dirname, "../fixtures/esparser/index.mjs"), + }, + }); + const stats = await compile(compiler); + + const codeFromBundle = getCodeFromBundle("style.sss", stats); + + expect(codeFromBundle.css).toMatchSnapshot("css"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); + it('should work with the "parser" option with "Function" value', async () => { const compiler = getCompiler("./sss/index.js", { postcssOptions: {