diff --git a/README.md b/README.md index 71dd723..e1ce600 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,27 @@ npm install --save-dev babel-plugin-inline-react-svg ] ] } +``` + +- *`root`* - A relative path string (Starting from CWD) that is used for alias resolution. +- *`alias`* - An object describing aliases for module resolution where the key is the alias to be used and the value is a string or array of paths. Example: + +```javascript +['babel-plugin-inline-react-svg', { + root: path.resolve(__dirname), + alias: { + images: 'src/images' + icons: ['src/images/icons', 'node_modules/external-module/icons'] + } +}], +``` + +```javascript +// Resolved to /src/images/logo.svg +import MySvg from 'images/logo.svg'; +// Checks first for /src/images/icons/cross.svg, then /node_modules/external-module/icons/cross.svg +import AnotherSvg from 'icons/cross.svg'; ``` ### Via CLI diff --git a/src/index.js b/src/index.js index ed9a63f..bc48e4e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,10 @@ -import { extname, dirname, parse as parseFilename } from 'path'; -import { readFileSync } from 'fs'; +import { + extname, + dirname, + parse as parseFilename, + resolve as resolvePath, +} from 'path'; +import { readFileSync, existsSync } from 'fs'; import { parse } from '@babel/parser'; import { declare } from '@babel/helper-plugin-utils'; import resolve from 'resolve'; @@ -48,7 +53,13 @@ export default declare(({ if (typeof importPath !== 'string') { throw new TypeError('`applyPlugin` `importPath` must be a string'); } - const { ignorePattern, caseSensitive, filename: providedFilename } = state.opts; + const { + ignorePattern, + caseSensitive, + filename: providedFilename, + root, + alias, + } = state.opts; const { file, filename } = state; if (ignorePattern) { // Only set the ignoreRegex once: @@ -61,14 +72,40 @@ export default declare(({ // This plugin only applies for SVGs: if (extname(importPath) === '.svg') { const iconPath = filename || providedFilename; - const svgPath = resolve.sync(importPath, { basedir: dirname(iconPath) }); - if (caseSensitive && !fileExistsWithCaseSync(svgPath)) { - throw new Error(`File path didn't match case of file on disk: ${svgPath}`); + const aliasPart = importPath.split('/')[0]; + const aliasMatch = alias ? alias[aliasPart] : undefined; + const svgPaths = []; + let chosenPath; + + if (aliasMatch) { + const resolveRoot = resolvePath(process.cwd(), root || './'); + const aliasMatches = typeof aliasMatch === 'string' ? [aliasMatch] : aliasMatch; + + aliasMatches.forEach((match) => { + const aliasedPath = resolvePath(resolveRoot, match); + svgPaths.push(resolvePath(aliasedPath, importPath.replace(`${aliasPart}/`, ''))); + }); + } else { + svgPaths.push(resolve.sync(importPath, { basedir: dirname(iconPath) })); } - if (!svgPath) { + + for (let i = 0; i < svgPaths.length; i += 1) { + const svgPath = svgPaths[i]; + if (caseSensitive && !fileExistsWithCaseSync(svgPath)) { + throw new Error(`File path didn't match case of file on disk: ${svgPath}`); + } + + if (svgPath && existsSync(svgPath)) { + chosenPath = svgPath; + break; + } + } + + if (!chosenPath) { throw new Error(`File path does not exist: ${importPath}`); } - const rawSource = readFileSync(svgPath, 'utf8'); + + const rawSource = readFileSync(chosenPath, 'utf8'); const optimizedSource = state.opts.svgo === false ? rawSource : optimize(rawSource, state.opts.svgo); diff --git a/test/fixtures/test-alias-case-sensitive.jsx b/test/fixtures/test-alias-case-sensitive.jsx new file mode 100644 index 0000000..311f8c8 --- /dev/null +++ b/test/fixtures/test-alias-case-sensitive.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import MySvg from 'myalias/CLOSE.svg'; + +export default function MyFunctionIcon() { + return ; +} + +export class MyClassIcon extends React.Component { + render() { + return ; + } +} diff --git a/test/fixtures/test-alias-folders.jsx b/test/fixtures/test-alias-folders.jsx new file mode 100644 index 0000000..88ee7d7 --- /dev/null +++ b/test/fixtures/test-alias-folders.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import MySvg from 'myalias/close.svg'; +import Svg2 from 'myalias/close3.svg'; + +export default function MyFunctionIcon() { + return ( + <> + + + + ); +} + +export class MyClassIcon extends React.Component { + render() { + return ( + <> + + + + ); + } +} diff --git a/test/fixtures/test-alias-multiple.jsx b/test/fixtures/test-alias-multiple.jsx new file mode 100644 index 0000000..7b2e23b --- /dev/null +++ b/test/fixtures/test-alias-multiple.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import MySvg from 'myalias/close.svg'; +import Svg2 from 'myalias/close2.svg'; + +export default function MyFunctionIcon() { + return ( + <> + + + + ); +} + +export class MyClassIcon extends React.Component { + render() { + return ( + <> + + + + ); + } +} diff --git a/test/fixtures/test-alias.jsx b/test/fixtures/test-alias.jsx new file mode 100644 index 0000000..8d2661e --- /dev/null +++ b/test/fixtures/test-alias.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import MySvg from 'myalias/close.svg'; + +export default function MyFunctionIcon() { + return ; +} + +export class MyClassIcon extends React.Component { + render() { + return ; + } +} diff --git a/test/fixtures/testfolder/close3.svg b/test/fixtures/testfolder/close3.svg new file mode 100644 index 0000000..58fd243 --- /dev/null +++ b/test/fixtures/testfolder/close3.svg @@ -0,0 +1,2 @@ + +This is an opening { and this is a closing }. diff --git a/test/sanity.js b/test/sanity.js index b04db4c..62528d7 100644 --- a/test/sanity.js +++ b/test/sanity.js @@ -177,3 +177,136 @@ transformFile('test/fixtures/test-export-default-as.jsx', { if (err) throw err; console.log('test/fixtures/test-export-default-as.jsx', result.code); }); + +/** + * Alias tests + */ +// Alias without root +transformFile('test/fixtures/test-alias.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + alias: { + myalias: 'test/fixtures', + }, + }], + ], +}, (err, result) => { + if (err) throw err; + console.log('test/fixtures/test-alias.jsx (without root)', result.code); +}); + +// Alias with root +transformFile('test/fixtures/test-alias.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + root: path.resolve(__dirname), + alias: { + myalias: 'fixtures', + }, + }], + ], +}, (err, result) => { + if (err) throw err; + console.log('test/fixtures/test-alias.jsx (with root)', result.code); +}); + +// Alias with root including subdirectory +transformFile('test/fixtures/test-alias.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + root: path.resolve(__dirname, '..', 'test'), + alias: { + myalias: 'fixtures', + }, + }], + ], +}, (err, result) => { + if (err) throw err; + console.log('test/fixtures/test-alias.jsx (with complex root)', result.code); +}); + +// Multiple aliases defined +transformFile('test/fixtures/test-alias-multiple.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + root: path.resolve(__dirname), + alias: { + myalias: 'fixtures', + myalias2: 'fixtures', + }, + }], + ], +}, (err, result) => { + if (err) throw err; + console.log('test/fixtures/test-alias.jsx (with multiple aliases)', result.code); +}); + +// Alias with multiple folders +transformFile('test/fixtures/test-alias-folders.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + root: path.resolve(__dirname), + alias: { + myalias: ['fixtures', 'fixtures/testfolder'], + }, + }], + ], +}, (err, result) => { + if (err) throw err; + console.log('test/fixtures/test-alias-folders.jsx', result.code); +}); + +// Error on import failure with aliases +transformFile('test/fixtures/test-alias.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + root: path.resolve(__dirname), + alias: { + myalias: 'fixtures/testfolder', + }, + }], + ], +}, (err, result) => { + if (err && err.message.indexOf('File path does not exist') !== -1) { + console.log('test/fixtures/test-alias.jsx (no svg)', 'Test passed: Expected file path does not exist was thrown'); + } else { + throw new Error(`Test failed: Expected file path does not exist wasn't thrown, got: ${err.message}`); + } +}); + +// Error on case mismatch +if (fs.existsSync(path.resolve('./PACKAGE.JSON'))) { + transformFile('test/fixtures/test-alias-case-sensitive.jsx', { + babelrc: false, + presets: ['@babel/preset-react'], + plugins: [ + [inlineReactSvgPlugin, { + caseSensitive: true, + root: path.resolve(__dirname), + alias: { + myalias: 'fixtures', + }, + }], + ], + }, (err) => { + if (err && err.message.indexOf('match case') !== -1) { + console.log('test/fixtures/test-alias-case-sensitive.jsx', 'Test passed: Expected case sensitive error was thrown'); + } else { + throw new Error(`Test failed: Expected case sensitive error wasn‘t thrown, got: ${err.message}`); + } + }); +} else { + console.log('# SKIP: alias case-sensitive check; on a case-sensitive filesystem'); +}