build: 公共依赖cdn解耦并增加开关控制 (#360)

* build: external public cdn localize 公共cdn依赖包支持本地化

* build: cdn外部依赖解耦,修复baseUrl获取

* build: cdn依赖解耦 修正baseUrl获取方式

* build: cdn依赖三方解耦,优化目录型拷贝

* build: cdn依赖解耦,解决目录复制文件的相对路径问题

* build(design-core): 修复monaco的worker使用cdn地址前缀的情况下打包失效

* build: 本地开发也支持脱离cdn三方依赖

* build: 去除代码注释

* build: 三方cdn依赖解耦  物料bundle文件依赖支持替换cdn依赖

* build(design-core) 去除冗余用不到的文件和配置项硬编码

* build: 公共依赖cdn解耦测试完成 开关默认关闭

* refactor: 简化三方cdn依赖解耦的正则,改为path方法

* refactor: 三方依赖解耦脚本代码优化,减少雷同的正则匹配

* refactor: 三方cdn依赖解耦脚本 简化合并正则

* build: 三方cdn解耦 物料本地打包逻辑优化修正

* build: 三方cdn依赖解耦,优化代码简化正则,修复计算版本号问题,修复函数名大小写问题

* build: 优化importMap版本站位符号

* build: 三方cdn物料解耦 变量改为环境变量

* build: 三方cdn依赖解耦 补充处理复制文件夹时候的去重

* fix: 三方cdn解耦, 解决本地启动问题

* feat(preview): preview也支持cdn依赖解耦

* refactor(desing-core/scripts): 重构复制cdn文件本地复制模块

* build:  重构预览的importMap复制逻辑,修复两个importMap文件不存在

* build: 进一步优化monacoEditor地址,直接打包不再跨网络获取

* refactor(design-core/scripts): 调整复制应用importMap函数的参数顺序,与其他结构类似

* refactor(design-core/script): 调整文件位置和文件引用顺序

* feat(design-core/preview): preview importMap.js 使用 importMap.json 数据归一

* fix(design-core/preview): 修复importJson的引用

* fix: mock端口号订正,解决element-plus拖入画布后无法渲染

* build(design-core/script): 修复临时安装包插件安装完包后返回目录不正确

* fix: 修正monaco-editor的worker资源的打包地址

* build(design-core/script): 修复临时安装包插件安装完包后返回目录不正确

* fix(design-core/preview): 修正cdn解耦preview获取动态的importMap.json的base路径问题

* build: 三方cdn解耦,优化拷贝脚本,修复文件夹当文件拷贝当文件夹是包路径时候目标路径版本号丢失

* build: 三方cdn解耦,解决包未安装的情况下,glob匹配不到文件导致打包不拷贝内容

* fix: 根据检视意见使用fs-extra readJsonSync替代utils工具函数

* fix: 根据检视意见修改函数名大小写和端口号读取

* feat: 三方物料解耦 preview变量替换增加注释

* feat: 根据检视意见修改函数名字

* refactor: 根据检视意见,将脚本函数的参数整改为对象,并把目录值迁移到参数默认值

* docs(design-core/scripts): 三方cdn解耦 修正描述错误

* fix: 三方cdn解耦 修正preview脚本TinyVue版本号
This commit is contained in:
rhlin 2024-05-12 23:47:02 -07:00 committed by GitHub
parent 3a66996fae
commit 899d616f7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 721 additions and 58 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ package-lock.json
yarn.lock yarn.lock
pnpm-lock.yaml pnpm-lock.yaml
lerna-debug.log lerna-debug.log
packages/design-core/bundle-deps
# local env files # local env files
.env.local .env.local

View File

@ -1,14 +1,14 @@
/** /**
* Copyright (c) 2023 - present TinyEngine Authors. * Copyright (c) 2023 - present TinyEngine Authors.
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
* *
* Use of this source code is governed by an MIT-style license. * Use of this source code is governed by an MIT-style license.
* *
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
* *
*/ */
export const NODE_UID = 'data-uid' export const NODE_UID = 'data-uid'
export const NODE_TAG = 'data-tag' export const NODE_TAG = 'data-tag'
@ -125,9 +125,10 @@ export const getClipboardSchema = (event) => translateStringToSchame(event.clipb
*/ */
export const dynamicImportComponents = async ({ package: pkg, script, components }) => { export const dynamicImportComponents = async ({ package: pkg, script, components }) => {
if (!script) return if (!script) return
const scriptUrl = script.startsWith('.') ? new URL(script, location.href).href : script
if (!window.TinyComponentLibs[pkg]) { if (!window.TinyComponentLibs[pkg]) {
const modules = await import(/* @vite-ignore */ script) const modules = await import(/* @vite-ignore */ scriptUrl)
window.TinyComponentLibs[pkg] = modules window.TinyComponentLibs[pkg] = modules
} }

View File

@ -2,6 +2,8 @@
NODE_ENV=production NODE_ENV=production
VITE_CDN_DOMAIN=https://npm.onmicrosoft.cn VITE_CDN_DOMAIN=https://npm.onmicrosoft.cn
VITE_LOCAL_IMPORT_MAPS=false
VITE_LOCAL_BUNDLE_DEPS=false
# VITE_ORIGIN= # VITE_ORIGIN=
# 错误监控上报 url # 错误监控上报 url

View File

@ -2,5 +2,7 @@
NODE_ENV=development NODE_ENV=development
VITE_CDN_DOMAIN=https://npm.onmicrosoft.cn VITE_CDN_DOMAIN=https://npm.onmicrosoft.cn
VITE_LOCAL_IMPORT_MAPS=false
VITE_LOCAL_BUNDLE_DEPS=false
# request data via alpha service # request data via alpha service
# VITE_ORIGIN= # VITE_ORIGIN=

View File

@ -2,4 +2,6 @@
NODE_ENV=production NODE_ENV=production
VITE_CDN_DOMAIN=https://npm.onmicrosoft.cn VITE_CDN_DOMAIN=https://npm.onmicrosoft.cn
#VITE_ORIGIN= VITE_LOCAL_IMPORT_MAPS=false
VITE_LOCAL_BUNDLE_DEPS=false
#VITE_ORIGIN=

View File

@ -4,7 +4,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="%VITE_CDN_DOMAIN%/@opentiny/vue-theme@3.14/index.css" rel="stylesheet" />
<style type="text/css"> <style type="text/css">
.loading-warp { .loading-warp {
display: flex; display: flex;

View File

@ -120,9 +120,11 @@
"rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-polyfill-node": "^0.12.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-visualizer": "^5.8.3", "rollup-plugin-visualizer": "^5.8.3",
"shelljs": "^0.8.5",
"svg-sprite-loader": "^6.0.11", "svg-sprite-loader": "^6.0.11",
"vite": "^4.3.7", "vite": "^4.3.7",
"vite-plugin-monaco-editor": "^1.0.10", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-static-copy": "^0.16.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vue-eslint-parser": "^8.0.1" "vue-eslint-parser": "^8.0.1"
}, },

View File

@ -0,0 +1,82 @@
import path from 'node:path'
import { readJsonSync } from 'fs-extra'
import { installPackageTemporary } from '../vite-plugins/installPackageTemporary'
import { configServerAddProxy } from '../vite-plugins/configureServerAddProxy'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import {
getCdnPathNpmInfoForSingleFile,
getPackageNeedToInstallAndFilesUsingSameVersion,
dedupeCopyFiles,
copyfileToDynamicSrcMapper
} from './locateCdnNpmInfo'
export function extraBundleCdnLink(filename, originCdnPrefix) {
const result = []
const bundle = readJsonSync(filename)
bundle.data?.materials?.components?.forEach((component) => {
if (component.npm) {
const possibleUrl = [component.npm.script, component.npm.css]
possibleUrl.forEach((url) => {
if (url?.startsWith(originCdnPrefix) && !result.includes(url)) {
result.push(url)
}
})
}
})
return result
}
export function replaceBundleCdnLink(bundle, fileMap) {
bundle.data?.materials?.components?.forEach((component) => {
if (component.npm) {
const possibleUrl = ['script', 'css']
possibleUrl.forEach((key) => {
const matchRule = fileMap.find((rule) => component.npm[key] === rule.originUrl)
if (matchRule) {
component.npm[key] = matchRule.newUrl
}
})
}
})
}
export function copyBundleDeps({
bundleFile,
targetBundleFile,
originCdnPrefix,
base,
dir = 'material-static',
bundleTempDir = 'bundle-deps/material-static'
}) {
const cdnFiles = extraBundleCdnLink(bundleFile, originCdnPrefix).map((url) =>
getCdnPathNpmInfoForSingleFile(url, originCdnPrefix, base, dir, false, bundleTempDir)
)
const { packages: packageNeedToInstall, files } = getPackageNeedToInstallAndFilesUsingSameVersion(cdnFiles)
const plugin = (isDev) => {
return [
...(isDev ? configServerAddProxy(targetBundleFile, targetBundleFile.replace(/\.([^.]+?$)/, '-local.$1')) : []),
...installPackageTemporary(packageNeedToInstall, bundleTempDir),
...viteStaticCopy({
targets: [
...dedupeCopyFiles(files).map(copyfileToDynamicSrcMapper),
{
src: bundleFile,
dest: path.dirname(targetBundleFile),
transform: (content) => {
const json = JSON.parse(content)
replaceBundleCdnLink(json, files)
return JSON.stringify(json, null, 2)
},
rename: (filename, fileExtension) =>
isDev ? `${filename}-local.${fileExtension}` : path.basename(targetBundleFile),
overwrite: true // 覆盖public的
}
]
})
]
}
return {
plugin
}
}

View File

@ -0,0 +1,67 @@
import { viteStaticCopy } from 'vite-plugin-static-copy'
import {
getPackageNeedToInstallAndFilesUsingSameVersion,
copyfileToDynamicSrcMapper,
dedupeCopyFiles,
getCdnPathNpmInfoForPackage,
getCdnPathNpmInfoForSingleFile
} from './locateCdnNpmInfo'
import { importmapPlugin } from '../externalDeps'
import { installPackageTemporary } from '../vite-plugins/installPackageTemporary'
export const copyLocalImportMap = ({
importMap,
styleUrls,
originCdnPrefix,
base,
dir = 'import-map-static',
bundleTempDir = 'bundle-deps/design-core-import-map',
packageCopy = [] // 值为importMap的imports的左值 (非右值的地址上的包名)
}) => {
const importMapFiles = Object.entries(importMap.imports)
.filter(([_libKey, libPath]) => libPath.startsWith(originCdnPrefix))
.map(([libKey, libPath]) => {
if (packageCopy.includes(libKey)) {
return getCdnPathNpmInfoForPackage(libPath, originCdnPrefix, base, dir, true, bundleTempDir)
}
return getCdnPathNpmInfoForSingleFile(libPath, originCdnPrefix, base, dir, false, bundleTempDir)
})
const styleFiles = styleUrls
.filter((styleUrl) => styleUrl.startsWith(originCdnPrefix))
.map((url) => getCdnPathNpmInfoForSingleFile(url, originCdnPrefix, base, dir, false), bundleTempDir)
const { packages: packageNeedToInstall, files } = getPackageNeedToInstallAndFilesUsingSameVersion(
importMapFiles.concat(styleFiles)
)
return [
...installPackageTemporary(packageNeedToInstall, bundleTempDir),
...viteStaticCopy({
targets: dedupeCopyFiles(files).map(copyfileToDynamicSrcMapper)
}),
{
config(config, { command }) {
// 处理devAlias带CDN域名 另外需要使得本地vue和importMap的vue是同一个实例
if (command === 'serve') {
config.resolve.alias = [
...config.resolve.alias,
{
find: /^vue$/,
replacement: `http://localhost:${config.server.port || 8080}/${
files.find(({ originUrl }) => importMap.imports.vue === originUrl).newUrl
}` // 实际端口号需要更具本地启动修改
}
]
}
}
},
importmapPlugin(
{
imports: Object.fromEntries(
Object.entries(importMap.imports).map(([k, v]) => [k, files.find((f) => f.originUrl === v)?.newUrl ?? v])
)
},
styleUrls.map((url) => styleFiles.find((f) => f.originUrl === url).newUrl ?? url)
)
]
}

View File

@ -0,0 +1,82 @@
import path from 'node:path'
import { readJsonSync } from 'fs-extra'
import {
getPackageNeedToInstallAndFilesUsingSameVersion,
copyfileToDynamicSrcMapper,
dedupeCopyFiles,
getCdnPathNpmInfoForPackage,
getCdnPathNpmInfoForSingleFile
} from './locateCdnNpmInfo'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import { installPackageTemporary } from '../vite-plugins/installPackageTemporary'
export function extraPreviewImport(filename, originCdnPrefix) {
const result = []
const importMap = readJsonSync(filename)
Object.entries(importMap.imports)?.forEach(([_key, location]) => {
const url = location.replace('${VITE_CDN_DOMAIN}', originCdnPrefix).replace('${opentinyVueVersion}', '~3.14')
if (url?.startsWith(originCdnPrefix) && !result.includes(url)) {
result.push(url)
}
})
return result
}
export function replacePreviewImport(importMap, fileMap, originCdnPrefix) {
return {
imports: Object.fromEntries(
Object.entries(importMap.imports)?.map(([key, location]) => {
// 这里的替换占位符规则等同于packages/design-core/src/preview/src/preview/importMap.js, 两边修改需要同步
const url = location.replace('${VITE_CDN_DOMAIN}', originCdnPrefix).replace('${opentinyVueVersion}', '~3.14')
const matchRule = fileMap.find((rule) => url === rule.originUrl)
if (matchRule) {
return [key, matchRule.newUrl]
}
return [key, location]
})
)
}
}
export function extraPreviewImportFile(filename, targetFileName, originCdnPrefix) {
return (fileMap) => [
{
src: filename,
dest: path.dirname(targetFileName),
transform: (content) => {
return JSON.stringify(replacePreviewImport(JSON.parse(content), fileMap, originCdnPrefix), null, 2)
},
rename: path.basename(targetFileName)
}
]
}
export function copyPreviewImportMap({
importMapJson,
targetImportMapJson,
originCdnPrefix,
base,
dir = 'preview-import-map-static',
bundleTempDir = 'bundle-deps/preview-import-map',
packageCopyLib = [] // 值为cdn地址上的包名
}) {
const cdnFiles = extraPreviewImport(importMapJson, originCdnPrefix).map((url) => {
const { packageName } = url.match(
new RegExp(`^${originCdnPrefix}/?(?<packageName>.+?)@(?<versionDemand>[^/]+)(?<filePathInPackage>.*?)$`)
).groups
if (packageCopyLib.includes(packageName)) {
return getCdnPathNpmInfoForPackage(url, originCdnPrefix, base, dir, true, bundleTempDir)
}
return getCdnPathNpmInfoForSingleFile(url, originCdnPrefix, base, dir, false, bundleTempDir)
})
const { packages: packageNeedToInstall, files } = getPackageNeedToInstallAndFilesUsingSameVersion(cdnFiles)
return [
...installPackageTemporary(packageNeedToInstall, bundleTempDir),
...viteStaticCopy({
targets: [
...dedupeCopyFiles(files).map(copyfileToDynamicSrcMapper),
...extraPreviewImportFile(importMapJson, targetImportMapJson, originCdnPrefix)(files)
]
})
]
}

View File

@ -0,0 +1,6 @@
export * from './copyBundleDeps'
export * from './copyImportMap'
export * from './copyPreviewImportMap'
export * from './utils'
export * from './locateCdnNpmInfo'
export * from './replaceImportPath.mjs'

View File

@ -0,0 +1,204 @@
import path from 'node:path'
import fs from 'node:fs'
import fg from 'fast-glob'
import { normalizePath } from 'vite'
import { readJsonSync } from 'fs-extra'
import { babelReplaceImportPathWithCertainFileName } from './replaceImportPath.mjs'
function transform(content, filename) {
if (filename.endsWith('.js')) {
const result = babelReplaceImportPathWithCertainFileName(content, filename, console)
return result.code || content
}
return content
}
function onlyFiles(globString) {
// viteStaticCopy 自带的glob匹配无法过滤目录 手动过滤目录作为数组传入
return fg.sync(globString + '/**/*', { onlyFiles: true }).map((p) => normalizePath(p))
}
function replaceTailSlash(pathStr) {
// 替换尾部的 / 可以把目录当文件复制
return pathStr.replace(/\/$/, '')
}
export function copyfileToDynamicSrcMapper({ src, dest, transform, rename, folder, ...rest }) {
// viteStaticCopy 自带的glob匹配无法过滤目录 手动过滤目录作为数组传入但是不存在的包需要推迟glob的时机到安装文件后
return {
...rest,
get src() {
// 注意对象不能被解构否则getter无法动态计算
return src || onlyFiles(folder)
},
dest,
transform,
rename
}
}
// 生成复制单个文件所需要的信息
export function getCdnPathNpmInfoForSingleFile(
url, // cdn托管的npm文件地址数组
originCdnPrefix, // cdn的前缀
base, // build构建的baseBASE_URL参数
dir, // 复制到目标的文件目录
transformIContent = false, // 是否需要转换内容, 如果传入url实际为目录则不会转会
tempDir = 'bundle-deps' // 新安装包的安装目录
) {
const baseSlash = base.endsWith('/') ? '' : '/'
const { packageName, versionDemand, filePathInPackage } = url.match(
new RegExp(`^${originCdnPrefix}/?(?<packageName>.+?)@(?<versionDemand>[^/]+)(?<filePathInPackage>.*?)$`)
).groups
let version = versionDemand
let isFolder = filePathInPackage.endsWith('/')
let src = replaceTailSlash(`node_modules/${packageName}${filePathInPackage}`)
const sourceExist = fs.existsSync(path.resolve(src))
let sourceExistExternal = false
if (sourceExist) {
const stat = fs.statSync(path.resolve(src))
if (stat.isDirectory()) {
isFolder = true
}
const content = readJsonSync(`node_modules/${packageName}/package.json`)
version = content.version // 忽略请求的包版本,使用本地包版本号
} else {
src = tempDir + '/' + src
sourceExistExternal = fs.existsSync(path.resolve(src)) // 安装过的不重新安装, 当且仅当所有包都安装过
if (sourceExistExternal) {
const packageJson = readJsonSync(`${tempDir}/node_modules/${packageName}/package.json`)
version = packageJson.version // 如果重新安装这个版本号还需要刷新
}
}
const updateVersion = (version) => {
const destPackageDir = `${dir}/${packageName}@${version}`
const destFullPath = `${destPackageDir}${filePathInPackage}`
const destFullPathWithoutTailSlash = replaceTailSlash(destFullPath)
const dest = path.dirname(destFullPathWithoutTailSlash)
const rename = dest.startsWith(destPackageDir) ? null : path.basename(destFullPathWithoutTailSlash) // 版本号被截断,需要补充回去
return {
version,
newUrl: `${base}${baseSlash}${destFullPath}`,
dest,
rename
}
}
return {
originUrl: url,
// newUrl, // overwrite by updateVersion(version)
src,
// dest, // overwrite by updateVersion(version)
packageName,
// version, // overwrite by updateVersion(version)
versionDemand,
filePathInPackage,
sourceExist,
sourceExistExternal,
...updateVersion(version),
updateVersion,
transform: transformIContent && !isFolder ? transform : null
}
}
export function getCdnPathNpmInfoForPackage(
url, // cdn托管的npm文件地址数组
originCdnPrefix, // cdn的前缀
base, // build构建的baseBASE_URL参数
dir, // 复制到目标的文件目录
transformIContent = false, // 是否需要转换内容
tempDir = 'bundle-deps' // 新安装包的安装目录
) {
const baseSlash = base.endsWith('/') ? '' : '/'
const { packageName, versionDemand, filePathInPackage } = url.match(
new RegExp(`^${originCdnPrefix}/?(?<packageName>.+?)@(?<versionDemand>[^/]+)(?<filePathInPackage>.*?)$`)
).groups
let version = versionDemand
let src = `node_modules/${packageName}`
const sourceExist = fs.existsSync(path.resolve(src))
let sourceExistExternal = false
if (sourceExist) {
const content = readJsonSync(`${src}/package.json`)
version = content.version // 忽略请求的包版本,使用本地包版本号
} else {
src = tempDir + '/' + src
sourceExistExternal = fs.existsSync(path.resolve(src)) // 安装过的不重新安装, 当且仅当所有包都安装过
if (sourceExistExternal) {
const packageJson = readJsonSync(`${src}/package.json`)
version = packageJson.version // 如果重新安装这个版本号还需要刷新
}
}
const updateVersion = (version) => {
const packageDir = `${dir}/${packageName}@${version}`
const packageDirBasename = path.basename(packageDir)
return {
version,
newUrl: `${base}${baseSlash}${dir}/${packageName}@${version}${filePathInPackage}`,
dest: path.dirname(packageDir),
rename: (_filename, _fileExtension, fullPath) => `${packageDirBasename}${fullPath.replace(src, '')}`
}
}
return {
folder: src,
originUrl: url,
// newUrl, // overwrite by updateVersion(version)
src: sourceExist || sourceExistExternal ? onlyFiles(src) : null,
// dest, // overwrite by updateVersion(version)
packageName,
// version, // overwrite by updateVersion(version)
versionDemand,
filePathInPackage,
sourceExist,
sourceExistExternal,
...updateVersion(version),
updateVersion,
transform: transformIContent ? transform : null
}
}
export function dedupeCopyFiles(files) {
return files.reduce((acc, cur) => {
//去重,分别处理字符串和数组
if (
(cur.folder && !acc.some((item) => !!item.folder && item.folder === cur.folder && item.dest === cur.dest)) || // 文件夹拷贝
(typeof cur.src === 'string' && !acc.some((item) => item.src === cur.src && item.dest === cur.dest)) // 文件拷贝
) {
acc.push(cur)
}
return acc
}, [])
}
export function getPackageNeedToInstallAndFilesUsingSameVersion(files) {
const packageNeedToInstall = files
.filter((item) => !item.sourceExist)
.map(({ packageName, version, sourceExistExternal: exist }) => ({ packageName, version, exist }))
.reduce((acc, cur) => {
// 同个包避免多个版本只保留一个版本
if (!acc.some(({ packageName }) => cur.packageName === packageName)) {
acc.push(cur)
}
return acc
}, [])
let newFiles = null
if (packageNeedToInstall.length) {
// 确保同个包多个版本只能从一个版本引用文件
newFiles = files.map((file) => {
const samePackageDifferentVersion = packageNeedToInstall.find(
({ packageName, version }) => packageName === file.packageName && version !== file.version
)
if (samePackageDifferentVersion) {
return {
...file,
...file.updateVersion(samePackageDifferentVersion.version)
}
}
return file
})
}
return {
packages: packageNeedToInstall,
files: newFiles ?? files
}
}

View File

@ -0,0 +1,89 @@
import fs from 'node:fs'
import path from 'node:path'
import { parse } from '@babel/core'
import traversePkg from '@babel/traverse'
import generatePkg from '@babel/generator'
const traverse = traversePkg.default
const generate = generatePkg.default
export function relativePathPattern(relativePath) {
return './' + (path.sep === '/' ? relativePath : relativePath.replace(/\\/g, '/'))
}
export function resolvePath(importPath, currentFilePath) {
if (['js', 'mjs'].some(suffix =>importPath.endsWith(suffix))) { // 文件名已经带有.js.mjs后缀
return importPath
}
const parentPath = path.resolve(currentFilePath, '../')
const filePrefix = path.resolve(parentPath, importPath)
if (fs.existsSync(filePrefix)) {
const stat = fs.statSync(filePrefix)
if (stat.isDirectory()) {
let mainFileName = 'index.js'
const packageFile = path.resolve(filePrefix, 'package.json')
if (fs.existsSync(packageFile)) {
const packageFileContent = fs.readFileSync(packageFile, { encoding: 'utf-8' })
const packageJson = JSON.parse(packageFileContent)
mainFileName = packageJson.module || packageJson.main || mainFileName
}
const mainFile = path.resolve(filePrefix, mainFileName)
if (fs.existsSync(mainFile)) {
return relativePathPattern(path.relative(parentPath, mainFile))
}
return null
}
return importPath
}
const possibleSuffix = ['.js', '.mjs']
const suffix = possibleSuffix.find(suf => fs.existsSync(filePrefix + suf))
if (suffix) {
return relativePathPattern(path.relative(path.resolve(currentFilePath, '../'), filePrefix + suffix))
}
return null
}
// babel 替换 js的相对地址引用为确定文件后缀
export function babelReplaceImportPathWithCertainFileName(content, currentFilePath, logger = console) {
let fileChangedMark = false
let result = {
code: null,
success: [],
error: []
}
const ast = parse(content, { sourceType: 'module' })
traverse(ast, {
ImportOrExportDeclaration: (astPath) => {
const node = astPath.node
if (!node.source) {
return
}
const importPath = node.source.value
if(importPath.startsWith('.')) {
const certainPath = resolvePath(importPath, currentFilePath)
if(!certainPath) {
logger.warn(`File not found: ${importPath} used in ${currentFilePath}`)
result.error.push(importPath)
}
if(certainPath !== importPath) {
node.source.value = certainPath
fileChangedMark = true
result.success.push({before: importPath, after: certainPath})
}
}
}
})
if (fileChangedMark) {
const { code } = generate(ast, {
jsescOption: {
quotes: 'single'
}
})
result.code = code
}
return result
}

View File

@ -0,0 +1,5 @@
export function getBaseUrlFromCli(fallback = '') {
// 理论上要从resolvedConfig阶段的钩子里面拿到base由于插件嵌套插件子插件的配置项需要在resolveConfig前传入这里无法等resolvedConfig故手动获取命令行base
const index = process.argv?.indexOf('--base')
return index > -1 ? process.argv[index + 1] || fallback : fallback
}

View File

@ -0,0 +1,15 @@
export function configServerAddProxy(path, target) {
return [
{
name: 'vite-plugin-config-server-add-proxy',
configureServer(server) {
server.middlewares.use((req, _res, next) => {
if (req.url.includes(path)) {
req.url = req.url.replace(path, target)
}
next()
})
}
}
]
}

View File

@ -0,0 +1,47 @@
import fs from 'node:fs'
import path from 'node:path'
import shelljs from 'shelljs'
export function installPackageTemporary(packageNeedToInstall, tempDir, logger = console) {
return [
{
name: 'vite-plugin-install-package-temporary',
buildStart() {
if (packageNeedToInstall.every((pkg) => pkg.exist)) {
logger.info(`[vite-plugin-install-package-temporary]: bundle dependencies packages exist, skip install `)
return
}
let code = shelljs.mkdir('-p', tempDir).code
if (code === 0) {
//code 为 0 表示成功
fs.writeFileSync(
path.resolve(tempDir, 'package.json'),
JSON.stringify(
{
name: 'bundle-deps', // tempDir to 烤串
dependencies: Object.fromEntries(packageNeedToInstall.map((cur) => [cur.packageName, cur.version]))
},
null,
2
),
{ encoding: 'utf-8' }
)
}
code =
code ||
shelljs.cd(tempDir).code ||
shelljs.exec(`npm install --force`).code ||
shelljs.cd(path.relative(tempDir, '.')).code
if (code === 0) {
logger.info(
`[vite-plugin-install-package-temporary]: bundle dependencies package install success, total ${packageNeedToInstall.length} package(s)`
)
} else {
logger.warn(`[vite-plugin-install-package-temporary]: bundle dependencies package install failed`)
}
}
}
]
}

View File

@ -22,7 +22,7 @@ import { genSFCWithDefaultPlugin, parseRequiredBlocks } from '@opentiny/tiny-eng
import importMap from './importMap' import importMap from './importMap'
import srcFiles from './srcFiles' import srcFiles from './srcFiles'
import generateMetaFiles, { processAppJsCode } from './generate' import generateMetaFiles, { processAppJsCode } from './generate'
import { getSearchParams, fetchMetaData, fetchAppSchema, fetchBlockSchema } from './http' import { getSearchParams, fetchMetaData, fetchImportMap, fetchAppSchema, fetchBlockSchema } from './http'
import { PanelType, PreviewTips } from '../constant' import { PanelType, PreviewTips } from '../constant'
import { injectDebugSwitch } from './debugSwitch' import { injectDebugSwitch } from './debugSwitch'
import '@vue/repl/style.css' import '@vue/repl/style.css'
@ -51,8 +51,6 @@ export default {
} }
} }
store.setImportMap(importMap)
// store.setFilesstate.activeFile = state.files[filename]activeFile // store.setFilesstate.activeFile = state.files[filename]activeFile
const setFiles = async (newFiles, mainFileName) => { const setFiles = async (newFiles, mainFileName) => {
await store.setFiles(newFiles, mainFileName) await store.setFiles(newFiles, mainFileName)
@ -61,7 +59,7 @@ export default {
store['initTsConfig']() // d.ts便 store['initTsConfig']() // d.ts便
} }
const addUtilsImportMap = (utils = []) => { const addUtilsImportMap = (importMap, utils = []) => {
const utilsImportMaps = {} const utilsImportMaps = {}
utils.forEach(({ type, content: { package: packageName, cdnLink } }) => { utils.forEach(({ type, content: { package: packageName, cdnLink } }) => {
if (type === 'npm' && cdnLink) { if (type === 'npm' && cdnLink) {
@ -100,14 +98,27 @@ export default {
} }
const queryParams = getSearchParams() const queryParams = getSearchParams()
const getImportMap = async () => {
if (import.meta.env.VITE_LOCAL_BUNDLE_DEPS === 'true') {
const mapJSON = await fetchImportMap()
return {
imports: {
...mapJSON.imports,
...getSearchParams().scripts
}
}
}
return importMap
}
const promiseList = [ const promiseList = [
fetchAppSchema(queryParams?.app), fetchAppSchema(queryParams?.app),
fetchMetaData(queryParams), fetchMetaData(queryParams),
setFiles(srcFiles, 'src/Main.vue') setFiles(srcFiles, 'src/Main.vue'),
getImportMap()
] ]
Promise.all(promiseList).then(async ([appData, metaData]) => { Promise.all(promiseList).then(async ([appData, metaData, _void, importMapData]) => {
addUtilsImportMap(metaData.utils || []) addUtilsImportMap(importMapData, metaData.utils || [])
const blocks = await getBlocksSchema(queryParams.pageInfo?.schema) const blocks = await getBlocksSchema(queryParams.pageInfo?.schema)

View File

@ -51,5 +51,10 @@ export const fetchMetaData = async ({ platform, app, type, id, history, tenant }
}) })
: {} : {}
export const fetchImportMap = async () => {
const baseUrl = new URL(import.meta.env.BASE_URL, location.href)
return fetch(new URL('./preview-import-map-static/preview-importmap.json', baseUrl).href).then((res) => res.json())
}
export const fetchAppSchema = async (id) => http.get(`/app-center/v1/api/apps/schema/${id}`) export const fetchAppSchema = async (id) => http.get(`/app-center/v1/api/apps/schema/${id}`)
export const fetchBlockSchema = async (blockName) => http.get(`/material-center/api/block?label=${blockName}`) export const fetchBlockSchema = async (blockName) => http.get(`/material-center/api/block?label=${blockName}`)

View File

@ -13,37 +13,19 @@
// import { hyphenate } from '@vue/shared' // import { hyphenate } from '@vue/shared'
import { getSearchParams } from './http' import { getSearchParams } from './http'
import importMapJSON from './importMap.json'
import { VITE_CDN_DOMAIN } from '@opentiny/tiny-engine-controller/js/environments' import { VITE_CDN_DOMAIN } from '@opentiny/tiny-engine-controller/js/environments'
const importMap = {} const importMap = {}
const opentinyVueVersion = '~3.14' const opentinyVueVersion = '~3.14'
const tinyVue3Imports = { function replacePlaceholder(v) {
// 推荐之后统一使用@opentiny/vue去引入依赖兼容后续录入的组件来源于tiny-vue return v.replace('${VITE_CDN_DOMAIN}', VITE_CDN_DOMAIN).replace('${opentinyVueVersion}', opentinyVueVersion)
'@opentiny/vue': `${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue.mjs`,
'@opentiny/vue-icon': `${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue-icon.mjs`,
'@opentiny/vue-common': `${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue-common.mjs`,
'@opentiny/vue-locale': `${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue-locale.mjs`,
'@opentiny/vue-renderless/': `${VITE_CDN_DOMAIN}/@opentiny/vue-renderless@${opentinyVueVersion}/`
} }
importMap.imports = { importMap.imports = {
vue: `${VITE_CDN_DOMAIN}/vue@3.2.36/dist/vue.runtime.esm-browser.js`, ...Object.fromEntries(Object.entries(importMapJSON.imports).map(([k, v]) => [k, replacePlaceholder(v)])),
'vue/server-renderer': `${VITE_CDN_DOMAIN}/@vue/server-renderer@3.2.36/dist/server-renderer.esm-browser.js`,
'vue-i18n': `${VITE_CDN_DOMAIN}/vue-i18n@9.2.0-beta.36/dist/vue-i18n.esm-browser.js`,
'vue-router': `${VITE_CDN_DOMAIN}/vue-router@4.0.16/dist/vue-router.esm-browser.js`,
'@vue/devtools-api': `${VITE_CDN_DOMAIN}/@vue/devtools-api@6.5.1/lib/esm/index.js`,
'@vueuse/core': `${VITE_CDN_DOMAIN}/@vueuse/core@9.6.0/index.mjs`,
'@vueuse/shared': `${VITE_CDN_DOMAIN}/@vueuse/shared@9.6.0/index.mjs`,
axios: `${VITE_CDN_DOMAIN}/axios@1.0.0-alpha.1/dist/esm/axios.js`,
'axios-mock-adapter': `${VITE_CDN_DOMAIN}/axios-mock-adapter@1.21.1/dist/axios-mock-adapter.js`,
'@opentiny/tiny-engine-webcomponent-core': `${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-webcomponent-core@1/dist/tiny-engine-webcomponent-core.es.js`,
'@opentiny/tiny-engine-i18n-host': `${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-i18n-host@1/dist/tiny-engine-i18n-host.es.js`,
'@opentiny/tiny-engine-builtin-component': `${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-builtin-component@1/dist/index.js`,
'vue-demi': `${VITE_CDN_DOMAIN}/vue-demi@0.13.11/lib/index.mjs`,
pinia: `${VITE_CDN_DOMAIN}/pinia@2.0.22/dist/pinia.esm-browser.js`,
...tinyVue3Imports,
...getSearchParams().scripts ...getSearchParams().scripts
} }

View File

@ -0,0 +1,23 @@
{
"imports": {
"vue": "${VITE_CDN_DOMAIN}/vue@3.4.23/dist/vue.runtime.esm-browser.js",
"vue/server-renderer": "${VITE_CDN_DOMAIN}/@vue/server-renderer@3.4.23/dist/server-renderer.esm-browser.js",
"vue-i18n": "${VITE_CDN_DOMAIN}/vue-i18n@9.2.0-beta.36/dist/vue-i18n.esm-browser.js",
"vue-router": "${VITE_CDN_DOMAIN}/vue-router@4.0.16/dist/vue-router.esm-browser.js",
"@vue/devtools-api": "${VITE_CDN_DOMAIN}/@vue/devtools-api@6.5.1/lib/esm/index.js",
"@vueuse/core": "${VITE_CDN_DOMAIN}/@vueuse/core@9.6.0/index.mjs",
"@vueuse/shared": "${VITE_CDN_DOMAIN}/@vueuse/shared@9.6.0/index.mjs",
"axios": "${VITE_CDN_DOMAIN}/axios@1.0.0/dist/esm/axios.js",
"axios-mock-adapter": "${VITE_CDN_DOMAIN}/axios-mock-adapter@1.21.1/dist/axios-mock-adapter.js",
"@opentiny/tiny-engine-webcomponent-core": "${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-webcomponent-core@1/dist/tiny-engine-webcomponent-core.es.js",
"@opentiny/tiny-engine-i18n-host": "${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-i18n-host@1/dist/lowcode-design-i18n-host.es.js",
"@opentiny/tiny-engine-builtin-component": "${VITE_CDN_DOMAIN}/@opentiny/tiny-engine-builtin-component@1/dist/index.js",
"vue-demi": "${VITE_CDN_DOMAIN}/vue-demi@0.13.11/lib/index.mjs",
"pinia": "${VITE_CDN_DOMAIN}/pinia@2.0.22/dist/pinia.esm-browser.js",
"@opentiny/vue": "${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue.mjs",
"@opentiny/vue-icon": "${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue-icon.mjs",
"@opentiny/vue-common": "${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue-common.mjs",
"@opentiny/vue-locale": "${VITE_CDN_DOMAIN}/@opentiny/vue@${opentinyVueVersion}/runtime/tiny-vue-locale.mjs",
"@opentiny/vue-renderless/": "${VITE_CDN_DOMAIN}/@opentiny/vue-renderless@${opentinyVueVersion}/"
}
}

View File

@ -12,6 +12,7 @@ import lowcodeConfig from './config/lowcode.config'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { importmapPlugin } from './scripts/externalDeps' import { importmapPlugin } from './scripts/externalDeps'
import visualizer from 'rollup-plugin-visualizer' import visualizer from 'rollup-plugin-visualizer'
import { getBaseUrlFromCli, copyBundleDeps, copyPreviewImportMap, copyLocalImportMap } from './scripts/localCdnFile'
const origin = 'http://localhost:9090/' const origin = 'http://localhost:9090/'
@ -197,14 +198,16 @@ const commonAlias = {
} }
export default defineConfig(({ command, mode }) => { export default defineConfig(({ command, mode }) => {
const { VITE_CDN_DOMAIN } = loadEnv(mode, process.cwd(), '') const { VITE_CDN_DOMAIN, VITE_LOCAL_IMPORT_MAPS, VITE_LOCAL_BUNDLE_DEPS } = loadEnv(mode, process.cwd(), '')
const monacoPublicPath = { const isLocalImportMap = VITE_LOCAL_IMPORT_MAPS === 'true' // true公共依赖库使用本地打包文件false公共依赖库使用公共CDN
local: 'editor/monaco-workers', const isCopyBundleDeps = VITE_LOCAL_BUNDLE_DEPS === 'true' // true bundle里的cdn依赖处理成本地依赖 false 不处理
alpha: 'https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/monaco-assets',
prod: 'https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/monaco-assets'
}
let monacoEditorPluginInstance = monacoEditorPlugin({ publicPath: monacoPublicPath.local }) const monacoPublicPath = 'editor/monaco-workers'
const monacoEditorPluginInstance = monacoEditorPlugin({
publicPath: monacoPublicPath,
forceBuildCDN: true,
customDistPath: (_root, outDir, _base) => path.join(outDir, monacoPublicPath)
})
const htmlPlugin = (mode) => { const htmlPlugin = (mode) => {
const upgradeHttpsMetaTags = [] const upgradeHttpsMetaTags = []
const includeHtmls = ['index.html', 'preview.html', 'previewApp.html'] const includeHtmls = ['index.html', 'preview.html', 'previewApp.html']
@ -241,7 +244,7 @@ export default defineConfig(({ command, mode }) => {
} }
config.resolve.alias = [ config.resolve.alias = [
devVueAlias, ...(isLocalImportMap ? [] : [devVueAlias]),
...Object.entries({ ...commonAlias, ...devAlias }).map(([find, replacement]) => ({ ...Object.entries({ ...commonAlias, ...devAlias }).map(([find, replacement]) => ({
find, find,
replacement replacement
@ -251,8 +254,6 @@ export default defineConfig(({ command, mode }) => {
// command === 'build' // command === 'build'
config.resolve.alias = { ...commonAlias, ...prodAlias } config.resolve.alias = { ...commonAlias, ...prodAlias }
monacoEditorPluginInstance = monacoEditorPlugin({ publicPath: monacoPublicPath[mode] })
if (mode === 'prod') { if (mode === 'prod') {
config.build.minify = true config.build.minify = true
config.build.sourcemap = false config.build.sourcemap = false
@ -276,14 +277,49 @@ export default defineConfig(({ command, mode }) => {
'@opentiny/vue-common': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-common.mjs`, '@opentiny/vue-common': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-common.mjs`,
'@opentiny/vue-locale': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-locale.mjs`, '@opentiny/vue-locale': `${VITE_CDN_DOMAIN}/@opentiny/vue@${importMapVersions.tinyVue}/runtime/tiny-vue-locale.mjs`,
'@opentiny/vue-design-smb': `${VITE_CDN_DOMAIN}/@opentiny/vue-design-smb@${importMapVersions.tinyVue}/index.js`, '@opentiny/vue-design-smb': `${VITE_CDN_DOMAIN}/@opentiny/vue-design-smb@${importMapVersions.tinyVue}/index.js`,
'@opentiny/vue-theme/theme-tool': `${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/theme-tool`, '@opentiny/vue-theme/theme-tool': `${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/theme-tool.js`,
'@opentiny/vue-theme/theme': `${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/theme` '@opentiny/vue-theme/theme': `${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/theme/index.js`
} }
} }
const importMapStyles = [`${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/index.css`] const importMapStyles = [`${VITE_CDN_DOMAIN}/@opentiny/vue-theme@${importMapVersions.tinyVue}/index.css`]
config.plugins.push(monacoEditorPluginInstance, htmlPlugin(mode), importmapPlugin(importmap, importMapStyles)) config.plugins.push(
monacoEditorPluginInstance,
htmlPlugin(mode),
isLocalImportMap
? copyLocalImportMap({
importMap: importmap,
styleUrls: importMapStyles,
originCdnPrefix: VITE_CDN_DOMAIN,
base: getBaseUrlFromCli(config.base),
packageCopy: [
// 这两个包的js存在相对路径引用不能单独拷贝一个文件需要整个包拷贝
'@opentiny/vue-theme/theme-tool',
'@opentiny/vue-theme/theme'
]
})
: importmapPlugin(importmap, importMapStyles),
isCopyBundleDeps
? copyBundleDeps({
bundleFile: 'public/mock/bundle.json',
targetBundleFile: 'mock/bundle.json',
originCdnPrefix: VITE_CDN_DOMAIN, // mock 中bundle的域名当前和环境的VITE_CDN_DOMAIN一致
base: getBaseUrlFromCli(config.base)
}).plugin(command === 'serve')
: [],
isLocalImportMap
? copyPreviewImportMap({
importMapJson: './src/preview/src/preview/importMap.json',
targetImportMapJson: 'preview-import-map-static/preview-importmap.json',
originCdnPrefix: VITE_CDN_DOMAIN,
base: getBaseUrlFromCli(config.base),
packageCopyLib: [
// 以下的js存在相对路径引用不能单独拷贝一个文件需要整个包拷贝
'@vue/devtools-api'
]
})
: []
)
return config return config
}) })