diff --git a/examples/solid-demo/index.html b/examples/solid-demo/index.html new file mode 100644 index 000000000..b9a46f405 --- /dev/null +++ b/examples/solid-demo/index.html @@ -0,0 +1,13 @@ + + + + + + + Opentiny Solid 组件调试 + + +
+ + + diff --git a/examples/solid-demo/package.json b/examples/solid-demo/package.json new file mode 100644 index 000000000..8d2b715f3 --- /dev/null +++ b/examples/solid-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "@opentiny/docs-solid", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "solid-js": "^1.7.8", + "@opensolidtiny/solid": "~3.14.0", + "@opensolidtiny/solid-icon": "~3.14.0", + "@opensolidtiny/solid-common": "~3.14.0", + "@opentiny/vue-theme": "~3.14.0", + "@opentiny/vue-renderless": "~3.14.0" + }, + "devDependencies": { + "vite": "^4.4.5", + "vite-plugin-solid": "^2.7.0", + "vite-plugin-inspect": "^0.7.10" + } +} \ No newline at end of file diff --git a/examples/solid-demo/public/vite.svg b/examples/solid-demo/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/examples/solid-demo/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/solid-demo/src/App.tsx b/examples/solid-demo/src/App.tsx new file mode 100644 index 000000000..df71f4c96 --- /dev/null +++ b/examples/solid-demo/src/App.tsx @@ -0,0 +1,35 @@ +import { createMutable } from 'solid-js/store' +import { Button, Alert } from '@opensolidtiny/solid' +import { IconActivation } from '@opensolidtiny/solid-icon' + +// 在这里导入组件,进行 api 调试 +function App() { + const state = createMutable({ + value: 1 + }) + const hanleClick = () => { + state.value++ + } + + const alertSlots = { + title: 插槽标题 + } + + return ( +
+

{state.value}

+ + + + + + + +
+
+ +
+ ) +} + +export default App diff --git a/examples/solid-demo/src/main.css b/examples/solid-demo/src/main.css new file mode 100644 index 000000000..50154555f --- /dev/null +++ b/examples/solid-demo/src/main.css @@ -0,0 +1,4 @@ +.app { + margin: 10px; + width: 500px; +} diff --git a/examples/solid-demo/src/main.tsx b/examples/solid-demo/src/main.tsx new file mode 100644 index 000000000..2cda43153 --- /dev/null +++ b/examples/solid-demo/src/main.tsx @@ -0,0 +1,7 @@ +import { render } from 'solid-js/web' +import App from './App' +import './main.css' + +const root = document.getElementById('root') + +render(() => , root) diff --git a/examples/solid-demo/tsconfig.json b/examples/solid-demo/tsconfig.json new file mode 100644 index 000000000..a7fc6fbf2 --- /dev/null +++ b/examples/solid-demo/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/solid-demo/tsconfig.node.json b/examples/solid-demo/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/examples/solid-demo/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/solid-demo/vite.config.ts b/examples/solid-demo/vite.config.ts new file mode 100644 index 000000000..1d848af38 --- /dev/null +++ b/examples/solid-demo/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import solid from 'vite-plugin-solid' +import inspectPlugin from 'vite-plugin-inspect' + +export default defineConfig({ + plugins: [inspectPlugin(), solid()], + define: { + 'process.env': {} + } +}) diff --git a/examples/solid-docs/package.json b/examples/solid-docs/package.json index a55ab94ec..eda22834d 100644 --- a/examples/solid-docs/package.json +++ b/examples/solid-docs/package.json @@ -10,11 +10,20 @@ }, "dependencies": { "solid-js": "^1.7.8", - "@opentiny/solid": "workspace:~" + "@opentiny/solid": "workspace:~", + "@opentiny/solid-common": "workspace:~", + "@opentiny/vue-button": "workspace:~", + "@opentiny/vue-alert": "workspace:~", + "@opentiny/vue-badge": "workspace:~", + "@opentiny/vue-icon": "workspace:~", + "@opentiny/vue-switch": "workspace:~", + "@opentiny/vue-theme": "workspace:~" }, "devDependencies": { + "fs-extra": "^11.2.0", "vite": "^4.4.5", "vite-plugin-solid": "^2.7.0", - "vite-plugin-svgr": "^3.2.0" + "vite-plugin-inspect": "^0.7.10", + "@opentiny/vue-vite-template2jsx": "workspace:~" } -} +} \ No newline at end of file diff --git a/examples/solid-docs/src/App.tsx b/examples/solid-docs/src/App.tsx index 967bda74b..5e76518a3 100644 --- a/examples/solid-docs/src/App.tsx +++ b/examples/solid-docs/src/App.tsx @@ -1,10 +1,54 @@ -import { Button } from '@opentiny/solid' +import { createMutable } from 'solid-js/store' +import Button from './views/button' +import Alert from './views/alert' +import Icon from './views/icon' +import Switch from './views/switch' +import { Dynamic } from 'solid-js/web' +import './main.less' // 在这里导入组件,进行 api 调试 function App() { + const allViews = { + Button, + Alert, + Icon, + Switch + } + const state = createMutable({ + value: null, + active: '', + menus: ['Button', 'Alert', 'Icon', 'Switch'] + }) + + const getViewComponent = (name: string) => { + return allViews[name] || Button + } + + const hashchange = () => { + state.active = window.location.hash.replace('#/', '') || 'Button' + state.value = getViewComponent(state.active) + } + + window.addEventListener('hashchange', hashchange) + + hashchange() + return ( -
- +
+

+ + {(item) => ( + <> + + {item} + + / + + )} + +

