Skip to content

Commit

Permalink
v0.4.0: Going full speed [publish]
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnaudBarre committed Dec 10, 2022
1 parent 072875e commit ec71ef7
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 247 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.event.head_commit.message, '[publish]') }}
steps:
- uses: actions/checkout@v2
- run: yarn install --frozen-lockfile
- run: yarn prettier-ci
- run: yarn build
- uses: actions/checkout@v3
- uses: xhyrom/[email protected]
- run: bun i
- run: bun ci
- uses: ArnaudBarre/npm-publish@v1
with:
working-directory: dist
npm-token: ${{ secrets.NPM_TOKEN }}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 0.4.0

Turn SVG into React components, even faster.

This new version uses only regex and dangerouslySetInnerHTML to directly create a JS output.

Breaking changes:

- No more options available
- Expose the new `svgToJS` function instead of the previous `svgToJSX`

Compatible with Vite 4.

## 0.3.1

Forward ref to svg element
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Turn SVG into React components, without Babel.

## Why

While [svgr](https://github.com/gregberge/svgr) is great, it uses AST transformation from Babel, which is too slow (~300ms per SVG). This plugin uses regex manipulations for SVG -> JSX and esbuild for JSX -> JS (~10ms in average). It's working well for SVG optimized by [svgo](https://github.com/svg/svgo).
While [svgr](https://github.com/gregberge/svgr) is great, it uses AST transformation from Babel, which is too slow (~300ms per SVG). This plugin uses regex manipulations and `dangerouslySetInnerHTML`, which is almost instantaneous. It's working well for SVG optimized by [svgo](https://github.com/svg/svgo).

## Installation

Expand All @@ -21,7 +21,7 @@ import { defineConfig } from "vite";
import { svgPlugin } from "vite-plugin-fast-react-svg";

export default defineConfig({
plugins: [svgPlugin({ useInnerHTML: true })],
plugins: [svgPlugin()],
});
```

Expand All @@ -48,7 +48,3 @@ const Example = () => (
</>
);
```

## Options

**useInnerHTML**: Set to true to use `dangerouslySetInnerHTML` for SVG contents, which improve bundle size. Added in 0.3.0.
Binary file added bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[install.lockfile]
print = "yarn"
37 changes: 14 additions & 23 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
{
"name": "vite-plugin-fast-react-svg",
"description": "Turn SVG into React components, without Babel",
"version": "0.3.1",
"version": "0.4.0",
"license": "MIT",
"author": "Arnaud Barré (https://github.com/ArnaudBarre)",
"main": "dist/index.js",
"files": [
"dist",
"types.d.ts"
],
"repository": "github:ArnaudBarre/vite-plugin-fast-react-svg",
"keywords": [
"vite",
"vite-plugin",
"react",
"svg"
],
"scripts": {
"build": "tsc",
"build": "scripts/bundle.ts",
"prettier": "yarn prettier-ci --write",
"prettier-ci": "prettier --check '**/*.{ts,json,md,yml}'"
"prettier-ci": "prettier --ignore-path=.gitignore --check '**/*.{ts,json,md,yml}'",
"ci": "tsc && bun prettier-ci && bun run build"
},
"prettier": {
"trailingComma": "all"
},
"peerDependencies": {
"react": ">=16",
"vite": "^2 || ^3"
"vite": "^2 || ^3 || ^4"
},
"devDependencies": {
"@types/node": "^18.0.6",
"@types/react": "^18.0.15",
"prettier": "^2.7.1",
"typescript": "^4.7.4",
"vite": "^3.0.2"
"@nabla/tnode": "^0.8.0",
"@types/node": "^18.11.11",
"@types/react": "^18.0.26",
"prettier": "^2.8.1",
"typescript": "^4.9.3",
"vite": "^4.0.0-beta.4"
}
}
47 changes: 47 additions & 0 deletions scripts/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env tnode
import { rmSync, writeFileSync } from "fs";
import { execSync } from "child_process";
import { build } from "esbuild";

import * as packageJSON from "../package.json";

rmSync("dist", { force: true, recursive: true });

build({
bundle: true,
entryPoints: ["src/index.ts"],
outdir: "dist",
platform: "node",
target: "node14",
legalComments: "inline",
external: Object.keys(packageJSON.peerDependencies),
}).then(() => {
execSync("cp LICENSE README.md types.d.ts dist/");

writeFileSync(
"dist/index.d.ts",
`import { Plugin } from "vite";
export declare function svgPlugin(): Plugin;
export declare const svgToJS: (svg: string, production: boolean) => string;
`,
);

writeFileSync(
"dist/package.json",
JSON.stringify(
{
name: packageJSON.name,
description: "Turn SVG into React components, without Babel",
version: packageJSON.version,
author: "Arnaud Barré (https://github.com/ArnaudBarre)",
license: packageJSON.license,
repository: "github:ArnaudBarre/vite-plugin-fast-react-svg",
main: "index.js",
keywords: ["vite", "vite-plugin", "react", "svg"],
peerDependencies: packageJSON.peerDependencies,
},
null,
2,
),
);
});
66 changes: 34 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,58 @@
import { readFileSync } from "fs";
import { transform } from "esbuild";
import { Plugin } from "vite";

export function svgPlugin(opts?: { useInnerHTML?: boolean }): Plugin {
export function svgPlugin(): Plugin {
let production = false;
return {
name: "svg",
enforce: "pre",
config(_, env) {
production = env.command === "build";
},
async load(id) {
if (id.endsWith(".svg")) {
const { code, warnings } = await transform(
svgToJSX(readFileSync(id, "utf-8"), opts?.useInnerHTML),
{ loader: "jsx" }
);
for (const warning of warnings) {
console.log(warning.location, warning.text);
}
return code;
return svgToJS(readFileSync(id, "utf-8"), production);
}
if (id.endsWith(".svg?inline")) {
const base64 = Buffer.from(
readFileSync(id.replace("?inline", ""), "utf-8")
readFileSync(id.replace("?inline", ""), "utf-8"),
).toString("base64");
return `export default "data:image/svg+xml;base64,${base64}"`;
}
},
};
}

export const svgToJSX = (svg: string, useInnerHTML?: boolean) => {
let jsx: string;
if (useInnerHTML) {
const index = svg.indexOf(">");
const content = svg
.slice(index + 1, svg.indexOf("</svg>"))
.trim()
.replace(/\s+/g, " ");
jsx = `${updatePropsCase(
svg.slice(0, index)
)} ref={ref} {...props} dangerouslySetInnerHTML={{ __html: '${content}' }} />`;
} else {
jsx = updatePropsCase(svg).replace(">", " ref={ref} {...props}>");
const attributesRE = /\s([a-zA-Z0-9-:]+)=("[^"]*")/gu;

export const svgToJS = (svg: string, production: boolean) => {
const index = svg.indexOf(">");
const content = svg
.slice(index + 1, svg.indexOf("</svg>"))
.trim()
.replace(/\s+/g, " ");
let attributes = "";
for (const match of svg.slice(0, index).matchAll(attributesRE)) {
attributes += ` ${transformKey(match[1])}: ${match[2]},\n`;
}
return `import React from "react";const ReactComponent = React.forwardRef((props, ref) => (${jsx}));export default ReactComponent;`;
const jsxImport = production
? 'import { jsx } from "react/jsx-runtime";'
: 'import { jsxDEV as jsx } from "react/jsx-dev-runtime";';
return `${jsxImport}
import { forwardRef } from "react";
export default forwardRef((props, ref) => jsx("svg", {
${attributes} ref,
...props,
dangerouslySetInnerHTML: { __html: '${content}' }
})
);`;
};

const updatePropsCase = (svg: string) =>
svg.replace(/\s([a-z-:]*)="[^"]*"/gu, (string, key: string) => {
if (key.startsWith("data-")) return string;
const keyWithoutDashes = camelCaseOn(key, "-");
const keyWithoutDots = camelCaseOn(keyWithoutDashes, ":");
return string.replace(key, keyWithoutDots);
});
const transformKey = (key: string) => {
if (key.startsWith("data-")) return `"${key}"`;
const keyWithoutDashes = camelCaseOn(key, "-");
return camelCaseOn(keyWithoutDashes, ":");
};

const camelCaseOn = (string: string, delimiter: string) =>
string
Expand Down
6 changes: 4 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
"module": "CommonJS",
"lib": ["ES2020"],
"target": "ES2020",
"declaration": true,
"outDir": "dist",
"skipLibCheck": true,

/* Transpile with esbuild */
"noEmit": true,
"isolatedModules": true,

/* Imports */
"moduleResolution": "node", // Allow `index` imports
"resolveJsonModule": true, // Allow json import
Expand Down
Loading

0 comments on commit ec71ef7

Please sign in to comment.