shadcn-vue/apps/www/scripts/build-registry.ts
2024-11-21 11:52:31 +08:00

916 lines
28 KiB
TypeScript

import type { z } from 'zod'
import type {
Registry,
RegistryEntry,
registryItemTypeSchema,
} from '../registry/schema'
// @sts-nocheck
import { existsSync, promises as fs } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { template } from 'lodash-es'
import { rimraf } from 'rimraf'
import { registry } from '../registry'
import { buildRegistry as crawlContent } from '../registry/crawl-content'
import { baseColors } from '../registry/registry-base-colors'
import { colorMapping, colors } from '../registry/registry-colors'
import { iconLibraries, icons } from '../registry/registry-icons'
import { styles } from '../registry/registry-styles'
import {
registryEntrySchema,
registrySchema,
} from '../registry/schema'
import { fixImport } from './fix-import'
const REGISTRY_PATH = path.join(process.cwd(), 'src/public/r')
const REGISTRY_INDEX_WHITELIST: z.infer<typeof registryItemTypeSchema>[] = [
'registry:ui',
'registry:lib',
'registry:hook',
'registry:theme',
'registry:block',
'registry:example',
]
// const project = new Project({
// compilerOptions: {},
// })
async function createTempSourceFile(filename: string) {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'shadcn-'))
return path.join(dir, filename)
}
// ----------------------------------------------------------------------------
// Build __registry__/index.ts.
// ----------------------------------------------------------------------------
async function buildRegistry(registry: Registry) {
let index = `// @ts-nocheck
// This file is autogenerated by scripts/build-registry.ts
// Do not edit this file directly.
export const Index: Record<string, any> = {
`
for (const style of styles) {
index += ` "${style.name}": {`
// Build style index.
for (const item of registry) {
const resolveFiles = item.files?.map(
file =>
`registry/${style.name}/${
typeof file === 'string' ? file : file.path
}`,
)
if (!resolveFiles) {
continue
}
const type = item.type.split(':')[1]
let sourceFilename = ''
// const chunks: any = []
if (item.type === 'registry:block') {
const file = resolveFiles[0]
const filename = path.basename(file)
let raw: string
try {
raw = await fs.readFile(file, 'utf8')
}
catch (error) {
continue
}
// const tempFile = await createTempSourceFile(filename)
// const sourceFile = project.createSourceFile(tempFile, raw, {
// scriptKind: ScriptKind.TS,
// })
// const description = sourceFile
// .getVariableDeclaration('description')
// ?.getInitializerOrThrow()
// .asKindOrThrow(SyntaxKind.StringLiteral)
// .getLiteralValue()
// item.description = description ?? ''
// // Find all imports.
// const imports = new Map<
// string,
// {
// module: string
// text: string
// isDefault?: boolean
// }
// >()
// sourceFile.getImportDeclarations().forEach((node) => {
// const module = node.getModuleSpecifier().getLiteralValue()
// node.getNamedImports().forEach((item) => {
// imports.set(item.getText(), {
// module,
// text: node.getText(),
// })
// })
// const defaultImport = node.getDefaultImport()
// if (defaultImport) {
// imports.set(defaultImport.getText(), {
// module,
// text: defaultImport.getText(),
// isDefault: true,
// })
// }
// })
// Find all opening tags with x-chunk attribute.
// const components = sourceFile
// .getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
// .filter((node) => {
// return node.getAttribute('x-chunk') !== undefined
// })
// chunks = await Promise.all(
// components.map(async (component, index) => {
// const chunkName = `${item.name}-chunk-${index}`
// // Get the value of x-chunk attribute.
// const attr = component
// .getAttributeOrThrow('x-chunk')
// .asKindOrThrow(SyntaxKind.JsxAttribute)
// const description = attr
// .getInitializerOrThrow()
// .asKindOrThrow(SyntaxKind.StringLiteral)
// .getLiteralValue()
// // Delete the x-chunk attribute.
// attr.remove()
// // Add a new attribute to the component.
// component.addAttribute({
// name: 'x-chunk',
// initializer: `"${chunkName}"`,
// })
// // Get the value of x-chunk-container attribute.
// const containerAttr = component
// .getAttribute('x-chunk-container')
// ?.asKindOrThrow(SyntaxKind.JsxAttribute)
// const containerClassName = containerAttr
// ?.getInitializer()
// ?.asKindOrThrow(SyntaxKind.StringLiteral)
// .getLiteralValue()
// containerAttr?.remove()
// const parentJsxElement = component.getParentIfKindOrThrow(
// SyntaxKind.JsxElement,
// )
// // Find all opening tags on component.
// const children = parentJsxElement
// .getDescendantsOfKind(SyntaxKind.JsxOpeningElement)
// .map((node) => {
// return node.getTagNameNode().getText()
// })
// .concat(
// parentJsxElement
// .getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
// .map((node) => {
// return node.getTagNameNode().getText()
// }),
// )
// const componentImports = new Map<
// string,
// string | string[] | Set<string>
// >()
// children.forEach((child) => {
// const importLine = imports.get(child)
// if (importLine) {
// const imports = componentImports.get(importLine.module) || []
// const newImports = importLine.isDefault
// ? importLine.text
// : new Set([...imports, child])
// componentImports.set(
// importLine.module,
// importLine?.isDefault ? newImports : Array.from(newImports),
// )
// }
// })
// const componnetImportLines = Array.from(
// componentImports.keys(),
// ).map((key) => {
// const values = componentImports.get(key)
// const specifier = Array.isArray(values)
// ? `{${values.join(',')}}`
// : values
// return `import ${specifier} from "${key}"`
// })
// const code = `
// 'use client'
// ${componnetImportLines.join('\n')}
// export default function Component() {
// return (${parentJsxElement.getText()})
// }`
// const targetFile = file.replace(item.name, `${chunkName}`)
// const targetFilePath = path.join(
// cwd(),
// `registry/${style.name}/${type}/${chunkName}.ts`,
// )
// // Write component file.
// rimraf.sync(targetFilePath)
// await fs.writeFile(targetFilePath, code, 'utf8')
// return {
// name: chunkName,
// description,
// component: `React.lazy(() => import("@/registry/${style.name}/${type}/${chunkName}")),`,
// file: targetFile,
// container: {
// className: containerClassName,
// },
// }
// }),
// )
// // Write the source file for blocks only.
sourceFilename = `__registry__/${style.name}/${type}/${item.name}.ts`
if (item.files) {
const files = item.files.map(file =>
typeof file === 'string'
? { type: 'registry:page', path: file }
: file,
)
if (files?.length) {
sourceFilename = `__registry__/${style.name}/${files[0].path}`
}
}
const sourcePath = path.join(process.cwd(), sourceFilename)
if (!existsSync(sourcePath)) {
await fs.mkdir(sourcePath, { recursive: true })
}
rimraf.sync(sourcePath)
// await fs.writeFile(sourcePath, sourceFile.getText())
await fs.writeFile(sourcePath, raw)
}
let componentPath = `@/registry/${style.name}/${type}/${item.name}`
if (item.files) {
const files = item.files.map(file =>
typeof file === 'string'
? { type: 'registry:page', path: file }
: file,
)
if (files?.length) {
componentPath = `@/registry/${style.name}/${files[0].path}`
}
}
index += `
"${item.name}": {
name: "${item.name}",
description: "${item.description ?? ''}",
type: "${item.type}",
registryDependencies: ${JSON.stringify(item.registryDependencies)},
files: [${item.files?.map((file) => {
const filePath = `registry/${style.name}/${
typeof file === 'string' ? file : file.path
}`
const resolvedFilePath = path.resolve(filePath)
return typeof file === 'string'
? `"${resolvedFilePath}"`
: `{
path: "${filePath}",
type: "${file.type}",
target: "${file.target ?? ''}"
}`
})}],
component: React.lazy(() => import("${componentPath}")),
source: "${sourceFilename}",
category: "${item.category ?? ''}",
subcategory: "${item.subcategory ?? ''}"
},`
}
// TODO: maybe implement chunk?
// chunks: [${chunks.map(
// chunk => `{
// name: "${chunk.name}",
// description: "${chunk.description ?? 'No description'}",
// component: ${chunk.component}
// file: "${chunk.file}",
// container: {
// className: "${chunk.container.className}"
// }
// }`,
// )}]
index += `
},`
}
index += `
}
`
// ----------------------------------------------------------------------------
// Build registry/index.json.
// ----------------------------------------------------------------------------
const items = registry
.filter(item => ['registry:ui'].includes(item.type))
.map((item) => {
return {
...item,
files: item.files?.map((_file) => {
const file
= typeof _file === 'string'
? {
path: _file,
type: item.type,
}
: _file
return file
}),
}
})
const registryJson = JSON.stringify(items, null, 2)
rimraf.sync(path.join(REGISTRY_PATH, 'index.json'))
await fs.writeFile(
path.join(REGISTRY_PATH, 'index.json'),
registryJson,
'utf8',
)
// Write style index.
rimraf.sync(path.join(process.cwd(), '__registry__/index.ts'))
await fs.writeFile(path.join(process.cwd(), '__registry__/index.ts'), index)
}
// ----------------------------------------------------------------------------
// Build registry/styles/[style]/[name].json.
// ----------------------------------------------------------------------------
async function buildStyles(registry: Registry) {
for (const style of styles) {
const targetPath = path.join(REGISTRY_PATH, 'styles', style.name)
// Create directory if it doesn't exist.
if (!existsSync(targetPath)) {
await fs.mkdir(targetPath, { recursive: true })
}
for (const item of registry) {
if (!REGISTRY_INDEX_WHITELIST.includes(item.type)) {
continue
}
let files
if (item.files) {
files = await Promise.all(
item.files.map(async (_file) => {
const file
= typeof _file === 'string'
? {
path: _file,
type: item.type,
content: '',
target: '',
}
: _file
let content: string
try {
content = await fs.readFile(
path.join(process.cwd(), 'registry', style.name, file.path),
'utf8',
)
// Only fix imports for v0- blocks.
if (item.name.startsWith('v0-')) {
content = fixImport(content)
}
}
catch (error) {
return
}
// TODO: remove meta content
// const tempFile = await createTempSourceFile(file.path)
// const sourceFile = project.createSourceFile(tempFile, content, {
// scriptKind: ScriptKind.TS,
// })
// sourceFile.getVariableDeclaration('iframeHeight')?.remove()
// sourceFile.getVariableDeclaration('containerClassName')?.remove()
// sourceFile.getVariableDeclaration('description')?.remove()
let target = file.target || ''
if ((!target || target === '') && item.name.startsWith('v0-')) {
const fileName = file.path.split('/').pop()
if (
file.type === 'registry:block'
|| file.type === 'registry:component'
|| file.type === 'registry:example'
) {
target = `components/${fileName}`
}
if (file.type === 'registry:ui') {
target = `components/ui/${fileName}`
}
if (file.type === 'registry:hook') {
target = `hooks/${fileName}`
}
if (file.type === 'registry:lib') {
target = `lib/${fileName}`
}
}
return {
path: file.path,
type: file.type,
// content: sourceFile.getText(),
content,
target,
}
}),
)
}
const payload = registryEntrySchema
.omit({
// source: true,
category: true,
subcategory: true,
// chunks: true,
})
.safeParse({
...item,
files,
})
if (payload.success) {
await fs.writeFile(
path.join(targetPath, `${item.name}.json`),
JSON.stringify(payload.data, null, 2),
'utf8',
)
}
}
}
// ----------------------------------------------------------------------------
// Build registry/styles/index.json.
// ----------------------------------------------------------------------------
const stylesJson = JSON.stringify(styles, null, 2)
await fs.writeFile(
path.join(REGISTRY_PATH, 'styles/index.json'),
stylesJson,
'utf8',
)
}
// ----------------------------------------------------------------------------
// Build registry/styles/[name]/index.json.
// ----------------------------------------------------------------------------
async function buildStylesIndex() {
for (const style of styles) {
const targetPath = path.join(REGISTRY_PATH, 'styles', style.name)
const dependencies = [
'tailwindcss-animate',
'class-variance-authority',
'lucide-react',
]
// TODO: Remove this when we migrate to lucide-react.
// if (style.name === "new-york") {
// dependencies.push("@radix-ui/react-icons")
// }
const payload: RegistryEntry = {
name: style.name,
type: 'registry:style',
dependencies,
registryDependencies: ['utils'],
tailwind: {
config: {
plugins: [`require("tailwindcss-animate")`],
},
},
cssVars: {},
files: [],
}
await fs.writeFile(
path.join(targetPath, 'index.json'),
JSON.stringify(payload, null, 2),
'utf8',
)
}
}
// ----------------------------------------------------------------------------
// Build registry/colors/index.json.
// ----------------------------------------------------------------------------
async function buildThemes() {
const colorsTargetPath = path.join(REGISTRY_PATH, 'colors')
rimraf.sync(colorsTargetPath)
if (!existsSync(colorsTargetPath)) {
await fs.mkdir(colorsTargetPath, { recursive: true })
}
const colorsData: Record<string, any> = {}
for (const [color, value] of Object.entries(colors)) {
if (typeof value === 'string') {
colorsData[color] = value
continue
}
if (Array.isArray(value)) {
colorsData[color] = value.map(item => ({
...item,
rgbChannel: item.rgb.replace(/^rgb\((\d+),(\d+),(\d+)\)$/, '$1 $2 $3'),
hslChannel: item.hsl.replace(
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
'$1 $2 $3',
),
}))
continue
}
if (typeof value === 'object') {
colorsData[color] = {
...value,
rgbChannel: value.rgb.replace(/^rgb\((\d+),(\d+),(\d+)\)$/, '$1 $2 $3'),
hslChannel: value.hsl.replace(
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
'$1 $2 $3',
),
}
continue
}
}
await fs.writeFile(
path.join(colorsTargetPath, 'index.json'),
JSON.stringify(colorsData, null, 2),
'utf8',
)
// ----------------------------------------------------------------------------
// Build registry/colors/[base].json.
// ----------------------------------------------------------------------------
const BASE_STYLES = `@tailwind base;
@tailwind components;
@tailwind utilities;
`
const BASE_STYLES_WITH_VARIABLES = `@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: <%- colors.light["background"] %>;
--foreground: <%- colors.light["foreground"] %>;
--card: <%- colors.light["card"] %>;
--card-foreground: <%- colors.light["card-foreground"] %>;
--popover: <%- colors.light["popover"] %>;
--popover-foreground: <%- colors.light["popover-foreground"] %>;
--primary: <%- colors.light["primary"] %>;
--primary-foreground: <%- colors.light["primary-foreground"] %>;
--secondary: <%- colors.light["secondary"] %>;
--secondary-foreground: <%- colors.light["secondary-foreground"] %>;
--muted: <%- colors.light["muted"] %>;
--muted-foreground: <%- colors.light["muted-foreground"] %>;
--accent: <%- colors.light["accent"] %>;
--accent-foreground: <%- colors.light["accent-foreground"] %>;
--destructive: <%- colors.light["destructive"] %>;
--destructive-foreground: <%- colors.light["destructive-foreground"] %>;
--border: <%- colors.light["border"] %>;
--input: <%- colors.light["input"] %>;
--ring: <%- colors.light["ring"] %>;
--radius: 0.5rem;
--chart-1: <%- colors.light["chart-1"] %>;
--chart-2: <%- colors.light["chart-2"] %>;
--chart-3: <%- colors.light["chart-3"] %>;
--chart-4: <%- colors.light["chart-4"] %>;
--chart-5: <%- colors.light["chart-5"] %>;
}
.dark {
--background: <%- colors.dark["background"] %>;
--foreground: <%- colors.dark["foreground"] %>;
--card: <%- colors.dark["card"] %>;
--card-foreground: <%- colors.dark["card-foreground"] %>;
--popover: <%- colors.dark["popover"] %>;
--popover-foreground: <%- colors.dark["popover-foreground"] %>;
--primary: <%- colors.dark["primary"] %>;
--primary-foreground: <%- colors.dark["primary-foreground"] %>;
--secondary: <%- colors.dark["secondary"] %>;
--secondary-foreground: <%- colors.dark["secondary-foreground"] %>;
--muted: <%- colors.dark["muted"] %>;
--muted-foreground: <%- colors.dark["muted-foreground"] %>;
--accent: <%- colors.dark["accent"] %>;
--accent-foreground: <%- colors.dark["accent-foreground"] %>;
--destructive: <%- colors.dark["destructive"] %>;
--destructive-foreground: <%- colors.dark["destructive-foreground"] %>;
--border: <%- colors.dark["border"] %>;
--input: <%- colors.dark["input"] %>;
--ring: <%- colors.dark["ring"] %>;
--chart-1: <%- colors.dark["chart-1"] %>;
--chart-2: <%- colors.dark["chart-2"] %>;
--chart-3: <%- colors.dark["chart-3"] %>;
--chart-4: <%- colors.dark["chart-4"] %>;
--chart-5: <%- colors.dark["chart-5"] %>;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}`
for (const baseColor of ['slate', 'gray', 'zinc', 'neutral', 'stone']) {
const base: Record<string, any> = {
inlineColors: {},
cssVars: {},
}
for (const [mode, values] of Object.entries(colorMapping)) {
base.inlineColors[mode] = {}
base.cssVars[mode] = {}
for (const [key, value] of Object.entries(values)) {
if (typeof value === 'string') {
// Chart colors do not have a 1-to-1 mapping with tailwind colors.
if (key.startsWith('chart-')) {
base.cssVars[mode][key] = value
continue
}
const resolvedColor = value.replace(/\{\{base\}\}-/g, `${baseColor}-`)
base.inlineColors[mode][key] = resolvedColor
const [resolvedBase, scale] = resolvedColor.split('-')
const color = scale
? colorsData[resolvedBase].find(
(item: any) => item.scale === Number.parseInt(scale),
)
: colorsData[resolvedBase]
if (color) {
base.cssVars[mode][key] = color.hslChannel
}
}
}
}
// Build css vars.
base.inlineColorsTemplate = template(BASE_STYLES)({})
base.cssVarsTemplate = template(BASE_STYLES_WITH_VARIABLES)({
colors: base.cssVars,
})
await fs.writeFile(
path.join(REGISTRY_PATH, `colors/${baseColor}.json`),
JSON.stringify(base, null, 2),
'utf8',
)
// ----------------------------------------------------------------------------
// Build registry/themes.css
// ----------------------------------------------------------------------------
const THEME_STYLES_WITH_VARIABLES = `
.theme-<%- theme %> {
--background: <%- colors.light["background"] %>;
--foreground: <%- colors.light["foreground"] %>;
--muted: <%- colors.light["muted"] %>;
--muted-foreground: <%- colors.light["muted-foreground"] %>;
--popover: <%- colors.light["popover"] %>;
--popover-foreground: <%- colors.light["popover-foreground"] %>;
--card: <%- colors.light["card"] %>;
--card-foreground: <%- colors.light["card-foreground"] %>;
--border: <%- colors.light["border"] %>;
--input: <%- colors.light["input"] %>;
--primary: <%- colors.light["primary"] %>;
--primary-foreground: <%- colors.light["primary-foreground"] %>;
--secondary: <%- colors.light["secondary"] %>;
--secondary-foreground: <%- colors.light["secondary-foreground"] %>;
--accent: <%- colors.light["accent"] %>;
--accent-foreground: <%- colors.light["accent-foreground"] %>;
--destructive: <%- colors.light["destructive"] %>;
--destructive-foreground: <%- colors.light["destructive-foreground"] %>;
--ring: <%- colors.light["ring"] %>;
--radius: <%- colors.light["radius"] %>;
}
.dark .theme-<%- theme %> {
--background: <%- colors.dark["background"] %>;
--foreground: <%- colors.dark["foreground"] %>;
--muted: <%- colors.dark["muted"] %>;
--muted-foreground: <%- colors.dark["muted-foreground"] %>;
--popover: <%- colors.dark["popover"] %>;
--popover-foreground: <%- colors.dark["popover-foreground"] %>;
--card: <%- colors.dark["card"] %>;
--card-foreground: <%- colors.dark["card-foreground"] %>;
--border: <%- colors.dark["border"] %>;
--input: <%- colors.dark["input"] %>;
--primary: <%- colors.dark["primary"] %>;
--primary-foreground: <%- colors.dark["primary-foreground"] %>;
--secondary: <%- colors.dark["secondary"] %>;
--secondary-foreground: <%- colors.dark["secondary-foreground"] %>;
--accent: <%- colors.dark["accent"] %>;
--accent-foreground: <%- colors.dark["accent-foreground"] %>;
--destructive: <%- colors.dark["destructive"] %>;
--destructive-foreground: <%- colors.dark["destructive-foreground"] %>;
--ring: <%- colors.dark["ring"] %>;
}`
const themeCSS = []
for (const theme of baseColors) {
themeCSS.push(
template(THEME_STYLES_WITH_VARIABLES)({
colors: theme.cssVars,
theme: theme.name,
}),
)
}
await fs.writeFile(
path.join(REGISTRY_PATH, `themes.css`),
themeCSS.join('\n'),
'utf8',
)
// ----------------------------------------------------------------------------
// Build registry/themes/[theme].json
// ----------------------------------------------------------------------------
rimraf.sync(path.join(REGISTRY_PATH, 'themes'))
for (const baseColor of ['slate', 'gray', 'zinc', 'neutral', 'stone']) {
const payload: Record<string, any> = {
name: baseColor,
label: baseColor.charAt(0).toUpperCase() + baseColor.slice(1),
cssVars: {},
}
for (const [mode, values] of Object.entries(colorMapping)) {
payload.cssVars[mode] = {}
for (const [key, value] of Object.entries(values)) {
if (typeof value === 'string') {
const resolvedColor = value.replace(/\{\{base\}\}-/g, `${baseColor}-`)
payload.cssVars[mode][key] = resolvedColor
const [resolvedBase, scale] = resolvedColor.split('-')
const color = scale
? colorsData[resolvedBase].find(
(item: any) => item.scale === Number.parseInt(scale),
)
: colorsData[resolvedBase]
if (color) {
payload.cssVars[mode][key] = color.hslChannel
}
}
}
}
const targetPath = path.join(REGISTRY_PATH, 'themes')
// Create directory if it doesn't exist.
if (!existsSync(targetPath)) {
await fs.mkdir(targetPath, { recursive: true })
}
await fs.writeFile(
path.join(targetPath, `${payload.name}.json`),
JSON.stringify(payload, null, 2),
'utf8',
)
}
}
}
// ----------------------------------------------------------------------------
// Build registry/icons/index.json.
// ----------------------------------------------------------------------------
async function buildIcons() {
const iconsTargetPath = path.join(REGISTRY_PATH, 'icons')
rimraf.sync(iconsTargetPath)
if (!existsSync(iconsTargetPath)) {
await fs.mkdir(iconsTargetPath, { recursive: true })
}
const iconsData = icons
await fs.writeFile(
path.join(iconsTargetPath, 'index.json'),
JSON.stringify(iconsData, null, 2),
'utf8',
)
}
// ----------------------------------------------------------------------------
// Build __registry__/icons.ts.
// ----------------------------------------------------------------------------
async function buildRegistryIcons() {
let index = `// @ts-nocheck
// This file is autogenerated by scripts/build-registry.ts
// Do not edit this file directly.
import * as React from "react"
export const Icons = {
`
for (const [icon, libraries] of Object.entries(icons)) {
index += ` "${icon}": {`
for (const [library, componentName] of Object.entries(libraries)) {
const packageName = iconLibraries[library as keyof typeof iconLibraries].package
if (packageName) {
index += `
${library}: React.lazy(() => import("${packageName}").then(mod => ({
default: mod.${componentName}
}))),`
}
}
index += `
},`
}
index += `
}
`
// Write style index.
rimraf.sync(path.join(process.cwd(), '__registry__/icons.ts'))
await fs.writeFile(
path.join(process.cwd(), '__registry__/icons.ts'),
index,
'utf8',
)
}
try {
const content = await crawlContent()
const result = registrySchema.safeParse([...registry, ...content])
if (!result.success) {
console.error(result.error)
process.exit(1)
}
await buildRegistry(result.data)
await buildStyles(result.data)
await buildStylesIndex()
await buildThemes()
await buildRegistryIcons()
await buildIcons()
// eslint-disable-next-line no-console
console.log('✅ Done!')
}
catch (error) {
console.error(error)
process.exit(1)
}