From 0b40262eaa8244d6e80bd92ef798bbd7843d892d Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 13 Nov 2017 14:29:45 -0800 Subject: [PATCH] =?UTF-8?q?Support=20`export=20{=20default=20}=20from=20?= =?UTF-8?q?=E2=80=98path/to.svg`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #30. --- src/index.js | 173 +++++++++++++++++------------ test/fixtures/test-export-from.jsx | 1 + test/sanity.js | 15 +++ 3 files changed, 120 insertions(+), 69 deletions(-) create mode 100644 test/fixtures/test-export-from.jsx diff --git a/src/index.js b/src/index.js index 92dd3b8..6276812 100644 --- a/src/index.js +++ b/src/index.js @@ -19,8 +19,87 @@ const buildSvgWithDefaults = template(` SVG_NAME.defaultProps = SVG_DEFAULT_PROPS_CODE; `); +const exportSVG = template(` + var SVG_NAME = function SVG_NAME(props) { return SVG_CODE; }; + export { SVG_NAME }; +`); + +const exportSVGwithDefaults = template(` + var SVG_NAME = function SVG_NAME(props) { return SVG_CODE; }; + SVG_NAME.defaultProps = SVG_DEFAULT_PROPS_CODE; + export { SVG_NAME }; +`); + let ignoreRegex; +function transform(t, path, state) { + const { source } = path.node; + if (!source) { return false; } + const importPath = source.value; + const { ignorePattern, caseSensitive } = state.opts; + if (ignorePattern) { + // Only set the ignoreRegex once: + ignoreRegex = ignoreRegex || new RegExp(ignorePattern); + // Test if we should ignore this: + if (ignoreRegex.test(importPath)) { + return false; + } + } + // This plugin only applies for SVGs: + if (extname(importPath) === '.svg') { + // We only support the import default specifier, so let's use that identifier: + const importIdentifier = path.node.specifiers[0].local; + const iconPath = state.file.opts.filename; + const svgPath = resolveFrom(dirname(iconPath), importPath); + if (caseSensitive && !fileExistsWithCaseSync(svgPath)) { + throw new Error(`File path didn't match case of file on disk: ${svgPath}`); + } + if (!svgPath) { + throw new Error(`File path does not exist: ${importPath}`); + } + const rawSource = readFileSync(svgPath, 'utf8'); + const optimizedSource = state.opts.svgo === false + ? rawSource + : optimize(rawSource, state.opts.svgo); + + const escapeSvgSource = escapeBraces(optimizedSource); + + const parsedSvgAst = parse(escapeSvgSource, { + sourceType: 'module', + plugins: ['jsx'], + }); + + traverse(parsedSvgAst, transformSvg(t)); + + const svgCode = traverse.removeProperties(parsedSvgAst.program.body[0].expression); + + const opts = { + SVG_NAME: importIdentifier, + SVG_CODE: svgCode, + }; + + // Move props off of element and into defaultProps + if (svgCode.openingElement.attributes.length > 1) { + const keepProps = []; + const defaultProps = []; + + svgCode.openingElement.attributes.forEach((prop) => { + if (prop.type === 'JSXSpreadAttribute') { + keepProps.push(prop); + } else { + defaultProps.push(t.objectProperty(t.identifier(prop.name.name), prop.value)); + } + }); + + svgCode.openingElement.attributes = keepProps; + opts.SVG_DEFAULT_PROPS_CODE = t.objectExpression(defaultProps); + } + + return opts; + } + return false; +} + export default ({ types: t }) => ({ visitor: { Program: { @@ -36,78 +115,34 @@ export default ({ types: t }) => ({ } }, }, - ImportDeclaration(path, state) { - const importPath = path.node.source.value; - const { ignorePattern, caseSensitive } = state.opts; - const { file } = state; - if (ignorePattern) { - // Only set the ignoreRegex once: - ignoreRegex = ignoreRegex || new RegExp(ignorePattern); - // Test if we should ignore this: - if (ignoreRegex.test(importPath)) { - return; - } + ExportNamedDeclaration(path, state) { + const opts = transform(t, path, state); + if (!opts) { return; } + + if (opts.SVG_DEFAULT_PROPS_CODE) { + const svgReplacement = exportSVGwithDefaults(opts); + path.replaceWithMultiple(svgReplacement); + } else { + const svgReplacement = exportSVG(opts); + path.replaceWith(svgReplacement); } - // This plugin only applies for SVGs: - if (extname(importPath) === '.svg') { - // We only support the import default specifier, so let's use that identifier: - const importIdentifier = path.node.specifiers[0].local; - const iconPath = state.file.opts.filename; - const svgPath = resolveFrom(dirname(iconPath), importPath); - if (caseSensitive && !fileExistsWithCaseSync(svgPath)) { - throw new Error(`File path didn't match case of file on disk: ${svgPath}`); - } - if (!svgPath) { - throw new Error(`File path does not exist: ${importPath}`); - } - const rawSource = readFileSync(svgPath, 'utf8'); - const optimizedSource = state.opts.svgo === false - ? rawSource - : optimize(rawSource, state.opts.svgo); - - const escapeSvgSource = escapeBraces(optimizedSource); - - const parsedSvgAst = parse(escapeSvgSource, { - sourceType: 'module', - plugins: ['jsx'], - }); - - traverse(parsedSvgAst, transformSvg(t)); - - const svgCode = traverse.removeProperties(parsedSvgAst.program.body[0].expression); - - const opts = { - SVG_NAME: importIdentifier, - SVG_CODE: svgCode, - }; - - // Move props off of element and into defaultProps - if (svgCode.openingElement.attributes.length > 1) { - const keepProps = []; - const defaultProps = []; - - svgCode.openingElement.attributes.forEach((prop) => { - if (prop.type === 'JSXSpreadAttribute') { - keepProps.push(prop); - } else { - defaultProps.push(t.objectProperty(t.identifier(prop.name.name), prop.value)); - } - }); - - svgCode.openingElement.attributes = keepProps; - opts.SVG_DEFAULT_PROPS_CODE = t.objectExpression(defaultProps); - } + const { file } = state; + file.get('ensureReact')(); + file.set('ensureReact', () => {}); + }, + ImportDeclaration(path, state) { + const opts = transform(t, path, state); + if (!opts) { return; } - if (opts.SVG_DEFAULT_PROPS_CODE) { - const svgReplacement = buildSvgWithDefaults(opts); - path.replaceWithMultiple(svgReplacement); - } else { - const svgReplacement = buildSvg(opts); - path.replaceWith(svgReplacement); - } - file.get('ensureReact')(); - file.set('ensureReact', () => {}); + if (opts.SVG_DEFAULT_PROPS_CODE) { + const svgReplacement = buildSvgWithDefaults(opts); + path.replaceWithMultiple(svgReplacement); + } else { + const svgReplacement = buildSvg(opts); + path.replaceWith(svgReplacement); } + const { file } = state; + file.get('ensureReact')(); }, }, }); diff --git a/test/fixtures/test-export-from.jsx b/test/fixtures/test-export-from.jsx new file mode 100644 index 0000000..36da401 --- /dev/null +++ b/test/fixtures/test-export-from.jsx @@ -0,0 +1 @@ +export { default } from './close.svg'; diff --git a/test/sanity.js b/test/sanity.js index 9f5d6f6..ccdc22b 100644 --- a/test/sanity.js +++ b/test/sanity.js @@ -81,3 +81,18 @@ transformFile('test/fixtures/test-no-svg-or-react.js', { throw new Error('Test failed: React import was present'); } }); + +transformFile('test/fixtures/test-export-from.jsx', { + babelrc: false, + presets: ['react'], + plugins: [ + '../../src/index', + ], +}, (err, result) => { + if (err) throw err; + console.log('test/fixtures/test-export-from.jsx', result.code); + assertReactImport(result); + if (!/React\.createElement/.test(result.code)) { + throw new Error('no react elements found'); + } +});