@easytwin/devkit 0.1.1 → 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.
- package/README.md +6 -5
- package/dist/bin.js +1873 -165
- package/dist/index.d.ts +440 -54
- package/dist/index.js +2133 -332
- package/dist/runtime/twin-app-host.js +265 -0
- package/dist/runtime/twin-apps.js +48 -0
- package/dist/runtime/twin-runtime.js +128301 -0
- package/dist/runtime-types/index.d.ts +6 -1
- package/package.json +5 -3
- package/skills/easytwin-bootstrap/SKILL.md +7 -5
- package/skills/easytwin-core/SKILL.md +2 -2
- package/skills/easytwin-core/references/engine.md +2 -2
- package/skills/easytwin-develop/SKILL.md +9 -6
- package/skills/easytwin-render/SKILL.md +56 -17
- package/skills/easytwin-render/references/intro.md +8 -4
- package/skills/easytwin-render/references/scene-and-assets.md +1 -1
- package/skills/easytwin-scene/SKILL.md +57 -54
- package/skills/easytwin-test/SKILL.md +47 -0
- package/skills/easytwin-upload/SKILL.md +39 -25
- package/dist/bin.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
var CONFIG_FILE_NAME = "easytwin.config.json";
|
|
5
5
|
var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
|
|
6
|
-
var TEST_BASE_URL = "http://
|
|
6
|
+
var TEST_BASE_URL = "http://172.16.125.3:10100/";
|
|
7
7
|
var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
8
8
|
var MOCK_APP_ID = "test";
|
|
9
9
|
var MOCK_APP_SECRET = "test";
|
|
@@ -52,8 +52,40 @@ function validateConfigShape(value) {
|
|
|
52
52
|
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
53
53
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
54
54
|
if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
|
|
55
|
+
const scenes = parseConfigScenes(v.scenes);
|
|
56
|
+
if (scenes) config.scenes = scenes;
|
|
55
57
|
return config;
|
|
56
58
|
}
|
|
59
|
+
function parseConfigScenes(value) {
|
|
60
|
+
if (value === void 0) return void 0;
|
|
61
|
+
if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
62
|
+
return value.map((item, i) => {
|
|
63
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
64
|
+
throw new ConfigError(`scenes[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
65
|
+
}
|
|
66
|
+
const it = item;
|
|
67
|
+
if (typeof it.id !== "string" || it.id.length === 0) {
|
|
68
|
+
throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 id`);
|
|
69
|
+
}
|
|
70
|
+
if (typeof it.name !== "string" || it.name.length === 0) {
|
|
71
|
+
throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 name`);
|
|
72
|
+
}
|
|
73
|
+
const scene = { id: it.id, name: it.name };
|
|
74
|
+
if (it.linkedSceneId !== void 0) {
|
|
75
|
+
if (typeof it.linkedSceneId !== "string") throw new ConfigError(`scenes[${i}].linkedSceneId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
76
|
+
if (it.linkedSceneId.length > 0) scene.linkedSceneId = it.linkedSceneId;
|
|
77
|
+
}
|
|
78
|
+
if (it.snapshotUrl !== void 0) {
|
|
79
|
+
if (typeof it.snapshotUrl !== "string") throw new ConfigError(`scenes[${i}].snapshotUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
80
|
+
if (it.snapshotUrl.length > 0) scene.snapshotUrl = it.snapshotUrl;
|
|
81
|
+
}
|
|
82
|
+
if (it.defaultLoading !== void 0) {
|
|
83
|
+
if (typeof it.defaultLoading !== "boolean") throw new ConfigError(`scenes[${i}].defaultLoading \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
84
|
+
scene.defaultLoading = it.defaultLoading;
|
|
85
|
+
}
|
|
86
|
+
return scene;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
57
89
|
async function readConfigFile(cwd) {
|
|
58
90
|
const file = configFilePath(cwd);
|
|
59
91
|
let raw;
|
|
@@ -76,7 +108,8 @@ function resolveConfig(file, env = process.env) {
|
|
|
76
108
|
return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
77
109
|
}
|
|
78
110
|
async function loadConfig(cwd, env = process.env) {
|
|
79
|
-
|
|
111
|
+
const file = await readConfigFile(cwd);
|
|
112
|
+
return resolveConfig(file, env);
|
|
80
113
|
}
|
|
81
114
|
async function writeConfigFile(cwd, config) {
|
|
82
115
|
const file = configFilePath(cwd);
|
|
@@ -84,6 +117,7 @@ async function writeConfigFile(cwd, config) {
|
|
|
84
117
|
if (config.env) body.env = config.env;
|
|
85
118
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
86
119
|
if (config.ossUrl) body.ossUrl = config.ossUrl;
|
|
120
|
+
if (config.scenes !== void 0) body.scenes = config.scenes;
|
|
87
121
|
await fs.mkdir(cwd, { recursive: true });
|
|
88
122
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
89
123
|
return file;
|
|
@@ -111,18 +145,29 @@ async function initConfig(input, cwd) {
|
|
|
111
145
|
const gitignore = await appendGitignore(cwd);
|
|
112
146
|
return { configFile, gitignore };
|
|
113
147
|
}
|
|
148
|
+
async function writeConfigScenes(cwd, scenes) {
|
|
149
|
+
const file = await readConfigFile(cwd);
|
|
150
|
+
await writeConfigFile(cwd, { ...file, scenes });
|
|
151
|
+
}
|
|
114
152
|
|
|
115
153
|
// src/client.ts
|
|
116
154
|
import http from "http";
|
|
117
155
|
import https from "https";
|
|
118
|
-
import { URL } from "url";
|
|
119
|
-
var
|
|
120
|
-
var
|
|
121
|
-
|
|
156
|
+
import { URL as URL2 } from "url";
|
|
157
|
+
var APP_ID_HEADER = "x-app-id";
|
|
158
|
+
var AUTH_HEADER = "x-app-secret";
|
|
159
|
+
function enc(id) {
|
|
160
|
+
return encodeURIComponent(id);
|
|
161
|
+
}
|
|
122
162
|
var ENDPOINTS = {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
/** POST 已关联场景列表(分享路径,空 body)。 */
|
|
164
|
+
linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
|
|
165
|
+
/** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
|
|
166
|
+
workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
|
|
167
|
+
/** POST 一次推送 create/update/delete(分享路径)。 */
|
|
168
|
+
workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
|
|
169
|
+
/** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
|
|
170
|
+
workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
|
|
126
171
|
};
|
|
127
172
|
var EasyTwinApiError = class extends Error {
|
|
128
173
|
status;
|
|
@@ -150,28 +195,40 @@ function parseResponseBody(buffer) {
|
|
|
150
195
|
return text;
|
|
151
196
|
}
|
|
152
197
|
}
|
|
198
|
+
function isEnvelope(value) {
|
|
199
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "success" in value;
|
|
200
|
+
}
|
|
201
|
+
function unwrapBody(parsed, status) {
|
|
202
|
+
if (!isEnvelope(parsed)) return parsed;
|
|
203
|
+
if (parsed.success === false) {
|
|
204
|
+
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? "\u8BF7\u6C42\u5931\u8D25", parsed);
|
|
205
|
+
}
|
|
206
|
+
if (parsed.success === true && "data" in parsed) return parsed.data;
|
|
207
|
+
return parsed;
|
|
208
|
+
}
|
|
153
209
|
var EasyTwinClient = class {
|
|
154
210
|
baseUrl;
|
|
155
|
-
/**
|
|
156
|
-
|
|
211
|
+
/** twin runtime / 场景快照资产根(baseOSSUrl)。 */
|
|
212
|
+
ossUrl;
|
|
213
|
+
/** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
|
|
157
214
|
appId;
|
|
215
|
+
/** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
216
|
+
mock;
|
|
158
217
|
appSecret;
|
|
159
218
|
constructor(config) {
|
|
160
219
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
220
|
+
this.ossUrl = config.ossUrl.replace(/\/+$/, "");
|
|
161
221
|
this.mock = config.mock;
|
|
162
222
|
this.appId = config.appId;
|
|
163
223
|
this.appSecret = config.appSecret;
|
|
164
224
|
}
|
|
165
225
|
/** 全仓唯一认证头注入点。 */
|
|
166
226
|
authHeaders() {
|
|
167
|
-
return {
|
|
168
|
-
[AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
|
|
169
|
-
[APP_ID_HEADER]: this.appId
|
|
170
|
-
};
|
|
227
|
+
return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
|
|
171
228
|
}
|
|
172
229
|
/** JSON 请求(原生 fetch)。 */
|
|
173
|
-
async request(
|
|
174
|
-
const url = `${this.baseUrl}${
|
|
230
|
+
async request(path9, options = {}) {
|
|
231
|
+
const url = `${this.baseUrl}${path9}`;
|
|
175
232
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
176
233
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
177
234
|
headers["Content-Type"] = "application/json";
|
|
@@ -193,8 +250,8 @@ var EasyTwinClient = class {
|
|
|
193
250
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
194
251
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
195
252
|
*/
|
|
196
|
-
async upload(
|
|
197
|
-
const url = new
|
|
253
|
+
async upload(path9, options) {
|
|
254
|
+
const url = new URL2(`${this.baseUrl}${path9}`);
|
|
198
255
|
const mod = url.protocol === "https:" ? https : http;
|
|
199
256
|
const headers = {
|
|
200
257
|
...this.authHeaders(),
|
|
@@ -227,8 +284,8 @@ var EasyTwinClient = class {
|
|
|
227
284
|
return this.handleStatus(raw.status, raw.body);
|
|
228
285
|
}
|
|
229
286
|
handleStatus(status, body) {
|
|
230
|
-
if (status >= 200 && status < 300) return parseResponseBody(body);
|
|
231
287
|
const parsed = parseResponseBody(body);
|
|
288
|
+
if (status >= 200 && status < 300) return unwrapBody(parsed, status);
|
|
232
289
|
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
|
|
233
290
|
}
|
|
234
291
|
};
|
|
@@ -237,17 +294,89 @@ var EasyTwinClient = class {
|
|
|
237
294
|
import { promises as fs2 } from "fs";
|
|
238
295
|
import path2 from "path";
|
|
239
296
|
import { fileURLToPath } from "url";
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
297
|
+
function asRecord(value) {
|
|
298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
299
|
+
}
|
|
300
|
+
function asString(value) {
|
|
301
|
+
return typeof value === "string" ? value : "";
|
|
302
|
+
}
|
|
303
|
+
function asBoolean(value) {
|
|
304
|
+
return value === true;
|
|
305
|
+
}
|
|
306
|
+
function extractSceneArray(data) {
|
|
307
|
+
if (Array.isArray(data)) return data;
|
|
308
|
+
const root = asRecord(data);
|
|
309
|
+
if (Array.isArray(root.data)) return root.data;
|
|
310
|
+
if (Array.isArray(root.list)) return root.list;
|
|
311
|
+
if (Array.isArray(root.scenes)) return root.scenes;
|
|
312
|
+
const nested = asRecord(root.data);
|
|
313
|
+
if (Array.isArray(nested.scenes)) return nested.scenes;
|
|
314
|
+
if (Array.isArray(nested.list)) return nested.list;
|
|
315
|
+
throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
316
|
+
}
|
|
317
|
+
function normalizeLinkedScenes(data) {
|
|
318
|
+
const list = extractSceneArray(data);
|
|
243
319
|
return list.map((item) => {
|
|
244
|
-
const it = item
|
|
245
|
-
|
|
320
|
+
const it = asRecord(item);
|
|
321
|
+
const sceneKey = asString(it.sceneKey);
|
|
322
|
+
const sourceProjectName = asString(it.sourceProjectName);
|
|
323
|
+
const sourceSceneName = asString(it.sourceSceneName);
|
|
324
|
+
return {
|
|
325
|
+
id: asString(it.id),
|
|
326
|
+
sceneKey,
|
|
327
|
+
name: sourceProjectName || sourceSceneName || sceneKey,
|
|
328
|
+
defaultLoading: asBoolean(it.defaultLoading),
|
|
329
|
+
sourceSceneId: asString(it.sourceSceneId),
|
|
330
|
+
sourceProjectId: asString(it.sourceProjectId),
|
|
331
|
+
sourceSceneName,
|
|
332
|
+
sourceProjectName,
|
|
333
|
+
sourceLost: asBoolean(it.sourceLost),
|
|
334
|
+
snapshotUrl: asString(it.snapshotUrl),
|
|
335
|
+
snapshotUpdatedAt: asString(it.snapshotUpdatedAt)
|
|
336
|
+
};
|
|
246
337
|
});
|
|
247
338
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
339
|
+
function normalizeSceneList(data) {
|
|
340
|
+
return normalizeLinkedScenes(data).map((s) => ({
|
|
341
|
+
id: s.sceneKey,
|
|
342
|
+
name: s.name,
|
|
343
|
+
linkedSceneId: s.id,
|
|
344
|
+
snapshotUrl: s.snapshotUrl,
|
|
345
|
+
defaultLoading: s.defaultLoading
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
function toConfigScenes(scenes) {
|
|
349
|
+
return scenes.map((s) => {
|
|
350
|
+
const item = { id: s.id, name: s.name };
|
|
351
|
+
if (s.linkedSceneId) item.linkedSceneId = s.linkedSceneId;
|
|
352
|
+
if (s.snapshotUrl) item.snapshotUrl = s.snapshotUrl;
|
|
353
|
+
if (s.defaultLoading !== void 0) item.defaultLoading = s.defaultLoading;
|
|
354
|
+
return item;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function resolveSnapshotUrl(ossUrl, snapshotUrl) {
|
|
358
|
+
if (/^https?:\/\//i.test(snapshotUrl)) return snapshotUrl;
|
|
359
|
+
const base = ossUrl.replace(/\/+$/, "");
|
|
360
|
+
const rel = snapshotUrl.replace(/^\/+/, "");
|
|
361
|
+
return `${base}/${rel}`;
|
|
362
|
+
}
|
|
363
|
+
async function fetchSnapshotJson(ossUrl, snapshotUrl) {
|
|
364
|
+
if (snapshotUrl.length === 0) throw new Error("\u573A\u666F\u5FEB\u7167\u5730\u5740\u4E3A\u7A7A");
|
|
365
|
+
const url = resolveSnapshotUrl(ossUrl, snapshotUrl);
|
|
366
|
+
let res;
|
|
367
|
+
try {
|
|
368
|
+
res = await fetch(url);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
throw new EasyTwinApiError(0, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:${url}:${err instanceof Error ? err.message : String(err)}`);
|
|
371
|
+
}
|
|
372
|
+
if (!res.ok) {
|
|
373
|
+
throw new EasyTwinApiError(res.status, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:HTTP ${res.status} ${url}`);
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
return await res.json();
|
|
377
|
+
} catch {
|
|
378
|
+
throw new Error(`\u573A\u666F\u5FEB\u7167\u4E0D\u662F\u5408\u6CD5 JSON:${url}`);
|
|
379
|
+
}
|
|
251
380
|
}
|
|
252
381
|
var EXAMPLE_SCENE_FILE = "scene.example.json";
|
|
253
382
|
var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
|
|
@@ -279,22 +408,34 @@ function deriveExampleSceneId(payload) {
|
|
|
279
408
|
async function exampleSceneSummary(exampleFile) {
|
|
280
409
|
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
281
410
|
}
|
|
411
|
+
async function fetchLinkedScenes(client) {
|
|
412
|
+
return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
|
|
413
|
+
}
|
|
282
414
|
async function listScenes(client, options = {}) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return
|
|
415
|
+
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
|
|
416
|
+
if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
|
|
417
|
+
return scenes;
|
|
286
418
|
}
|
|
287
419
|
async function pullScene(client, id, options = {}) {
|
|
288
420
|
if (client.mock) {
|
|
289
|
-
const
|
|
290
|
-
const sceneId = deriveExampleSceneId(
|
|
421
|
+
const payload2 = await loadExampleScene(options.exampleFile);
|
|
422
|
+
const sceneId = deriveExampleSceneId(payload2);
|
|
291
423
|
if (sceneId !== id) {
|
|
292
424
|
throw new Error(`\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F:\u793A\u4F8B\u573A\u666F id \u4E3A ${sceneId},\u6536\u5230 ${id}(\u672C\u5730\u4EC5\u63D0\u4F9B ${EXAMPLE_SCENE_FILE} \u8FD9\u4E00\u4E2A\u573A\u666F)`);
|
|
293
425
|
}
|
|
294
|
-
return { id: sceneId, name: MOCK_SCENE_NAME, payload };
|
|
426
|
+
return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
|
|
427
|
+
}
|
|
428
|
+
const data = await fetchLinkedScenes(client);
|
|
429
|
+
const scenes = normalizeLinkedScenes(data);
|
|
430
|
+
const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
|
|
431
|
+
if (!hit) {
|
|
432
|
+
const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
|
|
433
|
+
throw new Error(
|
|
434
|
+
available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
|
|
435
|
+
);
|
|
295
436
|
}
|
|
296
|
-
const
|
|
297
|
-
return
|
|
437
|
+
const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
|
|
438
|
+
return { id: hit.sceneKey, name: hit.name, payload };
|
|
298
439
|
}
|
|
299
440
|
async function saveSceneJson(scene, out) {
|
|
300
441
|
await fs2.mkdir(path2.dirname(out), { recursive: true });
|
|
@@ -355,7 +496,43 @@ function parseSceneStructure(scene) {
|
|
|
355
496
|
// src/upload.ts
|
|
356
497
|
import { promises as fs3 } from "fs";
|
|
357
498
|
import path3 from "path";
|
|
358
|
-
var
|
|
499
|
+
var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
500
|
+
".git",
|
|
501
|
+
"node_modules",
|
|
502
|
+
"dist",
|
|
503
|
+
".easytwin",
|
|
504
|
+
".cursor",
|
|
505
|
+
".claude",
|
|
506
|
+
".qoder",
|
|
507
|
+
".vscode"
|
|
508
|
+
]);
|
|
509
|
+
var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
|
|
510
|
+
function isTsconfigFile(base) {
|
|
511
|
+
return /^tsconfig(\..+)?\.json$/i.test(base);
|
|
512
|
+
}
|
|
513
|
+
var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
|
|
514
|
+
function isWorkspaceSpecFile(relPath) {
|
|
515
|
+
const base = normalizePath(relPath).split("/").pop() ?? "";
|
|
516
|
+
return /\.spec\.ts$/i.test(base);
|
|
517
|
+
}
|
|
518
|
+
function isIgnoredWorkspacePath(relPath) {
|
|
519
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
520
|
+
if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
|
|
521
|
+
const base = parts[parts.length - 1];
|
|
522
|
+
if (base === void 0) return false;
|
|
523
|
+
if (WORKSPACE_IGNORED_FILES.has(base)) return true;
|
|
524
|
+
if (isTsconfigFile(base)) return true;
|
|
525
|
+
return base.toLowerCase().endsWith(".scene.json");
|
|
526
|
+
}
|
|
527
|
+
function defaultWorkspaceIgnore(relPath) {
|
|
528
|
+
return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
|
|
529
|
+
}
|
|
530
|
+
function combineUploadIgnore(extra) {
|
|
531
|
+
return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
532
|
+
}
|
|
533
|
+
async function collectWorkspaceCodeFiles(dir, ignore) {
|
|
534
|
+
return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
|
|
535
|
+
}
|
|
359
536
|
async function collectFiles(dir, ignore = () => false) {
|
|
360
537
|
const files = [];
|
|
361
538
|
async function walk(current, rel) {
|
|
@@ -364,7 +541,7 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
364
541
|
const relPath = path3.posix.join(rel, entry.name);
|
|
365
542
|
if (ignore(relPath)) continue;
|
|
366
543
|
if (entry.isDirectory()) {
|
|
367
|
-
if (
|
|
544
|
+
if (entry.name === ".git") continue;
|
|
368
545
|
await walk(path3.join(current, entry.name), relPath);
|
|
369
546
|
} else if (entry.isFile()) {
|
|
370
547
|
const absPath = path3.join(current, entry.name);
|
|
@@ -404,338 +581,447 @@ Content-Type: application/octet-stream\r
|
|
|
404
581
|
`, "utf8"));
|
|
405
582
|
return Buffer.concat(parts);
|
|
406
583
|
}
|
|
584
|
+
function asRecord2(value) {
|
|
585
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
586
|
+
}
|
|
587
|
+
function asString2(value) {
|
|
588
|
+
return typeof value === "string" ? value : "";
|
|
589
|
+
}
|
|
590
|
+
function asNumber(value) {
|
|
591
|
+
return typeof value === "number" && Number.isFinite(value) ? value : NaN;
|
|
592
|
+
}
|
|
593
|
+
function normalizePath(relPath) {
|
|
594
|
+
return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
595
|
+
}
|
|
596
|
+
function normalizeWorkspaceConfig(data) {
|
|
597
|
+
const it = asRecord2(data);
|
|
598
|
+
if (!Array.isArray(it.allowedFileExtensions)) throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
599
|
+
const maxCodeFiles = asNumber(it.maxCodeFiles);
|
|
600
|
+
const maxCodeDirectoryDepth = asNumber(it.maxCodeDirectoryDepth);
|
|
601
|
+
if (!Number.isFinite(maxCodeFiles) || !Number.isFinite(maxCodeDirectoryDepth)) {
|
|
602
|
+
throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
allowedFileExtensions: it.allowedFileExtensions.filter((e) => typeof e === "string"),
|
|
606
|
+
maxCodeFiles,
|
|
607
|
+
maxCodeDirectoryDepth
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function normalizeWorkspaceFiles(data) {
|
|
611
|
+
if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
612
|
+
return data.map((item) => {
|
|
613
|
+
const it = asRecord2(item);
|
|
614
|
+
return { id: asString2(it.id), filePath: asString2(it.filePath), content: asString2(it.content) };
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function extensionOf(relPath) {
|
|
618
|
+
const base = relPath.split("/").pop() ?? "";
|
|
619
|
+
const i = base.lastIndexOf(".");
|
|
620
|
+
if (i <= 0) return "";
|
|
621
|
+
return base.slice(i).toLowerCase();
|
|
622
|
+
}
|
|
623
|
+
function allowedExtensionSet(list) {
|
|
624
|
+
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
625
|
+
}
|
|
626
|
+
var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
|
|
627
|
+
function isWorkspaceCodeFile(relPath) {
|
|
628
|
+
return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
|
|
629
|
+
}
|
|
630
|
+
function directoryDepth(relPath) {
|
|
631
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
632
|
+
return Math.max(0, parts.length - 1);
|
|
633
|
+
}
|
|
634
|
+
function assertUploadable(files, config) {
|
|
635
|
+
const errors = [];
|
|
636
|
+
if (files.length > config.maxCodeFiles) {
|
|
637
|
+
errors.push(`\u6587\u4EF6\u6570 ${files.length} \u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeFiles}`);
|
|
638
|
+
}
|
|
639
|
+
const tooDeep = files.filter((f) => directoryDepth(f.relPath) > config.maxCodeDirectoryDepth);
|
|
640
|
+
if (tooDeep.length > 0) {
|
|
641
|
+
errors.push(
|
|
642
|
+
`\u76EE\u5F55\u6DF1\u5EA6\u8D85\u8FC7\u4E0A\u9650 ${config.maxCodeDirectoryDepth}:${tooDeep.map((f) => f.relPath).join(", ")}`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (config.allowedFileExtensions.length > 0) {
|
|
646
|
+
const allowed = allowedExtensionSet(config.allowedFileExtensions);
|
|
647
|
+
const bad = files.filter((f) => !allowed.has(extensionOf(f.relPath)));
|
|
648
|
+
if (bad.length > 0) {
|
|
649
|
+
errors.push(
|
|
650
|
+
`\u6269\u5C55\u540D\u4E0D\u5728\u5141\u8BB8\u5217\u8868(${config.allowedFileExtensions.join(", ")}):${bad.map((f) => f.relPath).join(", ")}`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
|
|
655
|
+
}
|
|
656
|
+
function countsFromPlan(plan) {
|
|
657
|
+
return {
|
|
658
|
+
created: plan.creates.length,
|
|
659
|
+
updated: plan.updates.length,
|
|
660
|
+
deleted: plan.deletes.length,
|
|
661
|
+
unchanged: plan.unchanged.length
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function planWorkspaceUpload(local, remote) {
|
|
665
|
+
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
666
|
+
const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
|
|
667
|
+
const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
|
|
668
|
+
const creates = [];
|
|
669
|
+
const updates = [];
|
|
670
|
+
const unchanged = [];
|
|
671
|
+
for (const item of local) {
|
|
672
|
+
const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
|
|
673
|
+
if (!remoteFile) creates.push(item);
|
|
674
|
+
else if (remoteFile.content !== item.content) {
|
|
675
|
+
updates.push({ file: item.file, id: remoteFile.id, content: item.content });
|
|
676
|
+
} else {
|
|
677
|
+
unchanged.push(item.file);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { creates, updates, deletes, unchanged };
|
|
681
|
+
}
|
|
682
|
+
function formatUploadResult(result) {
|
|
683
|
+
const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
|
|
684
|
+
return `${prefix}\u672C\u5730 ${result.fileCount} \u4E2A\u6587\u4EF6,\u5171 ${result.byteCount} bytes;\u65B0\u589E ${result.created},\u66F4\u65B0 ${result.updated},\u5220\u9664 ${result.deleted},\u672A\u53D8 ${result.unchanged}`;
|
|
685
|
+
}
|
|
407
686
|
async function mockUpload(dir, options) {
|
|
408
|
-
const ignore = options.ignore
|
|
409
|
-
const files = await
|
|
687
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
688
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
410
689
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
411
690
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
412
691
|
options.onProgress?.({ phase: "upload", current: total, total });
|
|
413
|
-
return { fileCount: files.length, byteCount: total, mock: true };
|
|
692
|
+
return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
|
|
693
|
+
}
|
|
694
|
+
function buildWorkspacePushBody(plan) {
|
|
695
|
+
const body = {};
|
|
696
|
+
if (plan.creates.length > 0) {
|
|
697
|
+
body.create = plan.creates.map((c) => ({
|
|
698
|
+
filePath: normalizePath(c.file.relPath),
|
|
699
|
+
content: c.content
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
if (plan.updates.length > 0) {
|
|
703
|
+
body.update = plan.updates.map((p) => ({
|
|
704
|
+
id: p.id,
|
|
705
|
+
content: p.content,
|
|
706
|
+
filePath: normalizePath(p.file.relPath)
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
if (plan.deletes.length > 0) {
|
|
710
|
+
body.delete = plan.deletes.map((d) => d.id);
|
|
711
|
+
}
|
|
712
|
+
return Object.keys(body).length > 0 ? body : null;
|
|
414
713
|
}
|
|
415
714
|
async function uploadDirectory(client, dir, options = {}) {
|
|
416
715
|
if (client.mock) return mockUpload(dir, options);
|
|
417
|
-
const ignore = options.ignore
|
|
418
|
-
const files = await
|
|
716
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
717
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
419
718
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
420
719
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
421
|
-
|
|
720
|
+
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
721
|
+
const local = [];
|
|
422
722
|
for (const file of files) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
723
|
+
local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
|
|
724
|
+
}
|
|
725
|
+
const plan = planWorkspaceUpload(local, remote);
|
|
726
|
+
const counts = countsFromPlan(plan);
|
|
727
|
+
const pushBody = buildWorkspacePushBody(plan);
|
|
728
|
+
if (pushBody) {
|
|
729
|
+
await client.request(ENDPOINTS.workspaceCodePush(), {
|
|
730
|
+
method: "POST",
|
|
731
|
+
body: JSON.stringify(pushBody)
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
let sent = 0;
|
|
735
|
+
const bump = (file) => {
|
|
432
736
|
sent += file.size;
|
|
433
737
|
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
434
|
-
}
|
|
435
|
-
|
|
738
|
+
};
|
|
739
|
+
for (const c of plan.creates) bump(c.file);
|
|
740
|
+
for (const p of plan.updates) bump(p.file);
|
|
741
|
+
for (const u of plan.unchanged) bump(u);
|
|
742
|
+
if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
|
|
743
|
+
return { fileCount: files.length, byteCount: total, ...counts };
|
|
436
744
|
}
|
|
437
745
|
|
|
438
|
-
// src/
|
|
439
|
-
import {
|
|
746
|
+
// src/workspace.ts
|
|
747
|
+
import { promises as fs4 } from "fs";
|
|
748
|
+
import path5 from "path";
|
|
749
|
+
|
|
750
|
+
// src/assetsPath.ts
|
|
440
751
|
import path4 from "path";
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
var META_FILE_NAME = ".easytwin-meta.json";
|
|
453
|
-
var EASYTWIN_TYPES_DIR = ".easytwin/types";
|
|
454
|
-
var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
455
|
-
{
|
|
456
|
-
compilerOptions: {
|
|
457
|
-
target: "ES2022",
|
|
458
|
-
module: "ESNext",
|
|
459
|
-
moduleResolution: "bundler",
|
|
460
|
-
strict: true,
|
|
461
|
-
skipLibCheck: true,
|
|
462
|
-
noEmit: true,
|
|
463
|
-
paths: {
|
|
464
|
-
"@easytwin/runtime": [".easytwin/types"]
|
|
465
|
-
}
|
|
466
|
-
},
|
|
467
|
-
include: ["src/**/*.ts"]
|
|
468
|
-
},
|
|
469
|
-
null,
|
|
470
|
-
2
|
|
471
|
-
)}
|
|
472
|
-
`;
|
|
473
|
-
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
474
|
-
"skipLibCheck": true,
|
|
475
|
-
"paths": {
|
|
476
|
-
"@easytwin/runtime": [".easytwin/types"]
|
|
477
|
-
}`;
|
|
478
|
-
var RUN_CONTEXT_DECL = `
|
|
479
|
-
/** \u9884\u89C8\u9875 Run \u6309\u94AE\u6CE8\u5165\u7684\u8FD0\u884C\u4E0A\u4E0B\u6587(\u89C1 D12)\u3002 */
|
|
480
|
-
export interface EasyTwinRunContext {
|
|
481
|
-
engine: RuntimeEngine;
|
|
482
|
-
runtime: typeof import("@easytwin/runtime");
|
|
483
|
-
sceneJson: SceneJson;
|
|
752
|
+
function resolveWorkspaceAssetPath(cwd, relPath) {
|
|
753
|
+
const root = path4.resolve(cwd);
|
|
754
|
+
const input = relPath.trim();
|
|
755
|
+
if (!input) throw new Error("assets \u8DEF\u5F84\u4E3A\u7A7A");
|
|
756
|
+
if (path4.isAbsolute(input)) throw new Error(`assets \u53EA\u63A5\u53D7\u5DE5\u4F5C\u533A\u76F8\u5BF9\u8DEF\u5F84,\u62D2\u7EDD\u7EDD\u5BF9\u8DEF\u5F84: ${relPath}`);
|
|
757
|
+
const resolved = path4.resolve(root, input);
|
|
758
|
+
const rel = path4.relative(root, resolved);
|
|
759
|
+
if (rel.startsWith("..") || path4.isAbsolute(rel)) {
|
|
760
|
+
throw new Error(`assets \u8DEF\u5F84\u9003\u9038\u5DE5\u4F5C\u533A: ${relPath}`);
|
|
761
|
+
}
|
|
762
|
+
return resolved;
|
|
484
763
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
764
|
+
|
|
765
|
+
// src/workspace.ts
|
|
766
|
+
var DEFAULT_ENTRY_PATH = "src/main.ts";
|
|
767
|
+
var DEFAULT_MAIN_TS = `import { TwinApp, type TwinAppContext } from "@easytwin/apps";
|
|
768
|
+
|
|
769
|
+
export default class App extends TwinApp {
|
|
770
|
+
async init(ctx: TwinAppContext) {
|
|
771
|
+
// engine / container \u5DF2\u6709;scene / camera \u4E3A null
|
|
772
|
+
void ctx;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async onSceneLoaded(ctx: TwinAppContext) {
|
|
776
|
+
// \u573A\u666F\u5B57\u6BB5\u6709\u503C;\u5728\u6B64\u52A0\u7269\u4F53 / \u7ED1\u4E8B\u4EF6,\u5E76\u7528 ctx.sceneCleanup \u5BF9\u79F0\u62C6\u9664
|
|
777
|
+
void ctx;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
onUpdate(ctx: TwinAppContext, delta: number, elapsed: number) {
|
|
781
|
+
void ctx;
|
|
782
|
+
void delta;
|
|
783
|
+
void elapsed;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
onDispose(ctx: TwinAppContext) {
|
|
787
|
+
void ctx;
|
|
788
|
+
}
|
|
492
789
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return [target];
|
|
790
|
+
`;
|
|
791
|
+
function defaultPullIgnore(relPath) {
|
|
792
|
+
return defaultWorkspaceIgnore(relPath);
|
|
497
793
|
}
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
|
|
794
|
+
function isSafeRelPath(relPath) {
|
|
795
|
+
const n = normalizePath(relPath);
|
|
796
|
+
if (!n) return false;
|
|
797
|
+
if (n.toLowerCase() === "easytwin.config.json") return false;
|
|
798
|
+
const base = n.split("/").pop() ?? "";
|
|
799
|
+
if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
|
|
800
|
+
if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
|
|
801
|
+
const parts = n.split("/");
|
|
802
|
+
if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
|
|
803
|
+
return true;
|
|
504
804
|
}
|
|
505
|
-
function
|
|
506
|
-
|
|
507
|
-
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
|
|
508
|
-
}
|
|
509
|
-
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
510
|
-
const fromDist = path4.join(here, "runtime-types", "index.d.ts");
|
|
511
|
-
const fromSrc = path4.resolve(here, "lib", "index.d.ts");
|
|
512
|
-
if (existsSync(fromDist)) return fromDist;
|
|
513
|
-
if (existsSync(fromSrc)) return fromSrc;
|
|
514
|
-
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
|
|
805
|
+
function normalizeEol(text) {
|
|
806
|
+
return text.replace(/\r\n/g, "\n");
|
|
515
807
|
}
|
|
516
|
-
|
|
517
|
-
return
|
|
808
|
+
function combineIgnore(extra) {
|
|
809
|
+
return (relPath) => defaultPullIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
518
810
|
}
|
|
519
|
-
async function
|
|
520
|
-
const marker = path4.join(skillsDir, ".easytwin-source.json");
|
|
521
|
-
try {
|
|
522
|
-
const meta = await readJson(marker);
|
|
523
|
-
if (typeof meta.version === "string") return meta.version;
|
|
524
|
-
} catch {
|
|
525
|
-
}
|
|
811
|
+
async function readLocalText(absPath) {
|
|
526
812
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return
|
|
813
|
+
return await fs4.readFile(absPath, "utf8");
|
|
814
|
+
} catch (err) {
|
|
815
|
+
const code = err.code;
|
|
816
|
+
if (code === "ENOENT") return void 0;
|
|
817
|
+
throw err;
|
|
531
818
|
}
|
|
532
819
|
}
|
|
533
|
-
async function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
for (const entry of entries) {
|
|
537
|
-
const s = path4.join(src, entry.name);
|
|
538
|
-
const d = path4.join(dest, entry.name);
|
|
539
|
-
if (entry.isDirectory()) await copyDir(s, d);
|
|
540
|
-
else if (entry.isFile()) await fs4.copyFile(s, d);
|
|
541
|
-
}
|
|
820
|
+
async function fetchRemoteFiles(client) {
|
|
821
|
+
if (client.mock) return [];
|
|
822
|
+
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
542
823
|
}
|
|
543
|
-
async function
|
|
544
|
-
|
|
824
|
+
async function planWorkspacePull(client, dir, options = {}) {
|
|
825
|
+
const ignore = combineIgnore(options.ignore);
|
|
826
|
+
const remoteRaw = await fetchRemoteFiles(client);
|
|
827
|
+
const skippedRemotePaths = [];
|
|
828
|
+
const remoteByPath = /* @__PURE__ */ new Map();
|
|
829
|
+
for (const file of remoteRaw) {
|
|
830
|
+
const rel = normalizePath(file.filePath);
|
|
831
|
+
if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
|
|
832
|
+
skippedRemotePaths.push(rel || file.filePath);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
remoteByPath.set(rel, file.content);
|
|
836
|
+
}
|
|
837
|
+
let localFiles = [];
|
|
545
838
|
try {
|
|
546
|
-
|
|
547
|
-
} catch {
|
|
548
|
-
|
|
839
|
+
localFiles = await collectFiles(dir, ignore);
|
|
840
|
+
} catch (err) {
|
|
841
|
+
const code = err.code;
|
|
842
|
+
if (code !== "ENOENT") throw err;
|
|
549
843
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
844
|
+
const localByPath = /* @__PURE__ */ new Map();
|
|
845
|
+
for (const file of localFiles) {
|
|
846
|
+
const rel = normalizePath(file.relPath);
|
|
847
|
+
if (!isWorkspaceCodeFile(rel)) continue;
|
|
848
|
+
const content = await readLocalText(file.absPath);
|
|
849
|
+
if (content !== void 0) localByPath.set(rel, content);
|
|
850
|
+
}
|
|
851
|
+
const changes = [];
|
|
852
|
+
const seen = /* @__PURE__ */ new Set();
|
|
853
|
+
for (const [rel, remoteContent] of remoteByPath) {
|
|
854
|
+
seen.add(rel);
|
|
855
|
+
const localContent = localByPath.get(rel);
|
|
856
|
+
if (localContent === void 0) {
|
|
857
|
+
changes.push({ path: rel, kind: "remote-only", remoteContent });
|
|
858
|
+
} else if (normalizeEol(localContent) === normalizeEol(remoteContent)) {
|
|
859
|
+
changes.push({ path: rel, kind: "identical", localContent, remoteContent });
|
|
556
860
|
} else {
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
dStat = await fs4.stat(d);
|
|
560
|
-
} catch {
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
564
|
-
if (!(await fs4.readFile(s)).equals(await fs4.readFile(d))) return false;
|
|
861
|
+
changes.push({ path: rel, kind: "modified", localContent, remoteContent });
|
|
565
862
|
}
|
|
566
863
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const entries = [];
|
|
579
|
-
for (const name of SKILL_NAMES) {
|
|
580
|
-
const src = path4.join(sourceRoot, name);
|
|
581
|
-
const dest = path4.join(targetRoot, name);
|
|
582
|
-
const exists = await fs4.stat(dest).then(() => true).catch(() => false);
|
|
583
|
-
const matches = await dirMatches(src, dest);
|
|
584
|
-
const action = actionFor(exists, matches);
|
|
585
|
-
if (action !== "unchanged") {
|
|
586
|
-
await fs4.rm(dest, { recursive: true, force: true });
|
|
587
|
-
await copyDir(src, dest);
|
|
864
|
+
for (const [rel, localContent] of localByPath) {
|
|
865
|
+
if (seen.has(rel)) continue;
|
|
866
|
+
changes.push({ path: rel, kind: "local-only", localContent });
|
|
867
|
+
}
|
|
868
|
+
const remoteEmpty = remoteByPath.size === 0;
|
|
869
|
+
let seededDefaults = false;
|
|
870
|
+
if (remoteEmpty) {
|
|
871
|
+
const localMain = localByPath.get(DEFAULT_ENTRY_PATH);
|
|
872
|
+
if (localMain === void 0) {
|
|
873
|
+
changes.push({ path: DEFAULT_ENTRY_PATH, kind: "seed", remoteContent: DEFAULT_MAIN_TS });
|
|
874
|
+
seededDefaults = true;
|
|
588
875
|
}
|
|
589
|
-
entries.push({ name, action });
|
|
590
876
|
}
|
|
591
|
-
|
|
592
|
-
return {
|
|
877
|
+
changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
878
|
+
return {
|
|
879
|
+
remoteEmpty,
|
|
880
|
+
seededDefaults,
|
|
881
|
+
mock: client.mock || void 0,
|
|
882
|
+
skippedRemotePaths,
|
|
883
|
+
changes
|
|
884
|
+
};
|
|
593
885
|
}
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
""
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
""
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
886
|
+
async function applyWorkspacePull(dir, plan, options = {}) {
|
|
887
|
+
const written = [];
|
|
888
|
+
const skippedConflicts = [];
|
|
889
|
+
for (const change of plan.changes) {
|
|
890
|
+
if (change.kind === "identical" || change.kind === "local-only") continue;
|
|
891
|
+
if (change.kind === "modified" && !options.force) {
|
|
892
|
+
skippedConflicts.push(change.path);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const content = change.remoteContent ?? "";
|
|
896
|
+
const abs = resolveWorkspaceAssetPath(dir, change.path);
|
|
897
|
+
await fs4.mkdir(path5.dirname(abs), { recursive: true });
|
|
898
|
+
await fs4.writeFile(abs, content, "utf8");
|
|
899
|
+
written.push(change.path);
|
|
900
|
+
}
|
|
901
|
+
return { written, skippedConflicts };
|
|
610
902
|
}
|
|
611
|
-
async function
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
content = await fs4.readFile(agentsFile, "utf8");
|
|
618
|
-
} catch {
|
|
619
|
-
exists = false;
|
|
903
|
+
async function pullWorkspace(client, dir, options = {}) {
|
|
904
|
+
const plan = await planWorkspacePull(client, dir, { ignore: options.ignore });
|
|
905
|
+
if (options.dryRun) {
|
|
906
|
+
const skippedConflicts = plan.changes.filter((c) => c.kind === "modified" && !options.force).map((c) => c.path);
|
|
907
|
+
return { plan, written: [], skippedConflicts, dryRun: true, mock: plan.mock };
|
|
620
908
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
909
|
+
const applied = await applyWorkspacePull(dir, plan, { force: options.force });
|
|
910
|
+
return { plan, ...applied, mock: plan.mock };
|
|
911
|
+
}
|
|
912
|
+
var KIND_LABEL = {
|
|
913
|
+
identical: "same ",
|
|
914
|
+
"remote-only": "create",
|
|
915
|
+
"local-only": "keep ",
|
|
916
|
+
modified: "modify",
|
|
917
|
+
seed: "seed "
|
|
918
|
+
};
|
|
919
|
+
function formatWorkspacePullPlan(plan) {
|
|
920
|
+
const lines = [];
|
|
921
|
+
if (plan.mock) lines.push("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u6309\u8FDC\u7AEF\u4E3A\u7A7A\u5904\u7406");
|
|
922
|
+
const remoteCount = plan.changes.filter((c) => c.kind !== "local-only" && c.kind !== "seed").length;
|
|
923
|
+
if (plan.remoteEmpty) {
|
|
924
|
+
lines.push(
|
|
925
|
+
plan.seededDefaults ? `\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u5C06\u5199\u5165\u9ED8\u8BA4 ${DEFAULT_ENTRY_PATH}` : "\u8FDC\u7AEF 0 \u4E2A\u6587\u4EF6,\u672C\u5730\u5DF2\u6709\u5165\u53E3,\u672A\u6539\u52A8\u9ED8\u8BA4\u6A21\u677F"
|
|
926
|
+
);
|
|
629
927
|
} else {
|
|
630
|
-
|
|
631
|
-
action = current === segment ? "unchanged" : "updated";
|
|
632
|
-
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
928
|
+
lines.push(`\u8FDC\u7AEF ${remoteCount} \u4E2A\u6587\u4EF6`);
|
|
633
929
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
930
|
+
for (const change of plan.changes) {
|
|
931
|
+
let extra = "";
|
|
932
|
+
if (change.kind === "modified") extra = " (\u51B2\u7A81,\u9ED8\u8BA4\u4E0D\u8986\u76D6)";
|
|
933
|
+
if (change.kind === "local-only") extra = " (\u4EC5\u672C\u5730)";
|
|
934
|
+
if (change.kind === "seed") extra = " (\u9ED8\u8BA4 TwinApp)";
|
|
935
|
+
lines.push(` ${KIND_LABEL[change.kind]} ${change.path}${extra}`);
|
|
637
936
|
}
|
|
638
|
-
|
|
937
|
+
if (plan.skippedRemotePaths.length > 0) {
|
|
938
|
+
lines.push(`\u8DF3\u8FC7\u975E\u6CD5/\u51ED\u636E\u8DEF\u5F84: ${plan.skippedRemotePaths.join(", ")}`);
|
|
939
|
+
}
|
|
940
|
+
return lines.join("\n");
|
|
639
941
|
}
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
} catch {
|
|
649
|
-
exists = false;
|
|
942
|
+
function formatWorkspacePullResult(result) {
|
|
943
|
+
const lines = [formatWorkspacePullPlan(result.plan)];
|
|
944
|
+
if (result.dryRun) {
|
|
945
|
+
lines.push("dry-run:\u672A\u5199\u76D8");
|
|
946
|
+
if (result.skippedConflicts.length > 0) {
|
|
947
|
+
lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force)`);
|
|
948
|
+
}
|
|
949
|
+
return lines.join("\n");
|
|
650
950
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
951
|
+
if (result.written.length > 0) {
|
|
952
|
+
lines.push(`\u5DF2\u5199\u5165 ${result.written.length} \u4E2A\u6587\u4EF6: ${result.written.join(", ")}`);
|
|
953
|
+
} else {
|
|
954
|
+
lines.push("\u672A\u5199\u5165\u6587\u4EF6");
|
|
655
955
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
let pathsHint;
|
|
659
|
-
try {
|
|
660
|
-
const existing = await fs4.readFile(tsconfigPath, "utf8");
|
|
661
|
-
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
662
|
-
else {
|
|
663
|
-
tsconfig = "manual-paths";
|
|
664
|
-
pathsHint = TSCONFIG_PATHS_HINT;
|
|
665
|
-
}
|
|
666
|
-
} catch {
|
|
667
|
-
await fs4.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
668
|
-
tsconfig = "created";
|
|
956
|
+
if (result.skippedConflicts.length > 0) {
|
|
957
|
+
lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force): ${result.skippedConflicts.join(", ")}`);
|
|
669
958
|
}
|
|
670
|
-
return
|
|
959
|
+
return lines.join("\n");
|
|
671
960
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
675
|
-
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
676
|
-
const summaries = [];
|
|
677
|
-
for (const target of targets) {
|
|
678
|
-
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path4.join(options.cwd, ".cursor", "skills"), version));
|
|
679
|
-
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path4.join(options.cwd, ".claude", "skills"), version));
|
|
680
|
-
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path4.join(options.cwd, ".qoder", "skills"), version));
|
|
681
|
-
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
682
|
-
}
|
|
683
|
-
const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
|
|
684
|
-
return { summaries, types };
|
|
961
|
+
function workspacePullHasConflicts(plan) {
|
|
962
|
+
return plan.changes.some((c) => c.kind === "modified");
|
|
685
963
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
const version = await readDevkitVersion(sourceRoot);
|
|
689
|
-
const targets = [];
|
|
690
|
-
const dirTargets = ["cursor", "claude", "qoder"];
|
|
691
|
-
const DIR_ROOTS = {
|
|
692
|
-
cursor: ".cursor/skills",
|
|
693
|
-
claude: ".claude/skills",
|
|
694
|
-
qoder: ".qoder/skills"
|
|
695
|
-
};
|
|
696
|
-
for (const target of dirTargets) {
|
|
697
|
-
const root = path4.join(cwd, DIR_ROOTS[target]);
|
|
698
|
-
const missing = [];
|
|
699
|
-
for (const name of SKILL_NAMES) {
|
|
700
|
-
if (!await fs4.stat(path4.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
|
|
701
|
-
}
|
|
702
|
-
let stale = false;
|
|
703
|
-
try {
|
|
704
|
-
const meta = await readJson(path4.join(root, META_FILE_NAME));
|
|
705
|
-
stale = meta.version !== version;
|
|
706
|
-
} catch {
|
|
707
|
-
stale = missing.length === 0;
|
|
708
|
-
}
|
|
709
|
-
targets.push({
|
|
710
|
-
target,
|
|
711
|
-
synced: missing.length === 0 && !stale,
|
|
712
|
-
reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
let agentsContent = "";
|
|
716
|
-
try {
|
|
717
|
-
agentsContent = await fs4.readFile(path4.join(cwd, "AGENTS.md"), "utf8");
|
|
718
|
-
} catch {
|
|
719
|
-
}
|
|
720
|
-
const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
|
|
721
|
-
targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
|
|
722
|
-
let typesSynced = false;
|
|
723
|
-
try {
|
|
724
|
-
const dts = await fs4.readFile(path4.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
|
|
725
|
-
typesSynced = dts.includes("export interface EasyTwinRunContext");
|
|
726
|
-
} catch {
|
|
727
|
-
typesSynced = false;
|
|
728
|
-
}
|
|
729
|
-
return { cwd, targets, typesSynced };
|
|
964
|
+
function workspacePullPendingWrites(plan, force = false) {
|
|
965
|
+
return plan.changes.filter((c) => c.kind === "seed" || c.kind === "remote-only" || force && c.kind === "modified").map((c) => c.path);
|
|
730
966
|
}
|
|
731
967
|
|
|
968
|
+
// src/workspaceTests.ts
|
|
969
|
+
import { promises as fs6 } from "fs";
|
|
970
|
+
import path7 from "path";
|
|
971
|
+
|
|
732
972
|
// src/bundle.ts
|
|
733
973
|
import * as esbuild from "esbuild-wasm";
|
|
734
974
|
import { promises as fs5 } from "fs";
|
|
735
|
-
import
|
|
975
|
+
import path6 from "path";
|
|
976
|
+
|
|
977
|
+
// src/apps.ts
|
|
978
|
+
var PORTABLE_RUNTIME_EXPORTS = [
|
|
979
|
+
"THREE",
|
|
980
|
+
"RuntimeEngine",
|
|
981
|
+
"SceneManager",
|
|
982
|
+
"LoadSceneMode",
|
|
983
|
+
"convertObjToComponentJson"
|
|
984
|
+
];
|
|
985
|
+
var TwinApp = class {
|
|
986
|
+
};
|
|
987
|
+
function defineApp(app) {
|
|
988
|
+
return app;
|
|
989
|
+
}
|
|
990
|
+
var LIFECYCLE_NAMES = [
|
|
991
|
+
"init",
|
|
992
|
+
"onUpdate",
|
|
993
|
+
"onBeforeSceneUnload",
|
|
994
|
+
"onSceneLoaded",
|
|
995
|
+
"onDispose",
|
|
996
|
+
"onError"
|
|
997
|
+
];
|
|
998
|
+
function isLifecycleApp(value) {
|
|
999
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1000
|
+
return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
|
|
1001
|
+
}
|
|
1002
|
+
function createAppInstance(appExport) {
|
|
1003
|
+
if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
|
|
1004
|
+
return new appExport();
|
|
1005
|
+
}
|
|
1006
|
+
if (isLifecycleApp(appExport)) return appExport;
|
|
1007
|
+
throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
|
|
1008
|
+
}
|
|
1009
|
+
function portableRuntimeWarning(names) {
|
|
1010
|
+
const extra = [...new Set(names)].filter(
|
|
1011
|
+
(n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
|
|
1012
|
+
);
|
|
1013
|
+
const ns = [...names].includes("*");
|
|
1014
|
+
if (!ns && extra.length === 0) return void 0;
|
|
1015
|
+
const detail = ns ? "namespace import *" : extra.sort().join(", ");
|
|
1016
|
+
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`;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/bundle.ts
|
|
736
1020
|
var USER_ENTRY = "src/main.ts";
|
|
737
1021
|
var DEFAULT_BUNDLE_OUT = "dist/main.js";
|
|
738
1022
|
var RUNTIME_MODULE = "@easytwin/runtime";
|
|
1023
|
+
var APPS_MODULE = "@easytwin/apps";
|
|
1024
|
+
var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
|
|
739
1025
|
var BundleError = class extends Error {
|
|
740
1026
|
constructor(message) {
|
|
741
1027
|
super(message);
|
|
@@ -743,7 +1029,7 @@ var BundleError = class extends Error {
|
|
|
743
1029
|
}
|
|
744
1030
|
};
|
|
745
1031
|
function isRelativeOrAbsolute(spec) {
|
|
746
|
-
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") ||
|
|
1032
|
+
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path6.isAbsolute(spec);
|
|
747
1033
|
}
|
|
748
1034
|
function whitelistPlugin() {
|
|
749
1035
|
return {
|
|
@@ -752,11 +1038,11 @@ function whitelistPlugin() {
|
|
|
752
1038
|
build2.onResolve({ filter: /.*/ }, (args) => {
|
|
753
1039
|
if (args.kind === "entry-point") return void 0;
|
|
754
1040
|
if (isRelativeOrAbsolute(args.path)) return void 0;
|
|
755
|
-
if (args.path
|
|
1041
|
+
if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
|
|
756
1042
|
return {
|
|
757
1043
|
errors: [
|
|
758
1044
|
{
|
|
759
|
-
text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
|
|
1045
|
+
text: `\u68C0\u6D4B\u5230\u5916\u90E8\u4F9D\u8D56 ${args.path},\u7528\u6237\u4EE3\u7801\u53EA\u80FD\u4F9D\u8D56 @easytwin/runtime \u4E0E @easytwin/apps\u3002\u4E0D\u8981 npm install,\u7C7B\u578B\u7531 easytwin skills sync \u5206\u53D1,\u8FD0\u884C\u8D70\u9884\u89C8\u9875 Run \u6216 easytwin bundle\u3002`
|
|
760
1046
|
}
|
|
761
1047
|
]
|
|
762
1048
|
};
|
|
@@ -770,14 +1056,29 @@ function formatEsbuildMessages(messages) {
|
|
|
770
1056
|
return `${loc}${m.text}`;
|
|
771
1057
|
}).join("\n");
|
|
772
1058
|
}
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
1059
|
+
function collectBundledRuntimeExports(code) {
|
|
1060
|
+
const names = [];
|
|
1061
|
+
if (/import\s+\*\s+as\s+[\w$]+\s+from\s*["']@easytwin\/runtime["']/.test(code)) names.push("*");
|
|
1062
|
+
const named = /import\s*\{([^}]+)\}\s*from\s*["']@easytwin\/runtime["']/g;
|
|
1063
|
+
for (const match of code.matchAll(named)) {
|
|
1064
|
+
const body = match[1] ?? "";
|
|
1065
|
+
for (const part of body.split(",")) {
|
|
1066
|
+
const token = part.replace(/\btype\b/g, "").trim();
|
|
1067
|
+
if (!token) continue;
|
|
1068
|
+
const id = token.split(/\s+as\s+/)[0]?.trim();
|
|
1069
|
+
if (id) names.push(id);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return names;
|
|
1073
|
+
}
|
|
1074
|
+
async function bundleWorkspaceModule(options) {
|
|
1075
|
+
const cwd = path6.resolve(options.cwd);
|
|
1076
|
+
const entry = path6.isAbsolute(options.entry) ? options.entry : path6.join(cwd, options.entry);
|
|
776
1077
|
try {
|
|
777
1078
|
await fs5.access(entry);
|
|
778
1079
|
} catch {
|
|
779
1080
|
throw new BundleError(
|
|
780
|
-
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002
|
|
1081
|
+
options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
|
|
781
1082
|
);
|
|
782
1083
|
}
|
|
783
1084
|
let result;
|
|
@@ -808,16 +1109,28 @@ async function bundleUserCode(options) {
|
|
|
808
1109
|
if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
|
|
809
1110
|
const code = file.text;
|
|
810
1111
|
const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
|
|
1112
|
+
const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
|
|
1113
|
+
if (portable) warnings.push(portable);
|
|
811
1114
|
if (options.outFile) {
|
|
812
|
-
const outFile =
|
|
813
|
-
await fs5.mkdir(
|
|
1115
|
+
const outFile = path6.isAbsolute(options.outFile) ? options.outFile : path6.join(cwd, options.outFile);
|
|
1116
|
+
await fs5.mkdir(path6.dirname(outFile), { recursive: true });
|
|
814
1117
|
await fs5.writeFile(outFile, code, "utf8");
|
|
815
1118
|
}
|
|
816
1119
|
return { code, warnings };
|
|
817
1120
|
}
|
|
1121
|
+
async function bundleUserCode(options) {
|
|
1122
|
+
const cwd = path6.resolve(options.cwd);
|
|
1123
|
+
const entry = path6.join(cwd, USER_ENTRY);
|
|
1124
|
+
return bundleWorkspaceModule({
|
|
1125
|
+
cwd: options.cwd,
|
|
1126
|
+
entry: USER_ENTRY,
|
|
1127
|
+
outFile: options.outFile,
|
|
1128
|
+
missingEntryMessage: `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002\u8BF7\u5728\u5DE5\u4F5C\u533A\u521B\u5EFA src/main.ts,\u5E76 \`export default\` TwinApp \u5B50\u7C7B\u6216 defineApp({...})\u3002`
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
818
1131
|
async function typesMissingHint(cwd) {
|
|
819
1132
|
try {
|
|
820
|
-
await fs5.access(
|
|
1133
|
+
await fs5.access(path6.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
821
1134
|
return void 0;
|
|
822
1135
|
} catch {
|
|
823
1136
|
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
@@ -911,10 +1224,1451 @@ function remapErrorStack(stack, bundledCode) {
|
|
|
911
1224
|
return `${orig.source}:${orig.line}:${orig.column}`;
|
|
912
1225
|
});
|
|
913
1226
|
}
|
|
1227
|
+
|
|
1228
|
+
// src/workspaceTests.ts
|
|
1229
|
+
var IDENT = "[A-Za-z_$][\\w$]*";
|
|
1230
|
+
var FUNCTION_EXPORT = new RegExp(
|
|
1231
|
+
`export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
|
|
1232
|
+
"g"
|
|
1233
|
+
);
|
|
1234
|
+
var CONST_EXPORT = new RegExp(
|
|
1235
|
+
`export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
|
|
1236
|
+
"g"
|
|
1237
|
+
);
|
|
1238
|
+
function stripTsCommentsAndStringsKeepCode(source) {
|
|
1239
|
+
let out = "";
|
|
1240
|
+
let i = 0;
|
|
1241
|
+
const n = source.length;
|
|
1242
|
+
while (i < n) {
|
|
1243
|
+
const c = source[i];
|
|
1244
|
+
const next = source[i + 1];
|
|
1245
|
+
if (c === "/" && next === "/") {
|
|
1246
|
+
i += 2;
|
|
1247
|
+
while (i < n && source[i] !== "\n") i++;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (c === "/" && next === "*") {
|
|
1251
|
+
i += 2;
|
|
1252
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
1253
|
+
i = Math.min(n, i + 2);
|
|
1254
|
+
out += " ";
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
1258
|
+
const quote = c;
|
|
1259
|
+
out += " ";
|
|
1260
|
+
i++;
|
|
1261
|
+
while (i < n) {
|
|
1262
|
+
const ch = source[i];
|
|
1263
|
+
if (ch === "\\") {
|
|
1264
|
+
i += 2;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (ch === quote) {
|
|
1268
|
+
i++;
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
i++;
|
|
1272
|
+
}
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
out += c;
|
|
1276
|
+
i++;
|
|
1277
|
+
}
|
|
1278
|
+
return out;
|
|
1279
|
+
}
|
|
1280
|
+
function matchingClose(src, openIndex, open, close) {
|
|
1281
|
+
let depth = 0;
|
|
1282
|
+
let angle = 0;
|
|
1283
|
+
let brace = 0;
|
|
1284
|
+
for (let i = openIndex; i < src.length; i++) {
|
|
1285
|
+
const c = src[i];
|
|
1286
|
+
if (c === open) depth++;
|
|
1287
|
+
else if (c === close) {
|
|
1288
|
+
depth--;
|
|
1289
|
+
if (depth === 0 && angle <= 0 && brace <= 0) return i;
|
|
1290
|
+
} else if (c === "<") angle++;
|
|
1291
|
+
else if (c === ">" && angle > 0) angle--;
|
|
1292
|
+
else if (c === "{") brace++;
|
|
1293
|
+
else if (c === "}" && brace > 0) brace--;
|
|
1294
|
+
}
|
|
1295
|
+
return -1;
|
|
1296
|
+
}
|
|
1297
|
+
function splitTopLevelParams(list) {
|
|
1298
|
+
const params = [];
|
|
1299
|
+
let current = "";
|
|
1300
|
+
let paren = 0;
|
|
1301
|
+
let angle = 0;
|
|
1302
|
+
let brace = 0;
|
|
1303
|
+
let bracket = 0;
|
|
1304
|
+
for (const c of list) {
|
|
1305
|
+
if (c === "(") paren++;
|
|
1306
|
+
else if (c === ")") paren--;
|
|
1307
|
+
else if (c === "<") angle++;
|
|
1308
|
+
else if (c === ">" && angle > 0) angle--;
|
|
1309
|
+
else if (c === "{") brace++;
|
|
1310
|
+
else if (c === "}" && brace > 0) brace--;
|
|
1311
|
+
else if (c === "[") bracket++;
|
|
1312
|
+
else if (c === "]" && bracket > 0) bracket--;
|
|
1313
|
+
if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
|
|
1314
|
+
if (current.trim()) params.push(current.trim());
|
|
1315
|
+
current = "";
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
current += c;
|
|
1319
|
+
}
|
|
1320
|
+
if (current.trim()) params.push(current.trim());
|
|
1321
|
+
return params;
|
|
1322
|
+
}
|
|
1323
|
+
function paramName(raw) {
|
|
1324
|
+
let s = raw.trim();
|
|
1325
|
+
if (!s || s === "this") return void 0;
|
|
1326
|
+
if (s.startsWith("{") || s.startsWith("[")) return "input";
|
|
1327
|
+
s = s.replace(/^\.\.\./, "");
|
|
1328
|
+
const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
|
|
1329
|
+
if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
|
|
1330
|
+
return token;
|
|
1331
|
+
}
|
|
1332
|
+
function collectFromPattern(source, pattern) {
|
|
1333
|
+
const found = [];
|
|
1334
|
+
pattern.lastIndex = 0;
|
|
1335
|
+
let match;
|
|
1336
|
+
while (match = pattern.exec(source)) {
|
|
1337
|
+
const name = match[1];
|
|
1338
|
+
if (!name) continue;
|
|
1339
|
+
const openIndex = match.index + match[0].length - 1;
|
|
1340
|
+
const closeIndex = matchingClose(source, openIndex, "(", ")");
|
|
1341
|
+
if (closeIndex < 0) continue;
|
|
1342
|
+
const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
|
|
1343
|
+
const hasInput = params.length >= 2;
|
|
1344
|
+
const second = hasInput ? paramName(params[1] ?? "") : void 0;
|
|
1345
|
+
found.push({
|
|
1346
|
+
id: name,
|
|
1347
|
+
file: "",
|
|
1348
|
+
name,
|
|
1349
|
+
hasInput,
|
|
1350
|
+
inputName: hasInput ? second : void 0
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
return found;
|
|
1354
|
+
}
|
|
1355
|
+
function parseExportedTestFunctions(source) {
|
|
1356
|
+
const text = stripTsCommentsAndStringsKeepCode(source);
|
|
1357
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1358
|
+
const out = [];
|
|
1359
|
+
for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
|
|
1360
|
+
if (seen.has(item.name)) continue;
|
|
1361
|
+
seen.add(item.name);
|
|
1362
|
+
out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
function workspaceTestId(file, name) {
|
|
1367
|
+
return `${normalizePath(file)}:${name}`;
|
|
1368
|
+
}
|
|
1369
|
+
async function listWorkspaceTests(cwd) {
|
|
1370
|
+
const root = path7.resolve(cwd);
|
|
1371
|
+
let files;
|
|
1372
|
+
try {
|
|
1373
|
+
files = await collectFiles(root, isIgnoredWorkspacePath);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
const code = err.code;
|
|
1376
|
+
if (code === "ENOENT") return [];
|
|
1377
|
+
throw err;
|
|
1378
|
+
}
|
|
1379
|
+
const tests = [];
|
|
1380
|
+
for (const file of files) {
|
|
1381
|
+
if (!isWorkspaceSpecFile(file.relPath)) continue;
|
|
1382
|
+
const posix = normalizePath(file.relPath);
|
|
1383
|
+
const source = await fs6.readFile(file.absPath, "utf8");
|
|
1384
|
+
for (const fn of parseExportedTestFunctions(source)) {
|
|
1385
|
+
tests.push({
|
|
1386
|
+
id: workspaceTestId(posix, fn.name),
|
|
1387
|
+
file: posix,
|
|
1388
|
+
name: fn.name,
|
|
1389
|
+
hasInput: fn.hasInput,
|
|
1390
|
+
inputName: fn.inputName
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
tests.sort((a, b) => a.id.localeCompare(b.id));
|
|
1395
|
+
return tests;
|
|
1396
|
+
}
|
|
1397
|
+
async function bundleWorkspaceTestFile(options) {
|
|
1398
|
+
const file = normalizePath(options.file);
|
|
1399
|
+
return bundleWorkspaceModule({
|
|
1400
|
+
cwd: options.cwd,
|
|
1401
|
+
entry: file,
|
|
1402
|
+
missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/skills.ts
|
|
1407
|
+
import { existsSync, promises as fs7 } from "fs";
|
|
1408
|
+
import path8 from "path";
|
|
1409
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1410
|
+
var SKILL_NAMES = [
|
|
1411
|
+
"easytwin-render",
|
|
1412
|
+
"easytwin-core",
|
|
1413
|
+
"easytwin-develop",
|
|
1414
|
+
"easytwin-bootstrap",
|
|
1415
|
+
"easytwin-scene",
|
|
1416
|
+
"easytwin-upload",
|
|
1417
|
+
"easytwin-test"
|
|
1418
|
+
];
|
|
1419
|
+
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
1420
|
+
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
1421
|
+
var META_FILE_NAME = ".easytwin-meta.json";
|
|
1422
|
+
var EASYTWIN_TYPES_DIR = ".easytwin/types";
|
|
1423
|
+
var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
1424
|
+
{
|
|
1425
|
+
compilerOptions: {
|
|
1426
|
+
target: "ES2022",
|
|
1427
|
+
module: "ESNext",
|
|
1428
|
+
moduleResolution: "bundler",
|
|
1429
|
+
strict: true,
|
|
1430
|
+
skipLibCheck: true,
|
|
1431
|
+
noEmit: true,
|
|
1432
|
+
paths: {
|
|
1433
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
1434
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
include: ["src/**/*.ts"]
|
|
1438
|
+
},
|
|
1439
|
+
null,
|
|
1440
|
+
2
|
|
1441
|
+
)}
|
|
1442
|
+
`;
|
|
1443
|
+
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
1444
|
+
"skipLibCheck": true,
|
|
1445
|
+
"paths": {
|
|
1446
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
1447
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
1448
|
+
}`;
|
|
1449
|
+
var APPS_TYPES_FILE = "apps.d.ts";
|
|
1450
|
+
var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
|
|
1451
|
+
|
|
1452
|
+
export type TwinAppContext = {
|
|
1453
|
+
app: {
|
|
1454
|
+
id: string;
|
|
1455
|
+
mode: "preview" | "publish";
|
|
1456
|
+
};
|
|
1457
|
+
engine: RuntimeEngine;
|
|
1458
|
+
container: HTMLElement;
|
|
1459
|
+
runtimeScene: RuntimeScene | null;
|
|
1460
|
+
scene: RuntimeScene["sceneObject"] | null;
|
|
1461
|
+
camera: RuntimeScene["camera"]["main"] | null;
|
|
1462
|
+
sceneManager: {
|
|
1463
|
+
currentSceneId: string;
|
|
1464
|
+
runtime: SceneManager | null;
|
|
1465
|
+
loadScene(sceneId: string): Promise<void>;
|
|
1466
|
+
};
|
|
1467
|
+
logger: {
|
|
1468
|
+
log(...args: unknown[]): void;
|
|
1469
|
+
info(...args: unknown[]): void;
|
|
1470
|
+
warn(...args: unknown[]): void;
|
|
1471
|
+
error(...args: unknown[]): void;
|
|
1472
|
+
};
|
|
1473
|
+
assets: {
|
|
1474
|
+
text(path: string): Promise<string>;
|
|
1475
|
+
json<T = unknown>(path: string): Promise<T>;
|
|
1476
|
+
};
|
|
1477
|
+
cleanup(fn: () => void | Promise<void>): void;
|
|
1478
|
+
sceneCleanup(fn: () => void | Promise<void>): void;
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
export declare abstract class TwinApp {
|
|
1482
|
+
init?(ctx: TwinAppContext): void | Promise<void>;
|
|
1483
|
+
onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
|
|
1484
|
+
onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
|
|
1485
|
+
onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
|
|
1486
|
+
onDispose?(ctx: TwinAppContext): void | Promise<void>;
|
|
1487
|
+
onError?(ctx: TwinAppContext, error: unknown): boolean | void;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
export declare function defineApp<T>(app: T): T;
|
|
1491
|
+
`;
|
|
1492
|
+
function buildRuntimeTypesContent(sourceDts) {
|
|
1493
|
+
return `${sourceDts.replace(/\s+$/, "")}
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
function normalizeTargets(target = "all") {
|
|
1497
|
+
if (target === "all") return ["cursor", "claude", "codex", "qoder"];
|
|
1498
|
+
if (Array.isArray(target)) return [...new Set(target)];
|
|
1499
|
+
return [target];
|
|
1500
|
+
}
|
|
1501
|
+
function resolveSkillsSourceDir() {
|
|
1502
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
1503
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
|
|
1504
|
+
}
|
|
1505
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1506
|
+
return path8.resolve(here, "..", "skills");
|
|
1507
|
+
}
|
|
1508
|
+
function resolveRuntimeTypesSourceFile() {
|
|
1509
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
1510
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
|
|
1511
|
+
}
|
|
1512
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1513
|
+
const fromDist = path8.join(here, "runtime-types", "index.d.ts");
|
|
1514
|
+
const fromSrc = path8.resolve(here, "lib", "index.d.ts");
|
|
1515
|
+
if (existsSync(fromDist)) return fromDist;
|
|
1516
|
+
if (existsSync(fromSrc)) return fromSrc;
|
|
1517
|
+
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
|
|
1518
|
+
}
|
|
1519
|
+
async function readJson(file) {
|
|
1520
|
+
return JSON.parse(await fs7.readFile(file, "utf8"));
|
|
1521
|
+
}
|
|
1522
|
+
async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
|
|
1523
|
+
const marker = path8.join(skillsDir, ".easytwin-source.json");
|
|
1524
|
+
try {
|
|
1525
|
+
const meta = await readJson(marker);
|
|
1526
|
+
if (typeof meta.version === "string") return meta.version;
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
const pkg = await readJson(path8.resolve(skillsDir, "..", "package.json"));
|
|
1531
|
+
return pkg.version;
|
|
1532
|
+
} catch {
|
|
1533
|
+
return "0.0.0";
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
async function copyDir(src, dest) {
|
|
1537
|
+
await fs7.mkdir(dest, { recursive: true });
|
|
1538
|
+
const entries = await fs7.readdir(src, { withFileTypes: true });
|
|
1539
|
+
for (const entry of entries) {
|
|
1540
|
+
const s = path8.join(src, entry.name);
|
|
1541
|
+
const d = path8.join(dest, entry.name);
|
|
1542
|
+
if (entry.isDirectory()) await copyDir(s, d);
|
|
1543
|
+
else if (entry.isFile()) await fs7.copyFile(s, d);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async function dirMatches(src, dest) {
|
|
1547
|
+
let sourceEntries;
|
|
1548
|
+
try {
|
|
1549
|
+
sourceEntries = await fs7.readdir(src);
|
|
1550
|
+
} catch {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
for (const name of sourceEntries) {
|
|
1554
|
+
const s = path8.join(src, name);
|
|
1555
|
+
const d = path8.join(dest, name);
|
|
1556
|
+
const sStat = await fs7.stat(s);
|
|
1557
|
+
if (sStat.isDirectory()) {
|
|
1558
|
+
if (!await dirMatches(s, d)) return false;
|
|
1559
|
+
} else {
|
|
1560
|
+
let dStat;
|
|
1561
|
+
try {
|
|
1562
|
+
dStat = await fs7.stat(d);
|
|
1563
|
+
} catch {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
1567
|
+
if (!(await fs7.readFile(s)).equals(await fs7.readFile(d))) return false;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
function actionFor(exists, matches) {
|
|
1573
|
+
if (!exists) return "created";
|
|
1574
|
+
return matches ? "unchanged" : "updated";
|
|
1575
|
+
}
|
|
1576
|
+
async function writeMeta(destDir, version) {
|
|
1577
|
+
const meta = { name: "@easytwin/devkit", version };
|
|
1578
|
+
await fs7.writeFile(path8.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
1579
|
+
}
|
|
1580
|
+
async function syncToDir(target, sourceRoot, targetRoot, version) {
|
|
1581
|
+
const entries = [];
|
|
1582
|
+
for (const name of SKILL_NAMES) {
|
|
1583
|
+
const src = path8.join(sourceRoot, name);
|
|
1584
|
+
const dest = path8.join(targetRoot, name);
|
|
1585
|
+
const exists = await fs7.stat(dest).then(() => true).catch(() => false);
|
|
1586
|
+
const matches = await dirMatches(src, dest);
|
|
1587
|
+
const action = actionFor(exists, matches);
|
|
1588
|
+
if (action !== "unchanged") {
|
|
1589
|
+
await fs7.rm(dest, { recursive: true, force: true });
|
|
1590
|
+
await copyDir(src, dest);
|
|
1591
|
+
}
|
|
1592
|
+
entries.push({ name, action });
|
|
1593
|
+
}
|
|
1594
|
+
await writeMeta(targetRoot, version);
|
|
1595
|
+
return { target, entries };
|
|
1596
|
+
}
|
|
1597
|
+
function buildCodexSegment(version) {
|
|
1598
|
+
return [
|
|
1599
|
+
CODEX_MARKER_BEGIN,
|
|
1600
|
+
"<!-- \u672C\u6BB5\u7531 `easytwin skills sync` \u7BA1\u7406,\u53EF\u6574\u6BB5\u66FF\u6362;\u624B\u52A8\u4FEE\u6539\u4F1A\u88AB\u4E0B\u6B21\u540C\u6B65\u8986\u76D6\u3002 -->",
|
|
1601
|
+
`EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
|
|
1602
|
+
"",
|
|
1603
|
+
"- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
|
|
1604
|
+
"- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
|
|
1605
|
+
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
|
|
1606
|
+
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
1607
|
+
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
1608
|
+
"- `easytwin-upload`:\u62C9\u53D6/\u4E0A\u4F20\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5199\u9ED8\u8BA4\u5165\u53E3;\u4E0A\u4F20\u5168\u91CF\u8986\u76D6\u4E0D\u53EF\u9006)\u3002",
|
|
1609
|
+
"- `easytwin-test`:\u5199\u5DE5\u4F5C\u533A `*.spec.ts`,\u9884\u89C8\u9875\u6309\u94AE/\u8F93\u5165\u6846\u8C03\u7528\u5BFC\u51FA\u51FD\u6570(\u4E0D\u540C\u6B65;CLI `easytwin preview` \u6216\u63D2\u4EF6)\u3002",
|
|
1610
|
+
"",
|
|
1611
|
+
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
1612
|
+
CODEX_MARKER_END
|
|
1613
|
+
].join("\n");
|
|
1614
|
+
}
|
|
1615
|
+
async function syncToCodex(sourceRoot, cwd, version) {
|
|
1616
|
+
const agentsFile = path8.join(cwd, "AGENTS.md");
|
|
1617
|
+
const segment = buildCodexSegment(version);
|
|
1618
|
+
let content = "";
|
|
1619
|
+
let exists = true;
|
|
1620
|
+
try {
|
|
1621
|
+
content = await fs7.readFile(agentsFile, "utf8");
|
|
1622
|
+
} catch {
|
|
1623
|
+
exists = false;
|
|
1624
|
+
}
|
|
1625
|
+
const start = content.indexOf(CODEX_MARKER_BEGIN);
|
|
1626
|
+
const end = content.indexOf(CODEX_MARKER_END);
|
|
1627
|
+
let action;
|
|
1628
|
+
let next;
|
|
1629
|
+
if (start === -1 || end === -1 || end < start) {
|
|
1630
|
+
action = exists ? "updated" : "created";
|
|
1631
|
+
next = content.length > 0 && !content.endsWith("\n") ? content + "\n\n" : content + (content.length > 0 ? "\n" : "");
|
|
1632
|
+
next += segment + "\n";
|
|
1633
|
+
} else {
|
|
1634
|
+
const current = content.slice(start, end + CODEX_MARKER_END.length);
|
|
1635
|
+
action = current === segment ? "unchanged" : "updated";
|
|
1636
|
+
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
1637
|
+
}
|
|
1638
|
+
if (action !== "unchanged") {
|
|
1639
|
+
await fs7.mkdir(cwd, { recursive: true });
|
|
1640
|
+
await fs7.writeFile(agentsFile, next, "utf8");
|
|
1641
|
+
}
|
|
1642
|
+
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
1643
|
+
}
|
|
1644
|
+
async function syncRuntimeTypes(cwd, typesSourceFile) {
|
|
1645
|
+
const destDir = path8.join(cwd, ".easytwin", "types");
|
|
1646
|
+
const destFile = path8.join(destDir, "index.d.ts");
|
|
1647
|
+
const appsFile = path8.join(destDir, APPS_TYPES_FILE);
|
|
1648
|
+
const content = buildRuntimeTypesContent(await fs7.readFile(typesSourceFile, "utf8"));
|
|
1649
|
+
let runtimeCurrent = "";
|
|
1650
|
+
let appsCurrent = "";
|
|
1651
|
+
let runtimeExists = true;
|
|
1652
|
+
let appsExists = true;
|
|
1653
|
+
try {
|
|
1654
|
+
runtimeCurrent = await fs7.readFile(destFile, "utf8");
|
|
1655
|
+
} catch {
|
|
1656
|
+
runtimeExists = false;
|
|
1657
|
+
}
|
|
1658
|
+
try {
|
|
1659
|
+
appsCurrent = await fs7.readFile(appsFile, "utf8");
|
|
1660
|
+
} catch {
|
|
1661
|
+
appsExists = false;
|
|
1662
|
+
}
|
|
1663
|
+
const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
|
|
1664
|
+
const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
|
|
1665
|
+
const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
|
|
1666
|
+
if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
|
|
1667
|
+
await fs7.mkdir(destDir, { recursive: true });
|
|
1668
|
+
if (runtimeAction !== "unchanged") await fs7.writeFile(destFile, content, "utf8");
|
|
1669
|
+
if (appsAction !== "unchanged") await fs7.writeFile(appsFile, APPS_DTS, "utf8");
|
|
1670
|
+
}
|
|
1671
|
+
const tsconfigPath = path8.join(cwd, "tsconfig.json");
|
|
1672
|
+
let tsconfig;
|
|
1673
|
+
let pathsHint;
|
|
1674
|
+
try {
|
|
1675
|
+
const existing = await fs7.readFile(tsconfigPath, "utf8");
|
|
1676
|
+
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
1677
|
+
else {
|
|
1678
|
+
tsconfig = "manual-paths";
|
|
1679
|
+
pathsHint = TSCONFIG_PATHS_HINT;
|
|
1680
|
+
}
|
|
1681
|
+
} catch {
|
|
1682
|
+
await fs7.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
1683
|
+
tsconfig = "created";
|
|
1684
|
+
}
|
|
1685
|
+
return { action, tsconfig, pathsHint };
|
|
1686
|
+
}
|
|
1687
|
+
async function syncSkills(options) {
|
|
1688
|
+
const targets = normalizeTargets(options.targets ?? "all");
|
|
1689
|
+
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
1690
|
+
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
1691
|
+
const summaries = [];
|
|
1692
|
+
for (const target of targets) {
|
|
1693
|
+
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path8.join(options.cwd, ".cursor", "skills"), version));
|
|
1694
|
+
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path8.join(options.cwd, ".claude", "skills"), version));
|
|
1695
|
+
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path8.join(options.cwd, ".qoder", "skills"), version));
|
|
1696
|
+
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
1697
|
+
}
|
|
1698
|
+
const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
|
|
1699
|
+
return { summaries, types };
|
|
1700
|
+
}
|
|
1701
|
+
async function detectSkillsStatus(cwd, sourceDir) {
|
|
1702
|
+
const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
|
|
1703
|
+
const version = await readDevkitVersion(sourceRoot);
|
|
1704
|
+
const targets = [];
|
|
1705
|
+
const dirTargets = ["cursor", "claude", "qoder"];
|
|
1706
|
+
const DIR_ROOTS = {
|
|
1707
|
+
cursor: ".cursor/skills",
|
|
1708
|
+
claude: ".claude/skills",
|
|
1709
|
+
qoder: ".qoder/skills"
|
|
1710
|
+
};
|
|
1711
|
+
for (const target of dirTargets) {
|
|
1712
|
+
const root = path8.join(cwd, DIR_ROOTS[target]);
|
|
1713
|
+
const missing = [];
|
|
1714
|
+
for (const name of SKILL_NAMES) {
|
|
1715
|
+
if (!await fs7.stat(path8.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
|
|
1716
|
+
}
|
|
1717
|
+
let stale = false;
|
|
1718
|
+
try {
|
|
1719
|
+
const meta = await readJson(path8.join(root, META_FILE_NAME));
|
|
1720
|
+
stale = meta.version !== version;
|
|
1721
|
+
} catch {
|
|
1722
|
+
stale = missing.length === 0;
|
|
1723
|
+
}
|
|
1724
|
+
targets.push({
|
|
1725
|
+
target,
|
|
1726
|
+
synced: missing.length === 0 && !stale,
|
|
1727
|
+
reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
let agentsContent = "";
|
|
1731
|
+
try {
|
|
1732
|
+
agentsContent = await fs7.readFile(path8.join(cwd, "AGENTS.md"), "utf8");
|
|
1733
|
+
} catch {
|
|
1734
|
+
}
|
|
1735
|
+
const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
|
|
1736
|
+
targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
|
|
1737
|
+
let typesSynced = false;
|
|
1738
|
+
try {
|
|
1739
|
+
const dts = await fs7.readFile(path8.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
|
|
1740
|
+
const appsDts = await fs7.readFile(path8.join(cwd, ".easytwin", "types", APPS_TYPES_FILE), "utf8");
|
|
1741
|
+
typesSynced = dts.includes("registerEngineTickListener") && appsDts.includes("export declare abstract class TwinApp");
|
|
1742
|
+
} catch {
|
|
1743
|
+
typesSynced = false;
|
|
1744
|
+
}
|
|
1745
|
+
return { cwd, targets, typesSynced };
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/twinAppHost.ts
|
|
1749
|
+
var LOCAL_LOAD_SCENE_ERROR = "\u672C\u5730\u9884\u89C8\u5355\u573A\u666F,\u4E0D\u80FD\u8C03\u7528 sceneManager.loadScene";
|
|
1750
|
+
var MISSING_TICK_ERROR = "\u5F53\u524D runtime \u4E0D\u652F\u6301\u5F15\u64CE\u65F6\u949F(\u7F3A\u5C11 registerEngineTickListener)\u3002\u65E0\u6CD5\u8FD0\u884C onUpdate\u3002";
|
|
1751
|
+
function isPromiseLike(value) {
|
|
1752
|
+
return typeof value === "object" && value !== null && "then" in value;
|
|
1753
|
+
}
|
|
1754
|
+
var TwinAppPreviewHost = class {
|
|
1755
|
+
constructor(options) {
|
|
1756
|
+
this.options = options;
|
|
1757
|
+
this.currentSceneId = options.sceneId;
|
|
1758
|
+
this.onEngineTick = (engine) => this.handleTick(engine);
|
|
1759
|
+
}
|
|
1760
|
+
options;
|
|
1761
|
+
app = null;
|
|
1762
|
+
engine = null;
|
|
1763
|
+
disposed = false;
|
|
1764
|
+
/** 用户应用已经成功 onSceneLoaded。 */
|
|
1765
|
+
scenePresented = false;
|
|
1766
|
+
tickAttached = false;
|
|
1767
|
+
sceneLoadTask = null;
|
|
1768
|
+
ctx = null;
|
|
1769
|
+
cleanups = [];
|
|
1770
|
+
sceneCleanups = [];
|
|
1771
|
+
warnedAsyncUpdate = false;
|
|
1772
|
+
userBlobUrl = null;
|
|
1773
|
+
currentSceneId;
|
|
1774
|
+
onEngineTick;
|
|
1775
|
+
getEngine() {
|
|
1776
|
+
return this.engine;
|
|
1777
|
+
}
|
|
1778
|
+
/** 给 spec 调用:场景字段取当前引擎主场景(不要求已 Run TwinApp)。 */
|
|
1779
|
+
getTestContext() {
|
|
1780
|
+
if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
|
|
1781
|
+
const ctx = this.getContext();
|
|
1782
|
+
const runtimeScene = this.engine.mainScene ?? null;
|
|
1783
|
+
ctx.runtimeScene = runtimeScene;
|
|
1784
|
+
ctx.scene = runtimeScene?.sceneObject ?? null;
|
|
1785
|
+
ctx.camera = runtimeScene?.camera?.main ?? null;
|
|
1786
|
+
return ctx;
|
|
1787
|
+
}
|
|
1788
|
+
async invokeTest(fn, input) {
|
|
1789
|
+
if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
|
|
1790
|
+
return fn(this.getTestContext(), input);
|
|
1791
|
+
}
|
|
1792
|
+
async boot() {
|
|
1793
|
+
const { runtime, containerId, ossUrl, customComponentDeps } = this.options;
|
|
1794
|
+
this.engine = await runtime.RuntimeEngine.create({
|
|
1795
|
+
containerId,
|
|
1796
|
+
baseOSSUrl: ossUrl,
|
|
1797
|
+
customComponentDeps,
|
|
1798
|
+
sceneResources: [],
|
|
1799
|
+
componentRels: [],
|
|
1800
|
+
enableResourcePersistence: false
|
|
1801
|
+
});
|
|
1802
|
+
await this.loadCurrentScene();
|
|
1803
|
+
}
|
|
1804
|
+
async run(code) {
|
|
1805
|
+
if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
|
|
1806
|
+
if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
1807
|
+
this.assertTickApi();
|
|
1808
|
+
try {
|
|
1809
|
+
await this.teardownApp({ keepEngine: true });
|
|
1810
|
+
const mod = await this.importUser(code);
|
|
1811
|
+
this.app = createAppInstance(mod.default);
|
|
1812
|
+
await this.app.init?.(this.getContext());
|
|
1813
|
+
await this.loadCurrentScene();
|
|
1814
|
+
this.scenePresented = true;
|
|
1815
|
+
await this.app.onSceneLoaded?.(this.getContext());
|
|
1816
|
+
this.attachTick();
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
await this.teardownApp({ keepEngine: true, ignoreHookErrors: true });
|
|
1819
|
+
throw error;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
async dispose() {
|
|
1823
|
+
if (this.disposed) return;
|
|
1824
|
+
this.disposed = true;
|
|
1825
|
+
await this.teardownApp({ keepEngine: false, ignoreHookErrors: true });
|
|
1826
|
+
}
|
|
1827
|
+
assertTickApi() {
|
|
1828
|
+
const { runtime } = this.options;
|
|
1829
|
+
if (typeof runtime.registerEngineTickListener !== "function" || typeof runtime.unregisterEngineTickListener !== "function") {
|
|
1830
|
+
throw new Error(MISSING_TICK_ERROR);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
async importUser(code) {
|
|
1834
|
+
if (this.options.importUserModule) return this.options.importUserModule(code);
|
|
1835
|
+
if (this.userBlobUrl) {
|
|
1836
|
+
try {
|
|
1837
|
+
URL.revokeObjectURL(this.userBlobUrl);
|
|
1838
|
+
} catch {
|
|
1839
|
+
}
|
|
1840
|
+
this.userBlobUrl = null;
|
|
1841
|
+
}
|
|
1842
|
+
const blob = new Blob([code], { type: "text/javascript" });
|
|
1843
|
+
this.userBlobUrl = URL.createObjectURL(blob);
|
|
1844
|
+
return import(
|
|
1845
|
+
/* @vite-ignore */
|
|
1846
|
+
this.userBlobUrl
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
async loadCurrentScene() {
|
|
1850
|
+
if (!this.engine) throw new Error("Engine is not created");
|
|
1851
|
+
if (this.sceneLoadTask) await this.sceneLoadTask;
|
|
1852
|
+
this.sceneLoadTask = this.options.loadScene(this.engine, this.options.sceneJson).finally(() => {
|
|
1853
|
+
this.sceneLoadTask = null;
|
|
1854
|
+
});
|
|
1855
|
+
await this.sceneLoadTask;
|
|
1856
|
+
}
|
|
1857
|
+
handleTick(engine) {
|
|
1858
|
+
if (this.disposed || this.sceneLoadTask || !this.scenePresented) return;
|
|
1859
|
+
try {
|
|
1860
|
+
const result = this.app?.onUpdate?.(
|
|
1861
|
+
this.getContext(),
|
|
1862
|
+
engine.time.deltaTime,
|
|
1863
|
+
engine.time.elapsedTime
|
|
1864
|
+
);
|
|
1865
|
+
if (isPromiseLike(result)) {
|
|
1866
|
+
void Promise.resolve(result).catch((error) => {
|
|
1867
|
+
console.error("async onUpdate rejected:", error);
|
|
1868
|
+
});
|
|
1869
|
+
if (!this.warnedAsyncUpdate) {
|
|
1870
|
+
this.warnedAsyncUpdate = true;
|
|
1871
|
+
console.warn("TwinApp.onUpdate \u5E94\u540C\u6B65\u6267\u884C\uFF1B\u672C\u6B21\u5DF2\u5FFD\u7565\u5176 Promise \u8FD4\u56DE\u503C");
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
this.handleUpdateError(error);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
handleUpdateError(error) {
|
|
1879
|
+
let shouldContinue = false;
|
|
1880
|
+
try {
|
|
1881
|
+
shouldContinue = this.app?.onError?.(this.getContext(), error) === true;
|
|
1882
|
+
} catch (onError) {
|
|
1883
|
+
void this.failRun(onError);
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (shouldContinue) {
|
|
1887
|
+
console.error("TwinApp.onUpdate threw:", error);
|
|
1888
|
+
this.options.log?.(`onUpdate \u629B\u9519\u5DF2 continue: ${error instanceof Error ? error.message : String(error)}`);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
void this.failRun(error);
|
|
1892
|
+
}
|
|
1893
|
+
async failRun(error) {
|
|
1894
|
+
this.options.log?.(`Run \u5931\u8D25 ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
1895
|
+
await this.teardownApp({ keepEngine: true, ignoreHookErrors: true });
|
|
1896
|
+
}
|
|
1897
|
+
attachTick() {
|
|
1898
|
+
if (!this.engine || this.tickAttached) return;
|
|
1899
|
+
this.options.runtime.registerEngineTickListener?.(this.engine, this.onEngineTick);
|
|
1900
|
+
this.tickAttached = true;
|
|
1901
|
+
}
|
|
1902
|
+
detachTick() {
|
|
1903
|
+
if (!this.engine || !this.tickAttached) return;
|
|
1904
|
+
this.options.runtime.unregisterEngineTickListener?.(this.engine, this.onEngineTick);
|
|
1905
|
+
this.tickAttached = false;
|
|
1906
|
+
}
|
|
1907
|
+
async teardownApp(options) {
|
|
1908
|
+
this.detachTick();
|
|
1909
|
+
await this.unloadUserScene({ ignoreErrors: options.ignoreHookErrors });
|
|
1910
|
+
if (this.app && this.engine) {
|
|
1911
|
+
try {
|
|
1912
|
+
await this.app.onDispose?.(this.getContext());
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
if (!options.ignoreHookErrors) throw error;
|
|
1915
|
+
console.warn("TwinApp.onDispose failed:", error);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
await this.runHooks(this.cleanups);
|
|
1919
|
+
this.cleanups = [];
|
|
1920
|
+
this.app = null;
|
|
1921
|
+
this.ctx = null;
|
|
1922
|
+
this.scenePresented = false;
|
|
1923
|
+
if (!options.keepEngine) {
|
|
1924
|
+
try {
|
|
1925
|
+
this.engine?.destroy();
|
|
1926
|
+
} catch {
|
|
1927
|
+
}
|
|
1928
|
+
this.engine = null;
|
|
1929
|
+
}
|
|
1930
|
+
if (this.userBlobUrl) {
|
|
1931
|
+
try {
|
|
1932
|
+
URL.revokeObjectURL(this.userBlobUrl);
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
this.userBlobUrl = null;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
async unloadUserScene(options) {
|
|
1939
|
+
if (!this.scenePresented) return;
|
|
1940
|
+
this.scenePresented = false;
|
|
1941
|
+
try {
|
|
1942
|
+
if (this.engine) await this.app?.onBeforeSceneUnload?.(this.getContext());
|
|
1943
|
+
} catch (error) {
|
|
1944
|
+
if (!options?.ignoreErrors) throw error;
|
|
1945
|
+
console.warn("TwinApp.onBeforeSceneUnload failed:", error);
|
|
1946
|
+
} finally {
|
|
1947
|
+
await this.runHooks(this.sceneCleanups);
|
|
1948
|
+
this.sceneCleanups = [];
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
async runHooks(hooks) {
|
|
1952
|
+
for (const fn of [...hooks].reverse()) {
|
|
1953
|
+
try {
|
|
1954
|
+
await fn();
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
console.warn("TwinApp cleanup failed:", error);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
getContext() {
|
|
1961
|
+
if (!this.engine) throw new Error("Engine is not created");
|
|
1962
|
+
const runtimeScene = this.scenePresented ? this.engine.mainScene ?? null : null;
|
|
1963
|
+
const scene = runtimeScene?.sceneObject ?? null;
|
|
1964
|
+
const camera = runtimeScene?.camera?.main ?? null;
|
|
1965
|
+
const sceneManagerRuntime = this.engine.getManager(this.options.runtime.SceneManager);
|
|
1966
|
+
if (!this.ctx) {
|
|
1967
|
+
this.ctx = {
|
|
1968
|
+
app: { id: this.options.appId, mode: "preview" },
|
|
1969
|
+
engine: this.engine,
|
|
1970
|
+
container: this.engine.container,
|
|
1971
|
+
runtimeScene,
|
|
1972
|
+
scene,
|
|
1973
|
+
camera,
|
|
1974
|
+
sceneManager: {
|
|
1975
|
+
currentSceneId: this.currentSceneId,
|
|
1976
|
+
runtime: sceneManagerRuntime,
|
|
1977
|
+
loadScene: () => Promise.reject(new Error(LOCAL_LOAD_SCENE_ERROR))
|
|
1978
|
+
},
|
|
1979
|
+
logger: {
|
|
1980
|
+
log: (...args) => console.log(...args),
|
|
1981
|
+
info: (...args) => console.info(...args),
|
|
1982
|
+
warn: (...args) => console.warn(...args),
|
|
1983
|
+
error: (...args) => console.error(...args)
|
|
1984
|
+
},
|
|
1985
|
+
assets: {
|
|
1986
|
+
text: (p) => this.options.readAsset(p),
|
|
1987
|
+
json: async (p) => JSON.parse(await this.options.readAsset(p))
|
|
1988
|
+
},
|
|
1989
|
+
cleanup: (fn) => {
|
|
1990
|
+
this.cleanups.push(fn);
|
|
1991
|
+
},
|
|
1992
|
+
sceneCleanup: (fn) => {
|
|
1993
|
+
this.sceneCleanups.push(fn);
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
return this.ctx;
|
|
1997
|
+
}
|
|
1998
|
+
this.ctx.engine = this.engine;
|
|
1999
|
+
this.ctx.container = this.engine.container;
|
|
2000
|
+
this.ctx.runtimeScene = runtimeScene;
|
|
2001
|
+
this.ctx.scene = scene;
|
|
2002
|
+
this.ctx.camera = camera;
|
|
2003
|
+
this.ctx.sceneManager.currentSceneId = this.currentSceneId;
|
|
2004
|
+
this.ctx.sceneManager.runtime = sceneManagerRuntime;
|
|
2005
|
+
return this.ctx;
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
// src/previewHtml.ts
|
|
2010
|
+
function escapeHtml(text) {
|
|
2011
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2012
|
+
}
|
|
2013
|
+
function escapeJsonForScript(json) {
|
|
2014
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2015
|
+
}
|
|
2016
|
+
function buildStatusHtml(message) {
|
|
2017
|
+
return `<!DOCTYPE html>
|
|
2018
|
+
<html lang="zh-CN">
|
|
2019
|
+
<head>
|
|
2020
|
+
<meta charset="UTF-8">
|
|
2021
|
+
</head>
|
|
2022
|
+
<body>
|
|
2023
|
+
<p>${escapeHtml(message)}</p>
|
|
2024
|
+
</body>
|
|
2025
|
+
</html>`;
|
|
2026
|
+
}
|
|
2027
|
+
var PREVIEW_STYLE = `
|
|
2028
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
|
|
2029
|
+
body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
|
|
2030
|
+
#stage { position: relative; flex: 1; min-height: 0; }
|
|
2031
|
+
#twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
|
|
2032
|
+
#twin-root canvas { display: block; }
|
|
2033
|
+
#status {
|
|
2034
|
+
position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
|
|
2035
|
+
padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
|
|
2036
|
+
}
|
|
2037
|
+
#status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
|
|
2038
|
+
/* \u4F5C\u8005\u6837\u5F0F display:flex \u4F1A\u8986\u76D6 UA \u7684 [hidden]{display:none},\u5FC5\u987B\u663E\u5F0F\u58F0\u660E\u624D\u80FD\u9690\u85CF\u52A0\u8F7D\u6587\u6848 */
|
|
2039
|
+
#status[hidden] { display: none; }
|
|
2040
|
+
#mock-banner {
|
|
2041
|
+
position: absolute; top: 0; left: 0; right: 0; z-index: 2;
|
|
2042
|
+
padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
|
|
2043
|
+
border-bottom: 1px solid #f0c36d; pointer-events: none;
|
|
2044
|
+
}
|
|
2045
|
+
#debug-log {
|
|
2046
|
+
display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
|
|
2047
|
+
max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
|
|
2048
|
+
background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
|
|
2049
|
+
white-space: pre-wrap; word-break: break-all; pointer-events: auto;
|
|
2050
|
+
}
|
|
2051
|
+
#debug-log.open { display: block; }
|
|
2052
|
+
#run-bar {
|
|
2053
|
+
flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
|
|
2054
|
+
padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
|
|
2055
|
+
}
|
|
2056
|
+
#run-bar button {
|
|
2057
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
2058
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
2059
|
+
}
|
|
2060
|
+
#run-bar button:disabled { opacity: .55; cursor: default; }
|
|
2061
|
+
#btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
|
|
2062
|
+
#run-status { color: #bbb; min-width: 4em; }
|
|
2063
|
+
#run-bar .spacer { flex: 1; }
|
|
2064
|
+
#test-panel {
|
|
2065
|
+
flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
|
|
2066
|
+
padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
|
|
2067
|
+
max-height: 30%; overflow: auto;
|
|
2068
|
+
}
|
|
2069
|
+
#test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
|
|
2070
|
+
#test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
|
|
2071
|
+
#test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
|
|
2072
|
+
#test-panel .test-empty { color: #777; }
|
|
2073
|
+
#test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
2074
|
+
#test-panel input.test-input {
|
|
2075
|
+
width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
|
|
2076
|
+
padding: 3px 6px; border-radius: 4px; font: 12px inherit;
|
|
2077
|
+
}
|
|
2078
|
+
#test-panel button {
|
|
2079
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
2080
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
2081
|
+
}
|
|
2082
|
+
#test-panel button:disabled { opacity: .55; cursor: default; }
|
|
2083
|
+
`;
|
|
2084
|
+
var RENDER_SCRIPT = `
|
|
2085
|
+
var statusEl = document.getElementById("status");
|
|
2086
|
+
var logEl = document.getElementById("debug-log");
|
|
2087
|
+
var engine = null;
|
|
2088
|
+
var runtime = null;
|
|
2089
|
+
var sceneJson = null;
|
|
2090
|
+
var host = null;
|
|
2091
|
+
var running = false;
|
|
2092
|
+
var runningTest = false;
|
|
2093
|
+
var testsReady = false;
|
|
2094
|
+
var hostKind = __HOST_KIND__;
|
|
2095
|
+
var vscodeApi = null;
|
|
2096
|
+
if (hostKind === "vscode") {
|
|
2097
|
+
try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
|
|
2098
|
+
}
|
|
2099
|
+
var pending = {};
|
|
2100
|
+
var seq = 0;
|
|
2101
|
+
var origFetch = window.fetch.bind(window);
|
|
2102
|
+
function now() { return new Date().toISOString().slice(11, 23); }
|
|
2103
|
+
function log(line) {
|
|
2104
|
+
var text = "[" + now() + "] " + line;
|
|
2105
|
+
if (logEl) {
|
|
2106
|
+
logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
|
|
2107
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
2108
|
+
}
|
|
2109
|
+
if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
|
|
2110
|
+
console.log("[EasyTwin]", line);
|
|
2111
|
+
}
|
|
2112
|
+
function nextId() { return String(++seq); }
|
|
2113
|
+
function handleHostMessage(msg) {
|
|
2114
|
+
if (!msg) return;
|
|
2115
|
+
if (msg.type === "proxy-fetch-result") {
|
|
2116
|
+
var p = pending[msg.id];
|
|
2117
|
+
if (!p) return;
|
|
2118
|
+
delete pending[msg.id];
|
|
2119
|
+
if (msg.error) p.reject(new Error(msg.error));
|
|
2120
|
+
else p.resolve(msg);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (msg.type === "bundle-result") {
|
|
2124
|
+
if (!msg.ok) {
|
|
2125
|
+
log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
2126
|
+
setRunStatus("\u7F16\u8BD1\u5931\u8D25");
|
|
2127
|
+
setRunBusy(false);
|
|
2128
|
+
if (logEl) logEl.classList.add("open");
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
|
|
2132
|
+
runUserCode(msg.code);
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
if (msg.type === "read-asset-result") {
|
|
2136
|
+
var ap = pending[msg.id];
|
|
2137
|
+
if (!ap) return;
|
|
2138
|
+
delete pending[msg.id];
|
|
2139
|
+
if (msg.error) ap.reject(new Error(msg.error));
|
|
2140
|
+
else ap.resolve(msg.text);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
if (msg.type === "run-error-mapped") {
|
|
2144
|
+
log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
if (msg.type === "tests") {
|
|
2148
|
+
renderTests(msg.tests || []);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (msg.type === "test-bundle") {
|
|
2152
|
+
if (!msg.ok) {
|
|
2153
|
+
log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
2154
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
2155
|
+
runningTest = false;
|
|
2156
|
+
setRunBusy(running);
|
|
2157
|
+
if (logEl) logEl.classList.add("open");
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
|
|
2161
|
+
runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
|
|
2162
|
+
log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
|
|
2163
|
+
setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
|
|
2164
|
+
}).catch(function (err) {
|
|
2165
|
+
log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
2166
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
2167
|
+
if (logEl) logEl.classList.add("open");
|
|
2168
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
2169
|
+
}).then(function () {
|
|
2170
|
+
runningTest = false;
|
|
2171
|
+
setRunBusy(running);
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
function httpJson(url, init) {
|
|
2176
|
+
return origFetch(url, init).then(function (res) {
|
|
2177
|
+
return res.json().then(function (body) {
|
|
2178
|
+
if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
|
|
2179
|
+
return body;
|
|
2180
|
+
});
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
function postToHost(msg) {
|
|
2184
|
+
if (vscodeApi) {
|
|
2185
|
+
vscodeApi.postMessage(msg);
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
|
|
2189
|
+
if (msg.type === "run") {
|
|
2190
|
+
httpJson("/api/bundle", { method: "POST" }).then(function (body) {
|
|
2191
|
+
handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
|
|
2192
|
+
}).catch(function (err) {
|
|
2193
|
+
handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
|
|
2194
|
+
});
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (msg.type === "run-error") {
|
|
2198
|
+
httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
|
|
2199
|
+
handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
|
|
2200
|
+
}).catch(function (err) {
|
|
2201
|
+
log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
|
|
2202
|
+
});
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
if (msg.type === "read-asset") {
|
|
2206
|
+
httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
|
|
2207
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
|
|
2208
|
+
}).catch(function (err) {
|
|
2209
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
|
|
2210
|
+
});
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (msg.type === "list-tests") {
|
|
2214
|
+
httpJson("/api/tests").then(function (body) {
|
|
2215
|
+
handleHostMessage({ type: "tests", tests: body.tests || [] });
|
|
2216
|
+
}).catch(function (err) {
|
|
2217
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
2218
|
+
});
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (msg.type === "run-test") {
|
|
2222
|
+
httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
|
|
2223
|
+
handleHostMessage(Object.assign({ type: "test-bundle" }, body));
|
|
2224
|
+
}).catch(function (err) {
|
|
2225
|
+
handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function showStatus(text, isError) {
|
|
2230
|
+
if (!statusEl) return;
|
|
2231
|
+
statusEl.textContent = text;
|
|
2232
|
+
statusEl.className = isError ? "error" : "";
|
|
2233
|
+
statusEl.hidden = false;
|
|
2234
|
+
if (isError && logEl) logEl.classList.add("open");
|
|
2235
|
+
}
|
|
2236
|
+
function hideStatus() { if (statusEl) statusEl.hidden = true; }
|
|
2237
|
+
function formatError(err) {
|
|
2238
|
+
var msg = err && err.message ? err.message : String(err);
|
|
2239
|
+
var stack = err && err.stack ? "\\n" + err.stack : "";
|
|
2240
|
+
return msg + stack;
|
|
2241
|
+
}
|
|
2242
|
+
window.addEventListener("message", function (ev) {
|
|
2243
|
+
handleHostMessage(ev.data);
|
|
2244
|
+
});
|
|
2245
|
+
function b64ToBuf(b64) {
|
|
2246
|
+
var bin = atob(b64);
|
|
2247
|
+
var bytes = new Uint8Array(bin.length);
|
|
2248
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2249
|
+
return bytes.buffer;
|
|
2250
|
+
}
|
|
2251
|
+
function proxyFetch(url, method) {
|
|
2252
|
+
return new Promise(function (resolve, reject) {
|
|
2253
|
+
if (!vscodeApi) {
|
|
2254
|
+
reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
var id = nextId();
|
|
2258
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
2259
|
+
vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
|
|
2260
|
+
}).then(function (msg) {
|
|
2261
|
+
var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
|
|
2262
|
+
return new Response(buf, {
|
|
2263
|
+
status: msg.status || 0,
|
|
2264
|
+
statusText: msg.statusText || "",
|
|
2265
|
+
headers: msg.headers || {}
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
function isHttpUrl(url) {
|
|
2270
|
+
return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
|
|
2271
|
+
}
|
|
2272
|
+
function isPlainHttp(url) {
|
|
2273
|
+
return typeof url === "string" && url.indexOf("http://") === 0;
|
|
2274
|
+
}
|
|
2275
|
+
if (hostKind === "vscode") {
|
|
2276
|
+
window.fetch = function (input, init) {
|
|
2277
|
+
var url = typeof input === "string" ? input : (input && input.url);
|
|
2278
|
+
log("fetch " + url);
|
|
2279
|
+
if (isPlainHttp(url)) {
|
|
2280
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
2281
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
2282
|
+
return res;
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
return origFetch(input, init).catch(function (err) {
|
|
2286
|
+
log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
|
|
2287
|
+
if (!isHttpUrl(url)) throw err;
|
|
2288
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
2289
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
2290
|
+
return res;
|
|
2291
|
+
});
|
|
2292
|
+
});
|
|
2293
|
+
};
|
|
2294
|
+
var origOpen = XMLHttpRequest.prototype.open;
|
|
2295
|
+
var origSend = XMLHttpRequest.prototype.send;
|
|
2296
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
2297
|
+
this.__etMethod = method;
|
|
2298
|
+
this.__etUrl = String(url);
|
|
2299
|
+
return origOpen.apply(this, arguments);
|
|
2300
|
+
};
|
|
2301
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
2302
|
+
var xhr = this;
|
|
2303
|
+
var url = xhr.__etUrl;
|
|
2304
|
+
if (!isPlainHttp(url)) return origSend.call(this, body);
|
|
2305
|
+
log("xhr proxy " + xhr.__etMethod + " " + url);
|
|
2306
|
+
proxyFetch(url, xhr.__etMethod).then(function (res) {
|
|
2307
|
+
return res.arrayBuffer().then(function (buf) {
|
|
2308
|
+
var text = "";
|
|
2309
|
+
try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
|
|
2310
|
+
Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
|
|
2311
|
+
Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
|
|
2312
|
+
Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
|
|
2313
|
+
Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
|
|
2314
|
+
var rt = xhr.responseType;
|
|
2315
|
+
var response = buf;
|
|
2316
|
+
if (rt === "" || rt === "text") response = text;
|
|
2317
|
+
else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
|
|
2318
|
+
Object.defineProperty(xhr, "response", { configurable: true, value: response });
|
|
2319
|
+
Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
|
|
2320
|
+
if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
|
|
2321
|
+
xhr.dispatchEvent(new Event("load"));
|
|
2322
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
2323
|
+
});
|
|
2324
|
+
}).catch(function (err) {
|
|
2325
|
+
log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
2326
|
+
if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
|
|
2327
|
+
xhr.dispatchEvent(new Event("error"));
|
|
2328
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
2329
|
+
});
|
|
2330
|
+
};
|
|
2331
|
+
function patchHttpSrc(proto, prop) {
|
|
2332
|
+
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
2333
|
+
if (!desc || typeof desc.set !== "function") return;
|
|
2334
|
+
Object.defineProperty(proto, prop, {
|
|
2335
|
+
configurable: true,
|
|
2336
|
+
enumerable: desc.enumerable,
|
|
2337
|
+
get: function () { return desc.get.call(this); },
|
|
2338
|
+
set: function (value) {
|
|
2339
|
+
var el = this;
|
|
2340
|
+
var url = String(value);
|
|
2341
|
+
if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
|
|
2342
|
+
log("media proxy " + url);
|
|
2343
|
+
proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
|
|
2344
|
+
desc.set.call(el, URL.createObjectURL(blob));
|
|
2345
|
+
}).catch(function (err) {
|
|
2346
|
+
log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
2347
|
+
try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
patchHttpSrc(HTMLImageElement.prototype, "src");
|
|
2353
|
+
patchHttpSrc(HTMLMediaElement.prototype, "src");
|
|
2354
|
+
}
|
|
2355
|
+
document.getElementById("btn-debug").addEventListener("click", function () {
|
|
2356
|
+
if (logEl) logEl.classList.toggle("open");
|
|
2357
|
+
});
|
|
2358
|
+
var btnDevtools = document.getElementById("btn-devtools");
|
|
2359
|
+
var btnOutput = document.getElementById("btn-output");
|
|
2360
|
+
if (hostKind === "http") {
|
|
2361
|
+
if (btnDevtools) btnDevtools.hidden = true;
|
|
2362
|
+
if (btnOutput) btnOutput.hidden = true;
|
|
2363
|
+
}
|
|
2364
|
+
if (btnDevtools) btnDevtools.addEventListener("click", function () {
|
|
2365
|
+
postToHost({ type: "open-devtools" });
|
|
2366
|
+
});
|
|
2367
|
+
if (btnOutput) btnOutput.addEventListener("click", function () {
|
|
2368
|
+
postToHost({ type: "show-output" });
|
|
2369
|
+
});
|
|
2370
|
+
function setRunStatus(text) {
|
|
2371
|
+
var el = document.getElementById("run-status");
|
|
2372
|
+
if (el) el.textContent = text;
|
|
2373
|
+
}
|
|
2374
|
+
function setRunBusy(busy) {
|
|
2375
|
+
running = busy;
|
|
2376
|
+
var btn = document.getElementById("btn-run");
|
|
2377
|
+
if (btn) btn.disabled = !!busy || runningTest;
|
|
2378
|
+
syncTestControls();
|
|
2379
|
+
}
|
|
2380
|
+
function syncTestControls() {
|
|
2381
|
+
var panel = document.getElementById("test-panel");
|
|
2382
|
+
if (!panel) return;
|
|
2383
|
+
var disabled = !testsReady || running || runningTest;
|
|
2384
|
+
var nodes = panel.querySelectorAll("button.test-run, input.test-input");
|
|
2385
|
+
for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
|
|
2386
|
+
}
|
|
2387
|
+
function renderTests(tests) {
|
|
2388
|
+
var list = document.getElementById("test-list");
|
|
2389
|
+
if (!list) return;
|
|
2390
|
+
list.textContent = "";
|
|
2391
|
+
if (!tests || !tests.length) {
|
|
2392
|
+
var empty = document.createElement("span");
|
|
2393
|
+
empty.className = "test-empty";
|
|
2394
|
+
empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
|
|
2395
|
+
list.appendChild(empty);
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
var groups = {};
|
|
2399
|
+
var order = [];
|
|
2400
|
+
for (var i = 0; i < tests.length; i++) {
|
|
2401
|
+
var t = tests[i];
|
|
2402
|
+
if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
|
|
2403
|
+
groups[t.file].push(t);
|
|
2404
|
+
}
|
|
2405
|
+
for (var g = 0; g < order.length; g++) {
|
|
2406
|
+
var file = order[g];
|
|
2407
|
+
var heading = document.createElement("span");
|
|
2408
|
+
heading.className = "test-file";
|
|
2409
|
+
heading.textContent = file;
|
|
2410
|
+
list.appendChild(heading);
|
|
2411
|
+
var items = groups[file];
|
|
2412
|
+
for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
|
|
2413
|
+
}
|
|
2414
|
+
syncTestControls();
|
|
2415
|
+
}
|
|
2416
|
+
function makeTestControl(t) {
|
|
2417
|
+
var wrap = document.createElement("span");
|
|
2418
|
+
wrap.className = "test-item";
|
|
2419
|
+
if (t.hasInput) {
|
|
2420
|
+
var input = document.createElement("input");
|
|
2421
|
+
input.type = "text";
|
|
2422
|
+
input.className = "test-input";
|
|
2423
|
+
input.placeholder = t.inputName || "input";
|
|
2424
|
+
var btn = document.createElement("button");
|
|
2425
|
+
btn.type = "button";
|
|
2426
|
+
btn.className = "test-run";
|
|
2427
|
+
btn.textContent = t.name;
|
|
2428
|
+
btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
|
|
2429
|
+
input.addEventListener("keydown", function (ev) {
|
|
2430
|
+
if (ev.key === "Enter") requestRunTest(t.id, input.value);
|
|
2431
|
+
});
|
|
2432
|
+
wrap.appendChild(input);
|
|
2433
|
+
wrap.appendChild(btn);
|
|
2434
|
+
} else {
|
|
2435
|
+
var only = document.createElement("button");
|
|
2436
|
+
only.type = "button";
|
|
2437
|
+
only.className = "test-run";
|
|
2438
|
+
only.textContent = t.name;
|
|
2439
|
+
only.addEventListener("click", function () { requestRunTest(t.id); });
|
|
2440
|
+
wrap.appendChild(only);
|
|
2441
|
+
}
|
|
2442
|
+
return wrap;
|
|
2443
|
+
}
|
|
2444
|
+
function requestRunTest(id, input) {
|
|
2445
|
+
if (!testsReady || running || runningTest) return;
|
|
2446
|
+
if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
|
|
2447
|
+
runningTest = true;
|
|
2448
|
+
setRunBusy(running);
|
|
2449
|
+
setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
|
|
2450
|
+
log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
|
|
2451
|
+
postToHost({ type: "run-test", id: id, input: input });
|
|
2452
|
+
}
|
|
2453
|
+
async function runExportedTest(code, exportName, input, hasInput) {
|
|
2454
|
+
var blob = new Blob([code], { type: "text/javascript" });
|
|
2455
|
+
var url = URL.createObjectURL(blob);
|
|
2456
|
+
try {
|
|
2457
|
+
var mod = await import(url);
|
|
2458
|
+
var fn = mod[exportName];
|
|
2459
|
+
if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
|
|
2460
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
|
|
2461
|
+
var ctx = host.getTestContext();
|
|
2462
|
+
var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
|
|
2463
|
+
await Promise.resolve(result);
|
|
2464
|
+
} finally {
|
|
2465
|
+
URL.revokeObjectURL(url);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
function applyRenderPatch() {
|
|
2469
|
+
// runtime \u6E32\u67D3\u5206\u652F:\u7248\u672C <= 0.0.31 \u8D70\u666E\u901A/\u540E\u5904\u7406\u6E32\u67D3,\u7248\u672C > 0.0.31 \u8981\u6C42\u5B58\u5728 postprocessingComponent,
|
|
2470
|
+
// \u5426\u5219\u6240\u6709\u5206\u652F\u90FD\u88AB\u8DF3\u8FC7(renderer.info.render.frame \u6052\u4E3A 0)\u5BFC\u81F4\u9ED1\u5C4F\u3002\u6B64\u5904\u6309 runtime \u8BED\u4E49\u515C\u5E95\u3002
|
|
2471
|
+
var scene = engine && engine.mainScene;
|
|
2472
|
+
if (
|
|
2473
|
+
scene && scene.rootComponent && scene.rootComponent.version &&
|
|
2474
|
+
!scene.postprocessingComponent &&
|
|
2475
|
+
runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
|
|
2476
|
+
) {
|
|
2477
|
+
log("\u8865\u6E32\u67D3:\u573A\u666F\u7248\u672C " + scene.rootComponent.version + " \u65E0\u540E\u5904\u7406\u7EC4\u4EF6,runtime \u9AD8\u7248\u672C\u6E32\u67D3\u5206\u652F\u4E3A\u7A7A,\u6CE8\u518C\u6BCF\u5E27\u6E32\u67D3\u56DE\u8C03");
|
|
2478
|
+
scene.registerRenderCallback(function () {
|
|
2479
|
+
scene.renderer.render(scene.sceneObject, scene.camera.main);
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
async function loadHostScene(nextEngine, nextJson) {
|
|
2484
|
+
engine = nextEngine;
|
|
2485
|
+
var sceneManager = engine.getManager(runtime.SceneManager);
|
|
2486
|
+
await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
|
|
2487
|
+
applyRenderPatch();
|
|
2488
|
+
}
|
|
2489
|
+
function readAsset(relPath) {
|
|
2490
|
+
return new Promise(function (resolve, reject) {
|
|
2491
|
+
var id = nextId();
|
|
2492
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
2493
|
+
postToHost({ type: "read-asset", id: id, path: relPath });
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
async function runUserCode(code) {
|
|
2497
|
+
setRunStatus("\u8FD0\u884C\u4E2D\u2026");
|
|
2498
|
+
try {
|
|
2499
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
2500
|
+
await host.run(code);
|
|
2501
|
+
log("Run \u5B8C\u6210");
|
|
2502
|
+
setRunStatus("\u8FD0\u884C\u4E2D");
|
|
2503
|
+
} catch (err) {
|
|
2504
|
+
log("Run \u5931\u8D25 " + formatError(err));
|
|
2505
|
+
setRunStatus("\u5931\u8D25");
|
|
2506
|
+
if (logEl) logEl.classList.add("open");
|
|
2507
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
2508
|
+
} finally {
|
|
2509
|
+
setRunBusy(false);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
document.getElementById("btn-run").addEventListener("click", function () {
|
|
2513
|
+
if (running || runningTest) return;
|
|
2514
|
+
if (!engine) {
|
|
2515
|
+
log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
setRunBusy(true);
|
|
2519
|
+
setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
|
|
2520
|
+
log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
|
|
2521
|
+
postToHost({ type: "run" });
|
|
2522
|
+
});
|
|
2523
|
+
document.getElementById("btn-refresh-tests").addEventListener("click", function () {
|
|
2524
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
|
|
2525
|
+
postToHost({ type: "list-tests" });
|
|
2526
|
+
});
|
|
2527
|
+
function readEmbeddedTests() {
|
|
2528
|
+
var el = document.getElementById("workspace-tests");
|
|
2529
|
+
if (!el || !el.textContent) return [];
|
|
2530
|
+
try { return JSON.parse(el.textContent); } catch (e) { return []; }
|
|
2531
|
+
}
|
|
2532
|
+
renderTests(readEmbeddedTests());
|
|
2533
|
+
function normalizeHierarchyConfig(vo) {
|
|
2534
|
+
var objs = vo && vo.objs ? vo.objs : [];
|
|
2535
|
+
for (var i = 0; i < objs.length; i++) {
|
|
2536
|
+
var hc = objs[i].hierarchyConfig || {};
|
|
2537
|
+
if (typeof hc.active !== "boolean") {
|
|
2538
|
+
hc.active = hc.inActive === true ? false : hc.visible !== false;
|
|
2539
|
+
}
|
|
2540
|
+
if (typeof hc.lock !== "boolean") hc.lock = false;
|
|
2541
|
+
if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
|
|
2542
|
+
objs[i].hierarchyConfig = hc;
|
|
2543
|
+
}
|
|
2544
|
+
return vo;
|
|
2545
|
+
}
|
|
2546
|
+
function toSceneJson(runtime, raw) {
|
|
2547
|
+
var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
|
|
2548
|
+
if (vo && vo.sceneComponent) {
|
|
2549
|
+
return {
|
|
2550
|
+
id: vo.id || "preview",
|
|
2551
|
+
name: vo.name || "\u573A\u666F\u9884\u89C8",
|
|
2552
|
+
sceneComponent: vo.sceneComponent
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
vo = normalizeHierarchyConfig(vo);
|
|
2556
|
+
var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
|
|
2557
|
+
var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
|
|
2558
|
+
return {
|
|
2559
|
+
id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
|
|
2560
|
+
name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
|
|
2561
|
+
sceneComponent: runtime.convertObjToComponentJson(vo)
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
try {
|
|
2565
|
+
showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
|
|
2566
|
+
log("runtimeUri=" + __RUNTIME_URI__);
|
|
2567
|
+
log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
|
|
2568
|
+
log("1/4 import twin-runtime / TwinApp host");
|
|
2569
|
+
runtime = await import("@easytwin/runtime");
|
|
2570
|
+
var hostMod = await import(__HOST_URI__);
|
|
2571
|
+
log("2/4 parse scene JSON");
|
|
2572
|
+
var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
|
|
2573
|
+
sceneJson = toSceneJson(runtime, sceneVo);
|
|
2574
|
+
log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
|
|
2575
|
+
log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
|
|
2576
|
+
host = new hostMod.TwinAppPreviewHost({
|
|
2577
|
+
runtime: runtime,
|
|
2578
|
+
containerId: "twin-root",
|
|
2579
|
+
ossUrl: __BASE_OSS_URL__,
|
|
2580
|
+
appId: __APP_ID__,
|
|
2581
|
+
sceneId: sceneJson.id,
|
|
2582
|
+
sceneJson: sceneJson,
|
|
2583
|
+
customComponentDeps: {
|
|
2584
|
+
"@easytwin/runtime": runtime,
|
|
2585
|
+
"@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
|
|
2586
|
+
react: { createElement: function () { return null; }, Fragment: "div" }
|
|
2587
|
+
},
|
|
2588
|
+
loadScene: loadHostScene,
|
|
2589
|
+
readAsset: readAsset,
|
|
2590
|
+
log: log
|
|
2591
|
+
});
|
|
2592
|
+
await host.boot();
|
|
2593
|
+
engine = host.getEngine();
|
|
2594
|
+
log("4/4 loadScene");
|
|
2595
|
+
log("\u5B8C\u6210");
|
|
2596
|
+
hideStatus();
|
|
2597
|
+
testsReady = true;
|
|
2598
|
+
setRunBusy(false);
|
|
2599
|
+
} catch (err) {
|
|
2600
|
+
log("\u5931\u8D25 " + formatError(err));
|
|
2601
|
+
showStatus("\u573A\u666F\u6E32\u67D3\u5931\u8D25: " + (err && err.message ? err.message : String(err)) + "\\n(\u70B9\u53F3\u4E0B\u89D2\u300C\u8C03\u8BD5\u65E5\u5FD7\u300D\u67E5\u770B\u6B65\u9AA4\u4E0E\u8BF7\u6C42 URL;\u300C\u5F00\u53D1\u8005\u5DE5\u5177\u300D\u6253\u5F00 webview DevTools)", true);
|
|
2602
|
+
}
|
|
2603
|
+
window.addEventListener("pagehide", function () {
|
|
2604
|
+
if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
|
|
2605
|
+
});
|
|
2606
|
+
`;
|
|
2607
|
+
function originOf(url) {
|
|
2608
|
+
return new URL(url).origin;
|
|
2609
|
+
}
|
|
2610
|
+
function buildPreviewHtml(options) {
|
|
2611
|
+
const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
|
|
2612
|
+
const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
|
|
2613
|
+
const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
|
|
2614
|
+
const appId = options.appId ?? "preview";
|
|
2615
|
+
const hostKind = options.host ?? "vscode";
|
|
2616
|
+
const apiOrigin = originOf(baseUrl);
|
|
2617
|
+
const ossOrigin = originOf(ossUrl);
|
|
2618
|
+
const runtimeOrigin = originOf(runtimeUri);
|
|
2619
|
+
const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
|
|
2620
|
+
const renderScript = RENDER_SCRIPT.replaceAll("__RUNTIME_URI__", JSON.stringify(runtimeUri)).replaceAll("__HOST_URI__", JSON.stringify(hostUri)).replaceAll("__BASE_OSS_URL__", JSON.stringify(ossUrl)).replaceAll("__APP_ID__", JSON.stringify(appId)).replaceAll("__HOST_KIND__", JSON.stringify(hostKind));
|
|
2621
|
+
const importMap = JSON.stringify({
|
|
2622
|
+
imports: {
|
|
2623
|
+
"@easytwin/runtime": runtimeUri,
|
|
2624
|
+
"@easytwin/apps": appsUri
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
const testsJson = JSON.stringify(options.tests ?? []);
|
|
2628
|
+
const mockBanner = mock ? `<div id="mock-banner"><b>\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F</b>:\u573A\u666F\u6570\u636E\u6765\u81EA\u672C\u5730 <code>scene.example.json</code>,\u672A\u8BF7\u6C42\u573A\u666F\u63A5\u53E3;\u4E09\u7EF4\u9884\u89C8\u8D70\u6253\u5305\u7684 twin runtime,\u7CFB\u7EDF\u5E93\u6309\u5B98\u65B9 OSS \u5728\u7EBF\u52A0\u8F7D\u3002</div>` : "";
|
|
2629
|
+
return `<!DOCTYPE html>
|
|
2630
|
+
<html lang="zh-CN">
|
|
2631
|
+
<head>
|
|
2632
|
+
<meta charset="UTF-8">
|
|
2633
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc}; worker-src blob: data: 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc}; child-src blob: data:; img-src ${apiOrigin} ${ossOrigin} ${resourceSrc} https: http: data: blob:; media-src ${apiOrigin} ${ossOrigin} https: http: data: blob:; connect-src 'self' ${apiOrigin} ${ossOrigin} ${resourceSrc} https: http: data: blob:; font-src ${apiOrigin} ${ossOrigin} https: data:;">
|
|
2634
|
+
<title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
|
|
2635
|
+
<style>${PREVIEW_STYLE}</style>
|
|
2636
|
+
<script type="importmap">${importMap}</script>
|
|
2637
|
+
</head>
|
|
2638
|
+
<body>
|
|
2639
|
+
<div id="stage">
|
|
2640
|
+
<div id="twin-root"></div>
|
|
2641
|
+
${mockBanner}
|
|
2642
|
+
<div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
|
|
2643
|
+
<pre id="debug-log"></pre>
|
|
2644
|
+
</div>
|
|
2645
|
+
<div id="test-panel">
|
|
2646
|
+
<span class="test-label">\u6D4B\u8BD5</span>
|
|
2647
|
+
<div id="test-list"></div>
|
|
2648
|
+
<button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
|
|
2649
|
+
</div>
|
|
2650
|
+
<div id="run-bar">
|
|
2651
|
+
<button type="button" id="btn-run">Run</button>
|
|
2652
|
+
<span id="run-status">\u5C31\u7EEA</span>
|
|
2653
|
+
<span class="spacer"></span>
|
|
2654
|
+
<button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
|
|
2655
|
+
<button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
|
|
2656
|
+
<button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
|
|
2657
|
+
</div>
|
|
2658
|
+
<script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
|
|
2659
|
+
<script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
|
|
2660
|
+
<script type="module">
|
|
2661
|
+
${renderScript}
|
|
2662
|
+
</script>
|
|
2663
|
+
</body>
|
|
2664
|
+
</html>`;
|
|
2665
|
+
}
|
|
914
2666
|
export {
|
|
2667
|
+
APPS_DTS,
|
|
2668
|
+
APPS_MODULE,
|
|
2669
|
+
APPS_TYPES_FILE,
|
|
915
2670
|
APP_ID_HEADER,
|
|
916
2671
|
AUTH_HEADER,
|
|
917
|
-
BEARER_PREFIX,
|
|
918
2672
|
BundleError,
|
|
919
2673
|
CODEX_MARKER_BEGIN,
|
|
920
2674
|
CODEX_MARKER_END,
|
|
@@ -922,6 +2676,8 @@ export {
|
|
|
922
2676
|
ConfigError,
|
|
923
2677
|
DEFAULT_BASE_URL,
|
|
924
2678
|
DEFAULT_BUNDLE_OUT,
|
|
2679
|
+
DEFAULT_ENTRY_PATH,
|
|
2680
|
+
DEFAULT_MAIN_TS,
|
|
925
2681
|
DEFAULT_OSS_URL,
|
|
926
2682
|
EASYTWIN_TYPES_DIR,
|
|
927
2683
|
ENDPOINTS,
|
|
@@ -930,37 +2686,76 @@ export {
|
|
|
930
2686
|
EasyTwinClient,
|
|
931
2687
|
GITIGNORE_ENTRY,
|
|
932
2688
|
GITIGNORE_FILE_NAME,
|
|
2689
|
+
LOCAL_LOAD_SCENE_ERROR,
|
|
933
2690
|
META_FILE_NAME,
|
|
934
2691
|
MINIMAL_TSCONFIG,
|
|
2692
|
+
MISSING_TICK_ERROR,
|
|
935
2693
|
MOCK_APP_ID,
|
|
936
2694
|
MOCK_APP_SECRET,
|
|
937
2695
|
MOCK_SCENE_NAME,
|
|
2696
|
+
PORTABLE_RUNTIME_EXPORTS,
|
|
938
2697
|
RUNTIME_MODULE,
|
|
939
2698
|
SKILL_NAMES,
|
|
940
2699
|
TEST_BASE_URL,
|
|
941
2700
|
TSCONFIG_PATHS_HINT,
|
|
2701
|
+
TwinApp,
|
|
2702
|
+
TwinAppPreviewHost,
|
|
942
2703
|
USER_ENTRY,
|
|
2704
|
+
WORKSPACE_CODE_EXTENSIONS,
|
|
2705
|
+
WORKSPACE_IGNORED_DIRS,
|
|
2706
|
+
WORKSPACE_IGNORED_FILES,
|
|
943
2707
|
appendGitignore,
|
|
2708
|
+
applyWorkspacePull,
|
|
2709
|
+
assertUploadable,
|
|
944
2710
|
buildMultipartBody,
|
|
2711
|
+
buildPreviewHtml,
|
|
945
2712
|
buildRuntimeTypesContent,
|
|
2713
|
+
buildStatusHtml,
|
|
2714
|
+
buildWorkspacePushBody,
|
|
946
2715
|
bundleUserCode,
|
|
2716
|
+
bundleWorkspaceModule,
|
|
2717
|
+
bundleWorkspaceTestFile,
|
|
947
2718
|
collectFiles,
|
|
948
2719
|
configFilePath,
|
|
2720
|
+
createAppInstance,
|
|
2721
|
+
defaultPullIgnore,
|
|
2722
|
+
defaultWorkspaceIgnore,
|
|
2723
|
+
defineApp,
|
|
949
2724
|
deriveExampleSceneId,
|
|
950
2725
|
detectSkillsStatus,
|
|
2726
|
+
directoryDepth,
|
|
2727
|
+
escapeHtml,
|
|
2728
|
+
escapeJsonForScript,
|
|
951
2729
|
exampleSceneSummary,
|
|
952
2730
|
extractInlineSourceMap,
|
|
2731
|
+
extractSceneArray,
|
|
2732
|
+
formatUploadResult,
|
|
2733
|
+
formatWorkspacePullPlan,
|
|
2734
|
+
formatWorkspacePullResult,
|
|
953
2735
|
initConfig,
|
|
2736
|
+
isIgnoredWorkspacePath,
|
|
954
2737
|
isMockCredentials,
|
|
2738
|
+
isSafeRelPath,
|
|
2739
|
+
isWorkspaceCodeFile,
|
|
2740
|
+
isWorkspaceSpecFile,
|
|
955
2741
|
listScenes,
|
|
2742
|
+
listWorkspaceTests,
|
|
956
2743
|
loadConfig,
|
|
957
2744
|
loadExampleScene,
|
|
958
|
-
|
|
2745
|
+
normalizeLinkedScenes,
|
|
2746
|
+
normalizePath,
|
|
959
2747
|
normalizeSceneList,
|
|
960
2748
|
normalizeTargets,
|
|
2749
|
+
normalizeWorkspaceConfig,
|
|
2750
|
+
normalizeWorkspaceFiles,
|
|
961
2751
|
parseConfig,
|
|
2752
|
+
parseExportedTestFunctions,
|
|
962
2753
|
parseSceneStructure,
|
|
2754
|
+
planWorkspacePull,
|
|
2755
|
+
planWorkspaceUpload,
|
|
2756
|
+
portableRuntimeWarning,
|
|
963
2757
|
pullScene,
|
|
2758
|
+
pullWorkspace,
|
|
964
2759
|
readConfigFile,
|
|
965
2760
|
readDevkitVersion,
|
|
966
2761
|
remapErrorStack,
|
|
@@ -968,11 +2763,17 @@ export {
|
|
|
968
2763
|
resolveExampleScenePath,
|
|
969
2764
|
resolveRuntimeTypesSourceFile,
|
|
970
2765
|
resolveSkillsSourceDir,
|
|
2766
|
+
resolveSnapshotUrl,
|
|
2767
|
+
resolveWorkspaceAssetPath,
|
|
971
2768
|
saveSceneJson,
|
|
972
2769
|
syncSkills,
|
|
2770
|
+
toConfigScenes,
|
|
973
2771
|
typesMissingHint,
|
|
974
2772
|
uploadDirectory,
|
|
975
2773
|
validateConfigShape,
|
|
976
|
-
|
|
2774
|
+
workspacePullHasConflicts,
|
|
2775
|
+
workspacePullPendingWrites,
|
|
2776
|
+
workspaceTestId,
|
|
2777
|
+
writeConfigFile,
|
|
2778
|
+
writeConfigScenes
|
|
977
2779
|
};
|
|
978
|
-
//# sourceMappingURL=index.js.map
|