import virtualTemplatePlugin from '@opentiny-internal/unplugin-virtual-template/vite'
import { getBabelOutputPlugin } from '@rollup/plugin-babel'
import fs from 'fs-extra'
import { createRequire } from 'node:module'
import path from 'node:path'
import { build, defineConfig } from 'vite'
import dtsPlugin from 'vite-plugin-dts'
import vue3SvgPlugin from 'vite-svg-loader'
import { getAlias, pathFromWorkspaceRoot } from '../../config/vite'
import { external } from '../../shared/config'
import type { Module } from '../../shared/module-utils'
import { getAllIcons, getAllModules, getByName } from '../../shared/module-utils'
import { capitalizeKebabCase, getPatchVersion, isValidVersion, kebabCase, logGreen } from '../../shared/utils'
import generatePackageJsonPlugin from './rollup/generate-package-json'
import inlineChunksPlugin from './rollup/inline-chunks'
import replaceModuleNamePlugin from './rollup/replace-module-name'
export const pathFromPackages = (...args) => pathFromWorkspaceRoot('packages', ...args)
export const require = createRequire(import.meta.url)
export const requireModules = (id: string) => require(require.resolve(pathFromWorkspaceRoot(id)))
export const getVuePlugins = (vueVersion: string) => {
const pluginMap = {
'2': () => {
const vue2Plugin = requireModules('examples/vue2/node_modules/vite-plugin-vue2').createVuePlugin
const vue2SvgPlugin = requireModules('examples/vue2/node_modules/vite-plugin-vue2-svg').createSvgPlugin
return [
vue2Plugin({
jsx: true,
template: {
compilerOptions: {
comments: false
}
}
}),
vue2SvgPlugin({
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
}
}
},
'prefixIds'
]
}
})
]
},
'2.7': () => {
const vue27Plugin = requireModules('examples/vue2.7/node_modules/@vitejs/plugin-vue2')
const vue27JsxPlugin = requireModules('examples/vue2.7/node_modules/@vitejs/plugin-vue2-jsx')
const vue2SvgPlugin = requireModules('examples/vue2/node_modules/vite-plugin-vue2-svg').createSvgPlugin
const vue27Compiler = requireModules('examples/vue2.7/node_modules/vue/compiler-sfc')
return [
vue27Plugin({
compiler: vue27Compiler,
template: {
compilerOptions: {
comments: false
}
}
}),
vue27JsxPlugin({ injectH: false }),
vue2SvgPlugin({
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
}
}
},
'prefixIds'
]
}
})
]
},
'3': () => {
const vue3Plugin = requireModules('examples/vue3/node_modules/@vitejs/plugin-vue')
const vue3JsxPlugin = requireModules('examples/vue3/node_modules/@vitejs/plugin-vue-jsx')
return [
vue3Plugin({
template: {
compilerOptions: {
comments: false
}
}
}),
vue3JsxPlugin(),
vue3SvgPlugin({
defaultImport: 'component',
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false
}
}
},
'prefixIds'
]
}
})
]
}
}
return pluginMap[vueVersion]() || []
}
export const ns = (ver) => ({ '2': '2', '2.7': '2', '3': '3' })[ver] || ''
export interface BaseConfig {
vueVersion: string
dtsInclude: string[] | Set<string>
dts: boolean
buildTarget: string
npmScope?: string
isRuntime: boolean
design: string
}
export const getBaseConfig = ({ vueVersion, dtsInclude, dts, buildTarget, isRuntime, design }: BaseConfig) => {
const compilerOptions = require(pathFromWorkspaceRoot(`tsconfig.vue${vueVersion}.json`)).compilerOptions
let versionTarget = isValidVersion(buildTarget) ? buildTarget : `${ns(vueVersion)}.${buildTarget}`
let onlyHasV3Version = isValidVersion(buildTarget) ? buildTarget : `3.${buildTarget}`
const isOnlyHasV3 = (key) => {
const onlyHasV3Packages = [
'@opentiny/vue-theme',
'@opentiny/vue-theme-saas',
'@opentiny/vue-renderless',
'@opentiny/vue-hooks',
'@opentiny/utils'
]
return onlyHasV3Packages.includes(key)
}
return defineConfig({
publicDir: false,
plugins: [
virtualTemplatePlugin({ include: ['**/packages/vue/**/src/index.ts', '**/packages/vue/**/src/index.vue'] }),
...getVuePlugins(vueVersion),
dts &&
dtsPlugin({
root: pathFromWorkspaceRoot(),
tsconfigPath: `tsconfig.vue${vueVersion}.json`,
aliasesExclude: [/@opentiny\/vue.+/],
compilerOptions: {
paths: {
...compilerOptions.paths,
'vue': [`packages/vue/node_modules/vue${vueVersion}`],
'@vue/runtime-core': ['packages/vue/node_modules/@vue/runtime-core'],
'@vue/runtime-dom': ['packages/vue/node_modules/@vue/runtime-dom'],
'@vue/composition-api': ['packages/vue/node_modules/@vue/composition-api']
}
},
include: [...dtsInclude, 'packages/vue/*.d.ts'],
exclude: ['**/__tests__', '**/__test__'],
beforeWriteFile: (filePath, content) => {
return {
filePath: filePath.replace('/vue/src', '').replace('\\vue\\src', ''),
content: content
.replace(/import\('[./]+vue-common.+'\)/, 'import("vue")')
.replace(/\("vue[1-9\.]+/g, '("vue')
}
}
}),
!isRuntime && inlineChunksPlugin({ deleteInlinedFiles: true }),
!isRuntime &&
generatePackageJsonPlugin({
beforeWriteFile: (filePath, content) => {
const dependencies = {}
const packageVersion = content.version
if (!buildTarget) {
onlyHasV3Version = packageVersion
versionTarget = `${vueVersion}${packageVersion.slice(1)}`
}
Object.entries(content.dependencies).forEach(([key, value]) => {
if (isOnlyHasV3(key)) {
dependencies[key] = getPatchVersion(onlyHasV3Version)
} else if ((value as string).includes('workspace:~')) {
dependencies[key] = getPatchVersion(versionTarget)
} else {
dependencies[key] = value
}
})
if (filePath.includes('vue-common') && vueVersion === '2') {
dependencies['@vue/composition-api'] = '1.7.2'
}
const matchList = [
'vue-icon',
'vue-icon-saas',
'vue-icon-multicolor',
'vue',
'design/aurora',
'design/saas',
'design/smb',
'vue-directive'
]
if (matchList.includes(filePath)) {
content.main = './index.js'
content.module = './index.js'
} else {
content.main = './lib/index.js'
content.module = './lib/index.js'
}
if (filePath === 'vue') {
;['README.md', 'README.zh-CN.md', 'LICENSE'].forEach((item) => {
fs.copySync(
pathFromWorkspaceRoot(item),
path.resolve(pathFromPackages(''), `dist${vueVersion}/@opentiny/vue/${item}`)
)
})
}
content.types = 'index.d.ts'
const matchTypeList = ['vue-common', 'vue-locale', 'vue-saas-common']
if (matchTypeList.some((item) => filePath.includes(item))) {
content.types = './src/index.d.ts'
}
content.version = versionTarget
content.dependencies = dependencies
delete content.devDependencies
delete content.private
delete content.exports
return {
filePath: filePath.replace(/[\\/]lib$/, ''),
content
}
}
}),
!isRuntime && replaceModuleNamePlugin(() => versionTarget)
],
resolve: {
extensions: ['.js', '.ts', '.tsx', '.vue'],
alias: {
...getAlias(vueVersion, '', design),
'@tiptap/vue': `${vueVersion === '2' ? '@tiptap/vue-2' : '@tiptap/vue-3'}`,
'@vue/babel-helper-vue-jsx-merge-props': 'node_modules/@vue/babel-helper-vue-jsx-merge-props/dist/helper.js'
}
},
define: {
'process.env.BUILD_TARGET': JSON.stringify('component')
}
})
}
async function batchBuildAll({ vueVersion, tasks, formats, message, emptyOutDir, dts, buildTarget, npmScope, design }) {
const rootDir = pathFromPackages('')
const outDir = path.resolve(rootDir, `dist${vueVersion}/${npmScope}`)
await batchBuild({
vueVersion,
tasks,
formats,
message,
emptyOutDir,
dts,
design
})
function toEntry(libs) {
return libs.reduce((result, { libPath, path: file }) => {
const tLibPath = libPath.replace('-lib/', '/lib/')
result[tLibPath] = pathFromPackages(file)
return result
}, {})
}
function toTsInclude(libs) {
return new Set(
libs
.filter((item) => ['module', 'component'].includes(item.type))
.map((lib) => `packages/${lib.dtsRoot ? lib.path : path.dirname(lib.path)}`)
)
}
async function batchBuild({ vueVersion, tasks, formats, message, emptyOutDir, dts, design }) {
if (tasks.length === 0) return
logGreen(`====== 开始构建 ${message} ======`)
const entry = toEntry(tasks)
const dtsInclude = toTsInclude(tasks) as BaseConfig['dtsInclude']
await build({
configFile: false,
...getBaseConfig({ vueVersion, dtsInclude, dts, buildTarget, isRuntime: false, design }),
build: {
emptyOutDir,
minify: false,
rollupOptions: {
plugins: [
getBabelOutputPlugin({
presets: [['@babel/preset-env', { loose: true, modules: false }]]
}) as any
],
external: (source, importer, isResolved) => {
if (isResolved || !importer) {
return false
}
if (design) {
if (source.includes('vue-design-') || source.includes('vue-icon') || source.includes('vue-common')) {
return false
}
} else if (/vue-icon(-saas|-multicolor)?\/index/.test(importer)) {
return /^\.\//.test(source)
}
if (/design\/(saas|aurora|)/.test(importer) && /\.png/.test(source)) {
return false
}
if (/vue-icon(-saas|-multicolor)?\/src\/.+.ts/.test(importer)) {
return !/\.svg/.test(source)
}
if (/src\/index/.test(importer)) {
return /^\.\/(pc|mobile|mobile-first)/.test(source) || external(source)
}
if (/vue\/(index|pc|mobile|mobile-first)\.ts$/.test(importer)) {
return true
}
return external(source)
},
output: {
strict: false,
manualChunks: {}
}
},
lib: {
entry,
formats,
fileName: (format, entryName) => `${entryName}.js`
},
outDir
}
})
}
}
export interface BuildUiOption {
vueVersions: string[]
buildTarget?: string
formats: string[]
clean: boolean
dts: boolean
scope?: string
min?: boolean
design?: string
isVisualizer?: boolean
}
function getEntryTasks(): Module[] {
return ['index', 'pc', 'mobile-first'].map((mode) => ({
path: `vue/${mode}.ts`,
dtsRoot: true,
libPath: `vue/${mode}`,
type: 'module',
name: kebabCase({ str: '@opentiny/vue' }),
global: capitalizeKebabCase('opentinyVue'),
importName: '@opentiny/vue'
}))
}
function getTasks(names: string[]): Module[] {
if (names.length === 0) {
return [...getAllModules(false), ...getEntryTasks()]
}
return names
.map((name) =>
getByName({
name: kebabCase({ str: name.replace('@opentiny/vue-', '') }),
isSort: false
})
)
.flat()
}
* TinyVue组件打包主入口
* @private
* @param {string[]} names 需要打包的名字,如果不传默认打包全量组件 例如只打包alert和button两个组件 pnpm build:ui alert button
* @param {BuildUiOption} buildUiOption 具体参数参考BuildUiOption接口
*/
export async function buildUi(
names: string[] = [],
{
vueVersions = ['2', '3'],
buildTarget,
formats = ['es'],
clean = false,
dts = true,
scope = '@opentiny',
design = ''
}: BuildUiOption
) {
let emptyOutDir = clean
let tasks = getTasks(names)
if (names.some((name) => name.includes('icon')) || !names.length) {
tasks.push(...getByName({ name: kebabCase({ str: 'icon-saas' }), isSort: false }))
tasks.push(...getByName({ name: kebabCase({ str: 'icon-multicolor' }), isSort: false }))
tasks.push(...getAllIcons())
}
if (names.some((name) => ['@opentiny/vue', '@opentiny/vue', 'vue'].includes(name))) {
tasks.push(...getEntryTasks())
}
for (const vueVersion of vueVersions) {
const message = `TINY for vue${vueVersion}: ${JSON.stringify(names.length ? names : '全量')}`
await batchBuildAll({ vueVersion, tasks, formats, message, emptyOutDir, dts, buildTarget, npmScope: scope, design })
emptyOutDir = false
}
}