@zerodep-dev/vue-tsx-pro-mini 0.0.0-stage → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/build.d.ts +104 -0
- package/dist/build.js +430 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +123 -0
- package/dist/elements-BDIG7-jU.js +63 -0
- package/dist/globals.d.ts +1 -0
- package/dist/globals.js +16 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.js +15 -0
- package/dist/page-upnBSowU.js +1219 -0
- package/dist/testing.d.ts +50 -0
- package/dist/testing.js +109 -0
- package/package.json +71 -3
- package/template/gitignore +2 -0
- package/template/index.html +17 -0
- package/template/mini.config.ts +12 -0
- package/template/package.json +22 -0
- package/template/src/app.ts +5 -0
- package/template/src/main.ts +5 -0
- package/template/src/pages/HomePage.tsx +25 -0
- package/template/tsconfig.json +16 -0
- package/template/vite.config.ts +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kenconnet666
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zerodep-dev/vue-tsx-pro-mini
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在微信、支付宝小程序里渲染 vue-tsx-pro 的组件:同一份 TSX 页面、同一个发布的组件库,H5 直接渲染 DOM,小程序经这里渲染。
|
|
4
|
+
还在概念验证之后的补全阶段(ROADMAP 阶段 17),没有发布。
|
|
5
|
+
|
|
6
|
+
## 怎么工作
|
|
7
|
+
|
|
8
|
+
- **渲染**:Vue 的自定义渲染器(`renderer.ts`)把虚拟节点落到 `MiniNode` 树上(`node.ts`);页面(`page.ts`)在微任务里把这一轮的变化
|
|
9
|
+
合成一次 setData——子节点变了的整段发,只是字段变了的按路径只发变了的那几个。
|
|
10
|
+
- **模板**:每种元素一套通用模板,按层展开(小程序的模板不能递归调用自己),数据里的 `t` 选模板(`build/templates.ts`)。
|
|
11
|
+
- **样式**:小程序不能在运行时插规则。样式引擎照常生成组合类,渲染时用 tsx 的 `inlineStyle` 按类名查回声明表,条件(属性选择器、
|
|
12
|
+
`:active`、`@media` 宽度)由 `style.ts` 按节点状态判定,展开成行内样式。主题变量与关键帧在构建时从包里收集成 app 样式表。
|
|
13
|
+
展开不了的写法(伪元素、`:nth-child`、标签与类名选择器、`@supports`)按不成立处理,开发构建的 console 里每种提示一次;
|
|
14
|
+
app 样式表里留着 WXSS 不认的写法(`:root`、`:not` 里的选择器列表)时构建时提示。
|
|
15
|
+
- **事件**:模板一律 catch 绑定,原生冒泡停在最内层,冒泡由运行时按节点树做(两端冒泡时 `target` 的含义不同,不能靠它去重)。
|
|
16
|
+
- **SVG**:小程序没有 SVG,`svg` 子树序列化成遮罩(`mask-image`),底色取 `currentColor`。
|
|
17
|
+
|
|
18
|
+
## 用法
|
|
19
|
+
|
|
20
|
+
新项目从模板建(`template/`:H5 与两端小程序共用页面):
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npx @zerodep-dev/vue-tsx-pro-mini create my-app
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// src/app.ts:声明页面,第一个是首页
|
|
28
|
+
import { definePages } from '@zerodep-dev/vue-tsx-pro-mini';
|
|
29
|
+
export default definePages({ index: HomePage });
|
|
30
|
+
|
|
31
|
+
// mini.config.ts:构建配置(MiniBuildOptions 去掉 root、platform)
|
|
32
|
+
import { defineMiniConfig } from '@zerodep-dev/vue-tsx-pro-mini/build';
|
|
33
|
+
import { vueTsxPro } from '@zerodep-dev/vue-tsx-pro-tsx/vite';
|
|
34
|
+
export default defineMiniConfig({ plugins: vueTsxPro(), appId: 'wx…', darkmode: true });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
命令(`vtp-mini`,在应用目录里运行):`build [wx] [my]` 构建两端或指定的一端;`dev [wx|my]` 监听;
|
|
38
|
+
`open [wx|my]` 在开发者工具里打开产物(微信按安装位置找命令行,装在别处时设 `WECHAT_DEVTOOLS_CLI`;支付宝用 `minidev ide`);
|
|
39
|
+
`create <目录>` 从模板建项目。也可以在脚本里直接调 `buildMini` / `watchMini`。
|
|
40
|
+
|
|
41
|
+
页面组件收到 `query` 属性(跳转时带的参数,已解码)。页面级的回调与跳转:`onPageShow`、`onPageHide`、`onReachBottom`(在 H5 里调用什么也不做),
|
|
42
|
+
`navigateTo('detail', { id: 42 })`、`redirectTo`、`reLaunch`、`navigateBack`(按 definePages 的页面名)。
|
|
43
|
+
|
|
44
|
+
监听模式(`vtp-mini dev`、`watchMini`):依赖(Vue、运行时、组件库)单独打成 `vtp-vendor.js`,只在依赖的列表变了时重打;
|
|
45
|
+
改了源码只重打应用自己的代码(约 0.3–0.5 s,带源码映射),开发者工具看到产物变了会自己重新编译。
|
|
46
|
+
|
|
47
|
+
授权类能力(`capability.ts`)是宿主约定的一部分:设备权限按微信的 scope、支付宝的 authSetting 查与申请,存储、登录、
|
|
48
|
+
支付、订阅消息、手机号按两端各自的接口调用,支付结果统一成 success / cancel / pending / fail;微信的隐私协议回调交给组件库的弹窗。
|
|
49
|
+
带 `open-type` 的 button 换成原生按钮模板(`btn`),平台事件统一成 `opentype`。`mini.config.ts` 的 `permissions`(位置权限的用途说明)
|
|
50
|
+
与 `requiredPrivateInfos` 写进微信的 app.json。
|
|
51
|
+
|
|
52
|
+
完整的例子是 `apps/mobile-demo`(`node build.ts` 两端都构建,`pnpm dev:mini` 监听微信端,`pnpm dev` 看 H5)。
|
|
53
|
+
|
|
54
|
+
## 验证与踩过的坑
|
|
55
|
+
|
|
56
|
+
- 微信:开发者工具自带技能 CLI,`wechatide -c <客户端名> <工具>`(导入项目、打开窗口、截图、读 console、自动化点击输入、执行 JS);
|
|
57
|
+
第一次调用要在开发者工具里点授权。项目要有真实 AppID,`touristappid` 会报 APPID_ERROR。
|
|
58
|
+
`wcsc.exe`(开发者工具安装目录的 `resources/app.asar.unpacked/node_modules/wcc-exec/`)能单独编译 WXSS,报错带行列。
|
|
59
|
+
- 支付宝:没有官方 MCP。`minidev build` 离线编译检查,`minidev ide -p <目录>` 在开发者工具里打开;IDE 有编译缓存,改了包要"清除构建缓存"。
|
|
60
|
+
- WXSS 不认 `:root`,和 `page` 写在同一个选择器列表里会让整条规则失效;`:not(a,b)` 这种选择器列表编不过。
|
|
61
|
+
- 支付宝的逻辑层跑在 Worker 里,旧环境没有 `globalThis`;编译器要看产物根目录的 `.browserslistrc` 才按新语法解析;
|
|
62
|
+
Vue 选项式 API 里 `computed` 那段(`set:e=>i.value=e`)它解析不了,构建时关掉选项式 API。
|
|
63
|
+
- 依赖(animejs)在模块顶层取 `setImmediate` / `requestAnimationFrame`,逻辑层没有:`./globals` 补上,排在打包入口最前面。
|
|
64
|
+
补上了微信里裸写的 `requestAnimationFrame` 仍然取不到(报 not a function):组件里要等视图层画完用宿主的 `nextFrame`(等 setData 生效)。
|
|
65
|
+
- 模板层数(24)用完后,更深的节点交给递归的自定义组件 `vtp-comp`(模板从第 0 层重新开始)。它要在 app.json 的 usingComponents 里全局登记,`n 只在页面的 json 里登记时微信不实例化;组件里的原生 tap 不可靠(按下时整段子树重发给组件属性,tap 会丢),
|
|
66
|
+
那一段的点击由运行时按 touchstart / touchend 合成(同一节点、移动不到 10px、不到 700ms)。
|
|
67
|
+
- 行内样式走字典:页面数据的 `ss` 里每种样式串只发一次,节点只带编号(`st`),模板写 `sd[i.st]`。1000 行单元格的首屏
|
|
68
|
+
setData 从 1.9 MB 降到 320 KB(微信单次上限 1 MB);基准在 `packages/mobile/src/.test/setdata.bench.test.tsx`。
|
|
69
|
+
传给模板时写成 `data="{{i: item, ss: ss}}"`(键与变量同名)微信的模板里取不到,换了名字(`sd: ss`)才行。
|
|
70
|
+
- Taro 按使用方源码扫描裁剪模板,预编译库里动态创建的元素没有模板;这里的模板按元素全量生成,不扫描源码。
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PluginOption } from "vite";
|
|
2
|
+
//#region src/build/platforms.d.ts
|
|
3
|
+
/** 微信与支付宝在产物上的差别:文件后缀、模板指令前缀、事件属性写法、项目配置文件 */
|
|
4
|
+
interface Platform {
|
|
5
|
+
readonly name: 'wx' | 'my';
|
|
6
|
+
readonly template: 'wxml' | 'axml';
|
|
7
|
+
readonly style: 'wxss' | 'acss';
|
|
8
|
+
/** 列表指令前缀:wx:for / a:for */
|
|
9
|
+
readonly directive: 'wx' | 'a';
|
|
10
|
+
/**
|
|
11
|
+
* 事件属性:catchtap / catchTap。用 catch 让原生冒泡停在最内层的节点,冒泡由运行时按节点树做(page.ts);
|
|
12
|
+
* 两端冒泡时 target 的含义不同(支付宝每一层的 target 都是当前节点),不能靠它去重
|
|
13
|
+
*/
|
|
14
|
+
event(name: NativeEventName): string;
|
|
15
|
+
/**
|
|
16
|
+
* 冒泡的事件属性:bindtouchstart / onTouchStart。按下、松开不能 catch:真机上 catch 掉之后不再产生 tap
|
|
17
|
+
* (模拟器照常有),冒泡带来的重复由运行时按事件时间去掉
|
|
18
|
+
*/
|
|
19
|
+
bubble(name: NativeEventName): string;
|
|
20
|
+
/**
|
|
21
|
+
* 手势节点(模板 touch)的 touchmove:平时 bind(页面照常滚动),节点要 catch 时(横向滑动的过程中)换成 catch。
|
|
22
|
+
* 微信的事件名可以是表达式;支付宝不行,只 bind
|
|
23
|
+
*/
|
|
24
|
+
touchMove: string;
|
|
25
|
+
/**
|
|
26
|
+
* 无障碍属性:节点数据里的读屏字段(page.ts 的 A11Y_ATTRS)写到模板上。微信的组件认 aria-role、aria-label 等;
|
|
27
|
+
* 支付宝没有对应的属性,空着
|
|
28
|
+
*/
|
|
29
|
+
a11y: string;
|
|
30
|
+
/** 开放能力按钮(模板 btn)的原生 button 上额外绑的属性与事件 */
|
|
31
|
+
openButton: string;
|
|
32
|
+
}
|
|
33
|
+
/** 模板上绑的原生事件 */
|
|
34
|
+
type NativeEventName = 'tap' | 'longpress' | 'touchstart' | 'touchmove' | 'touchend' | 'touchcancel' | 'input' | 'confirm' | 'focus' | 'blur' | 'load' | 'error';
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/build/index.d.ts
|
|
37
|
+
export interface MiniBuildOptions {
|
|
38
|
+
/** 应用目录 */
|
|
39
|
+
root: string;
|
|
40
|
+
/** 平台:wx 微信,my 支付宝 */
|
|
41
|
+
platform: Platform['name'];
|
|
42
|
+
/** 应用入口,相对 root;默认导出 definePages 的结果 */
|
|
43
|
+
entry?: string;
|
|
44
|
+
/** 产物目录,相对 root;默认 dist/<平台> */
|
|
45
|
+
outDir?: string;
|
|
46
|
+
/** 编译应用源码的插件(vueTsxPro()) */
|
|
47
|
+
plugins?: PluginOption[];
|
|
48
|
+
/** 微信的 AppID(project.config.json) */
|
|
49
|
+
appId?: string;
|
|
50
|
+
/** app.json 的 window */
|
|
51
|
+
window?: Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* 跟随系统的深浅色:微信要在 app.json 里开 darkmode 才会报告深色(ConfigProvider 的 theme="system" 用);
|
|
54
|
+
* 导航栏的颜色在 window 里写死时不受影响
|
|
55
|
+
*/
|
|
56
|
+
darkmode?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* 位置权限的用途说明,用户授权时看到(微信 app.json 的 permission;精确位置 location、模糊位置 fuzzyLocation)。
|
|
59
|
+
* 申请位置权限时必须写
|
|
60
|
+
*/
|
|
61
|
+
permissions?: {
|
|
62
|
+
location?: string;
|
|
63
|
+
fuzzyLocation?: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* 用到的地理位置接口(微信 app.json 的 requiredPrivateInfos,没声明的调用时报错):如 getLocation、chooseLocation、
|
|
67
|
+
* getFuzzyLocation。模糊位置与精确位置的接口互斥,同时声明时构建报错
|
|
68
|
+
*/
|
|
69
|
+
requiredPrivateInfos?: string[];
|
|
70
|
+
/** 开发构建:不压缩,Vue 带开发期检查 */
|
|
71
|
+
development?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** 应用目录里 mini.config.ts 的内容:vtp-mini 命令按它构建,root 是配置文件所在目录,平台与开发模式由命令决定 */
|
|
74
|
+
export type MiniConfig = Omit<MiniBuildOptions, 'root' | 'platform' | 'development'>;
|
|
75
|
+
/**
|
|
76
|
+
* 写 mini.config.ts 用,只为带上类型:
|
|
77
|
+
*
|
|
78
|
+
* export default defineMiniConfig({ plugins: vueTsxPro(), appId: 'wx…' });
|
|
79
|
+
*/
|
|
80
|
+
export declare const defineMiniConfig: (config: MiniConfig) => MiniConfig;
|
|
81
|
+
export declare function buildMini(options: MiniBuildOptions): Promise<{
|
|
82
|
+
outDir: string;
|
|
83
|
+
pages: readonly string[];
|
|
84
|
+
}>;
|
|
85
|
+
/** 监听模式的句柄 */
|
|
86
|
+
export interface MiniWatcher {
|
|
87
|
+
/** 停止监听 */
|
|
88
|
+
close(): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 监听模式:应用的包不打依赖(Vue、运行时、组件库……),对它们的 require 换成从 vtp-vendor.js 取;
|
|
92
|
+
* 依赖包只在依赖的列表变了时重打。源码改了只重打应用自己的代码,再重写小程序的文件,开发者工具看到产物变了会自己重新编译。
|
|
93
|
+
* 每次打完调用 onBuild(第几次、页面、耗时)
|
|
94
|
+
*
|
|
95
|
+
* const watcher = await watchMini({ ...options, development: true }, ({ pages, ms }) => console.log(ms));
|
|
96
|
+
*/
|
|
97
|
+
export declare function watchMini(options: MiniBuildOptions, onBuild?: (result: {
|
|
98
|
+
outDir: string;
|
|
99
|
+
pages: readonly string[];
|
|
100
|
+
ms: number;
|
|
101
|
+
count: number;
|
|
102
|
+
}) => void): Promise<MiniWatcher>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export {};
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { build } from "vite";
|
|
5
|
+
//#region src/build/app.ts
|
|
6
|
+
/** 精确位置的接口:与模糊位置 getFuzzyLocation 不能同时声明(微信编译时报错) */
|
|
7
|
+
const PRECISE_LOCATION = [
|
|
8
|
+
"getLocation",
|
|
9
|
+
"onLocationChange",
|
|
10
|
+
"startLocationUpdate",
|
|
11
|
+
"startLocationUpdateBackground"
|
|
12
|
+
];
|
|
13
|
+
/** 微信 app.json 独有的字段:深浅色、位置权限的用途说明、地理位置接口的声明 */
|
|
14
|
+
function wechatAppFields(options) {
|
|
15
|
+
const fields = {};
|
|
16
|
+
if (options.darkmode) fields.darkmode = true;
|
|
17
|
+
const { location, fuzzyLocation } = options.permissions ?? {};
|
|
18
|
+
if (location || fuzzyLocation) fields.permission = {
|
|
19
|
+
...location ? { "scope.userLocation": { desc: location } } : {},
|
|
20
|
+
...fuzzyLocation ? { "scope.userFuzzyLocation": { desc: fuzzyLocation } } : {}
|
|
21
|
+
};
|
|
22
|
+
const infos = options.requiredPrivateInfos;
|
|
23
|
+
if (infos?.length) {
|
|
24
|
+
const precise = infos.filter((name) => PRECISE_LOCATION.includes(name));
|
|
25
|
+
if (precise.length && infos.includes("getFuzzyLocation")) throw new Error(`requiredPrivateInfos 里 getFuzzyLocation 与精确位置的接口(${precise.join("、")})互斥,只能留一类`);
|
|
26
|
+
fields.requiredPrivateInfos = infos;
|
|
27
|
+
}
|
|
28
|
+
return fields;
|
|
29
|
+
}
|
|
30
|
+
/** app.json 的 window 按微信的字段写;支付宝的标题字段叫 defaultTitle,补上 */
|
|
31
|
+
function windowConfig(platform, window = {}) {
|
|
32
|
+
if (platform.name !== "my" || window.navigationBarTitleText === void 0) return window;
|
|
33
|
+
return {
|
|
34
|
+
...window,
|
|
35
|
+
defaultTitle: window.navigationBarTitleText
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/build/platforms.ts
|
|
40
|
+
/** 支付宝的事件名是驼峰:onTouchStart、catchLongTap */
|
|
41
|
+
const camel = {
|
|
42
|
+
tap: "Tap",
|
|
43
|
+
longpress: "LongTap",
|
|
44
|
+
touchstart: "TouchStart",
|
|
45
|
+
touchmove: "TouchMove",
|
|
46
|
+
touchend: "TouchEnd",
|
|
47
|
+
touchcancel: "TouchCancel",
|
|
48
|
+
input: "Input",
|
|
49
|
+
confirm: "Confirm",
|
|
50
|
+
focus: "Focus",
|
|
51
|
+
blur: "Blur",
|
|
52
|
+
load: "Load",
|
|
53
|
+
error: "Error"
|
|
54
|
+
};
|
|
55
|
+
const PLATFORMS = {
|
|
56
|
+
wx: {
|
|
57
|
+
name: "wx",
|
|
58
|
+
template: "wxml",
|
|
59
|
+
style: "wxss",
|
|
60
|
+
directive: "wx",
|
|
61
|
+
event: (name) => `catch${name}`,
|
|
62
|
+
bubble: (name) => `bind${name}`,
|
|
63
|
+
touchMove: `bindtouchmove="{{i.cm ? '' : 'eh'}}" catchtouchmove="{{i.cm ? 'eh' : ''}}"`,
|
|
64
|
+
a11y: "aria-role=\"{{i.ar}}\" aria-label=\"{{i.al}}\" aria-hidden=\"{{i.ahi}}\" aria-checked=\"{{i.ack}}\" aria-disabled=\"{{i.adi}}\" aria-selected=\"{{i.ase}}\" aria-expanded=\"{{i.aex}}\"",
|
|
65
|
+
openButton: "open-type=\"{{i.ot}}\" hover-class=\"none\" bindgetphonenumber=\"eh\" bindchooseavatar=\"eh\" bindagreeprivacyauthorization=\"eh\" binderror=\"eh\""
|
|
66
|
+
},
|
|
67
|
+
my: {
|
|
68
|
+
name: "my",
|
|
69
|
+
template: "axml",
|
|
70
|
+
style: "acss",
|
|
71
|
+
directive: "a",
|
|
72
|
+
event: (name) => `catch${camel[name]}`,
|
|
73
|
+
bubble: (name) => `on${camel[name]}`,
|
|
74
|
+
touchMove: "onTouchMove=\"eh\"",
|
|
75
|
+
a11y: "",
|
|
76
|
+
openButton: "open-type=\"{{i.ot}}\" scope=\"{{i.os}}\" onGetAuthorize=\"eh\" onError=\"eh\""
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/build/styles.ts
|
|
81
|
+
/**
|
|
82
|
+
* app 样式表:运行时没法插规则,构建时把"模块加载时就定下来的"静态规则收集出来——
|
|
83
|
+
* 主题变量组(:root 上的默认值,换成小程序的 page)与关键帧;组合类在渲染时展开成行内样式,不进这里。
|
|
84
|
+
* 规则来自打好的包自己(它导出 styleText),只含真正打进包里的模块。
|
|
85
|
+
*/
|
|
86
|
+
/** 元素的默认显示方式(运行时的 baseClassOf) */
|
|
87
|
+
const BASE = [
|
|
88
|
+
".vtp-inline{display:inline}",
|
|
89
|
+
".vtp-inline-block{display:inline-block}",
|
|
90
|
+
".vtp-btn{margin:0;padding:0;border:0;border-radius:0;background:none;font-size:inherit;line-height:inherit;color:inherit;text-align:inherit;width:auto;overflow:visible}",
|
|
91
|
+
".vtp-btn::after{display:none}"
|
|
92
|
+
].join("\n");
|
|
93
|
+
/** 引擎收集的规则原文 → app 样式表 */
|
|
94
|
+
function appStyles(styleText) {
|
|
95
|
+
const rules = topLevelRules(styleText).filter((rule) => rule.startsWith(":root") || rule.startsWith("@keyframes") || rule.startsWith("@media") && rule.includes(":root{")).map((rule) => rule.replaceAll(":root{", "page{"));
|
|
96
|
+
for (const rule of rules) {
|
|
97
|
+
const problem = WXSS_REJECTS.find(({ pattern }) => pattern.test(rule));
|
|
98
|
+
if (problem) console.warn(`[vtp-mini] app 样式表里 ${problem.reason},WXSS 编不过:${rule}`);
|
|
99
|
+
}
|
|
100
|
+
return [BASE, ...rules].join("\n");
|
|
101
|
+
}
|
|
102
|
+
/** WXSS 编译器不认的写法(微信开发者工具的 wcsc 报错):引擎改了规则的输出格式时在构建时就提示 */
|
|
103
|
+
const WXSS_REJECTS = [{
|
|
104
|
+
pattern: /:root\b/,
|
|
105
|
+
reason: ":root 没换成 page(和别的选择器写在同一个列表里)"
|
|
106
|
+
}, {
|
|
107
|
+
pattern: /:not\([^)]*,/,
|
|
108
|
+
reason: ":not 里写了选择器列表"
|
|
109
|
+
}];
|
|
110
|
+
/** 按顶层花括号配平切出规则(@media 等块规则整体算一条) */
|
|
111
|
+
function topLevelRules(css) {
|
|
112
|
+
const rules = [];
|
|
113
|
+
let depth = 0;
|
|
114
|
+
let start = 0;
|
|
115
|
+
for (let index = 0; index < css.length; index++) if (css[index] === "{") depth++;
|
|
116
|
+
else if (css[index] === "}" && --depth === 0) {
|
|
117
|
+
rules.push(css.slice(start, index + 1).trim());
|
|
118
|
+
start = index + 1;
|
|
119
|
+
}
|
|
120
|
+
return rules;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/build/templates.ts
|
|
124
|
+
/**
|
|
125
|
+
* 通用模板:每种元素一套模板,按层级展开(小程序的模板不能递归调用自己),
|
|
126
|
+
* 节点数据的 t 选模板(运行时见 page.ts)。层数用完后,更深的子节点交给递归的自定义组件 vtp-comp:
|
|
127
|
+
* 它的模板从第 0 层重新开始,所以嵌套多深都能画(同 Taro 的 comp)。
|
|
128
|
+
*/
|
|
129
|
+
/** base 模板文件(base.wxml / base.axml) */
|
|
130
|
+
function baseTemplate(platform) {
|
|
131
|
+
const on = (names) => names.map((name) => `${platform.event(name)}="eh"`).join(" ");
|
|
132
|
+
const bubbling = (names) => names.map((name) => `${platform.bubble(name)}="eh"`).join(" ");
|
|
133
|
+
const tapEvents = `${on(["tap", "longpress"])} ${bubbling([
|
|
134
|
+
"touchstart",
|
|
135
|
+
"touchend",
|
|
136
|
+
"touchcancel"
|
|
137
|
+
])}`;
|
|
138
|
+
const common = `id="{{i.id}}" class="{{i.c}}" style="{{sd[i.st]}}" data-s="{{i.s}}" ${platform.a11y}`;
|
|
139
|
+
const field = `maxlength="{{i.maxlength || -1}}" disabled="{{i.dis}}" focus="{{i.af}}" confirm-type="{{i.ct || 'done'}}"`;
|
|
140
|
+
const inputEvents = on([
|
|
141
|
+
"input",
|
|
142
|
+
"confirm",
|
|
143
|
+
"focus",
|
|
144
|
+
"blur"
|
|
145
|
+
]);
|
|
146
|
+
const templates = [];
|
|
147
|
+
for (let level = 0; level < 24; level++) {
|
|
148
|
+
const children = level + 1 < 24 ? `${loop(platform, "i.cn")}<template is="{{'vtp_' + item.t + '_${level + 1}'}}" data="{{i: item, sd: sd}}"/></block>` : `${loop(platform, "i.cn")}<vtp-comp i="{{item}}" sd="{{sd}}"/></block>`;
|
|
149
|
+
const view = (type, events) => `<template name="vtp_${type}_${level}"><view ${common} ${events}>${children}</view></template>`;
|
|
150
|
+
templates.push(view("v", tapEvents), view("lock", `${tapEvents} ${on(["touchmove"])}`), view("touch", `${tapEvents} ${platform.touchMove}`), `<template name="vtp_btn_${level}"><button ${platform.openButton} ${common} ${tapEvents}>${children}</button></template>`, `<template name="vtp_x_${level}">{{i.v}}</template>`, `<template name="vtp_n_${level}"><block></block></template>`, `<template name="vtp_svg_${level}"><view ${common} ${tapEvents}/></template>`, `<template name="vtp_image_${level}"><image src="{{i.src}}" mode="{{i.mode || 'scaleToFill'}}" lazy-load="{{i.lz}}" ${common} ${tapEvents} ${on(["load", "error"])}/></template>`, `<template name="vtp_input_${level}"><input value="{{i.value}}" placeholder="{{i.placeholder}}" type="{{i.type || 'text'}}" password="{{i.pw}}" ${field} ${common} ${tapEvents} ${inputEvents}/></template>`, `<template name="vtp_textarea_${level}"><textarea value="{{i.value}}" placeholder="{{i.placeholder}}" auto-height="{{i.ah}}" ${field} ${common} ${tapEvents} ${inputEvents}/></template>`);
|
|
151
|
+
}
|
|
152
|
+
return templates.join("\n");
|
|
153
|
+
}
|
|
154
|
+
/** 递归组件 vtp-comp 的模板:引入 base,从第 0 层画传进来的节点 */
|
|
155
|
+
function componentTemplate(platform) {
|
|
156
|
+
return [`<import src="../../base.${platform.template}"/>`, `<block ${platform.directive}:if="{{i.t}}"><template is="{{'vtp_' + i.t + '_0'}}" data="{{i: i, sd: sd}}"/></block>`].join("\n");
|
|
157
|
+
}
|
|
158
|
+
/** 页面模板:引入 base,从根节点的子节点开始渲染 */
|
|
159
|
+
function pageTemplate(platform) {
|
|
160
|
+
return [`<import src="../../base.${platform.template}"/>`, `${loop(platform, "root.cn")}<template is="{{'vtp_' + item.t + '_0'}}" data="{{i: item, sd: ss}}"/></block>`].join("\n");
|
|
161
|
+
}
|
|
162
|
+
const loop = (platform, list) => `<block ${platform.directive}:for="{{${list}}}" ${platform.directive}:key="s">`;
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/build/index.ts
|
|
165
|
+
/**
|
|
166
|
+
* 构建小程序:应用入口(definePages 的默认导出)连同运行时、Vue、组件库打成一个 CommonJS 包 vtp.js,
|
|
167
|
+
* 再生成小程序要的文件——app.js / app.json、每个页面的 js / json / 模板、通用模板、app 样式表、项目配置。
|
|
168
|
+
* 组件库按发布的产物(dist)打进来,不重新编译;应用自己的 TSX 由传入的插件(vueTsxPro)编译。
|
|
169
|
+
*
|
|
170
|
+
* await buildMini({ root: import.meta.dirname, platform: 'wx', plugins: vueTsxPro(), appId: 'wx…' });
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* 写 mini.config.ts 用,只为带上类型:
|
|
174
|
+
*
|
|
175
|
+
* export default defineMiniConfig({ plugins: vueTsxPro(), appId: 'wx…' });
|
|
176
|
+
*/
|
|
177
|
+
const defineMiniConfig = (config) => config;
|
|
178
|
+
async function buildMini(options) {
|
|
179
|
+
const { config, outDir, platform } = prepare(options, false);
|
|
180
|
+
await build(config);
|
|
181
|
+
return finish(options, platform, outDir, 0);
|
|
182
|
+
}
|
|
183
|
+
/** 监听模式下依赖单独成包的文件名 */
|
|
184
|
+
const VENDOR = "vtp-vendor.js";
|
|
185
|
+
/** 包名(依赖):不是相对路径、绝对路径、虚拟模块 */
|
|
186
|
+
const isBare = (id) => !/^(?:[./\\]|[A-Za-z]:|\0)/.test(id);
|
|
187
|
+
/**
|
|
188
|
+
* 监听模式:应用的包不打依赖(Vue、运行时、组件库……),对它们的 require 换成从 vtp-vendor.js 取;
|
|
189
|
+
* 依赖包只在依赖的列表变了时重打。源码改了只重打应用自己的代码,再重写小程序的文件,开发者工具看到产物变了会自己重新编译。
|
|
190
|
+
* 每次打完调用 onBuild(第几次、页面、耗时)
|
|
191
|
+
*
|
|
192
|
+
* const watcher = await watchMini({ ...options, development: true }, ({ pages, ms }) => console.log(ms));
|
|
193
|
+
*/
|
|
194
|
+
async function watchMini(options, onBuild) {
|
|
195
|
+
const { config, outDir, platform } = prepare(options, true);
|
|
196
|
+
const externals = /* @__PURE__ */ new Set();
|
|
197
|
+
config.plugins = [...await cacheTransforms(config.plugins ?? []), vendorRequires(externals)];
|
|
198
|
+
config.build.rolldownOptions.external = isBare;
|
|
199
|
+
const watcher = await build(config);
|
|
200
|
+
let count = 0;
|
|
201
|
+
let started = performance.now();
|
|
202
|
+
let vendorKey = "";
|
|
203
|
+
let queue = Promise.resolve();
|
|
204
|
+
watcher.on("event", (event) => {
|
|
205
|
+
if (event.code === "START") started = performance.now();
|
|
206
|
+
else if (event.code === "ERROR") console.error(event.error);
|
|
207
|
+
else if (event.code === "END") queue = queue.then(async () => {
|
|
208
|
+
const key = [...externals].sort().join("\n");
|
|
209
|
+
if (key !== vendorKey) {
|
|
210
|
+
vendorKey = key;
|
|
211
|
+
await buildVendor(options, outDir, [...externals].sort());
|
|
212
|
+
}
|
|
213
|
+
const result = finish(options, platform, outDir, ++count);
|
|
214
|
+
onBuild?.({
|
|
215
|
+
...result,
|
|
216
|
+
ms: Math.round(performance.now() - started),
|
|
217
|
+
count
|
|
218
|
+
});
|
|
219
|
+
}).catch((error) => console.error(error));
|
|
220
|
+
});
|
|
221
|
+
return { close: () => watcher.close() };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* 插件的 transform 按文件内容缓存:监听模式下每次重打都会把应用的每个文件再交给插件编译一遍(TSX 编译最费时),
|
|
225
|
+
* 内容没变的直接用上次的结果
|
|
226
|
+
*/
|
|
227
|
+
async function cacheTransforms(options) {
|
|
228
|
+
return (await flatPlugins(options)).map((plugin) => {
|
|
229
|
+
const hook = plugin.transform;
|
|
230
|
+
if (!hook) return plugin;
|
|
231
|
+
const handler = typeof hook === "function" ? hook : hook.handler;
|
|
232
|
+
const cache = /* @__PURE__ */ new Map();
|
|
233
|
+
const cached = async function(code, id, ...rest) {
|
|
234
|
+
const hit = cache.get(id);
|
|
235
|
+
if (hit?.code === code) return hit.result;
|
|
236
|
+
const result = await handler.call(this, code, id, ...rest);
|
|
237
|
+
cache.set(id, {
|
|
238
|
+
code,
|
|
239
|
+
result
|
|
240
|
+
});
|
|
241
|
+
return result;
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
...plugin,
|
|
245
|
+
transform: typeof hook === "function" ? cached : {
|
|
246
|
+
...hook,
|
|
247
|
+
handler: cached
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/** Vite 的插件选项可以嵌套数组、Promise,展开成插件列表 */
|
|
253
|
+
async function flatPlugins(option) {
|
|
254
|
+
const value = await option;
|
|
255
|
+
if (Array.isArray(value)) return (await Promise.all(value.map(flatPlugins))).flat();
|
|
256
|
+
return value ? [value] : [];
|
|
257
|
+
}
|
|
258
|
+
/** 应用的包里对依赖的 require 换成从 vtp-vendor.js 取(小程序的 require 只认相对路径),顺带记下用到了哪些依赖 */
|
|
259
|
+
function vendorRequires(externals) {
|
|
260
|
+
return {
|
|
261
|
+
name: "vtp-mini:vendor-requires",
|
|
262
|
+
renderChunk(code, chunk) {
|
|
263
|
+
for (const id of chunk.imports) if (isBare(id)) externals.add(id);
|
|
264
|
+
return {
|
|
265
|
+
code: code.replace(/require\((["'])([^"']+)\1\)/g, (match, _quote, id) => externals.has(id) ? `require("./${VENDOR}").modules[${JSON.stringify(id)}]` : match),
|
|
266
|
+
map: null
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** 依赖单独成包:各依赖的模块对象放进 modules(键是包名),补全全局函数的模块排在最前面 */
|
|
272
|
+
async function buildVendor(options, outDir, ids) {
|
|
273
|
+
const file = join(cacheDir(options.root), "vendor-entry.js");
|
|
274
|
+
writeFileSync(file, [
|
|
275
|
+
`import '@zerodep-dev/vue-tsx-pro-mini/globals';`,
|
|
276
|
+
...ids.map((id, index) => `import * as m${index} from ${JSON.stringify(id)};`),
|
|
277
|
+
`export const modules = { ${ids.map((id, index) => `${JSON.stringify(id)}: m${index}`).join(", ")} };`
|
|
278
|
+
].join("\n"));
|
|
279
|
+
const { config } = prepare(options, false, file);
|
|
280
|
+
await build({
|
|
281
|
+
...config,
|
|
282
|
+
plugins: [],
|
|
283
|
+
build: {
|
|
284
|
+
...config.build,
|
|
285
|
+
emptyOutDir: false,
|
|
286
|
+
sourcemap: false,
|
|
287
|
+
lib: {
|
|
288
|
+
entry: file,
|
|
289
|
+
formats: ["cjs"],
|
|
290
|
+
fileName: () => VENDOR
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** 两种构建共用的 Vite 配置 */
|
|
296
|
+
function prepare(options, watch, entryFile) {
|
|
297
|
+
const platform = PLATFORMS[options.platform];
|
|
298
|
+
const outDir = resolve(options.root, options.outDir ?? `dist/${platform.name}`);
|
|
299
|
+
const entry = entryFile ?? writeEntry(options.root, resolve(options.root, options.entry ?? "src/app.ts"));
|
|
300
|
+
const mode = options.development ? "development" : "production";
|
|
301
|
+
return {
|
|
302
|
+
config: {
|
|
303
|
+
root: options.root,
|
|
304
|
+
configFile: false,
|
|
305
|
+
logLevel: "warn",
|
|
306
|
+
mode,
|
|
307
|
+
plugins: options.plugins ?? [],
|
|
308
|
+
define: {
|
|
309
|
+
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
310
|
+
__VUE_OPTIONS_API__: "false",
|
|
311
|
+
__VUE_PROD_DEVTOOLS__: "false",
|
|
312
|
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "false"
|
|
313
|
+
},
|
|
314
|
+
build: {
|
|
315
|
+
outDir,
|
|
316
|
+
emptyOutDir: !watch,
|
|
317
|
+
minify: !options.development,
|
|
318
|
+
sourcemap: !!options.development,
|
|
319
|
+
target: "es2017",
|
|
320
|
+
lib: {
|
|
321
|
+
entry,
|
|
322
|
+
formats: ["cjs"],
|
|
323
|
+
fileName: () => "vtp.js"
|
|
324
|
+
},
|
|
325
|
+
rolldownOptions: { output: {
|
|
326
|
+
exports: "named",
|
|
327
|
+
codeSplitting: false
|
|
328
|
+
} },
|
|
329
|
+
watch: watch ? {} : null
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
outDir,
|
|
333
|
+
platform
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 打完之后:在 Node 里加载包(运行时只在页面 onLoad 时才碰小程序的全局 API)取页面列表与静态样式,写出小程序的其余文件。
|
|
338
|
+
* 包(与依赖包)复制到缓存目录里一个声明了 CommonJS 的子目录再 require(应用目录多半是 type: module);
|
|
339
|
+
* 每次一个新目录,躲开 require 的缓存
|
|
340
|
+
*/
|
|
341
|
+
function finish(options, platform, outDir, count) {
|
|
342
|
+
const dir = join(cacheDir(options.root), `run-${platform.name}-${count}`);
|
|
343
|
+
mkdirSync(dir, { recursive: true });
|
|
344
|
+
writeFileSync(join(dir, "package.json"), "{\"type\":\"commonjs\"}");
|
|
345
|
+
for (const name of ["vtp.js", VENDOR]) if (existsSync(join(outDir, name))) copyFileSync(join(outDir, name), join(dir, name));
|
|
346
|
+
const bundle = createRequire(join(dir, "vtp.js"))(join(dir, "vtp.js"));
|
|
347
|
+
rmSync(dir, {
|
|
348
|
+
recursive: true,
|
|
349
|
+
force: true
|
|
350
|
+
});
|
|
351
|
+
emit(outDir, platform, bundle, options);
|
|
352
|
+
return {
|
|
353
|
+
outDir,
|
|
354
|
+
pages: bundle.names
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/** 构建的中间文件放在应用的 node_modules/.cache 下,不污染源码目录 */
|
|
358
|
+
function cacheDir(root) {
|
|
359
|
+
const dir = join(root, "node_modules", ".cache", "vtp-mini");
|
|
360
|
+
mkdirSync(dir, { recursive: true });
|
|
361
|
+
return dir;
|
|
362
|
+
}
|
|
363
|
+
/** 打包入口:应用的页面加上静态样式的导出 */
|
|
364
|
+
function writeEntry(root, appEntry) {
|
|
365
|
+
const dir = cacheDir(root);
|
|
366
|
+
const file = join(dir, "entry.js");
|
|
367
|
+
writeFileSync(file, [
|
|
368
|
+
`import '@zerodep-dev/vue-tsx-pro-mini/globals';`,
|
|
369
|
+
`import pages from ${JSON.stringify(appEntry.replaceAll("\\", "/"))};`,
|
|
370
|
+
`export { styleText } from '@zerodep-dev/vue-tsx-pro-tsx/styles';`,
|
|
371
|
+
"export const names = pages.names;",
|
|
372
|
+
"export const page = (name) => pages.page(name);",
|
|
373
|
+
`export { dispatchToCurrentPage as eh } from '@zerodep-dev/vue-tsx-pro-mini';`
|
|
374
|
+
].join("\n"));
|
|
375
|
+
return file;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* 递归组件的脚本:节点数据从属性 i、样式字典从 sd 传进来,事件交给运行时(页面栈顶的页面)。
|
|
379
|
+
* 不渲染组件自己的节点(virtualHost),也用 app 的样式表(微信默认把组件的样式隔开)
|
|
380
|
+
*/
|
|
381
|
+
function componentScript(platform) {
|
|
382
|
+
return `const { eh } = require('../../vtp.js');\nComponent(${platform.name === "wx" ? `{ options: { virtualHost: true, styleIsolation: 'apply-shared' }, properties: { i: { type: Object, value: {} }, sd: { type: Object, value: {} } }, methods: { eh } }` : `{ options: { virtualHost: true }, props: { i: {}, sd: {} }, methods: { eh } }`});\n`;
|
|
383
|
+
}
|
|
384
|
+
function emit(outDir, platform, bundle, options) {
|
|
385
|
+
const write = (path, content) => {
|
|
386
|
+
mkdirSync(join(outDir, path, ".."), { recursive: true });
|
|
387
|
+
writeFileSync(join(outDir, path), content);
|
|
388
|
+
};
|
|
389
|
+
const pages = bundle.names.map((name) => `pages/${name}/index`);
|
|
390
|
+
const usingComponents = { "vtp-comp": "/components/vtp-comp/index" };
|
|
391
|
+
write("app.js", "require('./vtp.js');\nApp({});\n");
|
|
392
|
+
write("app.json", JSON.stringify({
|
|
393
|
+
pages,
|
|
394
|
+
window: windowConfig(platform, options.window),
|
|
395
|
+
usingComponents,
|
|
396
|
+
...platform.name === "wx" ? wechatAppFields(options) : {}
|
|
397
|
+
}, null, 2));
|
|
398
|
+
write(`app.${platform.style}`, appStyles(bundle.styleText()));
|
|
399
|
+
write(`base.${platform.template}`, baseTemplate(platform));
|
|
400
|
+
for (const name of bundle.names) {
|
|
401
|
+
write(`pages/${name}/index.js`, `Page(require('../../vtp.js').page(${JSON.stringify(name)}));\n`);
|
|
402
|
+
write(`pages/${name}/index.json`, JSON.stringify({ usingComponents }));
|
|
403
|
+
write(`pages/${name}/index.${platform.template}`, pageTemplate(platform));
|
|
404
|
+
}
|
|
405
|
+
write("components/vtp-comp/index.json", JSON.stringify({
|
|
406
|
+
component: true,
|
|
407
|
+
usingComponents
|
|
408
|
+
}));
|
|
409
|
+
write(`components/vtp-comp/index.${platform.template}`, componentTemplate(platform));
|
|
410
|
+
write("components/vtp-comp/index.js", componentScript(platform));
|
|
411
|
+
if (platform.name === "wx") write("project.config.json", JSON.stringify({
|
|
412
|
+
appid: options.appId ?? "touristappid",
|
|
413
|
+
compileType: "miniprogram",
|
|
414
|
+
libVersion: "latest",
|
|
415
|
+
setting: {
|
|
416
|
+
es6: true,
|
|
417
|
+
minified: false,
|
|
418
|
+
urlCheck: false
|
|
419
|
+
}
|
|
420
|
+
}, null, 2));
|
|
421
|
+
else {
|
|
422
|
+
write("mini.project.json", JSON.stringify({
|
|
423
|
+
format: 2,
|
|
424
|
+
compileOptions: {}
|
|
425
|
+
}, null, 2));
|
|
426
|
+
write(".browserslistrc", "defaults and fully supports es6-module");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
export { buildMini, defineMiniConfig, watchMini };
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|