Skip to content

Commit

Permalink
πŸ—πŸ› Annotate JSX SVG namespace during build-time (#36722)
Browse files Browse the repository at this point in the history
SVG Elements must be especially created with `createElementNS`.

This transforms SVG-related JSX elements in order to always include an `xmlns` prop that's used to create the DOM nodes correctly.

This prop is consumed by `core/dom/jsx`. We ignore files that do not import this module.

Fixes #36713
  • Loading branch information
alanorozco committed Nov 3, 2021
1 parent 3126c9c commit e70d7a6
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 1 deletion.
1 change: 1 addition & 0 deletions build-system/babel-config/minified-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function getMinifiedConfig() {
'./build-system/babel-plugins/babel-plugin-transform-fix-leading-comments',
'./build-system/babel-plugins/babel-plugin-transform-promise-resolve',
'./build-system/babel-plugins/babel-plugin-transform-rename-privates',
'./build-system/babel-plugins/babel-plugin-dom-jsx-svg-namespace',
reactJsxPlugin,
(argv.esm || argv.sxg) &&
'./build-system/babel-plugins/babel-plugin-transform-dev-methods',
Expand Down
1 change: 1 addition & 0 deletions build-system/babel-config/pre-closure-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function getPreClosureConfig() {
'./build-system/babel-plugins/babel-plugin-transform-inline-isenumvalue',
'./build-system/babel-plugins/babel-plugin-transform-fix-leading-comments',
'./build-system/babel-plugins/babel-plugin-transform-promise-resolve',
'./build-system/babel-plugins/babel-plugin-dom-jsx-svg-namespace',
reactJsxPlugin,
argv.esm || argv.sxg
? './build-system/babel-plugins/babel-plugin-transform-dev-methods'
Expand Down
1 change: 1 addition & 0 deletions build-system/babel-config/test-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function getTestConfig() {
'./build-system/babel-plugins/babel-plugin-transform-jss',
'./build-system/babel-plugins/babel-plugin-transform-promise-resolve',
'@babel/plugin-transform-classes',
'./build-system/babel-plugins/babel-plugin-dom-jsx-svg-namespace',
reactJsxPlugin,
].filter(Boolean);
const testPresets = [presetEnv];
Expand Down
1 change: 1 addition & 0 deletions build-system/babel-config/unminified-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function getUnminifiedConfig() {
'./build-system/babel-plugins/babel-plugin-transform-promise-resolve',
'./build-system/babel-plugins/babel-plugin-transform-amp-extension-call',
'@babel/plugin-transform-classes',
'./build-system/babel-plugins/babel-plugin-dom-jsx-svg-namespace',
reactJsxPlugin,
].filter(Boolean);
const unminifiedPresets = [presetEnv];
Expand Down
113 changes: 113 additions & 0 deletions build-system/babel-plugins/babel-plugin-dom-jsx-svg-namespace/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @fileoverview
* SVG Elements must be especially created with createElementNS.
*
* This transforms SVG-related JSX elements in order to always include an
* 'xmlns' prop that's used to create the DOM nodes correctly.
*
* This prop is consumed by core/dom/jsx. We ignore files that do not import
* this module.
*/

const svgTags = new Set([
'animate',
'circle',
'clipPath',
'defs',
'desc',
'ellipse',
'feBlend',
'feColorMatrix',
'feComponentTransfer',
'feComposite',
'feConvolveMatrix',
'feDiffuseLighting',
'feDisplacementMap',
'feDistantLight',
'feFlood',
'feFuncA',
'feFuncB',
'feFuncG',
'feFuncR',
'feGaussianBlur',
'feImage',
'feMerge',
'feMergeNode',
'feMorphology',
'feOffset',
'fePointLight',
'feSpecularLighting',
'feSpotLight',
'feTile',
'feTurbulence',
'filter',
'g',
'image',
'line',
'linearGradient',
'marker',
'mask',
'metadata',
'path',
'pattern',
'polygon',
'polyline',
'radialGradient',
'rect',
'stop',
'svg',
'switch',
'symbol',
'text',
'textPath',
'tspan',
'use',
'view',
]);

module.exports = function (babel) {
const {types: t} = babel;

let isCoreDomJsx = false;
return {
name: 'dom-jsx-svg-namespace',
visitor: {
Program: {
enter(path) {
isCoreDomJsx = false;
path.traverse({
ImportDeclaration(path) {
if (path.node.source.value.endsWith('/core/dom/jsx')) {
isCoreDomJsx = true;
path.stop();
}
},
});
},
},
JSXOpeningElement(path) {
if (!isCoreDomJsx) {
return;
}
const {name} = path.node.name;
if (name === 'foreignObject') {
throw path.buildCodeFrameError(
'<foreignObject> is not supported. See src/core/dom/jsx.js'
);
}
if (!svgTags.has(name)) {
return;
}
if (path.node.attributes.find((attr) => attr.name.name === 'xmlns')) {
return;
}
path.node.attributes.push(
t.jsxAttribute(
t.jsxIdentifier('xmlns'),
t.stringLiteral('http://www.w3.org/2000/svg')
)
);
},
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Import does not end with core/dom/jsx so we ignore this file.
/** @jsx Preact.createElement */
import * as Preact from 'preact';

() => <svg />;
() => <svg xmlns="http://www.w3.org/2000/svg" />;
() => <path foo="bar" />;
() => <circle foo="bar" />;

() => <div />;
() => <span class="whatever" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins": [
"../../../..",
"@babel/plugin-transform-react-jsx"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Import does not end with core/dom/jsx so we ignore this file.

/** @jsx Preact.createElement */
import * as Preact from 'preact';

() => Preact.createElement("svg", null);

() => Preact.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg"
});

() => Preact.createElement("path", {
foo: "bar"
});

() => Preact.createElement("circle", {
foo: "bar"
});

() => Preact.createElement("div", null);

() => Preact.createElement("span", {
class: "whatever"
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @jsx jsx.createElement */
import * as jsx from '../ANYWHERE_THAT_LEADS_TO/core/dom/jsx';

() => <svg />;
() => <svg xmlns="http://www.w3.org/2000/svg" />;
() => <path foo="bar" />;
() => <circle foo="bar" />;

() => <div />;
() => <span class="whatever" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins": [
"../../../..",
"@babel/plugin-transform-react-jsx"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @jsx jsx.createElement */
import * as jsx from '../ANYWHERE_THAT_LEADS_TO/core/dom/jsx';

() => jsx.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg"
});

() => jsx.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg"
});

() => jsx.createElement("path", {
foo: "bar",
xmlns: "http://www.w3.org/2000/svg"
});

() => jsx.createElement("circle", {
foo: "bar",
xmlns: "http://www.w3.org/2000/svg"
});

() => jsx.createElement("div", null);

() => jsx.createElement("span", {
class: "whatever"
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const runner = require('@babel/helper-plugin-test-runner').default;

runner(__dirname);
15 changes: 14 additions & 1 deletion src/core/dom/jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
* 🚫 No dangerouslySetInnerHTML
* - You should not do this anyway.
*
* 🚫 No SVG <foreignObject>
* - This tag is rather obscure. You should be able to restructure your tree.
* If you absolutely need it, get in touch with `@alanorozco` to consider
* enabling support.
*
* TODO(https://go.amp.dev/issue/36679): Lint these unsupported features.
*/
import {devAssert} from '#core/assert';
Expand Down Expand Up @@ -82,7 +87,15 @@ export function createElement(tag, props, ...children) {
if (typeof tag !== 'string') {
return tag({...props, children});
}
const element = self.document.createElement(tag);
// We expect all SVG-related tags to have `xmlns` set during build time.
// See babel-plugin-dom-jsx-svg-namespace
const xmlns = props?.['xmlns'];
if (xmlns) {
delete props['xmlns'];
}
const element = xmlns
? self.document.createElementNS(xmlns, tag)
: self.document.createElement(tag);
appendChild(element, children);
if (props) {
Object.keys(props).forEach((name) => {
Expand Down
16 changes: 16 additions & 0 deletions test/unit/core/dom/test-jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ describes.sandboxed('#core/dom/jsx', {}, (env) => {
);
});

it('renders with SVG namespace with props.xmlns', () => {
const xmlns = 'http://www.w3.org/2000/svg';
const withProp = createElement('svg', {xmlns});
expect(withProp.namespaceURI).to.equal(xmlns);
const withoutProp = createElement('svg');
expect(withoutProp.namespaceURI).to.not.equal(xmlns);
});

it('renders with SVG namespace with props.xmlns (compiled)', () => {
const xmlns = 'http://www.w3.org/2000/svg';
const withProp = <svg />;
expect(withProp.namespaceURI).to.equal(xmlns);
const withoutProp = <div />;
expect(withoutProp.namespaceURI).to.not.equal(xmlns);
});

describe('unsupported JSX features', () => {
it('does not support Fragments', () => {
allowConsoleError(() => {
Expand Down

0 comments on commit e70d7a6

Please sign in to comment.