@easytwin/devkit 0.1.2 → 0.1.3

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.
@@ -0,0 +1,265 @@
1
+ // src/twinAppHost.ts
2
+ import { createAppInstance } from "@easytwin/apps";
3
+ var LOCAL_LOAD_SCENE_ERROR = "\u672C\u5730\u9884\u89C8\u5355\u573A\u666F,\u4E0D\u80FD\u8C03\u7528 sceneManager.loadScene";
4
+ var MISSING_TICK_ERROR = "\u5F53\u524D runtime \u4E0D\u652F\u6301\u5F15\u64CE\u65F6\u949F(\u7F3A\u5C11 registerEngineTickListener)\u3002\u65E0\u6CD5\u8FD0\u884C onUpdate\u3002";
5
+ function isPromiseLike(value) {
6
+ return typeof value === "object" && value !== null && "then" in value;
7
+ }
8
+ var TwinAppPreviewHost = class {
9
+ constructor(options) {
10
+ this.options = options;
11
+ this.currentSceneId = options.sceneId;
12
+ this.onEngineTick = (engine) => this.handleTick(engine);
13
+ }
14
+ app = null;
15
+ engine = null;
16
+ disposed = false;
17
+ /** 用户应用已经成功 onSceneLoaded。 */
18
+ scenePresented = false;
19
+ tickAttached = false;
20
+ sceneLoadTask = null;
21
+ ctx = null;
22
+ cleanups = [];
23
+ sceneCleanups = [];
24
+ warnedAsyncUpdate = false;
25
+ userBlobUrl = null;
26
+ currentSceneId;
27
+ onEngineTick;
28
+ getEngine() {
29
+ return this.engine;
30
+ }
31
+ /** 给 spec 调用:场景字段取当前引擎主场景(不要求已 Run TwinApp)。 */
32
+ getTestContext() {
33
+ if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
34
+ const ctx = this.getContext();
35
+ const runtimeScene = this.engine.mainScene ?? null;
36
+ ctx.runtimeScene = runtimeScene;
37
+ ctx.scene = runtimeScene?.sceneObject ?? null;
38
+ ctx.camera = runtimeScene?.camera?.main ?? null;
39
+ return ctx;
40
+ }
41
+ async invokeTest(fn, input) {
42
+ if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
43
+ return fn(this.getTestContext(), input);
44
+ }
45
+ async boot() {
46
+ const { runtime, containerId, ossUrl, customComponentDeps } = this.options;
47
+ this.engine = await runtime.RuntimeEngine.create({
48
+ containerId,
49
+ baseOSSUrl: ossUrl,
50
+ customComponentDeps,
51
+ sceneResources: [],
52
+ componentRels: [],
53
+ enableResourcePersistence: false
54
+ });
55
+ await this.loadCurrentScene();
56
+ }
57
+ async run(code) {
58
+ if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
59
+ if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
60
+ this.assertTickApi();
61
+ try {
62
+ await this.teardownApp({ keepEngine: true });
63
+ const mod = await this.importUser(code);
64
+ this.app = createAppInstance(mod.default);
65
+ await this.app.init?.(this.getContext());
66
+ await this.loadCurrentScene();
67
+ this.scenePresented = true;
68
+ await this.app.onSceneLoaded?.(this.getContext());
69
+ this.attachTick();
70
+ } catch (error) {
71
+ await this.teardownApp({ keepEngine: true, ignoreHookErrors: true });
72
+ throw error;
73
+ }
74
+ }
75
+ async dispose() {
76
+ if (this.disposed) return;
77
+ this.disposed = true;
78
+ await this.teardownApp({ keepEngine: false, ignoreHookErrors: true });
79
+ }
80
+ assertTickApi() {
81
+ const { runtime } = this.options;
82
+ if (typeof runtime.registerEngineTickListener !== "function" || typeof runtime.unregisterEngineTickListener !== "function") {
83
+ throw new Error(MISSING_TICK_ERROR);
84
+ }
85
+ }
86
+ async importUser(code) {
87
+ if (this.options.importUserModule) return this.options.importUserModule(code);
88
+ if (this.userBlobUrl) {
89
+ try {
90
+ URL.revokeObjectURL(this.userBlobUrl);
91
+ } catch {
92
+ }
93
+ this.userBlobUrl = null;
94
+ }
95
+ const blob = new Blob([code], { type: "text/javascript" });
96
+ this.userBlobUrl = URL.createObjectURL(blob);
97
+ return import(
98
+ /* @vite-ignore */
99
+ this.userBlobUrl
100
+ );
101
+ }
102
+ async loadCurrentScene() {
103
+ if (!this.engine) throw new Error("Engine is not created");
104
+ if (this.sceneLoadTask) await this.sceneLoadTask;
105
+ this.sceneLoadTask = this.options.loadScene(this.engine, this.options.sceneJson).finally(() => {
106
+ this.sceneLoadTask = null;
107
+ });
108
+ await this.sceneLoadTask;
109
+ }
110
+ handleTick(engine) {
111
+ if (this.disposed || this.sceneLoadTask || !this.scenePresented) return;
112
+ try {
113
+ const result = this.app?.onUpdate?.(
114
+ this.getContext(),
115
+ engine.time.deltaTime,
116
+ engine.time.elapsedTime
117
+ );
118
+ if (isPromiseLike(result)) {
119
+ void Promise.resolve(result).catch((error) => {
120
+ console.error("async onUpdate rejected:", error);
121
+ });
122
+ if (!this.warnedAsyncUpdate) {
123
+ this.warnedAsyncUpdate = true;
124
+ console.warn("TwinApp.onUpdate \u5E94\u540C\u6B65\u6267\u884C\uFF1B\u672C\u6B21\u5DF2\u5FFD\u7565\u5176 Promise \u8FD4\u56DE\u503C");
125
+ }
126
+ }
127
+ } catch (error) {
128
+ this.handleUpdateError(error);
129
+ }
130
+ }
131
+ handleUpdateError(error) {
132
+ let shouldContinue = false;
133
+ try {
134
+ shouldContinue = this.app?.onError?.(this.getContext(), error) === true;
135
+ } catch (onError) {
136
+ void this.failRun(onError);
137
+ return;
138
+ }
139
+ if (shouldContinue) {
140
+ console.error("TwinApp.onUpdate threw:", error);
141
+ this.options.log?.(`onUpdate \u629B\u9519\u5DF2 continue: ${error instanceof Error ? error.message : String(error)}`);
142
+ return;
143
+ }
144
+ void this.failRun(error);
145
+ }
146
+ async failRun(error) {
147
+ this.options.log?.(`Run \u5931\u8D25 ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
148
+ await this.teardownApp({ keepEngine: true, ignoreHookErrors: true });
149
+ }
150
+ attachTick() {
151
+ if (!this.engine || this.tickAttached) return;
152
+ this.options.runtime.registerEngineTickListener?.(this.engine, this.onEngineTick);
153
+ this.tickAttached = true;
154
+ }
155
+ detachTick() {
156
+ if (!this.engine || !this.tickAttached) return;
157
+ this.options.runtime.unregisterEngineTickListener?.(this.engine, this.onEngineTick);
158
+ this.tickAttached = false;
159
+ }
160
+ async teardownApp(options) {
161
+ this.detachTick();
162
+ await this.unloadUserScene({ ignoreErrors: options.ignoreHookErrors });
163
+ if (this.app && this.engine) {
164
+ try {
165
+ await this.app.onDispose?.(this.getContext());
166
+ } catch (error) {
167
+ if (!options.ignoreHookErrors) throw error;
168
+ console.warn("TwinApp.onDispose failed:", error);
169
+ }
170
+ }
171
+ await this.runHooks(this.cleanups);
172
+ this.cleanups = [];
173
+ this.app = null;
174
+ this.ctx = null;
175
+ this.scenePresented = false;
176
+ if (!options.keepEngine) {
177
+ try {
178
+ this.engine?.destroy();
179
+ } catch {
180
+ }
181
+ this.engine = null;
182
+ }
183
+ if (this.userBlobUrl) {
184
+ try {
185
+ URL.revokeObjectURL(this.userBlobUrl);
186
+ } catch {
187
+ }
188
+ this.userBlobUrl = null;
189
+ }
190
+ }
191
+ async unloadUserScene(options) {
192
+ if (!this.scenePresented) return;
193
+ this.scenePresented = false;
194
+ try {
195
+ if (this.engine) await this.app?.onBeforeSceneUnload?.(this.getContext());
196
+ } catch (error) {
197
+ if (!options?.ignoreErrors) throw error;
198
+ console.warn("TwinApp.onBeforeSceneUnload failed:", error);
199
+ } finally {
200
+ await this.runHooks(this.sceneCleanups);
201
+ this.sceneCleanups = [];
202
+ }
203
+ }
204
+ async runHooks(hooks) {
205
+ for (const fn of [...hooks].reverse()) {
206
+ try {
207
+ await fn();
208
+ } catch (error) {
209
+ console.warn("TwinApp cleanup failed:", error);
210
+ }
211
+ }
212
+ }
213
+ getContext() {
214
+ if (!this.engine) throw new Error("Engine is not created");
215
+ const runtimeScene = this.scenePresented ? this.engine.mainScene ?? null : null;
216
+ const scene = runtimeScene?.sceneObject ?? null;
217
+ const camera = runtimeScene?.camera?.main ?? null;
218
+ const sceneManagerRuntime = this.engine.getManager(this.options.runtime.SceneManager);
219
+ if (!this.ctx) {
220
+ this.ctx = {
221
+ app: { id: this.options.appId, mode: "preview" },
222
+ engine: this.engine,
223
+ container: this.engine.container,
224
+ runtimeScene,
225
+ scene,
226
+ camera,
227
+ sceneManager: {
228
+ currentSceneId: this.currentSceneId,
229
+ runtime: sceneManagerRuntime,
230
+ loadScene: () => Promise.reject(new Error(LOCAL_LOAD_SCENE_ERROR))
231
+ },
232
+ logger: {
233
+ log: (...args) => console.log(...args),
234
+ info: (...args) => console.info(...args),
235
+ warn: (...args) => console.warn(...args),
236
+ error: (...args) => console.error(...args)
237
+ },
238
+ assets: {
239
+ text: (p) => this.options.readAsset(p),
240
+ json: async (p) => JSON.parse(await this.options.readAsset(p))
241
+ },
242
+ cleanup: (fn) => {
243
+ this.cleanups.push(fn);
244
+ },
245
+ sceneCleanup: (fn) => {
246
+ this.sceneCleanups.push(fn);
247
+ }
248
+ };
249
+ return this.ctx;
250
+ }
251
+ this.ctx.engine = this.engine;
252
+ this.ctx.container = this.engine.container;
253
+ this.ctx.runtimeScene = runtimeScene;
254
+ this.ctx.scene = scene;
255
+ this.ctx.camera = camera;
256
+ this.ctx.sceneManager.currentSceneId = this.currentSceneId;
257
+ this.ctx.sceneManager.runtime = sceneManagerRuntime;
258
+ return this.ctx;
259
+ }
260
+ };
261
+ export {
262
+ LOCAL_LOAD_SCENE_ERROR,
263
+ MISSING_TICK_ERROR,
264
+ TwinAppPreviewHost
265
+ };
@@ -0,0 +1,48 @@
1
+ // src/apps.ts
2
+ var PORTABLE_RUNTIME_EXPORTS = [
3
+ "THREE",
4
+ "RuntimeEngine",
5
+ "SceneManager",
6
+ "LoadSceneMode",
7
+ "convertObjToComponentJson"
8
+ ];
9
+ var TwinApp = class {
10
+ };
11
+ function defineApp(app) {
12
+ return app;
13
+ }
14
+ var LIFECYCLE_NAMES = [
15
+ "init",
16
+ "onUpdate",
17
+ "onBeforeSceneUnload",
18
+ "onSceneLoaded",
19
+ "onDispose",
20
+ "onError"
21
+ ];
22
+ function isLifecycleApp(value) {
23
+ if (typeof value !== "object" || value === null) return false;
24
+ return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
25
+ }
26
+ function createAppInstance(appExport) {
27
+ if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
28
+ return new appExport();
29
+ }
30
+ if (isLifecycleApp(appExport)) return appExport;
31
+ throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
32
+ }
33
+ function portableRuntimeWarning(names) {
34
+ const extra = [...new Set(names)].filter(
35
+ (n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
36
+ );
37
+ const ns = [...names].includes("*");
38
+ if (!ns && extra.length === 0) return void 0;
39
+ const detail = ns ? "namespace import *" : extra.sort().join(", ");
40
+ return `\u68C0\u6D4B\u5230 @easytwin/runtime \u5BFC\u51FA ${detail} \u4E0D\u5728\u5728\u7EBF\u7F16\u8BD1\u767D\u540D\u5355(${PORTABLE_RUNTIME_EXPORTS.join(", ")})\u5185\u3002\u672C\u5730\u9884\u89C8\u53EF\u7528,\u4E0A\u4F20\u5230\u5728\u7EBF TwinApp \u53EF\u80FD\u7F16\u4E0D\u8FC7\u3002`;
41
+ }
42
+ export {
43
+ PORTABLE_RUNTIME_EXPORTS,
44
+ TwinApp,
45
+ createAppInstance,
46
+ defineApp,
47
+ portableRuntimeWarning
48
+ };