Skip to content

Commit

Permalink
feat: support React Native
Browse files Browse the repository at this point in the history
  • Loading branch information
ambar committed Feb 21, 2024
1 parent afcda89 commit ba398e8
Show file tree
Hide file tree
Showing 20 changed files with 214 additions and 13 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Test

on:
push:
branches: [main, dev]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: test
run: |
yarn
yarn test
# yarn test --coverage
# - name: report
# uses: coverallsapp/github-action@v1.0.1
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# path-to-lcov: ./coverage/lcov.info
13 changes: 12 additions & 1 deletion packages/reiconify-loader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@ module.exports = {
oneOf: [
{
resourceQuery: /react/,
use: 'reiconify-loader',
use: {
loader: 'reiconify-loader',
// whether to use React Native
// options: {
// native: true,
// },
},
},
// optional fallback
{
use: 'file-loader',
},
Expand All @@ -31,8 +38,12 @@ module.exports = {
Import icons:

```js
// types for web
/// <reference types="reiconify-loader/client" />

// types for React Native
/// <reference types="reiconify-loader/native" />

// import React icon
import AlarmIcon from './icons/alarm.svg?react'

Expand Down
10 changes: 8 additions & 2 deletions packages/reiconify-loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import type {LoaderContext} from 'webpack'
import {callbackify} from 'util'
import transform from 'reiconify/lib/transform'

/**
* SVG to React Component loader
*/
export default function reiconifyLoader(
this: LoaderContext<{}>,
this: LoaderContext<{native: boolean}>,
source: string
) {
callbackify(() => transform(source, {baseName: 'base-icon'}))(this.async())
const {native} = this.getOptions()
callbackify(() =>
transform(source, native ? {native} : {baseName: 'base-icon'})
)(this.async())
}
6 changes: 6 additions & 0 deletions packages/reiconify-loader/native.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type {SvgProps} from 'react-native-svg'

declare module '*?react' {
const Icon: React.FC<SVGProps & {size?: string | number}>
export default Icon
}
11 changes: 10 additions & 1 deletion packages/reiconify-loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"license": "MIT",
"files": [
"client.d.ts",
"native.d.ts",
"dist"
],
"jest": {
Expand All @@ -27,9 +28,17 @@
"base-icon": "^2.2.1"
},
"peerDependencies": {
"webpack": "^4 || ^5"
"webpack": "^4 || ^5",
"react": "*",
"react-native-svg": "*"
},
"peerDependenciesMeta": {
"react-native-svg": {
"optional": true
}
},
"devDependencies": {
"identity-obj-proxy": "^3.0.0",
"memfs": "^3.4.7",
"webpack": "^5.74.0"
}
Expand Down
19 changes: 19 additions & 0 deletions packages/reiconify-loader/test/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,22 @@ export {
};
"
`;

exports[`compiler svg for RN 1`] = `
"var __assign = Object.assign;
import React from "react";
import * as svg from "react-native-svg";
function Icon(props) {
return /* @__PURE__ */ React.createElement(svg.Svg, __assign(__assign({
width: "24",
height: "24",
viewBox: "0 0 24 24"
}, props), size && {width: size, height: size}), /* @__PURE__ */ React.createElement(svg.Path, {
d: "M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"
}));
}
export {
Icon as default
};
"
`;
15 changes: 13 additions & 2 deletions packages/reiconify-loader/test/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import webpack from 'webpack'
import {createFsFromVolume, Volume} from 'memfs'

// https://webpack.js.org/contribute/writing-a-loader/#testing
export default function compiler(fixture) {
export default function compiler(fixture, native = false) {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
Expand All @@ -18,14 +18,25 @@ export default function compiler(fixture) {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
resolve: {
alias: {
// skip installing react-native-svg and react-native
'react-native-svg': 'identity-obj-proxy',
},
},
module: {
rules: [
{
test: /\.svg$/,
oneOf: [
{
resourceQuery: /react/,
use: require.resolve('../index.ts'),
use: {
loader: require.resolve('../index.ts'),
options: {
native,
},
},
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion packages/reiconify-loader/test/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/reiconify-loader/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ test('compiler svg', async () => {
const output = stats.toJson({source: true})?.modules?.[0]?.modules?.[0].source
expect(output).toMatchSnapshot()
})

test('compiler svg for RN', async () => {
const stats = await compiler('./icons/check.svg?react', true)
const output = stats.toJson({source: true})?.modules?.[0]?.modules?.[0].source
expect(output).toMatchSnapshot()
})
19 changes: 19 additions & 0 deletions packages/reiconify/lib/defaultConfig.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
const pascalCase = require('pascal-case')

const template = (data) => {
if (data.native) {
const jsxWithProps = data.jsxString.replace(
/<svg\.Svg([\s\S]*?)>/,
(match, group) =>
`<svg.Svg${group} {...props} {...(size && {width: size, height: size})}>`
)
// TODO: add size prop
return `
import React from 'react'
import * as svg from 'react-native-svg'
export default function ${data.name}(props) {
return ${jsxWithProps}
}
`.trim()
}
const hasBaseName = !!data.baseName
const tag = hasBaseName ? `SVG` : 'svg'
const jsxWithProps = data.jsxString
Expand Down Expand Up @@ -96,6 +111,9 @@ const indexTemplate = (names) => {
return lines.join('\n')
}

/**
* @type {import('./types').Options}
*/
const defaults = {
name: 'Icon',
baseName: undefined,
Expand All @@ -108,6 +126,7 @@ const defaults = {
indexTemplate,
svgoPlugins: [],
camelCaseProps: true,
native: false,
}

module.exports = defaults
20 changes: 20 additions & 0 deletions packages/reiconify/lib/svg2jsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const JSON5 = require('json5')
const mapKeys = require('lodash/mapKeys')
const camelCase = require('lodash/camelCase')
const styleToObject = require('style-to-object')
const pascalCase = require('pascal-case')

const toCamelCase = (s) =>
s.replace(/([-_:])([a-z])/g, (s, a, b) => b.toUpperCase())
Expand Down Expand Up @@ -62,6 +63,24 @@ const camelCaseNamespaceProps = {
},
}

/**
* @type {import('svgo').PluginDef}
*/
const reactNativeSVG = {
name: 'reactNativeSVG',
description: 'Convert SVG to React Native SVG',
fn: () => {
return {
element: {
enter: (item) => {
// use namespace import
item.name = `svg.${pascalCase(item.name)}`
},
},
}
},
}

// svgo 默认就会启用一批插件,参考:
// https://github.com/svg/svgo/issues/646
// https://github.com/BohemianCoding/svgo-compressor/blob/develop/src/defaultConfig.js
Expand Down Expand Up @@ -113,6 +132,7 @@ const createOptimizer = (options) => {
.concat(options.svgoPlugins)
.concat(options.camelCaseProps ? camelCaseProps : [])
.concat(options.camelCaseNamespaceProps ? camelCaseNamespaceProps : [])
.concat(options.native ? reactNativeSVG : [])

return async (svg) => {
const {data} = svgo.optimize(svg, {plugins})
Expand Down
14 changes: 12 additions & 2 deletions packages/reiconify/lib/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ const defaultConfig = require('./defaultConfig')
const svg2jsx = require('./svg2jsx')
const esTransform = require('./esTransform')

/**
* Convert SVG to JS
* @param {string} svg
* @param {import('./types').Options} options
* @returns {Promise<string>}
*/
const transform = async (svg, options) => {
const {
name,
Expand All @@ -11,18 +17,22 @@ const transform = async (svg, options) => {
defaultProps,
svgoPlugins,
camelCaseProps,
native = false,
usePrettier = false,
format = 'esm',
jsx = 'react',
} = Object.assign({}, defaultConfig, options)
const jsxString = await svg2jsx(svg, {svgoPlugins, camelCaseProps})
const jsxString = await svg2jsx(svg, {svgoPlugins, native, camelCaseProps})
let code = template({
name,
baseName,
baseClassName,
defaultProps,
jsxString,
native,
})
if (format !== 'jsx') {
// TODO: upgrade esbuild to support jsx automatic runtime
if (jsx !== 'preserve') {
code = await esTransform(code, {format})
}
return usePrettier ? require('./prettier')(code) : code
Expand Down
2 changes: 1 addition & 1 deletion packages/reiconify/lib/transformFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const transformFiles = async (options = {}) => {
camelCaseProps,
// format source only
usePrettier: true,
format: 'jsx',
jsx: 'preserve',
})
return {name, code}
})
Expand Down
16 changes: 16 additions & 0 deletions packages/reiconify/lib/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type Options = {
name?: 'Icon'
baseName?: string
baseClassName?: string
template?: (opts: Options) => string
baseTemplate?: (opts: Options) => string
defaultProps?: Record<string, unknown>
baseDefaultProps?: Record<string, unknown>
filenameTemplate?: (filename: string) => string
indexTemplate?: (names: string[]) => string
svgoPlugins?: import('svgo').PluginDef[]
format?: 'esm' | 'cjs'
jsx?: 'transform' | 'preserve'
camelCaseProps?: boolean
native?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ exports[`resolveConfig gets default config 1`] = `
"filenameTemplate": [Function],
"indexTemplate": [Function],
"name": "Icon",
"native": false,
"svgoPlugins": [],
"template": [Function],
}
Expand All @@ -31,6 +32,7 @@ exports[`resolveConfig overwrites default config 1`] = `
"filenameTemplate": [Function],
"indexTemplate": [Function],
"name": "Icon",
"native": false,
"svgoPlugins": [
{
"removeAttrs": {
Expand Down
9 changes: 8 additions & 1 deletion packages/vite-plugin-reiconify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import reiconify from 'vite-plugin-reiconify'
export default {
plugins: [
react(),
//
// for web
reiconify(),
// for React Native
// reiconify({native: true}),
],
}
```
Expand All @@ -24,8 +26,13 @@ Import icons:

```js
/// <reference types="vite/client" />

// types for web
/// <reference types="vite-plugin-reiconify/client" />

// types for React Native
/// <reference types="vite-plugin-reiconify/native" />

// top-level import
import AlarmIcon from './icons/alarm.svg?react'

Expand Down
Loading

0 comments on commit ba398e8

Please sign in to comment.