diff --git a/.eslintrc b/.eslintrc
index 9eaec07..c8052db 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,4 +1,9 @@
{
"extends": "airbnb",
- "root": true
+ "root": true,
+ "rules": {
+ "react/require-default-props": "off",
+ "react/jsx-filename-extension": "off",
+ "no-plusplus": "off"
+ }
}
diff --git a/.gitignore b/.gitignore
index 996d68d..9178f9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# gitignore
node_modules
lib
+*.tgz
# Only apps should have lockfiles
npm-shrinkwrap.json
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index cd3ca40..0000000
--- a/.npmignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules
-src
diff --git a/auto-id.js b/auto-id.js
new file mode 100644
index 0000000..f9305ad
--- /dev/null
+++ b/auto-id.js
@@ -0,0 +1,4 @@
+module.exports = {
+ AutoIdProvider: require('./lib/AutoIdProvider').default,
+ useId: require('./lib/useId').default,
+};
diff --git a/package.json b/package.json
index 220821c..2550f51 100644
--- a/package.json
+++ b/package.json
@@ -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": {
@@ -33,6 +38,7 @@
"@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",
@@ -40,15 +46,17 @@
"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"
}
diff --git a/src/AutoIdProvider.js b/src/AutoIdProvider.js
new file mode 100644
index 0000000..731a0cb
--- /dev/null
+++ b/src/AutoIdProvider.js
@@ -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 {children};
+}
+
+AutoIdProvider.Context = Context;
+
+AutoIdProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+ getId: PropTypes.func,
+};
diff --git a/src/index.js b/src/index.js
index ed9a63f..284939a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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) {
@@ -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
@@ -115,8 +142,6 @@ export default declare(({
const svgReplacement = buildSvg(opts);
path.replaceWith(svgReplacement);
}
- file.get('ensureReact')();
- file.set('ensureReact', () => {});
}
}
@@ -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')),
diff --git a/src/transformSvg.js b/src/transformSvg.js
index fcc84cf..1bca415 100644
--- a/src/transformSvg.js
+++ b/src/transformSvg.js
@@ -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
@@ -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
+ // `