Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support generate react component #4

Merged
merged 7 commits into from
Jun 24, 2024
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
138 changes: 38 additions & 100 deletions packages/varlet-icon-builder/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { getViConfig } from '../utils/config.js'
import { getViConfig, GenerateFramework } from '../utils/config.js'
import { resolve } from 'path'
import { bigCamelize } from '@varlet/shared'
import { compileSFC } from '../utils/compiler.js'
import { removeExtname } from '../utils/shared.js'
import esbuild from 'esbuild'
import fse from 'fs-extra'
import logger from '../utils/logger.js'
import { getTransformResult } from '../utils/esbuild.js'
import { generateVueSfc } from '../framework/vue3.js'
import { generateReactTsx } from '../framework/react.js'

export interface GenerateCommandOptions {
entry?: string
wrapperComponentName?: string
framework?: GenerateFramework
output?: {
components?: string
types?: string
Expand All @@ -18,13 +20,23 @@ export interface GenerateCommandOptions {
}
}

export interface GenerateModuleOptions {
entry: string
output: string
format: 'cjs' | 'esm'
framework: GenerateFramework
}

const INDEX_FILE = 'index.ts'
const INDEX_D_FILE = 'index.d.ts'

export async function generate(options: GenerateCommandOptions = {}) {
const config = (await getViConfig()) ?? {}

const entry = options.entry ?? config?.generate?.entry ?? './svg'
const wrapperComponentName = options.wrapperComponentName ?? config?.generate?.wrapperComponentName ?? 'XIcon'
const framework = options.framework ?? config?.generate?.framework ?? GenerateFramework.vue3

const componentsDir = resolve(
process.cwd(),
options.output?.components ?? config.generate?.output?.component ?? './svg-components',
Expand All @@ -33,11 +45,25 @@ export async function generate(options: GenerateCommandOptions = {}) {
const cjsDir = resolve(process.cwd(), options.output?.cjs ?? config.generate?.output?.cjs ?? './svg-cjs')
const typesDir = resolve(process.cwd(), options.output?.types ?? config.generate?.output?.types ?? './svg-types')

generateVueSfc(entry, componentsDir, wrapperComponentName)
if (framework === GenerateFramework.vue3) {
generateVueSfc(entry, componentsDir, wrapperComponentName)
} else if (framework === GenerateFramework.react) {
generateReactTsx(entry, componentsDir, wrapperComponentName)
}
generateIndexFile(componentsDir)
await Promise.all([
generateModule(componentsDir, esmDir, 'esm'),
generateModule(componentsDir, cjsDir, 'cjs'),
generateModule({
entry: componentsDir,
output: esmDir,
format: 'esm',
framework,
}),
generateModule({
entry: componentsDir,
output: cjsDir,
format: 'cjs',
framework,
}),
generateTypes(componentsDir, typesDir, wrapperComponentName),
])
logger.success('generate icons success')
Expand All @@ -54,10 +80,14 @@ export function getOutputExtname(format: 'cjs' | 'esm') {
return format === 'esm' ? '.mjs' : '.js'
}

export async function generateModule(entry: string, output: string, format: 'cjs' | 'esm') {
export async function generateModule(options: GenerateModuleOptions) {
const { output, format, entry, framework } = options

fse.removeSync(output)

const outputExtname = getOutputExtname(format)
const filenames = fse.readdirSync(entry)

const manifest = await Promise.all(
filenames.map((filename) => {
const file = resolve(process.cwd(), entry, filename)
Expand All @@ -67,16 +97,7 @@ export async function generateModule(entry: string, output: string, format: 'cjs
content = content.replace(/\.vue/g, outputExtname)
}

return esbuild
.transform(content, {
loader: 'ts',
target: 'es2016',
format,
})
.then(({ code }) => ({
code,
filename: filename.replace('.ts', outputExtname).replace('.vue', outputExtname),
}))
return getTransformResult(content, framework, format, filename, outputExtname)
}),
)

Expand All @@ -85,58 +106,6 @@ export async function generateModule(entry: string, output: string, format: 'cjs
})
}

export function generateVueSfc(entry: string, output: string, wrapperComponentName: string) {
fse.removeSync(output)

const filenames = fse.readdirSync(entry)
filenames.forEach((filename) => {
const file = resolve(process.cwd(), entry, filename)
const content = fse.readFileSync(file, 'utf-8')
const sfcContent = compileSvgToVueSfc(filename.replace('.svg', ''), content)

fse.outputFileSync(resolve(output, bigCamelize(filename.replace('.svg', '.vue'))), sfcContent)
})

fse.outputFileSync(
resolve(output, 'XIcon.vue'),
`\
<template>
<i :style="style">
<slot />
</i>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'

export default defineComponent({
name: '${wrapperComponentName}',
props: {
size: {
type: [String, Number],
default: '1em',
},
color: {
type: String,
default: 'currentColor',
}
},
setup(props) {
const style = computed(() => ({
display: 'inline-flex',
color: props.color,
'--x-icon-size': typeof props.size === 'number' ? \`\${props.size}px\` : props.size,
}))

return {
style
}
}
})
</script>`,
)
}

export function generateTypes(entry: string, output: string, wrapperComponentName: string) {
fse.removeSync(output)
const filenames = fse.readdirSync(entry).filter((filename) => filename !== INDEX_FILE)
Expand Down Expand Up @@ -187,34 +156,3 @@ export function generateIndexFile(dir: string) {

fse.outputFileSync(resolve(dir, INDEX_FILE), content)
}

export function injectSvgCurrentColor(content: string) {
if (!content.match(/fill=".+?"/g) && !content.match(/stroke=".+?"/g)) {
return content.replace('<svg', '<svg fill="currentColor"')
}

return content
.replace(/fill="(?!none).+?"/g, 'fill="currentColor"')
.replace(/stroke="(?!none).+?"/g, 'stroke="currentColor"')
}

export function injectSvgStyle(content: string) {
return content.replace('<svg', '<svg style="width: var(--x-icon-size); height: var(--x-icon-size)"')
}

export function compileSvgToVueSfc(name: string, content: string) {
content = injectSvgStyle(injectSvgCurrentColor(content.match(/<svg (.|\n|\r)*/)?.[0] ?? ''))
return `\
<template>
${content}
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
name: '${bigCamelize(name)}',
})
</script>
`
}
55 changes: 55 additions & 0 deletions packages/varlet-icon-builder/src/framework/react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import fse from 'fs-extra'
import { resolve } from 'path'
import { bigCamelize } from '@varlet/shared'
import { injectSvgCurrentColor, injectSvgStyle } from '../utils/shared'

export function generateReactTsx(entry: string, output: string, wrapperComponentName: string) {
fse.removeSync(output)

const filenames = fse.readdirSync(entry)
filenames.forEach((filename) => {
const file = resolve(process.cwd(), entry, filename)
const content = fse.readFileSync(file, 'utf-8')
const tsxContent = compileSvgToReactTsx(filename.replace('.svg', ''), content)

fse.outputFileSync(resolve(output, bigCamelize(filename.replace('.svg', '.tsx'))), tsxContent)
})

fse.outputFileSync(
resolve(output, `${wrapperComponentName}.tsx`),
`\
import React, { ReactNode, CSSProperties } from 'react';

export interface ${wrapperComponentName}Props {
size?: string | number;
color?: string;
children?: ReactNode;
}

const ${wrapperComponentName}: React.FC<${wrapperComponentName}Props> = ({ size = '1em', color = 'currentColor', children }) => {
const style: CSSProperties = {
display: 'inline-flex',
color,
'--x-icon-size': typeof size === 'number' ? \`\${size}px\` : size,
};

return <i style={style}>{children}</i>
};

export default ${wrapperComponentName};
`,
)
}

export function compileSvgToReactTsx(name: string, content: string) {
content = injectSvgStyle(injectSvgCurrentColor(content.match(/<svg (.|\n|\r)*/)?.[0] ?? ''))
return `\
import React from 'react';

const ${bigCamelize(name)}: React.FC = () => (
${content}
);

export default ${bigCamelize(name)};
`
}
73 changes: 73 additions & 0 deletions packages/varlet-icon-builder/src/framework/vue3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fse from 'fs-extra'
import { resolve } from 'path'
import { bigCamelize } from '@varlet/shared'
import { injectSvgCurrentColor, injectSvgStyle } from '../utils/shared'

export function generateVueSfc(entry: string, output: string, wrapperComponentName: string) {
fse.removeSync(output)

const filenames = fse.readdirSync(entry)
filenames.forEach((filename) => {
const file = resolve(process.cwd(), entry, filename)
const content = fse.readFileSync(file, 'utf-8')
const sfcContent = compileSvgToVueSfc(filename.replace('.svg', ''), content)

fse.outputFileSync(resolve(output, bigCamelize(filename.replace('.svg', '.vue'))), sfcContent)
})

fse.outputFileSync(
resolve(output, `${wrapperComponentName}.vue`),
`\
<template>
<i :style="style">
<slot />
</i>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'

export default defineComponent({
name: '${wrapperComponentName}',
props: {
size: {
type: [String, Number],
default: '1em',
},
color: {
type: String,
default: 'currentColor',
}
},
setup(props) {
const style = computed(() => ({
display: 'inline-flex',
color: props.color,
'--x-icon-size': typeof props.size === 'number' ? \`\${props.size}px\` : props.size,
}))

return {
style
}
}
})
</script>`,
)
}

export function compileSvgToVueSfc(name: string, content: string) {
content = injectSvgStyle(injectSvgCurrentColor(content.match(/<svg (.|\n|\r)*/)?.[0] ?? ''))
return `\
<template>
${content}
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
name: '${bigCamelize(name)}',
})
</script>
`
}
6 changes: 6 additions & 0 deletions packages/varlet-icon-builder/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { loadConfig } from 'unconfig'

export enum GenerateFramework {
vue3 = 'vue3',
react = 'react',
}

export interface VIConfig {
/**
* @default `varlet-icons`
Expand Down Expand Up @@ -96,6 +101,7 @@ export interface VIConfig {
generate?: {
entry?: string
wrapperComponentName?: string
framework?: GenerateFramework
output?: {
component?: string
types?: string
Expand Down
32 changes: 32 additions & 0 deletions packages/varlet-icon-builder/src/utils/esbuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { GenerateFramework } from './config.js'
import esbuild from 'esbuild'

export function getEsbuildLoader(framework: GenerateFramework) {
switch (framework) {
case GenerateFramework.vue3:
return 'ts'
case GenerateFramework.react:
return 'tsx'
default:
return 'ts'
}
}

export function getTransformResult(
content: string,
framework: GenerateFramework,
format: 'cjs' | 'esm',
filename: string,
outputExtname: string,
) {
return esbuild
.transform(content, {
loader: getEsbuildLoader(framework),
target: 'es2016',
format,
})
.then(({ code }) => ({
code,
filename: filename.replace('.ts', outputExtname).replace('.vue', outputExtname),
}))
}
Loading