+
+
) } diff --git a/examples/solid-docs/src/main.less b/examples/solid-docs/src/main.less new file mode 100644 index 000000000..3ce3f8120 --- /dev/null +++ b/examples/solid-docs/src/main.less @@ -0,0 +1,17 @@ +.app { + margin: 40px auto; + width: 640px; + + a.title { + color: #39f; + cursor: pointer; + display: inline-block; + margin:0 10px; + text-decoration: none; + + &.active { + font-weight: 600; + text-decoration: underline; + } + } +} diff --git a/examples/solid-docs/src/main.tsx b/examples/solid-docs/src/main.tsx index 2cda43153..0b2a3123a 100644 --- a/examples/solid-docs/src/main.tsx +++ b/examples/solid-docs/src/main.tsx @@ -1,6 +1,5 @@ import { render } from 'solid-js/web' import App from './App' -import './main.css' const root = document.getElementById('root') diff --git a/examples/solid-docs/src/views/alert.tsx b/examples/solid-docs/src/views/alert.tsx new file mode 100644 index 000000000..9affe1fef --- /dev/null +++ b/examples/solid-docs/src/views/alert.tsx @@ -0,0 +1,61 @@ +import TinyAlert from '@opentiny/vue-alert' + +export default function () { + const alertSlots = { + title: 插槽标题 + } + + const alertSlots2 = { + description: ( + +

插槽描述

+
+ ) + } + + const alertSlots3 = { + close: 插槽关闭 + } + + return ( +
+ + + 自定义内容 + +
+ + 确定 + 取消 + +
+ + 继续提交 + 取消操作 + +
+ +
+ + 继续提交 + 取消操作 + +
+ +
+ +
+ +
+ ) +} diff --git a/examples/solid-docs/src/views/button.tsx b/examples/solid-docs/src/views/button.tsx new file mode 100644 index 000000000..a01ab44e5 --- /dev/null +++ b/examples/solid-docs/src/views/button.tsx @@ -0,0 +1,60 @@ +import { createMutable } from 'solid-js/store' +import TinyButton from '@opentiny/vue-button' +import TinyIconSearch from '@opentiny/vue-icon/search/index.ts' + +export default function () { + const state = createMutable({ + value: 1 + }) + const hanleClick = () => { + state.value++ + } + + return ( +
+ 默认按钮 + 主要按钮 + 成功按钮 +
+
+ 信息按钮 + 警告按钮 + 危险按钮 +
+
+ 朴素按钮 + + + 加载中 + +
+
+ + + 纯文本按钮 +
+
+ + {' '} + 超大按钮{' '} + + + {' '} + 中等按钮{' '} + + + {' '} + 小型按钮{' '} + + + {' '} + 超小按钮{' '} + +
+
+ 点击事件 点击计数:{state.value} +
+
+
+ ) +} diff --git a/examples/solid-docs/src/views/icon.less b/examples/solid-docs/src/views/icon.less new file mode 100644 index 000000000..89f631e06 --- /dev/null +++ b/examples/solid-docs/src/views/icon.less @@ -0,0 +1,6 @@ +.icon-demo .tiny-svg { + fill: #8994aa; + margin: 20px 50px; + vertical-align: middle; + font-size: 48px; +} \ No newline at end of file diff --git a/examples/solid-docs/src/views/icon.tsx b/examples/solid-docs/src/views/icon.tsx new file mode 100644 index 000000000..d6d61677f --- /dev/null +++ b/examples/solid-docs/src/views/icon.tsx @@ -0,0 +1,20 @@ +import TinyIconActivation from '@opentiny/vue-icon/activation/index.ts' +import TinyIconShare from '@opentiny/vue-icon/share/index.ts' +import TinyIconDel from '@opentiny/vue-icon/del/index.ts' +import TinyIconWriting from '@opentiny/vue-icon/writing/index.ts' +import TinyIconAscending from '@opentiny/vue-icon/ascending/index.ts' +import TinyIconClockWork from '@opentiny/vue-icon/clock-work/index.ts' +import './icon.less' + +export default function () { + return ( +
+ + + + + + +
+ ) +} diff --git a/examples/solid-docs/src/views/switch.tsx b/examples/solid-docs/src/views/switch.tsx new file mode 100644 index 000000000..d20d5411b --- /dev/null +++ b/examples/solid-docs/src/views/switch.tsx @@ -0,0 +1,25 @@ +import TinySwitch from '@opentiny/vue-switch' +import { createMutable } from 'solid-js/store' + +export default function () { + const state = createMutable({ + value: true + }) + + const switchSlots = { + open: 打开, + close: 关闭 + } + + return ( +
+ +
+
+ +
+
+ +
+ ) +} diff --git a/examples/solid-docs/vite.config.ts b/examples/solid-docs/vite.config.ts index 163b240eb..74c335d03 100644 --- a/examples/solid-docs/vite.config.ts +++ b/examples/solid-docs/vite.config.ts @@ -1,7 +1,18 @@ import { defineConfig } from 'vite' import solid from 'vite-plugin-solid' -import svgr from 'vite-plugin-svgr' +import path from 'node:path' +import inspectPlugin from 'vite-plugin-inspect' +import vueTemplate2jsx from '@opentiny/vue-vite-template2jsx' export default defineConfig({ - plugins: [solid(), svgr()] + plugins: [inspectPlugin(), vueTemplate2jsx(), solid({ extensions: ['.js', '.ts', '.tsx', '.jsx', '.vue'] })], + define: { + 'process.env': {} + }, + resolve: { + alias: { + '@': path.resolve('./src'), + '@opentiny/solid-common': path.resolve('../../packages/solid/src/common/src/index.ts') + } + } }) diff --git a/internals/cli/src/commands/build/build-ui-solid.ts b/internals/cli/src/commands/build/build-ui-solid.ts new file mode 100644 index 000000000..c727fd49f --- /dev/null +++ b/internals/cli/src/commands/build/build-ui-solid.ts @@ -0,0 +1,278 @@ +import { getBabelOutputPlugin } from '@rollup/plugin-babel' +import { createRequire } from 'node:module' +import path from 'node:path' +import { build, defineConfig } from 'vite' +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 { logGreen, kebabCase, capitalizeKebabCase, getPatchVersion, isValidVersion } from '../../shared/utils' +import generatePackageJsonPlugin from './rollup/generate-package-json' +import inlineChunksPlugin from './rollup/inline-chunks' +import replaceModuleNamePlugin from './rollup/replace-module-name' +import solid from 'vite-plugin-solid' +import vueTemplate2jsx from '@opentiny/vue-vite-template2jsx' + +export const pathFromPackages = (...args) => pathFromWorkspaceRoot('packages', ...args) +export const require = createRequire(import.meta.url) +export const requireModules = (id: string) => require(require.resolve(pathFromWorkspaceRoot(id))) + +// 需要打包的solid组件 +const buildComponents = ['alert', 'button'] + +export interface BaseConfig { + buildTarget: string + npmScope?: string +} + +export const getBaseConfig = ({ buildTarget }: BaseConfig) => { + const versionTarget = isValidVersion(buildTarget) ? buildTarget : `3.${buildTarget}` + const themeAndRenderlessVersion = isValidVersion(buildTarget) ? buildTarget : `3.${buildTarget}` + const isThemeOrRenderless = (key) => key.includes('@opentiny/vue-theme') || key.includes('@opentiny/vue-renderless') + + return defineConfig({ + publicDir: false, + plugins: [ + inlineChunksPlugin({ deleteInlinedFiles: true }), + generatePackageJsonPlugin({ + beforeWriteFile: (filePath, content) => { + const dependencies = {} + + Object.entries(content.dependencies).forEach(([key, value]) => { + // dependencies里的@opentiny,统一使用:~x.x.0 + if (isThemeOrRenderless(key)) { + dependencies[key] = getPatchVersion(themeAndRenderlessVersion) + } else if ((value as string).includes('workspace:~')) { + dependencies[ + key.replace('@opentiny/vue', '@opensolidtiny/solid').replace('@opentiny/solid', '@opensolidtiny/solid') + ] = getPatchVersion(versionTarget) + } else { + dependencies[key] = value + } + }) + + const matchList = ['solid-icon', 'vue-icon-saas', 'solid'] + + // 如果是主入口、svg图标或者主题规范包则直接指向相同路径 + if (matchList.includes(filePath)) { + content.main = './index.js' + content.module = './index.js' + } else { + content.main = './lib/index.js' + content.module = './lib/index.js' + } + + content.name = content.name + .replace('@opentiny/vue', '@opensolidtiny/solid') + .replace('@opentiny/solid', '@opensolidtiny/solid') + content.version = versionTarget + content.dependencies = dependencies + + delete content.devDependencies + delete content.private + delete content.exports + + return { + filePath: filePath.replace(/[\\/]lib$/, ''), + content + } + } + }), + replaceModuleNamePlugin(versionTarget), + vueTemplate2jsx(), + solid({ extensions: ['.js', '.ts', '.tsx', '.jsx', '.vue'] }) + ], + resolve: { + extensions: ['.js', '.ts', '.tsx', '.vue'], + alias: { + ...getAlias(3, '', '') + } + }, + define: { + 'process.env.BUILD_TARGET': JSON.stringify('component') + } + }) +} + +async function batchBuildAll({ tasks, formats, message, emptyOutDir, buildTarget, npmScope }) { + const rootDir = pathFromPackages('') + const outDir = path.resolve(rootDir, `dist-solid/${npmScope}`) + await batchBuild({ + tasks, + formats, + message, + emptyOutDir + }) + + function toEntry(libs) { + return libs.reduce((result, { libPath, path: file }) => { + const tLibPath = libPath.replace('-lib/', '/lib/') + result[tLibPath] = pathFromPackages(file) + return result + }, {}) + } + + async function batchBuild({ tasks, formats, message, emptyOutDir }) { + if (tasks.length === 0) return + logGreen(`====== 开始构建 ${message} ======`) + const entry = toEntry(tasks) + + await build({ + configFile: false, + ...getBaseConfig({ buildTarget }), + build: { + emptyOutDir, + minify: false, + rollupOptions: { + plugins: [ + getBabelOutputPlugin({ + presets: [['@babel/preset-env', { loose: true, modules: false }]] + }) as any + ], + external: (source, importer, isResolved) => { + // vite打包入口文件或者没有解析过得包不能排除依赖 + if (isResolved || !importer) { + return false + } + + if (/vue-icon(-saas)?\/index/.test(importer)) { + // 图标入口排除子图标 + return /^\.\//.test(source) + } + + // 子图标排除周边引用, 这里注意不要排除svg图标 + if (/vue-icon(-saas)?\/.+\/index/.test(importer)) { + return !/\.svg/.test(source) + } + + if (/src\/index/.test(importer)) { + // 模块入口,pc/mobile 文件要分离,同时排除 node_modules 依赖 + return /^\.\/(pc|mobile|mobile-first)/.test(source) || external(source) + } + + // @opentiny/vue 总入口,需要排除所有依赖 + 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 { + buildTarget: string // 目标版本,必填, 不需要major位,因为需要同时打出vue2和vue3的包 + formats: string[] // 打包的格式 + clean: boolean // 是否清空build产物 + scope?: string // npm的组织名称 + min?: boolean // 是否压缩产物 +} + +function getEntryTasks(): Module[] { + // 读取TinyVue组件库入口文件 + return [ + { + path: `solid/index.ts`, + libPath: `solid/index`, + type: 'module', + name: kebabCase({ str: '@opensolidtiny/solid' }), + global: capitalizeKebabCase('opentinySolid'), + importName: '@opensolidtiny/solid' + } + ] +} + +function getSolidCommonTasks(): Module[] { + // 读取TinyVue组件库入口文件 + return [ + { + path: `solid/src/common/src/index.ts`, + libPath: `common/lib/index`, + type: 'module', + name: kebabCase({ str: '@opensolidtiny/solid-common' }), + global: capitalizeKebabCase('opentinySolidCommon'), + importName: '@opensolidtiny/solid-common' + } + ] +} + +function getTasks(names: string[]): Module[] { + // 没有指定组件,则全量构建 + if (names.length === 0) { + return [...getAllModules(false)] + } + + 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 buildSolid( + names: string[] = [], + { buildTarget = '3.14.0', formats = ['es'], clean = false, scope = '@opensolidtiny' }: BuildUiOption +) { + // 是否清空构建目录 + let emptyOutDir = clean + // 要构建的模块 + let tasks = getTasks(names).filter((item) => !item.path.includes('mobile')) + + // 如果指定了打包icon或者没有传入任何组件 + if (names.some((name) => name.includes('icon')) || !names.length) { + const icons = getAllIcons() + icons.forEach((item) => { + item.libPath = item.libPath.replace('vue-icon', 'solid-icon') + }) + + tasks.push(...icons) + } + + // 过虑出需要打包的solid组件入口 + tasks = tasks + .filter((item) => + buildComponents.some((value) => item.path.includes(`/${value}/`) || item.path.includes('vue-icon')) + ) + .filter((item) => !item.path.includes('icon-saas')) + + tasks.forEach((item) => { + if (item.libPath.includes('vue-icon')) { + item.libPath = item.libPath.replace('vue-icon', 'solid-icon') + } + }) + + // 打包入口文件 + if (names.length === 0 || names.some((name) => ['@opensolidtiny/solid', 'solid'].includes(name))) { + tasks.push(...getEntryTasks(), ...getSolidCommonTasks()) + } + + // 要构建的vue框架版本 + const message = `TINY for solid: ${JSON.stringify(names.length ? names : '全量')}` + await batchBuildAll({ tasks, formats, message, emptyOutDir, buildTarget, npmScope: scope }) + // 确保只运行一次 + emptyOutDir = false +} diff --git a/internals/cli/src/commands/build/index.ts b/internals/cli/src/commands/build/index.ts index f06f18494..f6b269854 100644 --- a/internals/cli/src/commands/build/index.ts +++ b/internals/cli/src/commands/build/index.ts @@ -4,3 +4,4 @@ export * from './build-runtime' export * from './build-ui-react' export * from './build-entry-react' export * from './build-chart-theme' +export * from './build-ui-solid' diff --git a/internals/vite-plugin-template2jsx/README.md b/internals/vite-plugin-template2jsx/README.md new file mode 100644 index 000000000..2a00fbc5f --- /dev/null +++ b/internals/vite-plugin-template2jsx/README.md @@ -0,0 +1,3 @@ +# vue-vite-template2jsx + +## 一个可以将vue模板转化成jsx语法的vite插件 diff --git a/internals/vite-plugin-template2jsx/index.js b/internals/vite-plugin-template2jsx/index.js new file mode 100644 index 000000000..dfe10ff93 --- /dev/null +++ b/internals/vite-plugin-template2jsx/index.js @@ -0,0 +1,143 @@ +import { createRequire } from 'node:module' +import { readFileSync, existsSync } from 'node:fs' +import { transformVueTemplateToSolid } from './src/plugin.js' +import { dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const require = createRequire(import.meta.url) + +const indexTemplatePath = path.resolve(__dirname, './template/src/index.tss') +const pcTemplatePath = path.resolve(__dirname, './template/src/pc.jsxs') + +// 多模板入口模板 +const indexTemplate = readFileSync(indexTemplatePath, { encoding: 'utf-8' }) + +// PC模板入口 +const pcTemplate = readFileSync(pcTemplatePath, { encoding: 'utf-8' }) + +const matchComponentRegx = /vue\/src\/([a-z]+)\/src/ + +const fundProps = (code, findStr) => { + let findIndex = code.indexOf(findStr) + let props = '' + + if (~findIndex) { + const endStr = 'export ' + let constantsPart = code.substring(findIndex + findStr.length, code.length) + props = constantsPart.substring(0, constantsPart.indexOf(endStr)) + } + + return props +} + +const getSvgContent = (svg) => { + return svg.substring(svg.indexOf('>') + 1, svg.length) +} + +const getDefaultValueFromEntry = (id) => { + const componentMatchs = id.match(matchComponentRegx) + const entryPath = id.replace('pc.vue', 'index.ts') + if (existsSync(entryPath)) { + if (componentMatchs?.[1]) { + const code = readFileSync(entryPath, { encoding: 'utf-8' }) + let constants = fundProps(code, '$constants =') || '{}' + let propsContent = fundProps(code, 'Props =') || fundProps(code, 'props :') + let props = {} + + if (propsContent) { + const propsContentParts = propsContent.split(/\n/) + propsContentParts.forEach((item, index) => { + if (item.indexOf('type:') && ['Boolean', 'String', 'Number'].some((type) => item.includes(type))) { + const name = propsContentParts[index - 1].replace(/\W/g, '') + let value = propsContentParts[index + 1] + + if (name && value.includes('default:')) { + const type = /['"]/.test(value) ? 'Char' : 'Other' + value = value.replace('default:', '').replace(/["|'|\s,]/g, '') + props[name] = { type, value } + } + } + }) + } + + return { + $constants: constants, + $props: props + } + } + } + + return { + $constants: '{}', + $props: {} + } +} + +export default function vueTemplate2Jsx() { + return { + name: '@opentiny/vue-vite-template2jsx', + enforce: 'pre', + transform(code, id) { + if (id.includes('vue-icon/src')) { + const svgName = code.match(/([a-z1-9-]+)\.svg/) + + if (svgName && svgName[0]) { + let svgPath + + try { + svgPath = require.resolve(`@opentiny/vue-theme/svgs/${svgName[0]}`) + } catch (e) { + svgPath = path.resolve(__dirname, `../../packages/theme/src/svgs/${svgName[0]}`) + } + + if (existsSync(svgPath)) { + let svgContent = readFileSync(svgPath, { encoding: 'utf-8' }) + + if (svgContent.includes('/, '') + } + + if (svgContent.includes('/, '') + } + + const viewBox = svgContent.match(/viewBox="[\d|\s]+"/)?.[0] || `viewBox="0 0 24 24"` + + return `export default function(props) { + return ( + ${getSvgContent(svgContent)} + ) + }` + } + } + + return code + } + + if (id.includes(`src/index.ts`) && !id.includes(`common/src/index.ts`)) { + return indexTemplate + } + + if (id.includes('pc.vue')) { + const componentMatchs = id.match(matchComponentRegx) + if (componentMatchs?.[1]) { + const mapData = getDefaultValueFromEntry(id) + const pcComponent = transformVueTemplateToSolid(pcTemplate, code, componentMatchs[1], mapData) + return pcComponent + } + } + } + } +} diff --git a/internals/vite-plugin-template2jsx/package.json b/internals/vite-plugin-template2jsx/package.json new file mode 100644 index 000000000..b51e78852 --- /dev/null +++ b/internals/vite-plugin-template2jsx/package.json @@ -0,0 +1,28 @@ +{ + "name": "@opentiny/vue-vite-template2jsx", + "version": "1.1.5", + "description": "A TinyVue vite import plugin", + "main": "index.js", + "module": "index.js", + "author": "Tiny Vue Team", + "license": "MIT", + "type": "module", + "repository": { + "type": "git", + "url": "git@github.com:opentiny/tiny-vue.git" + }, + "keywords": [ + "vite-plugin", + "TinyVue", + "vite" + ], + "dependencies": { + "lodash-es": "^4.17.21", + "fs-extra": "^11.2.0", + "vue-template-compiler": "2.6.14", + "html-dom-parser": "~5.0.8" + }, + "peerDependencies": { + "vite": ">=4" + } +} \ No newline at end of file diff --git a/internals/vite-plugin-template2jsx/src/helpers.js b/internals/vite-plugin-template2jsx/src/helpers.js new file mode 100644 index 000000000..4bda400ee --- /dev/null +++ b/internals/vite-plugin-template2jsx/src/helpers.js @@ -0,0 +1,222 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +export const builtinDirective = ['v-if', 'v-for', 'v-model', 'v-slot', 'v-else', 'v-else-if'] +export const builtinDirectivesWithoutVModel = builtinDirective.filter((x) => x !== 'v-model') + +export const isNamedSlot = function (dObj) { + return Object.keys(dObj.attribs).find(isSlotAttribute) +} + +export function getSlotName(dObj) { + if (!dObj.attribs) return 'default' + const attr = Object.keys(dObj.attribs).find(isSlotAttribute) + if (!isNamedSlot(dObj)) return 'default' + const prefix = attr.startsWith('#') ? '#' : 'v-slot:' + return attr.split(prefix)[1] +} + +export function getSlotProps(dObj) { + const attr = Object.keys(dObj.attribs).find(isSlotAttribute) + return dObj.attribs[attr] +} + +export const getCondition = function (dObj) { + return dObj.attribs['v-if'] || dObj.attribs['v-else-if'] +} + +export function isConditional(dObj) { + return isVIf(dObj) || isVElse(dObj) || isVElseIf(dObj) +} + +export function isVIf(dObj) { + return dObj.type !== 'text' && Object.prototype.hasOwnProperty.call(dObj.attribs, 'v-if') +} + +export function isVElseIf(dObj) { + return dObj.type !== 'text' && Object.prototype.hasOwnProperty.call(dObj.attribs, 'v-else-if') +} + +export function isVElse(dObj) { + return dObj.type !== 'text' && Object.prototype.hasOwnProperty.call(dObj.attribs, 'v-else') +} + +export function isVifOrVElseIf(dObj) { + return isVIf(dObj) || isVElseIf(dObj) +} + +export const isVFor = function (dObj) { + return dObj.attribs && Object.prototype.hasOwnProperty.call(dObj.attribs, 'v-for') +} + +export const isSlotAttribute = function (attr) { + return attr.startsWith('#') || attr.startsWith('v-slot') +} + +export function isValueAttribute(attr) { + return !['v-if', 'v-else', 'v-else-if', 'v-for', 'v-slot'].includes(attr) +} + +export function isTextBlank(txt) { + for (let i of txt) { + if (i !== ' ' && i !== '\n') { + return false + } + } + return true +} + +export function getTag(dObj) { + return dObj.name +} + +export function getFirstChild(dObj) { + return dObj.children.length && dObj.children[0] +} + +export function isEmptyObject(dObj) { + return dObj.type === 'text' && isTextBlank(dObj.data) +} + +export function isText(dObj) { + return dObj.type === 'text' +} + +export function isComponent(dObj) { + return dObj.name === 'component' +} + +export function isTransition(dObj) { + return ['transition-group', 'transition'].includes(dObj.name) +} + +export function getText(dObj) { + return dObj.data +} + +export function convertText(text) { + return text.replace(/{{/g, '{').replace(/}}/g, '}').replace(/\$t/g, 't') +} + +export function setDynamicComponent(dObj) { + let component = dObj.attribs[':is'] + dObj.attribs[':component'] = `resolveComponent(${component}, useIcons)` + delete dObj.attribs[':is'] + dObj.name = 'Dynamic' +} + +export function isBindClass(attrName) { + return attrName === ':class' +} + +export function isBindAll(attrName) { + return attrName === 'v-bind' +} + +export function getLoopCommand(dObj) { + const command = dObj.attribs['v-for'] + const separator = command.includes(' in ') ? ' in ' : ' of ' + const s = command.split(separator) + return `${s[1].trim()}.map(${s[0].trim()} ` +} + +export function isBindingAttrs(attrName) { + return attrName.startsWith(':') +} + +export const toCamelCase = function (s) { + let res = '' + for (let i = 0; i < s.length; i++) { + if (i > 0 && s[i - 1] === '-') { + res += s[i].toUpperCase() + } else if (s[i] !== '-') res += s[i] + } + return res +} + +export function getAttrName(attrName, prefix) { + return toCamelCase(attrName.split(prefix)[1]) +} + +export function isEventListener(attrName) { + return attrName.startsWith('@') +} + +export function getListenerName(attrName) { + const idx = attrName.indexOf('.') + if (idx === -1) return attrName.substr(1) + else return attrName.substr(1, idx - 1) +} + +export function getModifiers(attrName) { + let res = attrName.split('.') + res.shift() + return res +} + +export const convertEventListenerName = function (attrName) { + const name = getListenerName(attrName) + return toCamelCase(`on-${name}`).replace(/modelvalue/g, 'modelValue') +} + +export const convertListener = function (listener) { + const isFunction = listener.includes('=>') || (!listener.includes('(') && !listener.includes('=')) + return isFunction + ? listener + : listener.includes('$event') + ? `(v) => {${listener.replace(/\$event/g, 'v')}}` + : `() => ${listener}` +} + +export const convertEventListener = function (listener, modifiers = []) { + const eventName = convertListener(listener) + return modifiers.length === 0 + ? `${eventName}` + : `(event) => withModifiers(event, ${eventName}, [${modifiers.map((m) => `'${m}'`).join(', ')}])` +} + +export const isVModel = function (attrName) { + return attrName.startsWith('v-model') +} + +export const isBooleanAttrs = function (attrName) { + return attrName === '' +} + +export const isRef = function (attrName) { + return attrName === 'ref' +} + +export const isDynamic = function (attrName) { + return attrName === ':is' +} + +export const convertRefName = function (name) { + return toCamelCase(name) +} + +const filename = fileURLToPath(import.meta.url) +export const dirname = path.dirname(filename) + +export const capitalize = function (str) { + return typeof str === 'string' ? str.slice(0, 1).toUpperCase() + str.slice(1) : str +} + +export const capitalizeKebabCase = function (str, splitChar = '-') { + return typeof str === 'string' ? str.split(splitChar).map(capitalize).join('') : str +} + +export const kebabCase = (str, splitChar = '-') => { + if (!str || typeof str !== 'string') return str + + return str + .split('') + .map((char, index) => { + const charCod = char.charCodeAt(0) + + if (charCod < 65 || charCod > 122) return char + + return charCod >= 65 && charCod <= 90 ? (index !== 0 ? splitChar : '') + char.toLowerCase() : char + }) + .join('') +} diff --git a/internals/vite-plugin-template2jsx/src/plugin.js b/internals/vite-plugin-template2jsx/src/plugin.js new file mode 100644 index 000000000..04df3eadb --- /dev/null +++ b/internals/vite-plugin-template2jsx/src/plugin.js @@ -0,0 +1,95 @@ +import parse from 'html-dom-parser' +import renderVueDomObject from './render.js' +import { parseComponent } from 'vue-template-compiler' +import { template } from 'lodash-es' + +// 处理computed操作,如果想要具有响应式需要特殊处理 +const computedMap = { + button: ['buttonDisabled', 'plain', 'formDisabled'], + alert: ['getIcon', 'getTitle', 'alertClass', 'alertStyle'], + switch: ['wrapClasses', 'isDisplayOnly', 'innerClasses'] +} + +export const transformVueTemplateToSolid = (jsxTemplate, vueCode, componentName, mapData) => { + const component = parseComponent(vueCode) + + if (component.template && component.script) { + const useCommons = [ + 'useSetup', + 'getClassList', + 'mergeProps', + 'resolveComponent', + '$prefix', + 'TransitionGroup', + 'Transition', + 't', + 'withModifiers' + ] + const script = component.script.content + const startStr = 'props: [' + const sliceStr = script.substring(script.indexOf(startStr) + startStr.length) + const props = sliceStr + .substring(0, sliceStr.indexOf(']')) + .replace('...props,', '') + .replace(/[\n\s']/g, '') + .split(',') + + const specialVars = ['useIcons'] + const node = parse(component.template.content, { recognizeSelfClosing: true })[0] + const renderNode = renderVueDomObject(node) + const useApi = renderNode.useAttrs.realAttrs.filter( + (item) => !props.includes(item) && !useCommons.includes(item) && !specialVars.includes(item) + ) + let useProps = props.filter((item) => renderNode.useAttrs.realAttrs.includes(item)) + let jsxContent = renderNode.default.content.replace(/slots\.default/g, 'children') + let defaultProps = [] + let importIcons = script.match(/import\s?{\s?ico.+/) + let useIcons = '{}' + + if (computedMap[componentName]) { + computedMap[componentName].forEach((item) => { + jsxContent = jsxContent.replaceAll(`state.${item}`, `state.${item}()`) + }) + } + + if (mapData.$props) { + const getKeyValue = (key, props, char = '=') => { + return `${key} ${char} ${props.type === 'Char' ? `'${props.value}'` : props.value}` + } + + useProps = useProps.map((item) => { + const props = mapData.$props[item] + if (props) { + return getKeyValue(item, props) + } + + return item + }) + + for (let key in mapData.$props) { + const props = mapData.$props[key] + defaultProps.push(getKeyValue(key, props, ':')) + } + } + + importIcons = importIcons?.[0] + if (importIcons) { + importIcons = importIcons.replace(/icon/g, 'Icon') + useIcons = importIcons.match(/\{(.+)\}/)[0] || '{}' + } + + const realComponet = template(jsxTemplate)({ + USEAPI: useApi.join(','), + USEPROPS: useProps.join(','), + JSX: jsxContent, + NAME: componentName, + CONSTANTS: mapData.$constants || '{}', + DEFAULTPROPS: defaultProps.join(','), + IMPORTICONS: importIcons, + USEICONS: useIcons, + USECOMMONS: useCommons.join(',') + }) + + return realComponet + } +} diff --git a/internals/vite-plugin-template2jsx/src/render.js b/internals/vite-plugin-template2jsx/src/render.js new file mode 100644 index 000000000..76743f060 --- /dev/null +++ b/internals/vite-plugin-template2jsx/src/render.js @@ -0,0 +1,276 @@ +import { cloneDeep } from 'lodash-es' +import { + builtinDirectivesWithoutVModel, + isVModel, + convertText, + isSlotAttribute, + isBooleanAttrs, + isVIf, + isVElse, + isVFor, + isTextBlank, + isRef, + convertRefName, + getCondition, + getTag, + getFirstChild, + isEmptyObject, + isText, + getText, + isVElseIf, + getLoopCommand, + getSlotProps, + getSlotName, + isBindingAttrs, + getAttrName, + isEventListener, + getModifiers, + convertEventListenerName, + convertEventListener, + isBindClass, + isBindAll, + isComponent, + setDynamicComponent, + capitalizeKebabCase, + isTransition +} from './helpers.js' + +let renderDomAttrs = [] + +const renderSlots = (slots) => { + if (Object.keys(slots).length === 0) return '' + let res = ` v-slots={{ ` + Object.keys(slots).forEach((slotName) => { + let content = slots[slotName].content + if (!content.startsWith('<>')) content = `<> ${content} \n` + if (!isTextBlank(slots[slotName].content)) + res = res + `'${slotName}': (${slots[slotName].slotProps}) => ${content}, \n` + }) + res += ` }}` + return res +} + +const convertAttrs = function (attrs) { + const res = {} + + Object.keys(attrs).forEach((attrName) => { + if (isSlotAttribute(attrName) || isBindAll(attrName)) return + if (isBindingAttrs(attrName)) { + if (isBindClass(attrName)) { + res.classList = `{ getClassList(${attrs[attrName]}) }` + } else { + res[getAttrName(attrName, ':')] = `{ ${attrs[attrName]} }` + } + } else if (isEventListener(attrName)) { + const eventListenerName = convertEventListenerName(attrName) + const modifiers = getModifiers(attrName) + const listener = convertEventListener(attrs[attrName], modifiers) + res[eventListenerName] = `{${listener}}` + } else if (isVModel(attrName)) { + res[attrName] = `{${attrs[attrName]}}` + } else if (isRef(attrName)) { + res[attrName] = `{${convertRefName(attrs[attrName])}}` + } else if (isBooleanAttrs(attrs[attrName])) { + res[attrName] = '' + } else { + res[attrName] = `"${attrs[attrName]}"` + } + }) + return res +} + +const renderAttrs = function (attrs) { + let res = '' + Object.keys(attrs).forEach((attrName) => { + res += attrs[attrName] === '' ? `${attrName} ` : `${attrName}=${attrs[attrName]} ` // boolean attrs + }) + return res === '' ? '' : ` ${res}` +} + +const renderDomObj = function (dObj, needWrap = true) { + if (isEmptyObject(dObj)) { + return '' + } + + if (isText(dObj)) return convertText(getText(dObj)) + + if (isComponent(dObj)) { + setDynamicComponent(dObj) + } + + const tagName = getTag(dObj) + const slots = renderVueDomObject(getFirstChild(dObj)) + const attrs = convertAttrs(cloneDeep(dObj.attribs)) + let tag = tagName === 'template' ? '' : tagName + + builtinDirectivesWithoutVModel.forEach((attr) => delete attrs[attr]) + + if (Object.keys(dObj.attribs).length) { + renderDomAttrs.push(dObj.attribs) + if (dObj.name === 'slot') { + renderDomAttrs.push({ 'v-slot': `slots.${dObj.attribs.name}` }) + } + } + + if (tagName.indexOf('icon-') === 0) { + tag = capitalizeKebabCase(tag) + } else if (isTransition(dObj)) { + tag = capitalizeKebabCase(tag) + attrs.onExit = '{ (el, done) => { setTimeout(done, 300) } }' + + const getRealClassName = (part) => `"${(attrs.name + part).replace(/"/g, '')}"` + attrs.enterToClass = getRealClassName('-enter-from') + attrs.exitActiveClass = getRealClassName('-leave-active') + } + + if (Object.keys(slots).length < 2) { + let content = slots.default?.content || '' + if (!tag) { + let slotName = 'children' + + if (attrs.name) { + slotName = `slots.${attrs.name.replace(/["']/g, '')}` + } + + if (!content) { + return `\n { ${slotName} } \n` + } + + if (!/<[^>]+>/.test(content)) { + content = `${content}` + } + + if (needWrap) { + return `{ ${slotName} || ${content} }` + } + + return `(${slotName} || ${content})` + } + + return `<${tag}${renderAttrs(attrs)}> \n ${content}\n` + } + + return `<${tag}${renderAttrs(attrs)}${renderSlots(slots)}>\n` +} + +const renderVIfSegment = function (first, last, nodes, isRoot) { + const open = isRoot ? '{' : '(' + const close = isRoot ? '}' : ')' + const domContent = renderDomObj(nodes[first], false) + + if (first + 1 === last) { + return `${open} \n (${getCondition(nodes[first])}) && \n ${domContent} ${close} \n` + } else { + if (isVElse(nodes[first + 1])) { + return `${open} \n (${getCondition(nodes[first])}) ? \n ${domContent} : \n + ${renderDomObj(nodes[first + 1])} ${close} \n` + } else { + return `${open} \n (${getCondition(nodes[first])}) ? \n ${domContent} : \n + ${renderVIfSegment(first + 1, last, nodes)} ${close} \n` + } + } +} + +const renderVFor = function (dObj) { + return `{${getLoopCommand(dObj)} => \n ${renderDomObj(dObj)} )}` +} + +const renderVueDomObject = function (dObj) { + if (!dObj) return '' + let _dObj = cloneDeep(dObj) + let slots = {} + + slots.default = { slotProps: '', content: '', nodes: [] } + + function addNode(dObj) { + if (isEmptyObject(_dObj)) return + const slotName = getSlotName(dObj) + + if (dObj.name === 'slot') { + dObj.name = 'template' + } + + slots[slotName] = slots[slotName] || { slotProps: getSlotProps(dObj), nodes: [] } + slots[slotName].nodes.push(dObj) + } + + addNode(_dObj) + while (_dObj.next) { + _dObj = _dObj.next + addNode(_dObj) + } + + function renderNodes(nodes) { + let res = '' + for (let i = 0; i < nodes.length; i++) { + if (isVIf(nodes[i])) { + let j = i + 1 + while (j < nodes.length && isVElseIf(nodes[j])) j++ + if (j < nodes.length) { + if (isVElse(nodes[j])) j++ + } + res += renderVIfSegment(i, j, nodes, true) + i = j - 1 + } else if (isVFor(nodes[i])) { + res = res + renderVFor(nodes[i]) + } else { + res = res + renderDomObj(nodes[i]) + } + } + return res + } + + Object.keys(slots).forEach((slotName) => { + let nodes = slots[slotName].nodes + let content = renderNodes(nodes) + slots[slotName].content = content + }) + return slots +} + +const vueOperators = ':,v-,@'.split(',') +const globalProps = 'true,false'.split(',') +const charReg = /[\n\+\[\&\]!=\{\},\?\:\|]/g +const specialProps = 'a,$attrs,state,children,slots'.split(',') + +const getUseAttrs = function () { + let attrs = renderDomAttrs + .map((item) => { + return Object.entries(item) + .filter(([key, _]) => vueOperators.some((val) => key.indexOf(val) === 0)) + .map((arr) => arr[1]) + .map((key) => + key + .replace(charReg, ' ') + .split(' ') + .filter((name) => name && !name.includes('-') && !name.includes("'")) + ) + .flat() + }) + .flat() + .map((item) => { + return item.split(/[\(\)]/) + }) + .flat() + .filter((name) => name.length && !globalProps.includes(name)) + + attrs = [...new Set(attrs)] + + const realAttrs = [...new Set(attrs.map((attr) => attr.split('.')[0]))].filter((name) => !specialProps.includes(name)) + + return { + attrs, + realAttrs + } +} + +export default function (dObj) { + renderDomAttrs = [] + const component = renderVueDomObject(dObj) + const useAttrs = getUseAttrs() + + return { + ...component, + useAttrs + } +} diff --git a/internals/vite-plugin-template2jsx/template/index.tss b/internals/vite-plugin-template2jsx/template/index.tss new file mode 100644 index 000000000..3eeee0cef --- /dev/null +++ b/internals/vite-plugin-template2jsx/template/index.tss @@ -0,0 +1,3 @@ +import <%=UPPERNAME%> from './src' + +export default <%=UPPERNAME%> diff --git a/internals/vite-plugin-template2jsx/template/package.jsons b/internals/vite-plugin-template2jsx/template/package.jsons new file mode 100644 index 000000000..212acd6d9 --- /dev/null +++ b/internals/vite-plugin-template2jsx/template/package.jsons @@ -0,0 +1,17 @@ +{ + "name": "@opentiny/solid-<%=NAME%>", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/solid-common": "workspace:~", + "@opentiny/vue-theme": "workspace:~" + } +} diff --git a/internals/vite-plugin-template2jsx/template/src/index.tss b/internals/vite-plugin-template2jsx/template/src/index.tss new file mode 100644 index 000000000..089ced829 --- /dev/null +++ b/internals/vite-plugin-template2jsx/template/src/index.tss @@ -0,0 +1,11 @@ +import pc from './pc.vue' + +export default function (props) { + const { tiny_mode = 'pc' } = props + + const S = { + pc + }[tiny_mode] + + return S(props) +} diff --git a/internals/vite-plugin-template2jsx/template/src/pc.jsxs b/internals/vite-plugin-template2jsx/template/src/pc.jsxs new file mode 100644 index 000000000..b7e1bddbe --- /dev/null +++ b/internals/vite-plugin-template2jsx/template/src/pc.jsxs @@ -0,0 +1,15 @@ +import { renderless } from '@opentiny/vue-renderless/<%=NAME%>/vue' +import { <%=USECOMMONS%> } from '@opentiny/solid-common' +import '@opentiny/vue-theme/<%=NAME%>/index.less' +<%=IMPORTICONS%> +const useIcons = <%=USEICONS%> +const $constants = <%=CONSTANTS%> +export default function (props) { + const { children, slots = {}, <%=USEPROPS%> } = props + const { state, <%=USEAPI%> } = useSetup({ + props: mergeProps({<%=DEFAULTPROPS%>}, props), + renderless, + constants: $constants, + }) + return (<><%=JSX%>) +} \ No newline at end of file diff --git a/package.json b/package.json index 2d949b999..d3bf9b7a7 100644 --- a/package.json +++ b/package.json @@ -143,8 +143,10 @@ "prettier": "prettier --config .prettierrc --write .", "// ---------- openinula 相关脚本命令 ----------": "", "dev:openinula": "pnpm -C examples/openinula-docs run dev", - "// ---------- solid 相关脚本命令 ----------": "", - "dev:solid": "pnpm -C examples/solid-docs run dev" + "// ---------- 预览发布后的solid组件 ----------": "", + "preview:solid": "pnpm -C examples/solid-demo run dev", + "build:solid": "pnpm -C internals/cli build:solid", + "pub:solid": "pnpm --filter=\"./packages/dist-solid/**\" publish --no-git-checks --access=public --registry=https://registry.npmjs.org" }, "dependencies": { "@vue/composition-api": "1.7.2", diff --git a/packages/solid/index.ts b/packages/solid/index.ts index 91703514d..120126da5 100644 --- a/packages/solid/index.ts +++ b/packages/solid/index.ts @@ -1,9 +1,11 @@ import Button from '@opentiny/solid-button' +import Alert from '@opentiny/solid-alert' export const version = '1.0.0' -export { Button } +export { Button, Alert } export default { - Button + Button, + Alert } as any diff --git a/packages/solid/package.json b/packages/solid/package.json index 8e4e72fd4..a42f02914 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "@opentiny/solid-common": "workspace:~", - "@opentiny/solid-button": "workspace:~" + "@opentiny/vue-button": "workspace:~", + "@opentiny/vue-alert": "workspace:~" } -} +} \ No newline at end of file diff --git a/packages/solid/src/common/package.json b/packages/solid/src/common/package.json index 0e36a8e6a..b6bebd5ca 100644 --- a/packages/solid/src/common/package.json +++ b/packages/solid/src/common/package.json @@ -12,7 +12,7 @@ "dependencies": { "@opentiny/vue-renderless": "workspace:~", "@opentiny/vue-theme": "workspace:~", - "classnames": "^2.3.2", + "solid-transition-group": "0.2.3", "solid-js": "^1.7.8" } } diff --git a/packages/solid/src/common/src/index.ts b/packages/solid/src/common/src/index.ts index 83156f642..0c71eda8d 100644 --- a/packages/solid/src/common/src/index.ts +++ b/packages/solid/src/common/src/index.ts @@ -1,5 +1,7 @@ import * as hooks from 'solid-js' -import { createSignal, onCleanup, createMemo } from 'solid-js' +import { onCleanup, onMount, createResource, createEffect, on, mergeProps } from 'solid-js' +import { createMutable } from 'solid-js/store' +import { TransitionGroup, Transition } from 'solid-transition-group' import '@opentiny/vue-theme/base/index.less' const EVENTS_PREFIX = 'on' @@ -7,41 +9,12 @@ const EVENTS_PREFIX = 'on' // 处理solid事件触发机制 export const emit = (props) => - (evName, ...args) => { - const eventsName = `${EVENTS_PREFIX}${evName[0].toLocaleUpperCase()}${evName.slice(1)}` - if (props[eventsName] && typeof props[eventsName] === 'function') { - props[eventsName](...args) - } - } - -export const useSetState = (initialState) => { - const [state, setState] = createSignal(initialState, { equals: false }) - - return [state, setState] -} - -// props 应该不用做处理, props 都是 . 访问。 -export const reactive = (staticObject) => { - const [state, setState] = useSetState(staticObject) - - return new Proxy(state(), { - get(target, property) { - if (property === 'solidState') { - return state + (evName, ...args) => { + const eventsName = `${EVENTS_PREFIX}${evName[0].toLocaleUpperCase()}${evName.slice(1)}` + if (props[eventsName] && typeof props[eventsName] === 'function') { + props[eventsName](...args) } - if (typeof target[property] === 'function') { - return target[property](target) - } else { - return target[property] - } - }, - set(target, property, value) { - Reflect.set(target, property, value) - setState((val) => val) - return true } - }) -} // nextTick, 等待 dom 更新后触发回调 export const useNextTick = (callback) => { @@ -62,30 +35,142 @@ export const emitEvent = () => { } } -const computed = (callback) => { - try { - return createMemo(callback) - } catch (error) { - return [] - } +const watch = (valueFn, callback, options) => { + createEffect(on(valueFn, callback, { defer: !options.immediate })) } -export const useSetup = ({ props, renderless, extendOptions = { framework: 'Solid' } }) => { +export const t = (str) => str + +const reactive = (state) => { + const proxy = createMutable(state) + // 暂时解决嵌套computed导致禁用问题,后期再完善 + if (proxy.formDisabled) { + proxy.formDisabled = false + } + + if (proxy.disabled) { + proxy.disabled = false + } + + return proxy +} + +export const useSetup = ({ props, renderless, extendOptions = { framework: 'Solid' }, constants }) => { const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless const utils = { parent: {}, - emit: emit(props) + emit: emit(props), + constants, + nextTick: useNextTick, + t, + mode: 'pc' } const sdk = render( props, - { ...hooks, reactive, computed, useNextTick, inject: () => {}, watch: () => {}, onBeforeUnmount: onCleanup }, + { + ...hooks, + reactive, + computed: (callback) => { + const [computedValue, { mutate }] = createResource(() => { + return Promise.resolve().then(() => { + return callback() + }) + }) + + Promise.resolve().then(() => { + createEffect(() => { + mutate(callback()) + }) + }) + + return computedValue + }, + inject: () => { }, + watch, + watchEffect: createEffect, + onMounted: onMount, + onBeforeUnmount: onCleanup + }, utils, extendOptions ) return { ...sdk, - state: sdk.state.solidState, type: props.type ?? 'default' } } + +export const getType = (str, type = 'object') => { + return ( + { + '[object Number]': 'number', + '[object String]': 'string', + '[object Boolean]': 'boolean', + '[object Undefined]': 'undefined', + '[object Null]': 'null', + '[object Array]': 'array', + '[object Arguments]': 'arguments', + '[object Function]': 'function', + '[object Error]': 'error', + '[object Date]': 'date', + '[object RegExp]': 'regexp', + '[object Object]': 'object' + }[Object.prototype.toString.call(str)] === type + ) +} + +export const getClassList = (cls) => { + if (getType(cls)) { + return cls + } + + if (Array.isArray(cls)) { + const classList = {} + + cls.forEach((cls) => { + if (!cls) return + if (typeof cls === 'string') { + classList[cls] = true + } else if (typeof cls === 'object') { + if (Array.isArray(cls)) { + Object.assign(classList, getClassList(cls)) + } else { + Object.assign(classList, cls) + } + } + }) + + return classList + } + + return { + [cls]: true + } +} + +export const capitalize = function (str) { + return typeof str === 'string' ? str.slice(0, 1).toUpperCase() + str.slice(1) : str +} + +export const capitalizeKebabCase = function (str, splitChar = '-') { + return typeof str === 'string' ? str.split(splitChar).map(capitalize).join('') : str +} + +export const resolveComponent = (component, useComponents) => { + if (typeof component === 'string' && component.indexOf('icon-') === 0) { + const componentName = capitalizeKebabCase(component) + return useComponents[componentName] || componentName + } + return component +} + +export const $prefix = 'Tiny' + +export const withModifiers = function (event, eventCallback, keys) { + if (typeof eventCallback === 'function') { + eventCallback(event); + } +} + +export { mergeProps, TransitionGroup, Transition } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2d98772f4..48c675fbc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - packages/** - examples/* - internals/* + - solid-demo/* diff --git a/tsconfig.solid.json b/tsconfig.solid.json new file mode 100644 index 000000000..f8c5e211d --- /dev/null +++ b/tsconfig.solid.json @@ -0,0 +1,30 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "allowJs": true, + "noImplicitAny": false, + "baseUrl": ".", + "paths": { + "@opentiny/vue-autonavi-map": ["packages/vue/src/chart/autonavi-map"], + "@opentiny/vue-baidu-map": ["packages/vue/src/chart/baidu-map"], + "@opentiny/vue-chart-*": ["packages/vue/src/chart/chart-*"], + "@opentiny/vue-*": ["packages/vue-*", "packages/vue/src/*"], + "@opentiny/vue-renderless/types*": ["packages/renderless/types*"], + "@opentiny/vue-renderless*": ["packages/renderless/src*"], + "virtual:common/adapter/vue": ["packages/vue-common/src/adapter/vue3/index.ts"], + "virtual:locale/vue": ["packages/vue-locale/src/vue3/index.ts"] + }, + "types": ["node", "vite/client"] + }, + "vueCompilerOptions": { + "target": 3 + }, + "include": [ + "packages/**/*.ts", + "packages/**/*.tsx", + "packages/**/*.vue", + "examples/vue3/shims-app.d.ts", + "examples/vue3/shims-vue.d.ts" + ], + "exclude": ["**/node_modules", "**/dist*", "**/*.md"] +}