diff --git a/lib/svgo-node.js b/lib/svgo-node.js new file mode 100644 index 00000000..ae993571 --- /dev/null +++ b/lib/svgo-node.js @@ -0,0 +1,54 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { + extendDefaultPlugins, + optimize, + createContentItem, +} = require('./svgo.js'); + +const importConfig = async configFile => { + try { + const config = require(configFile); + if (config == null || typeof config !== 'object' || Array.isArray(config)) { + throw Error(`Invalid config file "${configFile}"`); + } + return config; + } catch (error) { + if (error.code === 'MODULE_NOT_FOUND') { + return null; + } + throw error; + } +}; + +const loadConfig = async (configFile, cwd = process.cwd()) => { + if (configFile != null) { + if (path.isAbsolute(configFile)) { + return await importConfig(configFile); + } else { + return await importConfig(path.join(cwd, configFile)); + } + } + let dir = cwd; + while (true) { + try { + const file = path.join(dir, "svgo.config.js"); + const stats = await fs.promises.stat(file); + if (stats.isFile()) { + return await importConfig(file); + } + } catch {} + const parent = path.dirname(dir); + if (dir === parent) { + return null; + } + dir = parent; + } +}; + +exports.loadConfig = loadConfig; +exports.extendDefaultPlugins = extendDefaultPlugins; +exports.optimize = optimize; +exports.createContentItem = createContentItem; diff --git a/lib/svgo.js b/lib/svgo.js index 5751b255..1eacb4cf 100755 --- a/lib/svgo.js +++ b/lib/svgo.js @@ -23,7 +23,10 @@ const { encodeSVGDatauri } = require('./svgo/tools.js'); exports.extendDefaultPlugins = extendDefaultPlugins; -const optimize = (svgstr, config = {}) => { +const optimize = (svgstr, config) => { + if (config == null) { + config = {}; + } if (typeof config !== 'object') { throw Error('Config should be an object') } diff --git a/lib/svgo/coa.js b/lib/svgo/coa.js index 83c35af7..43d19cbf 100644 --- a/lib/svgo/coa.js +++ b/lib/svgo/coa.js @@ -4,9 +4,8 @@ const FS = require('fs'); const PATH = require('path'); const chalk = require('chalk'); -const { optimize } = require('../svgo.js'); +const { loadConfig, optimize } = require('../svgo-node.js'); const pluginsMap = require('../../plugins/plugins.js'); -const YAML = require('js-yaml'); const PKG = require('../../package.json'); const { encodeSVGDatauri, decodeSVGDatauri } = require('./tools.js'); const regSVGFile = /\.svg$/; @@ -50,7 +49,7 @@ module.exports = function makeProgram(program) { .action(action); } -function action(args, opts) { +async function action(args, opts) { var input = opts.input || args; var output = opts.output; var config = {} @@ -105,26 +104,13 @@ function action(args, opts) { } // --config - if (opts.config) { - var configPath = PATH.resolve(opts.config), - configData; - try { - // require() adds some weird output on YML files - configData = FS.readFileSync(configPath, 'utf8'); - config = JSON.parse(configData); - } catch (err) { - if (err.code === 'ENOENT') { - return printErrorAndExit(`Error: couldn't find config file '${opts.config}'.`); - } else if (err.code === 'EISDIR') { - return printErrorAndExit(`Error: directory '${opts.config}' is not a config file.`); - } - config = YAML.safeLoad(configData); - config.__DIR = PATH.dirname(configPath); // will use it to resolve custom plugins defined via path - - if (!config || Array.isArray(config)) { - return printErrorAndExit(`Error: invalid config file '${opts.config}'.`); - } - } + try { + const loadedConfig = await loadConfig(opts.config); + if (loadedConfig != null) { + config = loadedConfig; + } + } catch (error) { + return printErrorAndExit(error.message); } // --quiet diff --git a/package-lock.json b/package-lock.json index 34ad34ed..b4ab3548 100644 --- a/package-lock.json +++ b/package-lock.json @@ -537,6 +537,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -992,7 +993,8 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, "estree-walker": { "version": "2.0.2", @@ -1510,6 +1512,7 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2404,7 +2407,8 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "stable": { "version": "0.1.8", diff --git a/package.json b/package.json index c3e7f820..3288d2b9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "type": "git", "url": "git://github.com/svg/svgo.git" }, - "main": "./lib/svgo.js", + "main": "./lib/svgo-node.js", "bin": { "svgo": "./bin/svgo" }, @@ -59,7 +59,6 @@ "css-select-base-adapter": "^0.1.1", "css-tree": "^1.1.2", "csso": "^4.2.0", - "js-yaml": "^3.13.1", "sax": "~1.2.4", "stable": "^0.1.8" }, diff --git a/test/coa/_index.js b/test/coa/_index.js index ae421dc3..ddd2d402 100644 --- a/test/coa/_index.js +++ b/test/coa/_index.js @@ -121,7 +121,7 @@ describe('coa', function() { const stdin = require('mock-stdin').stdin(); - setTimeout(() => { stdin.send(initialFile, 'ascii').end(); }, 0); + setTimeout(() => { stdin.send(initialFile, 'ascii').end(); }, 1000); runProgram(['--input', '-', '--output', 'temp.svg', '--string', fs.readFileSync(svgPath, 'utf8'), '--quiet']) .then(onComplete, onComplete); diff --git a/test/config/_index.js b/test/config/_index.js index 6c139951..060d9328 100644 --- a/test/config/_index.js +++ b/test/config/_index.js @@ -1,6 +1,8 @@ 'use strict'; +const path = require('path'); const { expect } = require('chai'); +const { loadConfig } = require('../../lib/svgo-node.js'); const { resolvePluginConfig, extendDefaultPlugins @@ -182,6 +184,66 @@ describe('config', function() { expect(extendedPlugins[extendedPlugins.length - 1].name).to.equal('customPlugin'); }); }); + + 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).to.deep.equal({ 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).to.deep.equal({ 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).to.deep.equal({ plugins: [] }); + }); + it('gives null module does not exist', async () => { + const absoluteConfig = await loadConfig( + path.join(process.cwd(), './test/config/fixtures/config.js'), + ); + expect(absoluteConfig).to.equal(null); + const searchedConfig = await loadConfig( + null, + path.join(process.cwd(), './test/config'), + ); + expect(searchedConfig).to.equal(null); + }); + 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).to.match(/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).to.match(/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).to.match(/Invalid config file/); + } + }); + }); }); function getPlugin(name, plugins) { diff --git a/test/config/fixtures/invalid-array.js b/test/config/fixtures/invalid-array.js new file mode 100644 index 00000000..e0a30c5d --- /dev/null +++ b/test/config/fixtures/invalid-array.js @@ -0,0 +1 @@ +module.exports = []; diff --git a/test/config/fixtures/invalid-null.js b/test/config/fixtures/invalid-null.js new file mode 100644 index 00000000..b894a23a --- /dev/null +++ b/test/config/fixtures/invalid-null.js @@ -0,0 +1 @@ +module.exports = null; diff --git a/test/config/fixtures/invalid-string.js b/test/config/fixtures/invalid-string.js new file mode 100644 index 00000000..9dc5fc1e --- /dev/null +++ b/test/config/fixtures/invalid-string.js @@ -0,0 +1 @@ +module.exports = ''; diff --git a/test/config/fixtures/one/two/config.js b/test/config/fixtures/one/two/config.js new file mode 100644 index 00000000..736d6d2d --- /dev/null +++ b/test/config/fixtures/one/two/config.js @@ -0,0 +1 @@ +module.exports = { plugins: [] }; diff --git a/test/config/fixtures/svgo.config.js b/test/config/fixtures/svgo.config.js new file mode 100644 index 00000000..736d6d2d --- /dev/null +++ b/test/config/fixtures/svgo.config.js @@ -0,0 +1 @@ +module.exports = { plugins: [] };