Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"extends": "airbnb",
"root": true
"root": true,
"rules": {
"react/require-default-props": "off",
"react/jsx-filename-extension": "off",
"no-plusplus": "off"
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# gitignore
node_modules
lib
*.tgz

# Only apps should have lockfiles
npm-shrinkwrap.json
Expand Down
2 changes: 0 additions & 2 deletions .npmignore

This file was deleted.

4 changes: 4 additions & 0 deletions auto-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
AutoIdProvider: require('./lib/AutoIdProvider').default,
useId: require('./lib/useId').default,
};
16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
"name": "babel-plugin-inline-react-svg",
"version": "1.1.1",
"description": "A babel plugin that optimizes and inlines SVGs for your react components.",
"files": [
"lib",
"auto-id.js"
],
"main": "lib/index.js",
"sideEffects": false,
"scripts": {
"test": "npm run tests-only",
"pretests-only": "npm run build",
"tests-only": "babel-node test/sanity.js",
"build": "babel src --out-dir lib",
"build": "babel src --out-dir lib --verbose",
"lint": "eslint src/",
"prepublish": "npm run build",
"prepare": "npm run build",
"pretest": "npm run lint"
},
"repository": {
Expand All @@ -33,22 +38,25 @@
"@babel/cli": "^7.4.3",
"@babel/core": "^7.0.0",
"@babel/node": "^7.2.2",
"@babel/preset-env": "^7.8.4",
"@babel/preset-react": "^7.0.0",
"babel-preset-airbnb": "^3.2.1",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.16.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-react": "^7.12.4",
"react": "^15.3.1"
"react": "^16.8.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
"@babel/core": "^7.0.0",
"react": "^16.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/parser": "^7.0.0",
"lodash.isplainobject": "^4.0.6",
"prop-types": "^15.7.2",
"resolve": "^1.10.0",
"svgo": "^0.7.2"
}
Expand Down
32 changes: 32 additions & 0 deletions src/AutoIdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useCallback, useRef } from 'react';
import PropTypes from 'prop-types';

let globalId = 0;

// By default, if no AutoIdProvider is present, this `globalId` is the ID
// counter. This will work fine for browser-only apps. For SSR support, use
// AutoIdProvider, which will have a scoped counter per request.
const Context = React.createContext(() => `svg-id-${globalId++}`);

/**
* Provide auto generated IDs for the `useId` hook, with support for a custom
* ID generator. Using this will ensure that each server-side request that
* renders your app has their own ID counter that starts at 0.
*/
export default function AutoIdProvider({ children, getId: customGetId }) {
const nextId = useRef(0);

const getId = useCallback(
() => (customGetId ? customGetId(nextId) : `svg-id-${nextId.current++}`),
[customGetId],
);

return <Context.Provider value={getId}>{children}</Context.Provider>;
}

AutoIdProvider.Context = Context;

