@zerodep-dev/vue-tsx-pro-tsx 0.0.0-stage → 0.1.0
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 +44 -2
- package/dist/compile-worker.d.ts +1 -0
- package/dist/compile-worker.js +23 -0
- package/dist/compiler-BDi92tYb.js +1647 -0
- package/dist/compiler.d.ts +104 -0
- package/dist/compiler.js +1 -0
- package/dist/detect-s3UM7xuN.js +17 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +283 -0
- package/dist/module--rQ2-rvm.js +194 -0
- package/dist/ssr-register-CZ_lIN2f.js +17 -0
- package/dist/state-YxL7IqUO.js +108 -0
- package/dist/styles.d.ts +15402 -0
- package/dist/styles.js +1282 -0
- package/dist/vite.d.ts +31 -0
- package/dist/vite.js +509 -0
- package/package.json +98 -3
- package/types/jsx-runtime.d.ts +174 -0
- package/types/runes.d.ts +100 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { FileResult, PluginItem, PluginObject, PluginPass, types } from "@babel/core";
|
|
2
|
+
import typescript from "@babel/plugin-transform-typescript";
|
|
3
|
+
import { VueJSXPluginOptions } from "@vue/babel-plugin-jsx";
|
|
4
|
+
//#region src/compiler/detect.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 编译之前的快速判断(不依赖 Babel):Vite 预设的入口只用这些,开发服务器启动时不必载入 Babel 与各插件
|
|
7
|
+
* (它们在编译线程里、第一次编译时才载入)。
|
|
8
|
+
*/
|
|
9
|
+
/** 文件的语言:.tsx、.jsx 才编译 JSX,.tsx 才在编译时去掉类型(.ts 的类型由打包工具去掉) */
|
|
10
|
+
type Language = 'ts' | 'tsx' | 'js' | 'jsx';
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/compiler/errors.d.ts
|
|
13
|
+
/** 符文误用时抛出的错误:带代码帧的说明,以及供编辑器/语言服务定位的起止位置 */
|
|
14
|
+
interface RunesError extends Error {
|
|
15
|
+
/** 出错代码的起止位置(行从 1、列从 0 开始,同 Babel) */
|
|
16
|
+
loc?: {
|
|
17
|
+
start: {
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
};
|
|
21
|
+
end: {
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** 编译警告:说明、位置,以及带代码帧的完整文本(给 Vite 输出) */
|
|
28
|
+
interface RunesWarning {
|
|
29
|
+
/** 说明文字 */
|
|
30
|
+
message: string;
|
|
31
|
+
/** 起止位置,同 RunesError.loc */
|
|
32
|
+
loc?: RunesError['loc'];
|
|
33
|
+
/** 带代码帧的完整文本 */
|
|
34
|
+
frame: string;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/compiler/compile.d.ts
|
|
38
|
+
/** 编译结果的 source map(Babel 生成的) */
|
|
39
|
+
type SourceMap = NonNullable<FileResult['map']>;
|
|
40
|
+
/** 去掉类型的插件(@babel/plugin-transform-typescript)的选项 */
|
|
41
|
+
type TypeScriptOptions = Parameters<typeof typescript>[1];
|
|
42
|
+
/** compile 的选项;没开的步骤不做 */
|
|
43
|
+
interface CompileOptions {
|
|
44
|
+
/** 文件路径(不带查询参数):报错与 source map 用它 */
|
|
45
|
+
filename: string;
|
|
46
|
+
/** 文件的语言,默认按扩展名;Vue 单文件组件里 lang="tsx" 的脚本由调用方传 'tsx' */
|
|
47
|
+
lang?: Language;
|
|
48
|
+
/** 编译符文与 _component 的 props,默认 true;依赖包里的文件传 false */
|
|
49
|
+
runes?: boolean;
|
|
50
|
+
/** 渲染优化(optimize.ts),默认 true */
|
|
51
|
+
optimize?: boolean;
|
|
52
|
+
/** JSX 里的动态样式调用改成响应式绑定(style-bindings.ts) */
|
|
53
|
+
styleBindings?: boolean;
|
|
54
|
+
/** 开发服务器:样式热更新原地生效,值是模块 id(相对项目根目录,style-hot.ts) */
|
|
55
|
+
styleHot?: string;
|
|
56
|
+
/** Vue JSX 插件的选项 */
|
|
57
|
+
jsx?: VueJSXPluginOptions;
|
|
58
|
+
/** 去掉 TS 类型的插件的选项 */
|
|
59
|
+
typescript?: TypeScriptOptions;
|
|
60
|
+
/** 追加在最后一遍末尾的插件(只在文件需要编译时运行) */
|
|
61
|
+
plugins?: PluginItem[];
|
|
62
|
+
/** 生成 source map,默认 true */
|
|
63
|
+
sourceMaps?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/** compile 的结果 */
|
|
66
|
+
interface CompileResult {
|
|
67
|
+
/** 编译后的代码 */
|
|
68
|
+
code: string;
|
|
69
|
+
/** 对应原始代码的 source map;关掉 sourceMaps 时为 null */
|
|
70
|
+
map: SourceMap | null;
|
|
71
|
+
/** 编译后的 AST:热更新据此找出文件里声明的组件 */
|
|
72
|
+
ast: types.File;
|
|
73
|
+
/** 不影响正确性、但多半不是作者本意的写法(符文) */
|
|
74
|
+
warnings: RunesWarning[];
|
|
75
|
+
/** 开了 styleHot 且文件里有顶层样式时:去掉样式参数后的源码哈希(style-hot.ts) */
|
|
76
|
+
styleSkeleton?: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 编译一个文件;什么都不用做(没有符文、样式绑定与热更新,也不是 JSX 文件)时返回 null。
|
|
80
|
+
* 写法错误时抛出带代码帧的错误。
|
|
81
|
+
*/
|
|
82
|
+
export declare function compile(code: string, options: CompileOptions): Promise<CompileResult | null>;
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/compiler/transform.d.ts
|
|
85
|
+
/** 符文编译的选项 */
|
|
86
|
+
interface RunesOptions {
|
|
87
|
+
/** 渲染优化(稳定插槽、缓存属性里的函数与常量,见 optimize.ts),默认 true */
|
|
88
|
+
optimize?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** 符文编译的 Babel 插件:误用直接抛出 RunesError,警告放在文件的 metadata.runesWarnings */
|
|
91
|
+
export declare function runesBabelPlugin(): PluginObject<PluginPass<RunesOptions>>;
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/compiler/index.d.ts
|
|
94
|
+
/**
|
|
95
|
+
* 只编译一个文件里的符文与 _component 参数解构;不需要编译时返回 null。
|
|
96
|
+
* 结果带 source map 与警告(只拿到初始值的读取等),误用直接抛出 RunesError。
|
|
97
|
+
*/
|
|
98
|
+
export declare function compileRunes(code: string, filename?: string, options?: RunesOptions): Promise<{
|
|
99
|
+
code: string;
|
|
100
|
+
map: SourceMap | null;
|
|
101
|
+
warnings: RunesWarning[];
|
|
102
|
+
} | null>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export type { CompileOptions, CompileResult, RunesError, RunesOptions, RunesWarning, SourceMap };
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as compile, t as compileRunes, r as runesBabelPlugin } from "./compiler-BDi92tYb.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/compiler/detect.ts
|
|
2
|
+
/** 样式入口(src/styles):编译器按从它导入的 create 找样式 */
|
|
3
|
+
const STYLE_SOURCE = "@zerodep-dev/vue-tsx-pro-tsx/styles";
|
|
4
|
+
/**
|
|
5
|
+
* 模块的语言:Vue 单文件组件里的脚本(X.vue?vue&type=script&lang.tsx)看查询参数,其他看扩展名;
|
|
6
|
+
* 不是脚本时返回 undefined
|
|
7
|
+
*/
|
|
8
|
+
function languageOf(id) {
|
|
9
|
+
const [file = "", query = ""] = id.split("?");
|
|
10
|
+
return (/(?:^|&)lang\.([jt]sx?)(?:&|$)/.exec(query) ?? /\.[cm]?([jt]sx?)$/.exec(file))?.[1];
|
|
11
|
+
}
|
|
12
|
+
/** Babel 解析插件:.ts 不能开 JSX,否则 <T>value 类型断言、<T>(x) => x 泛型箭头函数会被当作 JSX 解析 */
|
|
13
|
+
function parserPlugins(lang) {
|
|
14
|
+
return lang === "ts" ? ["typescript"] : ["jsx", "typescript"];
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { languageOf as n, parserPlugins as r, STYLE_SOURCE as t };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { InjectionKey, Ref, SetupContext, VNode, VNodeChild } from "vue";
|
|
2
|
+
//#region src/runtime/component.d.ts
|
|
3
|
+
/** setup 返回渲染函数;setup 本身只执行一次 */
|
|
4
|
+
type Render = () => VNodeChild;
|
|
5
|
+
/**
|
|
6
|
+
* 组件在类型上是“接收 props 的函数”,返回值带 '~tsx' 标记:JSX 类型(types/jsx-runtime.d.ts)
|
|
7
|
+
* 据此把它和 Vue 的其他函数式组件区分开,只有它的子节点按 children prop 检查。
|
|
8
|
+
* 标记放在返回值上而不是函数上,泛型组件的类型参数推导才不受影响。
|
|
9
|
+
*/
|
|
10
|
+
interface TsxNode extends VNode {
|
|
11
|
+
/** 只存在于类型上的标记,运行时没有这个属性 */
|
|
12
|
+
readonly '~tsx': true;
|
|
13
|
+
}
|
|
14
|
+
/** _component 的结果在类型上的样子:JSX 按参数 P 检查属性与 children */
|
|
15
|
+
type TsxComponent<P> = (props: P) => TsxNode;
|
|
16
|
+
/** _component 的第二个参数;一般不用写,编译器会补上 */
|
|
17
|
+
interface ComponentOptions {
|
|
18
|
+
/** 组件名,DevTools 与警告信息里显示;编译器从 const X = _component(...) 自动补上 */
|
|
19
|
+
name?: string;
|
|
20
|
+
/** Vue 的 props 声明(名字列表);编译器从参数解构自动生成 */
|
|
21
|
+
props?: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 类型上把组件描述为“接收 props 的函数”:JSX 直接按参数类型检查属性,
|
|
25
|
+
* 泛型组件 _component(<T,>(props: { items: T[] }) => …) 的类型参数也能保留下来。
|
|
26
|
+
*/
|
|
27
|
+
export declare function _component<P extends object = {}>(setup: (props: P) => Render, options?: ComponentOptions): TsxComponent<P>;
|
|
28
|
+
/**
|
|
29
|
+
* 参数解构里的 ...rest(编译器生成):没有声明为 prop 的属性(Vue 的 attrs),再加上 children。
|
|
30
|
+
* TypeScript 推出的 rest 类型含 children,运行时也要有:`<Collapse {...rest} />` 才能把内容转交下去。
|
|
31
|
+
* children 读取时才执行插槽;没有内容时不出现在键里。展开到原生元素前由 ui 的 elementProps 去掉。
|
|
32
|
+
* 已经单独解构出 children 时(withChildren = false)rest 里没有它,上层转交下来的 children 属性也去掉。
|
|
33
|
+
*/
|
|
34
|
+
export declare function restProps(props: {
|
|
35
|
+
children?: unknown;
|
|
36
|
+
}, ctx: SetupContext, withChildren?: boolean): object;
|
|
37
|
+
/** 交给子组件的插槽对象;$stable 告诉 Vue 父组件重新渲染时不必为了插槽更新子组件 */
|
|
38
|
+
type ForwardedSlots = {
|
|
39
|
+
default: unknown;
|
|
40
|
+
$stable: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* 本组件收到的内容是否稳定(编译器生成的调用):没有内容,或上层的插槽标了 $stable。
|
|
44
|
+
* 内容经 children 属性传来时不算(attrs 不是响应式的)
|
|
45
|
+
*/
|
|
46
|
+
export declare function childrenStable(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* 原样转交内容 <X>{children}</X>(编译器生成的调用):稳不稳定跟着本组件收到的,
|
|
49
|
+
* 上层的内容每次都变,交出去的也每次都变;带参数的渲染函数原样交出,由 X 调用。
|
|
50
|
+
*/
|
|
51
|
+
export declare function forwardChildren(props: {
|
|
52
|
+
children?: unknown;
|
|
53
|
+
}): ForwardedSlots;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/runtime/context.d.ts
|
|
56
|
+
/** createContext 的结果:同一个键的提供与读取 */
|
|
57
|
+
interface Context<T> {
|
|
58
|
+
/** Vue 的注入键:应用级提供时 `app.provide(Context.key, value)` */
|
|
59
|
+
readonly key: InjectionKey<T>;
|
|
60
|
+
/** 在组件 setup 里向子树提供值 */
|
|
61
|
+
provide(value: T): void;
|
|
62
|
+
/** 读取最近的祖先提供的值;没有时报错,说明要放在哪个组件之内 */
|
|
63
|
+
inject(): T;
|
|
64
|
+
/** 读取最近的祖先提供的值;没有时(包括在 setup 之外调用)返回 fallback */
|
|
65
|
+
inject<F>(fallback: F): T | F;
|
|
66
|
+
}
|
|
67
|
+
/** 声明一个带类型的上下文;name 用于报错信息(“需要放在 <name> 之内”)与调试 */
|
|
68
|
+
export declare function createContext<T>(name: string): Context<T>;
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/runtime/props.d.ts
|
|
71
|
+
type Props = Record<string, unknown>;
|
|
72
|
+
type Emit = (event: string, ...args: unknown[]) => void;
|
|
73
|
+
/** 编译器原样传入的字面量默认值 */
|
|
74
|
+
type Literal = string | number | boolean | bigint | null;
|
|
75
|
+
/**
|
|
76
|
+
* 普通 prop:父级传了值就用父级的,否则用默认值;组件内可以临时覆盖(见 overridable),
|
|
77
|
+
* 父级传入新值时以父级为准。
|
|
78
|
+
*
|
|
79
|
+
* 默认值第一次用到时才求值,并用 computed 缓存:
|
|
80
|
+
* - [] / {} / 函数调用每个实例各自一份,读多少次都是同一个值
|
|
81
|
+
* - 引用了其他 prop 的默认值(icon = pick(size))会随那个 prop 更新
|
|
82
|
+
*/
|
|
83
|
+
export declare function prop<T>(props: Props, key: string, fallback?: () => T): Ref<T>;
|
|
84
|
+
/**
|
|
85
|
+
* 组件里从没写入过的普通 prop(编译器确定时生成,大多数 prop 是这样):只读的 getter ref,
|
|
86
|
+
* 不像 prop 那样每个实例再建 computed、customRef 与同步 watch,创建快十倍。
|
|
87
|
+
* 默认值的处理与 prop 相同;有默认值时在 setup 里先读一次(prop 的 watch 也是这时求值),
|
|
88
|
+
* 默认值里调用的组合式函数(useId() 等)才拿得到组件实例
|
|
89
|
+
*/
|
|
90
|
+
export declare function readonlyProp<T>(props: Props, key: string, fallback?: (() => T) | Literal): Ref<T>;
|
|
91
|
+
/**
|
|
92
|
+
* 解构出的 children:每次读取都重新执行插槽,不缓存(普通 prop 用 computed 缓存)。
|
|
93
|
+
* 父级重新渲染时传来新的插槽,里面可能只有父级渲染时算好的值、没有响应式读取:
|
|
94
|
+
* 缓存不会失效,子组件就一直显示旧内容。子组件因父级更新而重新渲染时读到的总是新插槽。
|
|
95
|
+
*/
|
|
96
|
+
export declare function childrenProp<T>(props: Props, fallback?: () => T): Ref<T>;
|
|
97
|
+
/**
|
|
98
|
+
* 双向 prop,走标准 v-model 协议(父级 _属性={可写的值} 编译为 key + onUpdate:key)。
|
|
99
|
+
* 与 Vue 的 useModel 一样,父级同时传了值和 onUpdate:key 才算受控:
|
|
100
|
+
*
|
|
101
|
+
* - 受控:值以父级为准。写入后同步读取先拿到新值,本轮更新结束时回到父级的值,
|
|
102
|
+
* 所以父级拒绝或修正(例如限制最大值)时组件会跟着回去
|
|
103
|
+
* - 只传了值:作为初始值,组件可以在本地修改,父级传入新值时以父级为准
|
|
104
|
+
* - 都没传:就是组件自己的状态
|
|
105
|
+
*
|
|
106
|
+
* 父级传 undefined 等同没传,使用默认值(与普通 prop 一致);写入总会发出 update:key。
|
|
107
|
+
*/
|
|
108
|
+
export declare function bindable<T>(props: Props, key: string, fallback: (() => T) | undefined, emit: Emit): Ref<T>;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/runtime/state.d.ts
|
|
111
|
+
/**
|
|
112
|
+
* 跟随来源、可以临时覆盖的值(同 Svelte 5.25 的可覆盖派生):
|
|
113
|
+
* 读取时是 getter 的结果;写入后读到写入的值,直到 getter 的结果变化,再回到跟随来源。
|
|
114
|
+
*
|
|
115
|
+
* let likes = _derived(post.likes) 乐观更新:likes += 1,post.likes 一变就以它为准
|
|
116
|
+
* ({ value }) => … value = x 普通 prop:只改组件本地,父级传入新值时以父级为准
|
|
117
|
+
*/
|
|
118
|
+
export declare function overridable<T>(getter: () => T): Ref<T>;
|
|
119
|
+
/**
|
|
120
|
+
* 读取但不追踪(同 Svelte 的 untrack):在共用的读取 effect 里执行,读到的响应式值记在它身上、
|
|
121
|
+
* 变化时什么也不做,外层的 _effect、_derived、渲染函数不会因此依赖它们。用 Vue 公开的 ReactiveEffect,不碰内部 API。
|
|
122
|
+
* 渲染里会频繁调用(样式的响应式绑定每次渲染取一次值),所以复用同一个 effect,不每次新建作用域与 effect
|
|
123
|
+
*/
|
|
124
|
+
export declare function untrack<T>(read: () => T): T;
|
|
125
|
+
/**
|
|
126
|
+
* 去掉响应式代理的静态深拷贝(同 Svelte 的 $state.snapshot),交给 structuredClone、第三方库或日志。
|
|
127
|
+
* 普通对象、数组、Map、Set 逐层拷贝,Date、类型化数组、ArrayBuffer 复制一份,有 toJSON 的取它的结果,
|
|
128
|
+
* 其他值(类实例、函数等)原样保留。
|
|
129
|
+
*/
|
|
130
|
+
export declare function snapshot<T>(value: T): T;
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/runtime/effect.d.ts
|
|
133
|
+
/** 副作用函数可以返回清理函数:下次执行前、以及销毁时调用 */
|
|
134
|
+
type EffectFn = () => void | (() => void);
|
|
135
|
+
/** _effect / _effect.pre 编译后的调用:在当前作用域里创建,随组件销毁 */
|
|
136
|
+
export declare function effect(fn: EffectFn, flush?: 'pre' | 'post'): void;
|
|
137
|
+
/** _effect.root 编译后的调用:独立的作用域,返回手动清理函数 */
|
|
138
|
+
export declare function effectRoot(fn: EffectFn): () => void;
|
|
139
|
+
//#endregion
|
|
140
|
+
export type { ComponentOptions, Context, TsxComponent, TsxNode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { n as snapshot, r as untrack, t as overridable } from "./state-YxL7IqUO.js";
|
|
2
|
+
import { computed, customRef, effectScope, getCurrentInstance, getCurrentScope, hasInjectionContext, inject, nextTick, provide, toRef, watch, watchEffect } from "vue";
|
|
3
|
+
//#region src/runtime/component.ts
|
|
4
|
+
/**
|
|
5
|
+
* _component():唯一的组件边界。
|
|
6
|
+
*
|
|
7
|
+
* 一切基于 props:作者只写参数解构与默认值,编译器(compiler/props.ts)把它们换成
|
|
8
|
+
* prop 访问器,并补上组件名与 props 声明作为第二个参数。运行时这里只做两件事:
|
|
9
|
+
*
|
|
10
|
+
* 1. 返回一个普通的 Vue 组件对象(Vue 组件本来就是对象,不需要 defineComponent)
|
|
11
|
+
* 2. 把默认插槽映射成 props.children,内容也是 prop
|
|
12
|
+
*/
|
|
13
|
+
function _component(setup, options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
inheritAttrs: false,
|
|
16
|
+
...options,
|
|
17
|
+
setup: (props, ctx) => setup(withChildren(props, ctx, getCurrentInstance()), ctx)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** withChildren 包着的 Vue props 本身 */
|
|
21
|
+
const VUE_PROPS = Symbol("vueProps");
|
|
22
|
+
/** 只拦截 children,其余属性原样交给 Vue 的 props(保持依赖收集) */
|
|
23
|
+
function withChildren(props, ctx, instance) {
|
|
24
|
+
return new Proxy(props, { get(target, key, receiver) {
|
|
25
|
+
if (key === "children") return readChildren(ctx, instance);
|
|
26
|
+
return key === VUE_PROPS ? target : Reflect.get(target, key, receiver);
|
|
27
|
+
} });
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 编译器传给 prop 访问器的 props 背后的 Vue props:访问器只读声明过的 prop,直接读它(同样是响应式的),
|
|
31
|
+
* 不必每次再穿过 withChildren 这层 Proxy(渲染时读 prop 是最频繁的操作之一)
|
|
32
|
+
*/
|
|
33
|
+
function vueProps(props) {
|
|
34
|
+
return props[VUE_PROPS] ?? props;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 参数解构里的 ...rest(编译器生成):没有声明为 prop 的属性(Vue 的 attrs),再加上 children。
|
|
38
|
+
* TypeScript 推出的 rest 类型含 children,运行时也要有:`<Collapse {...rest} />` 才能把内容转交下去。
|
|
39
|
+
* children 读取时才执行插槽;没有内容时不出现在键里。展开到原生元素前由 ui 的 elementProps 去掉。
|
|
40
|
+
* 已经单独解构出 children 时(withChildren = false)rest 里没有它,上层转交下来的 children 属性也去掉。
|
|
41
|
+
*/
|
|
42
|
+
function restProps(props, ctx, withChildren = true) {
|
|
43
|
+
const hasChildren = () => withChildren && (ctx.slots.default != null || "children" in ctx.attrs);
|
|
44
|
+
return new Proxy(ctx.attrs, {
|
|
45
|
+
get: (target, key, receiver) => {
|
|
46
|
+
if (key !== "children") return Reflect.get(target, key, receiver);
|
|
47
|
+
return withChildren ? props.children : void 0;
|
|
48
|
+
},
|
|
49
|
+
has: (target, key) => key === "children" ? hasChildren() : Reflect.has(target, key),
|
|
50
|
+
ownKeys: (target) => {
|
|
51
|
+
const keys = Reflect.ownKeys(target);
|
|
52
|
+
const index = keys.indexOf("children");
|
|
53
|
+
if (index >= 0) keys.splice(index, 1);
|
|
54
|
+
if (hasChildren()) keys.push("children");
|
|
55
|
+
return keys;
|
|
56
|
+
},
|
|
57
|
+
getOwnPropertyDescriptor: (target, key) => {
|
|
58
|
+
if (key !== "children") return Reflect.getOwnPropertyDescriptor(target, key);
|
|
59
|
+
return hasChildren() ? {
|
|
60
|
+
value: props.children,
|
|
61
|
+
enumerable: true,
|
|
62
|
+
configurable: true,
|
|
63
|
+
writable: false
|
|
64
|
+
} : void 0;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* JSX 子节点被编译成默认插槽,这里还原成 children prop:
|
|
70
|
+
* - 带参数的函数子节点(<List>{(item) => …}</List>)是渲染函数,原样交给组件按需调用
|
|
71
|
+
* - 其余内容在读取时才执行插槽:读取发生在子组件的渲染函数里,内容是惰性的,依赖也归属子组件
|
|
72
|
+
*
|
|
73
|
+
* Vue 会把插槽统一包一层 (...args) => …,参数个数只能从 vnode 上的原始插槽判断。
|
|
74
|
+
*/
|
|
75
|
+
function readChildren(ctx, instance) {
|
|
76
|
+
const raw = instance.vnode.children?.default;
|
|
77
|
+
if (typeof raw === "function" && raw.length > 0) return raw;
|
|
78
|
+
return ctx.slots.default?.() ?? ctx.attrs.children;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 本组件收到的内容是否稳定(编译器生成的调用):没有内容,或上层的插槽标了 $stable。
|
|
82
|
+
* 内容经 children 属性传来时不算(attrs 不是响应式的)
|
|
83
|
+
*/
|
|
84
|
+
function childrenStable() {
|
|
85
|
+
const instance = getCurrentInstance();
|
|
86
|
+
if (!instance || "children" in instance.attrs) return false;
|
|
87
|
+
const received = instance.vnode.children;
|
|
88
|
+
return received == null || received.$stable === true;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 原样转交内容 <X>{children}</X>(编译器生成的调用):稳不稳定跟着本组件收到的,
|
|
92
|
+
* 上层的内容每次都变,交出去的也每次都变;带参数的渲染函数原样交出,由 X 调用。
|
|
93
|
+
*/
|
|
94
|
+
function forwardChildren(props) {
|
|
95
|
+
const raw = getCurrentInstance().vnode.children?.default;
|
|
96
|
+
const $stable = childrenStable();
|
|
97
|
+
if (typeof raw === "function" && raw.length > 0) return {
|
|
98
|
+
default: raw,
|
|
99
|
+
$stable
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
default: () => [props.children],
|
|
103
|
+
$stable
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/runtime/context.ts
|
|
108
|
+
/**
|
|
109
|
+
* 复合组件(Tabs、Select、Form 等)之间共享状态:声明一次,提供与读取都带类型。
|
|
110
|
+
*
|
|
111
|
+
* const TabsContext = createContext<TabsState>('Tabs');
|
|
112
|
+
* TabsContext.provide(state); // 父组件 setup 里
|
|
113
|
+
* const tabs = TabsContext.inject(); // 子组件 setup 里;不在父组件内时报错说明
|
|
114
|
+
* const form = FormContext.inject(null); // 可选:不在父组件内时返回给定的值
|
|
115
|
+
* app.provide(TabsContext.key, state); // 应用级提供(Vue 插件里)
|
|
116
|
+
*/
|
|
117
|
+
const MISSING = Symbol("missing");
|
|
118
|
+
/** 声明一个带类型的上下文;name 用于报错信息(“需要放在 <name> 之内”)与调试 */
|
|
119
|
+
function createContext(name) {
|
|
120
|
+
const key = Symbol(name);
|
|
121
|
+
return {
|
|
122
|
+
key,
|
|
123
|
+
provide: (value) => provide(key, value),
|
|
124
|
+
inject(...fallback) {
|
|
125
|
+
if (!hasInjectionContext()) {
|
|
126
|
+
if (fallback.length) return fallback[0];
|
|
127
|
+
throw new Error(`${name} 的上下文只能在组件的 setup 里读取`);
|
|
128
|
+
}
|
|
129
|
+
const value = inject(key, MISSING);
|
|
130
|
+
if (value !== MISSING) return value;
|
|
131
|
+
if (fallback.length) return fallback[0];
|
|
132
|
+
throw new Error(`${name} 的子组件需要放在 <${name}> 之内使用`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/runtime/props.ts
|
|
138
|
+
/**
|
|
139
|
+
* prop 访问器:编译器把 _component 的参数解构换成对这几个函数的调用。
|
|
140
|
+
* 作者不直接使用它们;思路来自 Svelte 5 的 $.prop。
|
|
141
|
+
*
|
|
142
|
+
* 返回的都是真正的 Vue Ref(读写改写为 .value),因此 _toRef(size) 可以交给 watch 等 Vue API。
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* 普通 prop:父级传了值就用父级的,否则用默认值;组件内可以临时覆盖(见 overridable),
|
|
146
|
+
* 父级传入新值时以父级为准。
|
|
147
|
+
*
|
|
148
|
+
* 默认值第一次用到时才求值,并用 computed 缓存:
|
|
149
|
+
* - [] / {} / 函数调用每个实例各自一份,读多少次都是同一个值
|
|
150
|
+
* - 引用了其他 prop 的默认值(icon = pick(size))会随那个 prop 更新
|
|
151
|
+
*/
|
|
152
|
+
function prop(props, key, fallback) {
|
|
153
|
+
return overridable(propReader(props, key, fallback));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 组件里从没写入过的普通 prop(编译器确定时生成,大多数 prop 是这样):只读的 getter ref,
|
|
157
|
+
* 不像 prop 那样每个实例再建 computed、customRef 与同步 watch,创建快十倍。
|
|
158
|
+
* 默认值的处理与 prop 相同;有默认值时在 setup 里先读一次(prop 的 watch 也是这时求值),
|
|
159
|
+
* 默认值里调用的组合式函数(useId() 等)才拿得到组件实例
|
|
160
|
+
*/
|
|
161
|
+
function readonlyProp(props, key, fallback) {
|
|
162
|
+
if (fallback !== void 0 && typeof fallback !== "function") {
|
|
163
|
+
const source = vueProps(props);
|
|
164
|
+
return toRef(() => source[key] !== void 0 ? source[key] : fallback);
|
|
165
|
+
}
|
|
166
|
+
const read = propReader(props, key, fallback);
|
|
167
|
+
if (fallback) read();
|
|
168
|
+
return toRef(read);
|
|
169
|
+
}
|
|
170
|
+
/** 父级传了值就用父级的,否则用默认值(第一次用到时求值,computed 缓存) */
|
|
171
|
+
function propReader(props, key, fallback) {
|
|
172
|
+
const source = vueProps(props);
|
|
173
|
+
let fallbackValue;
|
|
174
|
+
return () => {
|
|
175
|
+
const value = source[key];
|
|
176
|
+
if (value !== void 0 || !fallback) return value;
|
|
177
|
+
fallbackValue ??= computed(fallback);
|
|
178
|
+
return fallbackValue.value;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 解构出的 children:每次读取都重新执行插槽,不缓存(普通 prop 用 computed 缓存)。
|
|
183
|
+
* 父级重新渲染时传来新的插槽,里面可能只有父级渲染时算好的值、没有响应式读取:
|
|
184
|
+
* 缓存不会失效,子组件就一直显示旧内容。子组件因父级更新而重新渲染时读到的总是新插槽。
|
|
185
|
+
*/
|
|
186
|
+
function childrenProp(props, fallback) {
|
|
187
|
+
return customRef(() => ({
|
|
188
|
+
get: () => props.children ?? fallback?.(),
|
|
189
|
+
set: () => {}
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 双向 prop,走标准 v-model 协议(父级 _属性={可写的值} 编译为 key + onUpdate:key)。
|
|
194
|
+
* 与 Vue 的 useModel 一样,父级同时传了值和 onUpdate:key 才算受控:
|
|
195
|
+
*
|
|
196
|
+
* - 受控:值以父级为准。写入后同步读取先拿到新值,本轮更新结束时回到父级的值,
|
|
197
|
+
* 所以父级拒绝或修正(例如限制最大值)时组件会跟着回去
|
|
198
|
+
* - 只传了值:作为初始值,组件可以在本地修改,父级传入新值时以父级为准
|
|
199
|
+
* - 都没传:就是组件自己的状态
|
|
200
|
+
*
|
|
201
|
+
* 父级传 undefined 等同没传,使用默认值(与普通 prop 一致);写入总会发出 update:key。
|
|
202
|
+
*/
|
|
203
|
+
function bindable(props, key, fallback, emit) {
|
|
204
|
+
const instance = getCurrentInstance();
|
|
205
|
+
const source = vueProps(props);
|
|
206
|
+
const names = [key, hyphenate(key)];
|
|
207
|
+
const listeners = names.map((name) => `onUpdate:${name}`);
|
|
208
|
+
const passed = (candidates) => {
|
|
209
|
+
const raw = instance.vnode.props;
|
|
210
|
+
return !!raw && candidates.some((name) => name in raw);
|
|
211
|
+
};
|
|
212
|
+
const isControlled = () => passed(names) && passed(listeners);
|
|
213
|
+
const fallbackValue = fallback?.();
|
|
214
|
+
const parentValue = () => source[key] !== void 0 ? source[key] : fallbackValue;
|
|
215
|
+
let local = parentValue();
|
|
216
|
+
/** 受控时写入的值,只在本轮更新内有效 */
|
|
217
|
+
let pending;
|
|
218
|
+
let writes = 0;
|
|
219
|
+
return customRef((track, trigger) => {
|
|
220
|
+
watch(() => source[key], (value) => {
|
|
221
|
+
if (value !== void 0) local = value;
|
|
222
|
+
trigger();
|
|
223
|
+
}, { flush: "sync" });
|
|
224
|
+
return {
|
|
225
|
+
get() {
|
|
226
|
+
track();
|
|
227
|
+
if (pending) return pending.value;
|
|
228
|
+
return isControlled() ? parentValue() : local;
|
|
229
|
+
},
|
|
230
|
+
set(value) {
|
|
231
|
+
local = value;
|
|
232
|
+
if (isControlled()) {
|
|
233
|
+
pending = { value };
|
|
234
|
+
const write = ++writes;
|
|
235
|
+
nextTick(() => {
|
|
236
|
+
if (write !== writes) return;
|
|
237
|
+
pending = void 0;
|
|
238
|
+
trigger();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
trigger();
|
|
242
|
+
emit(`update:${key}`, value);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** checkedValue → checked-value:父级可能用连字符写法传入 */
|
|
248
|
+
function hyphenate(name) {
|
|
249
|
+
return name.replace(/\B([A-Z])/g, "-$1").toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/runtime/effect.ts
|
|
253
|
+
/**
|
|
254
|
+
* _effect 的运行时(作者不直接调用):语义同 Svelte 5 的 $effect,底层是 Vue 的 watchEffect。
|
|
255
|
+
*
|
|
256
|
+
* _effect(fn) → effect(fn) 挂载后、每次 DOM 更新后执行
|
|
257
|
+
* _effect.pre(fn) → effect(fn, 'pre') DOM 更新前执行
|
|
258
|
+
* _effect.root(fn) → effectRoot(fn) 不随组件销毁,返回手动清理函数
|
|
259
|
+
*/
|
|
260
|
+
/** _effect / _effect.pre 编译后的调用:在当前作用域里创建,随组件销毁 */
|
|
261
|
+
function effect(fn, flush = "post") {
|
|
262
|
+
if (!getCurrentScope()) throw new Error("_effect 需要在组件函数体、组件里调用的组合式函数、或另一个 _effect 里创建,否则不会自动停止;需要手动管理时用 _effect.root");
|
|
263
|
+
if (typeof document === "undefined") return;
|
|
264
|
+
watchEffect((onCleanup) => {
|
|
265
|
+
const scope = effectScope();
|
|
266
|
+
const cleanup = scope.run(fn);
|
|
267
|
+
onCleanup(() => {
|
|
268
|
+
scope.stop();
|
|
269
|
+
if (typeof cleanup === "function") cleanup();
|
|
270
|
+
});
|
|
271
|
+
}, { flush });
|
|
272
|
+
}
|
|
273
|
+
/** _effect.root 编译后的调用:独立的作用域,返回手动清理函数 */
|
|
274
|
+
function effectRoot(fn) {
|
|
275
|
+
const scope = effectScope(true);
|
|
276
|
+
const cleanup = scope.run(fn);
|
|
277
|
+
return () => {
|
|
278
|
+
scope.stop();
|
|
279
|
+
if (typeof cleanup === "function") cleanup();
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
export { _component, bindable, childrenProp, childrenStable, createContext, effect, effectRoot, forwardChildren, overridable, prop, readonlyProp, restProps, snapshot, untrack };
|