@weapp-vite/glass-easel-web-adapter 0.1.7 → 0.2.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/README.md +84 -3
- package/dist/index.d.mts +35 -28
- package/dist/index.mjs +203 -92
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,10 +1,89 @@
|
|
|
1
1
|
# @weapp-vite/glass-easel-web-adapter
|
|
2
2
|
|
|
3
|
-
实验性的 glass-easel Web
|
|
3
|
+
实验性的 glass-easel Web 组件后端。它使用官方组件树、模板编译器和浏览器 backend,提供独立的组件注册、挂载、数据更新、事件和生命周期桥接。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
每个 adapter 持有自己的 `ComponentSpace`。`usingComponents` 声明组件依赖,父模板中的默认和命名 slot 由官方组件树处理,投影内容随父级数据更新,并保留其事件处理上下文。注册支持 `data`、`properties`、`methods`、`observers`、`options` 和 `lifetimes`。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 组件树示例
|
|
8
|
+
|
|
9
|
+
在浏览器入口显式创建 adapter:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createGlassEaselWebAdapter } from '@weapp-vite/glass-easel-web-adapter'
|
|
13
|
+
|
|
14
|
+
const adapter = createGlassEaselWebAdapter()
|
|
15
|
+
|
|
16
|
+
adapter.registerComponent({
|
|
17
|
+
name: 'demo',
|
|
18
|
+
usingComponents: { counter: '/counter' },
|
|
19
|
+
data: { title: '计数器', last: 0 },
|
|
20
|
+
template: `
|
|
21
|
+
<counter label="{{title}}" bind:change="changed">
|
|
22
|
+
<b slot="heading">{{title}}</b>
|
|
23
|
+
<span>已提交 {{last}}</span>
|
|
24
|
+
</counter>
|
|
25
|
+
`,
|
|
26
|
+
methods: {
|
|
27
|
+
changed(event: { detail: { value: number } }) {
|
|
28
|
+
this.setData({ last: event.detail.value })
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
adapter.registerComponent({
|
|
34
|
+
name: 'counter',
|
|
35
|
+
properties: { label: String },
|
|
36
|
+
data: { count: 0 },
|
|
37
|
+
options: { multipleSlots: true },
|
|
38
|
+
template: `
|
|
39
|
+
<section>
|
|
40
|
+
<header><slot name="heading" /></header>
|
|
41
|
+
<button bind:click="increment">{{label}}:{{count}}</button>
|
|
42
|
+
<slot />
|
|
43
|
+
</section>
|
|
44
|
+
`,
|
|
45
|
+
methods: {
|
|
46
|
+
increment() {
|
|
47
|
+
const value = this.data.count + 1
|
|
48
|
+
this.setData({ count: value })
|
|
49
|
+
this.triggerEvent('change', { value })
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const container = document.createElement('div')
|
|
55
|
+
document.body.append(container)
|
|
56
|
+
const instance = adapter.mountComponent('demo', container)
|
|
57
|
+
|
|
58
|
+
adapter.updateComponent(instance, { title: '新的标题' })
|
|
59
|
+
console.log(adapter.getSnapshot(instance))
|
|
60
|
+
|
|
61
|
+
// 宿主页面卸载时调用;重复清理不会重复触发 detached。
|
|
62
|
+
export function cleanup() {
|
|
63
|
+
adapter.dispose()
|
|
64
|
+
container.remove()
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
可以先注册父组件,再注册子组件;挂载前必须完成整棵树的依赖注册。组件名使用项目内的 POSIX 组件路径,例如 `components/counter`;注册与挂载统一规范化前导 `/`、`.` 和路径内的 `..`,拒绝越出组件空间的路径、反斜杠、query、hash 与 URL scheme。`usingComponents` 支持官方的绝对和相对组件引用。方法与生命周期回调中的 `this` 是官方 core 的 `GeneralComponent`,并非完整的小程序 API adapter。
|
|
69
|
+
|
|
70
|
+
## 实例与资源所有权
|
|
71
|
+
|
|
72
|
+
- `mountComponent` 返回只读的 `GlassEaselWebInstance` 句柄;更新、触发事件、卸载和读取快照都要求由同一个 adapter 创建的实例,不能用普通对象代替。
|
|
73
|
+
- `updateComponent` 交给官方 `setData`,支持数据路径更新。快照读取实际组件数据,因此能观察到方法内部的更新;容器值使用官方深复制隔离快照,函数值保持可调用。列表重排由官方 keyed 更新保留节点身份。
|
|
74
|
+
- 同一个容器可以挂载多个实例,卸载只移除对应的根节点,保留已有兄弟内容。`virtualHost` 根组件由具有实体根节点的内部父组件承载。
|
|
75
|
+
- `unmountComponent` 递归触发官方组件树的 `detached` 并释放 backend 节点;`dispose` 清理 adapter 的全部挂载实例。二者可以重复调用,已卸载实例仍可读取快照。
|
|
76
|
+
- 官方 DOM backend 的事件委托监听绑定到 `document.body`,其 `destroy()` 没有提供移除这些监听的实现。adapter 按文档共享同一个 backend,避免多个根或 adapter 重复派发同一浏览器事件,只向官方树中仍挂载的节点派发。已卸载节点即使重新插入 DOM 也不再处理事件;文档级监听保留到文档销毁,不属于单个实例的卸载范围。
|
|
77
|
+
|
|
78
|
+
旧的第四个 `slots` 参数继续接受调用方控制的模板字符串,但现在会编译成真实父组件的 WXML,由官方 slot 机制投影,更新时不会再手工替换 DOM。需要父级作用域和事件处理的动态 slot 时,使用上面的父子组件模板写法。模板字符串应来自可信的应用源码。
|
|
79
|
+
|
|
80
|
+
## 当前范围
|
|
81
|
+
|
|
82
|
+
这是独立的组件后端实验,尚未接入 `wv dev -p web` / `wv build -p web` 的默认 runtime,也没有新增 renderer 配置。页面路由、WXSS/rpx、微信 API 和小程序原生组件适配仍由后续 Web host 集成承担。
|
|
83
|
+
|
|
84
|
+
当前注册阶段仍在浏览器中调用含 WASM 的模板编译器;生产接入前需要拆出构建期编译入口。官方 core 挂载会触发 `created` / `attached`,卸载会触发 `detached`;`ready` 需要宿主在渲染提交后驱动,当前 adapter 不自动补发。本阶段的浏览器验证不代表微信 DevTools、真机或六个平台的行为已达成一致。
|
|
85
|
+
|
|
86
|
+
所有模式都要求可用的官方 DOM backend;旧 `strict` 选项仅保留调用兼容,已移除字符串渲染降级。
|
|
8
87
|
|
|
9
88
|
## 验证
|
|
10
89
|
|
|
@@ -15,3 +94,5 @@ pnpm --filter @weapp-vite/glass-easel-web-adapter test:types
|
|
|
15
94
|
pnpm --filter @weapp-vite/glass-easel-web-adapter test:browser
|
|
16
95
|
pnpm --filter @weapp-vite/glass-easel-web-adapter build
|
|
17
96
|
```
|
|
97
|
+
|
|
98
|
+
包级测试覆盖组件空间隔离、真实 slot、属性与 observer、路径更新、列表节点身份和递归卸载;浏览器测试使用构建后的包,在一个页面中验证多实例、真实点击和组件事件,并在结束时关闭测试浏览器与服务。
|
package/dist/index.d.mts
CHANGED
|
@@ -1,53 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import * as glassEasel from "glass-easel";
|
|
2
|
+
type ComponentParams = glassEasel.typeUtils.ComponentParams<glassEasel.typeUtils.DataList, glassEasel.typeUtils.PropertyList, glassEasel.typeUtils.MethodList>;
|
|
3
|
+
interface GlassEaselWebHost {
|
|
3
4
|
createElement?: (tagName: string) => HTMLElement;
|
|
4
5
|
dispatchEvent?: (event: Event, context: unknown) => void;
|
|
5
6
|
warn?: (message: string) => void;
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
+
interface GlassEaselComponentDefinition {
|
|
8
9
|
name: string;
|
|
9
10
|
template: string;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
data?: ComponentParams['data'];
|
|
12
|
+
properties?: ComponentParams['properties'];
|
|
13
|
+
methods?: ComponentParams['methods'] & ThisType<glassEasel.GeneralComponent>;
|
|
14
|
+
lifetimes?: ComponentParams['lifetimes'] & ThisType<glassEasel.GeneralComponent>;
|
|
15
|
+
observers?: ComponentParams['observers'] & ThisType<glassEasel.GeneralComponent>;
|
|
16
|
+
usingComponents?: Record<string, string>;
|
|
17
|
+
options?: glassEasel.ComponentOptions;
|
|
13
18
|
}
|
|
14
|
-
|
|
19
|
+
interface GlassEaselWebAdapterOptions {
|
|
15
20
|
host?: GlassEaselWebHost;
|
|
21
|
+
/** 保留旧调用兼容;所有模式都要求官方 DOM backend,不再使用字符串降级渲染。 */
|
|
16
22
|
strict?: boolean;
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
interface GlassEaselWebSnapshot {
|
|
19
25
|
name: string;
|
|
20
26
|
props: Record<string, unknown>;
|
|
21
27
|
html: string;
|
|
22
28
|
lifecycle: string[];
|
|
23
29
|
disposed: boolean;
|
|
24
30
|
}
|
|
25
|
-
|
|
31
|
+
declare const instanceBrand: unique symbol;
|
|
32
|
+
interface GlassEaselWebInstance {
|
|
33
|
+
readonly [instanceBrand]: true;
|
|
34
|
+
readonly root: HTMLElement;
|
|
35
|
+
readonly definition: GlassEaselComponentDefinition;
|
|
36
|
+
readonly props: Readonly<Record<string, unknown>>;
|
|
37
|
+
readonly disposed: boolean;
|
|
38
|
+
readonly native: glassEasel.GeneralComponent;
|
|
39
|
+
}
|
|
40
|
+
interface GlassEaselWebAdapter {
|
|
26
41
|
registerComponent: (definition: GlassEaselComponentDefinition) => void;
|
|
27
42
|
mountComponent: (name: string, container: Element, props?: Record<string, unknown>, slots?: Record<string, string>) => GlassEaselWebInstance;
|
|
28
|
-
updateComponent: (instance:
|
|
29
|
-
triggerEvent: (instance:
|
|
30
|
-
unmountComponent: (instance:
|
|
31
|
-
getSnapshot: (instance:
|
|
43
|
+
updateComponent: (instance: GlassEaselWebInstance, props: Record<string, unknown>) => void;
|
|
44
|
+
triggerEvent: (instance: GlassEaselWebInstance, name: string, detail?: unknown) => void;
|
|
45
|
+
unmountComponent: (instance: GlassEaselWebInstance) => void;
|
|
46
|
+
getSnapshot: (instance: GlassEaselWebInstance) => GlassEaselWebSnapshot;
|
|
32
47
|
dispose: () => void;
|
|
33
48
|
}
|
|
34
|
-
|
|
35
|
-
export declare function createGlassEaselComponentDefinition(definition: GlassEaselComponentDefinition): unknown;
|
|
36
|
-
export interface GlassEaselWebInstance {
|
|
37
|
-
root: HTMLElement;
|
|
38
|
-
definition: GlassEaselComponentDefinition;
|
|
39
|
-
props: Record<string, unknown>;
|
|
40
|
-
disposed: boolean;
|
|
41
|
-
native?: any;
|
|
42
|
-
backendContext?: any;
|
|
43
|
-
lifecycle: string[];
|
|
44
|
-
}
|
|
45
|
-
export declare function createGlassEaselWebAdapter(options?: GlassEaselWebAdapterOptions): GlassEaselWebAdapter;
|
|
46
|
-
export interface GlassEaselRuntimeInfo {
|
|
49
|
+
interface GlassEaselRuntimeInfo {
|
|
47
50
|
version: string;
|
|
48
51
|
hasDomBackend: boolean;
|
|
49
52
|
hasTemplateCompiler: boolean;
|
|
50
53
|
}
|
|
51
|
-
/**
|
|
54
|
+
/** 独立定义不进入其他 adapter 的组件空间。 */
|
|
55
|
+
export declare function createGlassEaselComponentDefinition(definition: GlassEaselComponentDefinition): glassEasel.GeneralComponentDefinition;
|
|
56
|
+
/** 使用官方组件树承接模板、slot、数据更新和组件生命周期。 */
|
|
57
|
+
export declare function createGlassEaselWebAdapter(options?: GlassEaselWebAdapterOptions): GlassEaselWebAdapter;
|
|
58
|
+
/** 返回构建所使用的官方依赖版本和当前环境能力。 */
|
|
52
59
|
export declare function getGlassEaselRuntimeInfo(): GlassEaselRuntimeInfo;
|
|
53
|
-
|
|
60
|
+
export type { GlassEaselComponentDefinition, GlassEaselRuntimeInfo, GlassEaselWebAdapter, GlassEaselWebAdapterOptions, GlassEaselWebHost, GlassEaselWebInstance, GlassEaselWebSnapshot };
|
package/dist/index.mjs
CHANGED
|
@@ -1,135 +1,246 @@
|
|
|
1
1
|
import * as glassEasel from "glass-easel";
|
|
2
2
|
import { wxml } from "glass-easel-template-compiler";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import glassEaselPackage from "glass-easel/package.json" with { type: "json" };
|
|
4
|
+
// src/backend.ts
|
|
5
|
+
const documentBackends = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
function getDocumentBackend() {
|
|
7
|
+
if (typeof document === "undefined" || typeof glassEasel.CurrentWindowBackendContext !== "function") throw new Error("glass-easel Web backend is unavailable in this environment");
|
|
8
|
+
let backend = documentBackends.get(document);
|
|
9
|
+
if (!backend) {
|
|
10
|
+
backend = new glassEasel.CurrentWindowBackendContext();
|
|
11
|
+
backend.onEvent((element, ...args) => {
|
|
12
|
+
if (glassEasel.Element.isAttached(element)) return glassEasel.Event.triggerBackendEvent(element, ...args);
|
|
13
|
+
});
|
|
14
|
+
documentBackends.set(document, backend);
|
|
15
|
+
}
|
|
16
|
+
return backend;
|
|
17
|
+
}
|
|
18
|
+
// src/definition.ts
|
|
19
|
+
function normalizeComponentName(name) {
|
|
20
|
+
if (!name || /[\\?#:]/.test(name)) throw new TypeError("glass-easel component name must be a root-relative POSIX path");
|
|
21
|
+
const segments = [];
|
|
22
|
+
for (const segment of name.split("/")) {
|
|
23
|
+
if (!segment || segment === ".") continue;
|
|
24
|
+
if (segment === "..") {
|
|
25
|
+
if (!segments.length) throw new TypeError("glass-easel component name cannot escape its component space");
|
|
26
|
+
segments.pop();
|
|
27
|
+
} else segments.push(segment);
|
|
28
|
+
}
|
|
29
|
+
if (!segments.length) throw new TypeError("glass-easel component name must not be empty");
|
|
30
|
+
return segments.join("/");
|
|
31
|
+
}
|
|
32
|
+
function registerDefinition(space, definition, records) {
|
|
33
|
+
const lifetimes = { ...definition.lifetimes };
|
|
34
|
+
for (const name of [
|
|
35
|
+
"created",
|
|
36
|
+
"attached",
|
|
37
|
+
"ready",
|
|
38
|
+
"detached"
|
|
39
|
+
]) {
|
|
40
|
+
const callback = lifetimes[name];
|
|
41
|
+
lifetimes[name] = function(...args) {
|
|
42
|
+
let lifecycle = records.get(this);
|
|
43
|
+
if (!lifecycle) {
|
|
44
|
+
lifecycle = [];
|
|
45
|
+
records.set(this, lifecycle);
|
|
46
|
+
}
|
|
47
|
+
lifecycle.push(name);
|
|
48
|
+
return callback?.apply(this, args);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return space.define(definition.name).usingComponents(definition.usingComponents ?? {}).definition({
|
|
52
|
+
template: wxml(definition.template),
|
|
53
|
+
data: definition.data,
|
|
54
|
+
properties: definition.properties,
|
|
55
|
+
methods: definition.methods,
|
|
56
|
+
observers: definition.observers,
|
|
57
|
+
options: definition.options,
|
|
58
|
+
lifetimes
|
|
59
|
+
}).registerComponent().general();
|
|
60
|
+
}
|
|
61
|
+
/** 独立定义不进入其他 adapter 的组件空间。 */
|
|
5
62
|
function createGlassEaselComponentDefinition(definition) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (definition.lifetimes) params.lifetimes = definition.lifetimes;
|
|
11
|
-
return space.define().definition(params).registerComponent();
|
|
63
|
+
return registerDefinition(new glassEasel.ComponentSpace(), {
|
|
64
|
+
...definition,
|
|
65
|
+
name: normalizeComponentName(definition.name)
|
|
66
|
+
}, /* @__PURE__ */ new WeakMap());
|
|
12
67
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
68
|
+
/** 旧 slots 参数通过真实父组件投影,字符串仅作为调用方控制的 WXML 输入。 */
|
|
69
|
+
function createSlotParent(space, definition, slots) {
|
|
70
|
+
const content = Object.entries(slots).map(([name, source]) => {
|
|
71
|
+
return `<block${name === "default" ? "" : ` slot="${name.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<")}"`}>${source}</block>`;
|
|
72
|
+
}).join("");
|
|
73
|
+
return space.define().usingComponents({ "adapter-content": definition }).definition({ template: wxml(`<adapter-content>${content}</adapter-content>`) }).registerComponent().general();
|
|
17
74
|
}
|
|
75
|
+
// src/index.ts
|
|
76
|
+
/** 使用官方组件树承接模板、slot、数据更新和组件生命周期。 */
|
|
18
77
|
function createGlassEaselWebAdapter(options = {}) {
|
|
78
|
+
let space = new glassEasel.ComponentSpace();
|
|
79
|
+
space.updateComponentOptions({ writeIdToDOM: true });
|
|
19
80
|
const definitions = /* @__PURE__ */ new Map();
|
|
20
|
-
const
|
|
21
|
-
|
|
81
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
82
|
+
const registeredInputs = /* @__PURE__ */ new WeakMap();
|
|
83
|
+
const lifecycle = /* @__PURE__ */ new WeakMap();
|
|
84
|
+
const ownership = /* @__PURE__ */ new WeakMap();
|
|
85
|
+
const mounted = /* @__PURE__ */ new Set();
|
|
86
|
+
let disposed = false;
|
|
87
|
+
const assertActive = () => {
|
|
88
|
+
if (disposed) throw new Error("cannot use a disposed glass-easel adapter");
|
|
89
|
+
};
|
|
90
|
+
const getOwned = (instance) => {
|
|
91
|
+
const record = ownership.get(instance);
|
|
92
|
+
if (!record) throw new Error("glass-easel component is not owned by this adapter");
|
|
93
|
+
return record;
|
|
94
|
+
};
|
|
95
|
+
const getLive = (instance) => {
|
|
96
|
+
const record = getOwned(instance);
|
|
97
|
+
assertActive();
|
|
98
|
+
if (record.disposed || record.releasing) throw new Error("cannot use a disposed glass-easel component");
|
|
99
|
+
return record;
|
|
100
|
+
};
|
|
101
|
+
const release = (record) => {
|
|
102
|
+
if (record.disposed || record.releasing) return;
|
|
103
|
+
record.releasing = true;
|
|
104
|
+
try {
|
|
105
|
+
glassEasel.Element.pretendDetached(record.tree);
|
|
106
|
+
record.tree.destroyBackendElementOnSubtree();
|
|
107
|
+
record.instance.root.remove();
|
|
108
|
+
record.disposed = true;
|
|
109
|
+
mounted.delete(record);
|
|
110
|
+
} finally {
|
|
111
|
+
record.releasing = false;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const assertDependencies = (currentSpace, root) => {
|
|
115
|
+
const visited = /* @__PURE__ */ new Set();
|
|
116
|
+
const visit = (definition) => {
|
|
117
|
+
if (visited.has(definition)) return;
|
|
118
|
+
visited.add(definition);
|
|
119
|
+
for (const [alias, url] of Object.entries(registeredInputs.get(definition)?.usingComponents ?? {})) {
|
|
120
|
+
const dependency = currentSpace.getComponentByUrlWithoutDefault(url, definition.is);
|
|
121
|
+
if (!dependency || !registeredInputs.has(dependency)) throw new Error(`component dependency is not registered: ${definition.is} -> ${alias} (${url})`);
|
|
122
|
+
visit(dependency);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
visit(root);
|
|
126
|
+
};
|
|
22
127
|
return {
|
|
23
128
|
registerComponent(definition) {
|
|
129
|
+
assertActive();
|
|
24
130
|
if (!definition.name || !definition.template) throw new TypeError("glass-easel component requires name and template");
|
|
25
|
-
|
|
26
|
-
definitions.set(definition.name, {
|
|
131
|
+
definition = Object.freeze({
|
|
27
132
|
...definition,
|
|
28
|
-
|
|
133
|
+
name: normalizeComponentName(definition.name),
|
|
134
|
+
usingComponents: Object.freeze({ ...definition.usingComponents })
|
|
29
135
|
});
|
|
136
|
+
if (definitions.has(definition.name)) throw new Error(`component already registered: ${definition.name}`);
|
|
137
|
+
const registered = registerDefinition(space, definition, lifecycle);
|
|
138
|
+
definitions.set(definition.name, registered);
|
|
139
|
+
inputs.set(definition.name, definition);
|
|
140
|
+
registeredInputs.set(registered, definition);
|
|
30
141
|
},
|
|
31
142
|
mountComponent(name, container, props = {}, slots = {}) {
|
|
143
|
+
assertActive();
|
|
144
|
+
name = normalizeComponentName(name);
|
|
32
145
|
const definition = definitions.get(name);
|
|
33
146
|
if (!definition) throw new Error(`component is not registered: ${name}`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
root = container.firstElementChild;
|
|
47
|
-
projectSlots(root, slots);
|
|
48
|
-
} else {
|
|
49
|
-
root = render(definition.template, props, options.host ?? {});
|
|
50
|
-
container.append(root);
|
|
147
|
+
assertDependencies(space, definition);
|
|
148
|
+
const backend = getDocumentBackend();
|
|
149
|
+
if (container.ownerDocument !== document) throw new Error("glass-easel components must mount in the current document");
|
|
150
|
+
const hasParent = Object.keys(slots).length > 0 || definition.getComponentOptions().virtualHost;
|
|
151
|
+
const rootDefinition = hasParent ? createSlotParent(space, definition, slots) : definition;
|
|
152
|
+
const tree = glassEasel.Component.createWithContext("glass-easel-root", rootDefinition, backend, (component) => {
|
|
153
|
+
component.setData(props);
|
|
154
|
+
});
|
|
155
|
+
const native = hasParent ? tree.getShadowRoot()?.childNodes[0]?.asGeneralComponent() : tree;
|
|
156
|
+
if (!native) {
|
|
157
|
+
tree.destroyBackendElementOnSubtree();
|
|
158
|
+
throw new Error("glass-easel slot parent did not create its content component");
|
|
51
159
|
}
|
|
160
|
+
if (hasParent) native.setData(props);
|
|
161
|
+
const root = tree.getBackendElement();
|
|
162
|
+
let record;
|
|
52
163
|
const instance = {
|
|
53
164
|
root,
|
|
54
|
-
definition,
|
|
55
|
-
props: { ...props },
|
|
56
|
-
disposed: false,
|
|
57
165
|
native,
|
|
58
|
-
|
|
59
|
-
|
|
166
|
+
definition: inputs.get(name),
|
|
167
|
+
get props() {
|
|
168
|
+
return glassEasel.dataUtils.deepCopy(native.data, true);
|
|
169
|
+
},
|
|
170
|
+
get disposed() {
|
|
171
|
+
return record.disposed;
|
|
172
|
+
}
|
|
60
173
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]) instance.native?.addLifetimeListener?.(name, () => instance.lifecycle.push(name));
|
|
67
|
-
instances.add(instance);
|
|
68
|
-
return instance;
|
|
69
|
-
},
|
|
70
|
-
updateComponent(value, props) {
|
|
71
|
-
const instance = value;
|
|
72
|
-
if (instance.disposed) throw new Error("cannot update a disposed glass-easel component");
|
|
73
|
-
instance.props = {
|
|
74
|
-
...instance.props,
|
|
75
|
-
...props
|
|
174
|
+
record = {
|
|
175
|
+
tree,
|
|
176
|
+
instance,
|
|
177
|
+
disposed: false,
|
|
178
|
+
releasing: false
|
|
76
179
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
180
|
+
ownership.set(instance, record);
|
|
181
|
+
let placeholder;
|
|
182
|
+
let insertedPlaceholder = false;
|
|
183
|
+
try {
|
|
184
|
+
assertActive();
|
|
185
|
+
placeholder = options.host?.createElement?.("glass-easel-placeholder") ?? document.createElement("glass-easel-placeholder");
|
|
186
|
+
if (placeholder.parentNode || placeholder.ownerDocument !== document) throw new Error("glass-easel placeholder must be a new element in the current document");
|
|
187
|
+
assertActive();
|
|
188
|
+
container.append(placeholder);
|
|
189
|
+
insertedPlaceholder = true;
|
|
190
|
+
glassEasel.Element.replaceDocumentElement(tree, container, placeholder);
|
|
191
|
+
assertActive();
|
|
192
|
+
mounted.add(record);
|
|
193
|
+
return instance;
|
|
194
|
+
} catch (cause) {
|
|
195
|
+
if (insertedPlaceholder) placeholder?.remove();
|
|
196
|
+
release(record);
|
|
197
|
+
throw cause;
|
|
82
198
|
}
|
|
83
199
|
},
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
if (instance.
|
|
87
|
-
|
|
200
|
+
updateComponent(instance, props) {
|
|
201
|
+
const record = getLive(instance);
|
|
202
|
+
if (record.tree !== instance.native) record.tree.setData(props);
|
|
203
|
+
instance.native.setData(props);
|
|
204
|
+
},
|
|
205
|
+
triggerEvent(instance, name, detail) {
|
|
206
|
+
getLive(instance);
|
|
207
|
+
instance.native.triggerEvent(name, detail);
|
|
88
208
|
options.host?.dispatchEvent?.(new CustomEvent(name, { detail }), instance);
|
|
89
209
|
},
|
|
90
|
-
unmountComponent(
|
|
91
|
-
|
|
92
|
-
if (!instances.delete(instance) || instance.disposed) return;
|
|
93
|
-
instance.native?.triggerLifetime?.("detached");
|
|
94
|
-
if (!instance.native) instance.lifecycle.push("detached");
|
|
95
|
-
instance.root?.remove();
|
|
96
|
-
instance.disposed = true;
|
|
210
|
+
unmountComponent(instance) {
|
|
211
|
+
release(getOwned(instance));
|
|
97
212
|
},
|
|
98
|
-
getSnapshot(
|
|
99
|
-
const
|
|
213
|
+
getSnapshot(instance) {
|
|
214
|
+
const record = getOwned(instance);
|
|
100
215
|
return {
|
|
101
216
|
name: instance.definition.name,
|
|
102
|
-
props:
|
|
217
|
+
props: glassEasel.dataUtils.deepCopy(instance.native.data, true),
|
|
103
218
|
html: instance.root.innerHTML,
|
|
104
|
-
lifecycle: [...instance.
|
|
105
|
-
disposed:
|
|
219
|
+
lifecycle: [...lifecycle.get(instance.native) ?? []],
|
|
220
|
+
disposed: record.disposed
|
|
106
221
|
};
|
|
107
222
|
},
|
|
108
223
|
dispose() {
|
|
109
|
-
|
|
224
|
+
disposed = true;
|
|
225
|
+
const errors = [];
|
|
226
|
+
for (const record of mounted) try {
|
|
227
|
+
release(record);
|
|
228
|
+
} catch (cause) {
|
|
229
|
+
errors.push(cause);
|
|
230
|
+
}
|
|
110
231
|
definitions.clear();
|
|
232
|
+
inputs.clear();
|
|
233
|
+
space = void 0;
|
|
234
|
+
if (errors.length) throw new AggregateError(errors, "glass-easel adapter cleanup failed");
|
|
111
235
|
}
|
|
112
236
|
};
|
|
113
237
|
}
|
|
114
|
-
/**
|
|
115
|
-
function projectSlots(root, slots) {
|
|
116
|
-
let projected = false;
|
|
117
|
-
for (const slot of Array.from(root.querySelectorAll("slot"))) {
|
|
118
|
-
const content = slots[slot.getAttribute("name") || "default"];
|
|
119
|
-
if (content === void 0) continue;
|
|
120
|
-
const fragment = document.createRange().createContextualFragment(content);
|
|
121
|
-
slot.replaceWith(fragment);
|
|
122
|
-
projected = true;
|
|
123
|
-
}
|
|
124
|
-
if (!projected && slots.default !== void 0) (root.firstElementChild ?? root).insertAdjacentHTML("beforeend", slots.default);
|
|
125
|
-
}
|
|
126
|
-
/** 返回当前安装的 glass-easel 能力探针,供宿主决定是否启用实验后端。 */
|
|
238
|
+
/** 返回构建所使用的官方依赖版本和当前环境能力。 */
|
|
127
239
|
function getGlassEaselRuntimeInfo() {
|
|
128
240
|
return {
|
|
129
|
-
version:
|
|
130
|
-
hasDomBackend: typeof glassEasel.CurrentWindowBackendContext === "function",
|
|
241
|
+
version: glassEaselPackage.version,
|
|
242
|
+
hasDomBackend: typeof document !== "undefined" && typeof glassEasel.CurrentWindowBackendContext === "function",
|
|
131
243
|
hasTemplateCompiler: typeof wxml === "function"
|
|
132
244
|
};
|
|
133
245
|
}
|
|
134
|
-
//#endregion
|
|
135
246
|
export { createGlassEaselComponentDefinition, createGlassEaselWebAdapter, getGlassEaselRuntimeInfo };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weapp-vite/glass-easel-web-adapter",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Experimental glass-easel component backend for weapp-vite Web runtime",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/weapp-vite/weapp-vite#readme",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"playwright": "1.64.1",
|
|
45
45
|
"tsd": "0.33.0",
|
|
46
|
-
"tsdown": "0.23.
|
|
46
|
+
"tsdown": "0.23.2",
|
|
47
47
|
"tsx": "4.23.15",
|
|
48
48
|
"typescript": "^6.0.3",
|
|
49
|
-
"vite": "8.3.
|
|
49
|
+
"vite": "8.3.5",
|
|
50
50
|
"vitest": "~5.0.5"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|