diff --git a/lib/svgo-node.js b/lib/svgo-node.js index a80991f92..9e401811e 100644 --- a/lib/svgo-node.js +++ b/lib/svgo-node.js @@ -2,6 +2,7 @@ const os = require('os'); const fs = require('fs'); +const { pathToFileURL } = require('url'); const path = require('path'); const { extendDefaultPlugins, @@ -13,7 +14,25 @@ exports.extendDefaultPlugins = extendDefaultPlugins; exports.createContentItem = createContentItem; const importConfig = async (configFile) => { - const config = require(configFile); + let config; + try { + // dynamic import expects file url instead of path and may fail + // when windows path is provided + const { default: imported } = await import(pathToFileURL(configFile)); + config = imported; + } catch (importError) { + // TODO remove require in v3 + try { + config = require(configFile); + } catch (requireError) { + // throw original error if es module is detected + if (requireError.code === 'ERR_REQUIRE_ESM') { + throw importError; + } else { + throw requireError; + } + } + } if (config == null || typeof config !== 'object' || Array.isArray(config)) { throw Error(`Invalid config file "${configFile}"`); } @@ -40,9 +59,17 @@ const loadConfig = async (configFile, cwd = process.cwd()) => { let dir = cwd; // eslint-disable-next-line no-constant-condition while (true) { - const file = path.join(dir, 'svgo.config.js'); - if (await isFile(file)) { - return await importConfig(file); + const js = path.join(dir, 'svgo.config.js'); + if (await isFile(js)) { + return await importConfig(js); + } + const mjs = path.join(dir, 'svgo.config.mjs'); + if (await isFile(mjs)) { + return await importConfig(mjs); + } + const cjs = path.join(dir, 'svgo.config.cjs'); + if (await isFile(cjs)) { + return await importConfig(cjs); } const parent = path.dirname(dir); if (dir === parent) { diff --git a/lib/svgo-node.test.js b/lib/svgo-node.test.js index c80cd33b3..fe21e085d 100644 --- a/lib/svgo-node.test.js +++ b/lib/svgo-node.test.js @@ -5,7 +5,8 @@ */ const os = require('os'); -const { optimize } = require('./svgo-node.js'); +const path = require('path'); +const { optimize, loadConfig } = require('./svgo-node.js'); const describeLF = os.EOL === '\r\n' ? describe.skip : describe; const describeCRLF = os.EOL === '\r\n' ? describe : describe.skip; @@ -123,3 +124,94 @@ describeCRLF('with CRLF line-endings', () => { ); }); }); + +describe('loadConfig', () => { + const cwd = process.cwd(); + const fixtures = path.join(cwd, './test/fixtures/config-loader'); + + test('loads by absolute path', async () => { + expect(await loadConfig(path.join(fixtures, 'one/two/config.js'))).toEqual({ + plugins: [], + }); + }); + + test('loads by relative path to cwd', async () => { + const config = await loadConfig('one/two/config.js', fixtures); + expect(config).toEqual({ plugins: [] }); + }); + + test('searches in cwd and up', async () => { + expect(await loadConfig(null, path.join(fixtures, 'one/two'))).toEqual({ + plugins: [], + }); + expect( + await loadConfig(null, path.join(cwd, './test/fixtures/missing')) + ).toEqual(null); + // TODO remove check in v3 + if (process.version.startsWith('v10.') === false) { + expect(await loadConfig(null, path.join(fixtures, 'mjs'))).toEqual({ + plugins: ['mjs'], + }); + } + expect(await loadConfig(null, path.join(fixtures, 'cjs'))).toEqual({ + plugins: ['cjs'], + }); + }); + + test('fails when specified config does not exist', async () => { + try { + await loadConfig('{}'); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/Cannot find module/); + } + }); + + test('fails when exported config not an object', async () => { + try { + await loadConfig(path.join(fixtures, 'invalid-null.js')); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/Invalid config file/); + } + try { + await loadConfig(path.join(fixtures, 'invalid-array.js')); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/Invalid config file/); + } + try { + await loadConfig(path.join(fixtures, 'invalid-string.js')); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/Invalid config file/); + } + }); + + test('handles runtime errors properly', async () => { + try { + await loadConfig(path.join(fixtures, 'invalid-runtime.js')); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/plugins is not defined/); + } + // TODO remove check in v3 + if (process.version.startsWith('v10.') === false) { + try { + await loadConfig(path.join(fixtures, 'invalid-runtime.mjs')); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/plugins is not defined/); + } + } + }); + + test('handles MODULE_NOT_FOUND properly', async () => { + try { + await loadConfig(path.join(fixtures, 'module-not-found.js')); + expect.fail('Config is loaded successfully'); + } catch (error) { + expect(error.message).toMatch(/Cannot find module 'unknown-module'/); + } + }); +}); diff --git a/package.json b/package.json index 12ec32f2e..7db3ffc11 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "!**/*.test.js" ], "scripts": { - "test": "jest --coverage", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --maxWorkers=3 --coverage", "lint": "eslint --ignore-path .gitignore . && prettier --check \"**/*.js\" --ignore-path .gitignore", "fix": "eslint --ignore-path .gitignore --fix . && prettier --write \"**/*.js\" --ignore-path .gitignore", "typecheck": "tsc", diff --git a/test/config/_index.test.js b/test/config/_index.test.js index b45bfb694..1028153b7 100644 --- a/test/config/_index.test.js +++ b/test/config/_index.test.js @@ -1,7 +1,5 @@ 'use strict'; -const path = require('path'); -const { loadConfig } = require('../../lib/svgo-node.js'); const { resolvePluginConfig, extendDefaultPlugins, @@ -173,91 +171,6 @@ describe('config', function () { ); }); }); - - describe('config', () => { - it('is loaded by absolute path', async () => { - const config = await loadConfig( - path.join(process.cwd(), './test/config/fixtures/one/two/config.js') - ); - expect(config).toEqual({ plugins: [] }); - }); - it('is loaded by relative path to cwd', async () => { - const config = await loadConfig( - 'one/two/config.js', - path.join(process.cwd(), './test/config/fixtures') - ); - expect(config).toEqual({ plugins: [] }); - }); - it('is searched in cwd and up', async () => { - const config = await loadConfig( - null, - path.join(process.cwd(), './test/config/fixtures/one/two') - ); - expect(config).toEqual({ plugins: [] }); - }); - it('gives null when config is not found', async () => { - const config = await loadConfig( - null, - path.join(process.cwd(), './test/config') - ); - expect(config).toEqual(null); - }); - it('is failed when specified config does not exist', async () => { - try { - await loadConfig('{}'); - expect.fail('Config is loaded successfully'); - } catch (error) { - expect(error.message).toMatch(/Cannot find module/); - } - }); - it('is failed to load when module exports not an object', async () => { - try { - await loadConfig( - path.join(process.cwd(), './test/config/fixtures/invalid-null.js') - ); - expect.fail('Config is loaded successfully'); - } catch (error) { - expect(error.message).toMatch(/Invalid config file/); - } - try { - await loadConfig( - path.join(process.cwd(), './test/config/fixtures/invalid-array.js') - ); - expect.fail('Config is loaded successfully'); - } catch (error) { - expect(error.message).toMatch(/Invalid config file/); - } - try { - await loadConfig( - path.join(process.cwd(), './test/config/fixtures/invalid-string.js') - ); - expect.fail('Config is loaded successfully'); - } catch (error) { - expect(error.message).toMatch(/Invalid config file/); - } - }); - it('handles config errors properly', async () => { - try { - await loadConfig( - null, - path.join(process.cwd(), './test/config/fixtures/invalid/config') - ); - expect.fail('Config is loaded successfully'); - } catch (error) { - expect(error.message).toMatch(/plugins is not defined/); - } - }); - it('handles MODULE_NOT_FOUND properly', async () => { - try { - await loadConfig( - path.join(process.cwd(), './test/config/fixtures/module-not-found.js') - ); - expect.fail('Config is loaded successfully'); - } catch (error) { - expect(error.message).toMatch(/Cannot find module 'unknown-module'/); - } - }); - }); }); function getPlugin(name, plugins) { diff --git a/test/fixtures/config-loader/cjs/svgo.config.cjs b/test/fixtures/config-loader/cjs/svgo.config.cjs new file mode 100644 index 000000000..7823743bb --- /dev/null +++ b/test/fixtures/config-loader/cjs/svgo.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + plugins: ['cjs'], +}; diff --git a/test/config/fixtures/invalid-array.js b/test/fixtures/config-loader/invalid-array.js similarity index 100% rename from test/config/fixtures/invalid-array.js rename to test/fixtures/config-loader/invalid-array.js diff --git a/test/config/fixtures/invalid-null.js b/test/fixtures/config-loader/invalid-null.js similarity index 100% rename from test/config/fixtures/invalid-null.js rename to test/fixtures/config-loader/invalid-null.js diff --git a/test/config/fixtures/invalid/svgo.config.js b/test/fixtures/config-loader/invalid-runtime.js similarity index 100% rename from test/config/fixtures/invalid/svgo.config.js rename to test/fixtures/config-loader/invalid-runtime.js diff --git a/test/fixtures/config-loader/invalid-runtime.mjs b/test/fixtures/config-loader/invalid-runtime.mjs new file mode 100644 index 000000000..edc79a4ce --- /dev/null +++ b/test/fixtures/config-loader/invalid-runtime.mjs @@ -0,0 +1 @@ +export default { plugins }; // eslint-disable-line no-undef diff --git a/test/config/fixtures/invalid-string.js b/test/fixtures/config-loader/invalid-string.js similarity index 100% rename from test/config/fixtures/invalid-string.js rename to test/fixtures/config-loader/invalid-string.js diff --git a/test/fixtures/config-loader/mjs/svgo.config.mjs b/test/fixtures/config-loader/mjs/svgo.config.mjs new file mode 100644 index 000000000..e344bff2c --- /dev/null +++ b/test/fixtures/config-loader/mjs/svgo.config.mjs @@ -0,0 +1,3 @@ +export default { + plugins: ['mjs'], +}; diff --git a/test/config/fixtures/module-not-found.js b/test/fixtures/config-loader/module-not-found.js similarity index 100% rename from test/config/fixtures/module-not-found.js rename to test/fixtures/config-loader/module-not-found.js diff --git a/test/config/fixtures/one/two/config.js b/test/fixtures/config-loader/one/two/config.js similarity index 100% rename from test/config/fixtures/one/two/config.js rename to test/fixtures/config-loader/one/two/config.js diff --git a/test/config/fixtures/svgo.config.js b/test/fixtures/config-loader/svgo.config.js similarity index 100% rename from test/config/fixtures/svgo.config.js rename to test/fixtures/config-loader/svgo.config.js