AutoIdProvider.propTypes = {
children: PropTypes.node.isRequired,
getId: PropTypes.func,
};
55 changes: 48 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,40 @@ export default declare(({
SVG_NAME,
SVG_CODE,
SVG_DEFAULT_PROPS_CODE,
NUM_IDS = 0,
HOOK_NAME,
}) => {
const useIds = new Array(NUM_IDS).fill('HOOK_NAME()');
const namedTemplate = `
var SVG_NAME = function SVG_NAME(props) { return SVG_CODE; };
var SVG_NAME = function SVG_NAME(props) {
${NUM_IDS ? `var ids = [${useIds.join(', ')}];` : ''}
return SVG_CODE;
};
${SVG_DEFAULT_PROPS_CODE ? 'SVG_NAME.defaultProps = SVG_DEFAULT_PROPS_CODE;' : ''}
${IS_EXPORT ? 'export { SVG_NAME };' : ''}
`;
const anonymousTemplate = `
var Component = function (props) { return SVG_CODE; };
var Component = function (props) {
${NUM_IDS ? `var ids = [${useIds.join(', ')}];` : ''}
return SVG_CODE;
};
${SVG_DEFAULT_PROPS_CODE ? 'Component.defaultProps = SVG_DEFAULT_PROPS_CODE;' : ''}
Component.displayName = 'EXPORT_FILENAME';
export default Component;
`;

if (SVG_NAME !== 'default') {
return template(namedTemplate)({ SVG_NAME, SVG_CODE, SVG_DEFAULT_PROPS_CODE });
return template(namedTemplate)(NUM_IDS ? {
SVG_NAME, SVG_CODE, SVG_DEFAULT_PROPS_CODE, HOOK_NAME,
} : {
SVG_NAME, SVG_CODE, SVG_DEFAULT_PROPS_CODE,
});
}
return template(anonymousTemplate)({ SVG_CODE, SVG_DEFAULT_PROPS_CODE, EXPORT_FILENAME });
return template(anonymousTemplate)(NUM_IDS ? {
SVG_CODE, SVG_DEFAULT_PROPS_CODE, EXPORT_FILENAME, HOOK_NAME,
} : {
SVG_CODE, SVG_DEFAULT_PROPS_CODE, EXPORT_FILENAME,
});
};

function applyPlugin(importIdentifier, importPath, path, state, isExport, exportFilename) {
Expand Down Expand Up @@ -80,15 +97,25 @@ export default declare(({
plugins: ['jsx'],
});

traverse(parsedSvgAst, transformSvg(t));
const ids = new Map();
traverse(parsedSvgAst, transformSvg(t, { ids }));

const svgCode = traverse.removeProperties(parsedSvgAst.program.body[0].expression);

file.get('ensureAutoId')();
file.set('ensureAutoId', () => {});
file.get('ensureReact')();
file.set('ensureReact', () => {});

const hookIdentifier = path.scope.getBinding('useId').identifier;

const opts = {
SVG_NAME: importIdentifier,
SVG_CODE: svgCode,
IS_EXPORT: isExport,
EXPORT_FILENAME: exportFilename,
NUM_IDS: ids.size,
HOOK_NAME: hookIdentifier,
};

// Move props off of element and into defaultProps
Expand All @@ -115,8 +142,6 @@ export default declare(({
const svgReplacement = buildSvg(opts);
path.replaceWith(svgReplacement);
}
file.get('ensureReact')();
file.set('ensureReact', () => {});
}
}

Expand All @@ -130,6 +155,22 @@ export default declare(({
if (typeof filename === 'undefined' && typeof opts.filename !== 'string') {
throw new TypeError('the "filename" option is required when transforming code');
}

if (!path.scope.hasBinding('useId')) {
const autoIdModule = opts.autoIdModule || 'babel-plugin-inline-react-svg/auto-id';

const autoIdImportDeclaration = t.importDeclaration([
t.importSpecifier(t.identifier('useId'), t.identifier('useId')),
], t.stringLiteral(autoIdModule));

file.set('ensureAutoId', () => {
const [newPath] = path.unshiftContainer('body', autoIdImportDeclaration);
newPath.get('specifiers').forEach((specifier) => { path.scope.registerBinding('module', specifier); });
});
} else {
file.set('ensureAutoId', () => {});
}

if (!path.scope.hasBinding('React')) {
const reactImportDeclaration = t.importDeclaration([
t.importDefaultSpecifier(t.identifier('React')),
Expand Down
45 changes: 43 additions & 2 deletions src/transformSvg.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import { namespaceToCamel, hyphenToCamel } from './camelize';
import cssToObj from './cssToObj';

export default t => ({
JSXAttribute({ node }) {
export default (t, state) => ({
JSXAttribute({ node, parent }) {
const { name: originalName } = node;
if (t.isJSXNamespacedName(originalName)) {
// converts
Expand Down Expand Up @@ -40,6 +40,47 @@ export default t => ({
node.value = t.jSXExpressionContainer(t.objectExpression(properties));
}

if (originalName.name === 'id') {
// This plugin detects when props (including `id`) are on the root
// `<svg>` element and moves them to `defaultProps`. We don't want it
// to move the `ids[n]` expression there because it will break. Instead
// of messing with that behavior, let's just don't try to fiddle with
// `id` props on the root `<svg>`.
const isSvgId = Boolean(
parent
&& parent.type === 'JSXOpeningElement'
&& parent.name.name.toLowerCase() === 'svg',
);
if (!isSvgId) {
const idToRewrite = node.value.value;
let index = state.ids.get(idToRewrite);
if (index == null) {
index = state.ids.size;
state.ids.set(idToRewrite, index);
}
node.value = t.jSXExpressionContainer(
t.memberExpression(t.identifier('ids'), t.numericLiteral(index), true),
);
}
} else {
const idRefMatch = /^url\(['"]?#([^)]+)['"]?\)$/.exec(node.value.value);
if (idRefMatch) {
const idToRewrite = idRefMatch[1];
let index = state.ids.get(idToRewrite);
if (index == null) {
index = state.ids.size;
state.ids.set(idToRewrite, index);
}
node.value = t.jSXExpressionContainer(
t.binaryExpression('+',
t.binaryExpression('+',
t.stringLiteral('url(#'),
t.memberExpression(t.identifier('ids'), t.numericLiteral(index), true)),
t.stringLiteral(')')),
);
}
}

// converts
// <svg stroke-width="5">
// to
Expand Down
8 changes: 8 additions & 0 deletions src/useId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useContext, useState } from 'react';
import AutoIdProvider from './AutoIdProvider';

export default function useId(customId) {
const getId = useContext(AutoIdProvider.Context);
const [autoId] = useState(getId);
return customId || autoId;
}
12 changes: 12 additions & 0 deletions test/fixtures/ids.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions test/fixtures/test-ids.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import MySvg from './ids.svg';

export default function MyFunctionIcon() {
return <MySvg />;
}

export class MyClassIcon extends React.Component {
render() {
return <MySvg />;
}
}
11 changes: 11 additions & 0 deletions test/sanity.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,14 @@ transformFile('test/fixtures/test-export-default-as.jsx', {
if (err) throw err;
console.log('test/fixtures/test-export-default-as.jsx', result.code);
});

transformFile('test/fixtures/test-ids.jsx', {
babelrc: false,
presets: [['@babel/preset-env', { modules: 'commonjs' }], '@babel/preset-react'],
plugins: [
inlineReactSvgPlugin,
],
}, (err, result) => {
if (err) throw err;
console.log('test/fixtures/test-ids.jsx', result.code);
});