Skip to content

Commit

Permalink
Support es modules (#1583)
Browse files Browse the repository at this point in the history
Ref #1579

In config of course. Projects with type:module can now use modules to
export config

```js
export default {
  plugins: []
}
```

Also added support for resolving svgo.config.mjs and svgo.config.cjs.

Moved loadConfig tests to svgo-node tests.

mjs test is skipped for now in node 10, just don't use modules there
  • Loading branch information
TrySound authored Sep 23, 2021
1 parent 4c6a091 commit 7111c52
Show file tree
Hide file tree
Showing 14 changed files with 132 additions and 93 deletions.
35 changes: 31 additions & 4 deletions lib/svgo-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const os = require('os');
const fs = require('fs');
const { pathToFileURL } = require('url');
const path = require('path');
const {
extendDefaultPlugins,
Expand All @@ -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}"`);
}
Expand All @@ -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) {
Expand Down
94 changes: 93 additions & 1 deletion lib/svgo-node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'/);
}
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
87 changes: 0 additions & 87 deletions test/config/_index.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

const path = require('path');
const { loadConfig } = require('../../lib/svgo-node.js');
const {
resolvePluginConfig,
extendDefaultPlugins,
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/config-loader/cjs/svgo.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
plugins: ['cjs'],
};
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions test/fixtures/config-loader/invalid-runtime.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default { plugins }; // eslint-disable-line no-undef
File renamed without changes.
3 changes: 3 additions & 0 deletions test/fixtures/config-loader/mjs/svgo.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
plugins: ['mjs'],
};
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 7111c52

Please sign in to comment.