@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/bin.js
CHANGED
|
@@ -7,14 +7,14 @@ import { readFile } from "fs/promises";
|
|
|
7
7
|
import { createInterface } from "readline/promises";
|
|
8
8
|
import { stdin, stdout } from "process";
|
|
9
9
|
import { pathToFileURL } from "url";
|
|
10
|
-
import
|
|
10
|
+
import path10 from "path";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { promises as fs } from "fs";
|
|
14
14
|
import path from "path";
|
|
15
15
|
var CONFIG_FILE_NAME = "easytwin.config.json";
|
|
16
16
|
var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
|
|
17
|
-
var TEST_BASE_URL = "http://
|
|
17
|
+
var TEST_BASE_URL = "http://172.16.125.3:10100/";
|
|
18
18
|
var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
19
19
|
var MOCK_APP_ID = "test";
|
|
20
20
|
var MOCK_APP_SECRET = "test";
|
|
@@ -63,8 +63,40 @@ function validateConfigShape(value) {
|
|
|
63
63
|
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
64
64
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
65
65
|
if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
|
|
66
|
+
const scenes = parseConfigScenes(v.scenes);
|
|
67
|
+
if (scenes) config.scenes = scenes;
|
|
66
68
|
return config;
|
|
67
69
|
}
|
|
70
|
+
function parseConfigScenes(value) {
|
|
71
|
+
if (value === void 0) return void 0;
|
|
72
|
+
if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
73
|
+
return value.map((item, i) => {
|
|
74
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
75
|
+
throw new ConfigError(`scenes[${i}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
|
|
76
|
+
}
|
|
77
|
+
const it = item;
|
|
78
|
+
if (typeof it.id !== "string" || it.id.length === 0) {
|
|
79
|
+
throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 id`);
|
|
80
|
+
}
|
|
81
|
+
if (typeof it.name !== "string" || it.name.length === 0) {
|
|
82
|
+
throw new ConfigError(`scenes[${i}] \u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5 name`);
|
|
83
|
+
}
|
|
84
|
+
const scene = { id: it.id, name: it.name };
|
|
85
|
+
if (it.linkedSceneId !== void 0) {
|
|
86
|
+
if (typeof it.linkedSceneId !== "string") throw new ConfigError(`scenes[${i}].linkedSceneId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
87
|
+
if (it.linkedSceneId.length > 0) scene.linkedSceneId = it.linkedSceneId;
|
|
88
|
+
}
|
|
89
|
+
if (it.snapshotUrl !== void 0) {
|
|
90
|
+
if (typeof it.snapshotUrl !== "string") throw new ConfigError(`scenes[${i}].snapshotUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
91
|
+
if (it.snapshotUrl.length > 0) scene.snapshotUrl = it.snapshotUrl;
|
|
92
|
+
}
|
|
93
|
+
if (it.defaultLoading !== void 0) {
|
|
94
|
+
if (typeof it.defaultLoading !== "boolean") throw new ConfigError(`scenes[${i}].defaultLoading \u5FC5\u987B\u662F\u5E03\u5C14\u503C`);
|
|
95
|
+
scene.defaultLoading = it.defaultLoading;
|
|
96
|
+
}
|
|
97
|
+
return scene;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
68
100
|
async function readConfigFile(cwd) {
|
|
69
101
|
const file = configFilePath(cwd);
|
|
70
102
|
let raw;
|
|
@@ -87,7 +119,8 @@ function resolveConfig(file, env = process.env) {
|
|
|
87
119
|
return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
88
120
|
}
|
|
89
121
|
async function loadConfig(cwd, env = process.env) {
|
|
90
|
-
|
|
122
|
+
const file = await readConfigFile(cwd);
|
|
123
|
+
return resolveConfig(file, env);
|
|
91
124
|
}
|
|
92
125
|
async function writeConfigFile(cwd, config) {
|
|
93
126
|
const file = configFilePath(cwd);
|
|
@@ -95,6 +128,7 @@ async function writeConfigFile(cwd, config) {
|
|
|
95
128
|
if (config.env) body.env = config.env;
|
|
96
129
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
97
130
|
if (config.ossUrl) body.ossUrl = config.ossUrl;
|
|
131
|
+
if (config.scenes !== void 0) body.scenes = config.scenes;
|
|
98
132
|
await fs.mkdir(cwd, { recursive: true });
|
|
99
133
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
100
134
|
return file;
|
|
@@ -122,18 +156,29 @@ async function initConfig(input, cwd) {
|
|
|
122
156
|
const gitignore = await appendGitignore(cwd);
|
|
123
157
|
return { configFile, gitignore };
|
|
124
158
|
}
|
|
159
|
+
async function writeConfigScenes(cwd, scenes) {
|
|
160
|
+
const file = await readConfigFile(cwd);
|
|
161
|
+
await writeConfigFile(cwd, { ...file, scenes });
|
|
162
|
+
}
|
|
125
163
|
|
|
126
164
|
// src/client.ts
|
|
127
165
|
import http from "http";
|
|
128
166
|
import https from "https";
|
|
129
167
|
import { URL as URL2 } from "url";
|
|
130
|
-
var
|
|
131
|
-
var
|
|
132
|
-
|
|
168
|
+
var APP_ID_HEADER = "x-app-id";
|
|
169
|
+
var AUTH_HEADER = "x-app-secret";
|
|
170
|
+
function enc(id) {
|
|
171
|
+
return encodeURIComponent(id);
|
|
172
|
+
}
|
|
133
173
|
var ENDPOINTS = {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
174
|
+
/** POST 已关联场景列表(分享路径,空 body)。 */
|
|
175
|
+
linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
|
|
176
|
+
/** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
|
|
177
|
+
workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
|
|
178
|
+
/** POST 一次推送 create/update/delete(分享路径)。 */
|
|
179
|
+
workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
|
|
180
|
+
/** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
|
|
181
|
+
workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
|
|
137
182
|
};
|
|
138
183
|
var EasyTwinApiError = class extends Error {
|
|
139
184
|
status;
|
|
@@ -161,28 +206,40 @@ function parseResponseBody(buffer) {
|
|
|
161
206
|
return text;
|
|
162
207
|
}
|
|
163
208
|
}
|
|
209
|
+
function isEnvelope(value) {
|
|
210
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "success" in value;
|
|
211
|
+
}
|
|
212
|
+
function unwrapBody(parsed, status) {
|
|
213
|
+
if (!isEnvelope(parsed)) return parsed;
|
|
214
|
+
if (parsed.success === false) {
|
|
215
|
+
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? "\u8BF7\u6C42\u5931\u8D25", parsed);
|
|
216
|
+
}
|
|
217
|
+
if (parsed.success === true && "data" in parsed) return parsed.data;
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
164
220
|
var EasyTwinClient = class {
|
|
165
221
|
baseUrl;
|
|
166
|
-
/**
|
|
167
|
-
|
|
222
|
+
/** twin runtime / 场景快照资产根(baseOSSUrl)。 */
|
|
223
|
+
ossUrl;
|
|
224
|
+
/** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
|
|
168
225
|
appId;
|
|
226
|
+
/** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
227
|
+
mock;
|
|
169
228
|
appSecret;
|
|
170
229
|
constructor(config) {
|
|
171
230
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
231
|
+
this.ossUrl = config.ossUrl.replace(/\/+$/, "");
|
|
172
232
|
this.mock = config.mock;
|
|
173
233
|
this.appId = config.appId;
|
|
174
234
|
this.appSecret = config.appSecret;
|
|
175
235
|
}
|
|
176
236
|
/** 全仓唯一认证头注入点。 */
|
|
177
237
|
authHeaders() {
|
|
178
|
-
return {
|
|
179
|
-
[AUTH_HEADER]: `${BEARER_PREFIX} ${this.appSecret}`,
|
|
180
|
-
[APP_ID_HEADER]: this.appId
|
|
181
|
-
};
|
|
238
|
+
return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
|
|
182
239
|
}
|
|
183
240
|
/** JSON 请求(原生 fetch)。 */
|
|
184
|
-
async request(
|
|
185
|
-
const url = `${this.baseUrl}${
|
|
241
|
+
async request(path11, options = {}) {
|
|
242
|
+
const url = `${this.baseUrl}${path11}`;
|
|
186
243
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
187
244
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
188
245
|
headers["Content-Type"] = "application/json";
|
|
@@ -204,8 +261,8 @@ var EasyTwinClient = class {
|
|
|
204
261
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
205
262
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
206
263
|
*/
|
|
207
|
-
async upload(
|
|
208
|
-
const url = new URL2(`${this.baseUrl}${
|
|
264
|
+
async upload(path11, options) {
|
|
265
|
+
const url = new URL2(`${this.baseUrl}${path11}`);
|
|
209
266
|
const mod = url.protocol === "https:" ? https : http;
|
|
210
267
|
const headers = {
|
|
211
268
|
...this.authHeaders(),
|
|
@@ -238,8 +295,8 @@ var EasyTwinClient = class {
|
|
|
238
295
|
return this.handleStatus(raw.status, raw.body);
|
|
239
296
|
}
|
|
240
297
|
handleStatus(status, body) {
|
|
241
|
-
if (status >= 200 && status < 300) return parseResponseBody(body);
|
|
242
298
|
const parsed = parseResponseBody(body);
|
|
299
|
+
if (status >= 200 && status < 300) return unwrapBody(parsed, status);
|
|
243
300
|
throw new EasyTwinApiError(status, messageFromBody(parsed) ?? `HTTP ${status}`, parsed);
|
|
244
301
|
}
|
|
245
302
|
};
|
|
@@ -248,17 +305,89 @@ var EasyTwinClient = class {
|
|
|
248
305
|
import { promises as fs2 } from "fs";
|
|
249
306
|
import path2 from "path";
|
|
250
307
|
import { fileURLToPath } from "url";
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
308
|
+
function asRecord(value) {
|
|
309
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
310
|
+
}
|
|
311
|
+
function asString(value) {
|
|
312
|
+
return typeof value === "string" ? value : "";
|
|
313
|
+
}
|
|
314
|
+
function asBoolean(value) {
|
|
315
|
+
return value === true;
|
|
316
|
+
}
|
|
317
|
+
function extractSceneArray(data) {
|
|
318
|
+
if (Array.isArray(data)) return data;
|
|
319
|
+
const root = asRecord(data);
|
|
320
|
+
if (Array.isArray(root.data)) return root.data;
|
|
321
|
+
if (Array.isArray(root.list)) return root.list;
|
|
322
|
+
if (Array.isArray(root.scenes)) return root.scenes;
|
|
323
|
+
const nested = asRecord(root.data);
|
|
324
|
+
if (Array.isArray(nested.scenes)) return nested.scenes;
|
|
325
|
+
if (Array.isArray(nested.list)) return nested.list;
|
|
326
|
+
throw new Error("scene list \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
327
|
+
}
|
|
328
|
+
function normalizeLinkedScenes(data) {
|
|
329
|
+
const list = extractSceneArray(data);
|
|
254
330
|
return list.map((item) => {
|
|
255
|
-
const it = item
|
|
256
|
-
|
|
331
|
+
const it = asRecord(item);
|
|
332
|
+
const sceneKey = asString(it.sceneKey);
|
|
333
|
+
const sourceProjectName = asString(it.sourceProjectName);
|
|
334
|
+
const sourceSceneName = asString(it.sourceSceneName);
|
|
335
|
+
return {
|
|
336
|
+
id: asString(it.id),
|
|
337
|
+
sceneKey,
|
|
338
|
+
name: sourceProjectName || sourceSceneName || sceneKey,
|
|
339
|
+
defaultLoading: asBoolean(it.defaultLoading),
|
|
340
|
+
sourceSceneId: asString(it.sourceSceneId),
|
|
341
|
+
sourceProjectId: asString(it.sourceProjectId),
|
|
342
|
+
sourceSceneName,
|
|
343
|
+
sourceProjectName,
|
|
344
|
+
sourceLost: asBoolean(it.sourceLost),
|
|
345
|
+
snapshotUrl: asString(it.snapshotUrl),
|
|
346
|
+
snapshotUpdatedAt: asString(it.snapshotUpdatedAt)
|
|
347
|
+
};
|
|
257
348
|
});
|
|
258
349
|
}
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
350
|
+
function normalizeSceneList(data) {
|
|
351
|
+
return normalizeLinkedScenes(data).map((s) => ({
|
|
352
|
+
id: s.sceneKey,
|
|
353
|
+
name: s.name,
|
|
354
|
+
linkedSceneId: s.id,
|
|
355
|
+
snapshotUrl: s.snapshotUrl,
|
|
356
|
+
defaultLoading: s.defaultLoading
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
function toConfigScenes(scenes) {
|
|
360
|
+
return scenes.map((s) => {
|
|
361
|
+
const item = { id: s.id, name: s.name };
|
|
362
|
+
if (s.linkedSceneId) item.linkedSceneId = s.linkedSceneId;
|
|
363
|
+
if (s.snapshotUrl) item.snapshotUrl = s.snapshotUrl;
|
|
364
|
+
if (s.defaultLoading !== void 0) item.defaultLoading = s.defaultLoading;
|
|
365
|
+
return item;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function resolveSnapshotUrl(ossUrl, snapshotUrl) {
|
|
369
|
+
if (/^https?:\/\//i.test(snapshotUrl)) return snapshotUrl;
|
|
370
|
+
const base = ossUrl.replace(/\/+$/, "");
|
|
371
|
+
const rel = snapshotUrl.replace(/^\/+/, "");
|
|
372
|
+
return `${base}/${rel}`;
|
|
373
|
+
}
|
|
374
|
+
async function fetchSnapshotJson(ossUrl, snapshotUrl) {
|
|
375
|
+
if (snapshotUrl.length === 0) throw new Error("\u573A\u666F\u5FEB\u7167\u5730\u5740\u4E3A\u7A7A");
|
|
376
|
+
const url = resolveSnapshotUrl(ossUrl, snapshotUrl);
|
|
377
|
+
let res;
|
|
378
|
+
try {
|
|
379
|
+
res = await fetch(url);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
throw new EasyTwinApiError(0, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:${url}:${err instanceof Error ? err.message : String(err)}`);
|
|
382
|
+
}
|
|
383
|
+
if (!res.ok) {
|
|
384
|
+
throw new EasyTwinApiError(res.status, `\u62C9\u53D6\u573A\u666F\u5FEB\u7167\u5931\u8D25:HTTP ${res.status} ${url}`);
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
return await res.json();
|
|
388
|
+
} catch {
|
|
389
|
+
throw new Error(`\u573A\u666F\u5FEB\u7167\u4E0D\u662F\u5408\u6CD5 JSON:${url}`);
|
|
390
|
+
}
|
|
262
391
|
}
|
|
263
392
|
var EXAMPLE_SCENE_FILE = "scene.example.json";
|
|
264
393
|
var MOCK_SCENE_NAME = "\u672C\u5730\u793A\u4F8B\u573A\u666F";
|
|
@@ -290,22 +419,34 @@ function deriveExampleSceneId(payload) {
|
|
|
290
419
|
async function exampleSceneSummary(exampleFile) {
|
|
291
420
|
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
292
421
|
}
|
|
422
|
+
async function fetchLinkedScenes(client) {
|
|
423
|
+
return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
|
|
424
|
+
}
|
|
293
425
|
async function listScenes(client, options = {}) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return
|
|
426
|
+
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
|
|
427
|
+
if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
|
|
428
|
+
return scenes;
|
|
297
429
|
}
|
|
298
430
|
async function pullScene(client, id, options = {}) {
|
|
299
431
|
if (client.mock) {
|
|
300
|
-
const
|
|
301
|
-
const sceneId = deriveExampleSceneId(
|
|
432
|
+
const payload2 = await loadExampleScene(options.exampleFile);
|
|
433
|
+
const sceneId = deriveExampleSceneId(payload2);
|
|
302
434
|
if (sceneId !== id) {
|
|
303
435
|
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)`);
|
|
304
436
|
}
|
|
305
|
-
return { id: sceneId, name: MOCK_SCENE_NAME, payload };
|
|
437
|
+
return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
|
|
438
|
+
}
|
|
439
|
+
const data = await fetchLinkedScenes(client);
|
|
440
|
+
const scenes = normalizeLinkedScenes(data);
|
|
441
|
+
const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
|
|
442
|
+
if (!hit) {
|
|
443
|
+
const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
|
|
444
|
+
throw new Error(
|
|
445
|
+
available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
|
|
446
|
+
);
|
|
306
447
|
}
|
|
307
|
-
const
|
|
308
|
-
return
|
|
448
|
+
const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
|
|
449
|
+
return { id: hit.sceneKey, name: hit.name, payload };
|
|
309
450
|
}
|
|
310
451
|
async function saveSceneJson(scene, out) {
|
|
311
452
|
await fs2.mkdir(path2.dirname(out), { recursive: true });
|
|
@@ -316,7 +457,43 @@ async function saveSceneJson(scene, out) {
|
|
|
316
457
|
// src/upload.ts
|
|
317
458
|
import { promises as fs3 } from "fs";
|
|
318
459
|
import path3 from "path";
|
|
319
|
-
var
|
|
460
|
+
var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
461
|
+
".git",
|
|
462
|
+
"node_modules",
|
|
463
|
+
"dist",
|
|
464
|
+
".easytwin",
|
|
465
|
+
".cursor",
|
|
466
|
+
".claude",
|
|
467
|
+
".qoder",
|
|
468
|
+
".vscode"
|
|
469
|
+
]);
|
|
470
|
+
var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
|
|
471
|
+
function isTsconfigFile(base) {
|
|
472
|
+
return /^tsconfig(\..+)?\.json$/i.test(base);
|
|
473
|
+
}
|
|
474
|
+
var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
|
|
475
|
+
function isWorkspaceSpecFile(relPath) {
|
|
476
|
+
const base = normalizePath(relPath).split("/").pop() ?? "";
|
|
477
|
+
return /\.spec\.ts$/i.test(base);
|
|
478
|
+
}
|
|
479
|
+
function isIgnoredWorkspacePath(relPath) {
|
|
480
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
481
|
+
if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
|
|
482
|
+
const base = parts[parts.length - 1];
|
|
483
|
+
if (base === void 0) return false;
|
|
484
|
+
if (WORKSPACE_IGNORED_FILES.has(base)) return true;
|
|
485
|
+
if (isTsconfigFile(base)) return true;
|
|
486
|
+
return base.toLowerCase().endsWith(".scene.json");
|
|
487
|
+
}
|
|
488
|
+
function defaultWorkspaceIgnore(relPath) {
|
|
489
|
+
return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
|
|
490
|
+
}
|
|
491
|
+
function combineUploadIgnore(extra) {
|
|
492
|
+
return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
493
|
+
}
|
|
494
|
+
async function collectWorkspaceCodeFiles(dir, ignore) {
|
|
495
|
+
return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
|
|
496
|
+
}
|
|
320
497
|
async function collectFiles(dir, ignore = () => false) {
|
|
321
498
|
const files = [];
|
|
322
499
|
async function walk(current, rel) {
|
|
@@ -325,7 +502,7 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
325
502
|
const relPath = path3.posix.join(rel, entry.name);
|
|
326
503
|
if (ignore(relPath)) continue;
|
|
327
504
|
if (entry.isDirectory()) {
|
|
328
|
-
if (
|
|
505
|
+
if (entry.name === ".git") continue;
|
|
329
506
|
await walk(path3.join(current, entry.name), relPath);
|
|
330
507
|
} else if (entry.isFile()) {
|
|
331
508
|
const absPath = path3.join(current, entry.name);
|
|
@@ -338,67 +515,344 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
338
515
|
files.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
339
516
|
return files;
|
|
340
517
|
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
518
|
+
function asRecord2(value) {
|
|
519
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
520
|
+
}
|
|
521
|
+
function asString2(value) {
|
|
522
|
+
return typeof value === "string" ? value : "";
|
|
523
|
+
}
|
|
524
|
+
function normalizePath(relPath) {
|
|
525
|
+
return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
526
|
+
}
|
|
527
|
+
function normalizeWorkspaceFiles(data) {
|
|
528
|
+
if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
529
|
+
return data.map((item) => {
|
|
530
|
+
const it = asRecord2(item);
|
|
531
|
+
return { id: asString2(it.id), filePath: asString2(it.filePath), content: asString2(it.content) };
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function extensionOf(relPath) {
|
|
535
|
+
const base = relPath.split("/").pop() ?? "";
|
|
536
|
+
const i = base.lastIndexOf(".");
|
|
537
|
+
if (i <= 0) return "";
|
|
538
|
+
return base.slice(i).toLowerCase();
|
|
539
|
+
}
|
|
540
|
+
function allowedExtensionSet(list) {
|
|
541
|
+
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
542
|
+
}
|
|
543
|
+
var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
|
|
544
|
+
function isWorkspaceCodeFile(relPath) {
|
|
545
|
+
return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
|
|
546
|
+
}
|
|
547
|
+
function countsFromPlan(plan) {
|
|
548
|
+
return {
|
|
549
|
+
created: plan.creates.length,
|
|
550
|
+
updated: plan.updates.length,
|
|
551
|
+
deleted: plan.deletes.length,
|
|
552
|
+
unchanged: plan.unchanged.length
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function planWorkspaceUpload(local, remote) {
|
|
556
|
+
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
557
|
+
const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
|
|
558
|
+
const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
|
|
559
|
+
const creates = [];
|
|
560
|
+
const updates = [];
|
|
561
|
+
const unchanged = [];
|
|
562
|
+
for (const item of local) {
|
|
563
|
+
const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
|
|
564
|
+
if (!remoteFile) creates.push(item);
|
|
565
|
+
else if (remoteFile.content !== item.content) {
|
|
566
|
+
updates.push({ file: item.file, id: remoteFile.id, content: item.content });
|
|
567
|
+
} else {
|
|
568
|
+
unchanged.push(item.file);
|
|
569
|
+
}
|
|
351
570
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
\r
|
|
358
|
-
`,
|
|
359
|
-
"utf8"
|
|
360
|
-
)
|
|
361
|
-
);
|
|
362
|
-
parts.push(file.content);
|
|
363
|
-
parts.push(Buffer.from(`\r
|
|
364
|
-
--${boundary}--\r
|
|
365
|
-
`, "utf8"));
|
|
366
|
-
return Buffer.concat(parts);
|
|
571
|
+
return { creates, updates, deletes, unchanged };
|
|
572
|
+
}
|
|
573
|
+
function formatUploadResult(result) {
|
|
574
|
+
const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
|
|
575
|
+
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}`;
|
|
367
576
|
}
|
|
368
577
|
async function mockUpload(dir, options) {
|
|
369
|
-
const ignore = options.ignore
|
|
370
|
-
const files = await
|
|
578
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
579
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
371
580
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
372
581
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
373
582
|
options.onProgress?.({ phase: "upload", current: total, total });
|
|
374
|
-
return { fileCount: files.length, byteCount: total, mock: true };
|
|
583
|
+
return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
|
|
584
|
+
}
|
|
585
|
+
function buildWorkspacePushBody(plan) {
|
|
586
|
+
const body = {};
|
|
587
|
+
if (plan.creates.length > 0) {
|
|
588
|
+
body.create = plan.creates.map((c) => ({
|
|
589
|
+
filePath: normalizePath(c.file.relPath),
|
|
590
|
+
content: c.content
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
if (plan.updates.length > 0) {
|
|
594
|
+
body.update = plan.updates.map((p) => ({
|
|
595
|
+
id: p.id,
|
|
596
|
+
content: p.content,
|
|
597
|
+
filePath: normalizePath(p.file.relPath)
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
if (plan.deletes.length > 0) {
|
|
601
|
+
body.delete = plan.deletes.map((d) => d.id);
|
|
602
|
+
}
|
|
603
|
+
return Object.keys(body).length > 0 ? body : null;
|
|
375
604
|
}
|
|
376
605
|
async function uploadDirectory(client, dir, options = {}) {
|
|
377
606
|
if (client.mock) return mockUpload(dir, options);
|
|
378
|
-
const ignore = options.ignore
|
|
379
|
-
const files = await
|
|
607
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
608
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
380
609
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
381
610
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
382
|
-
|
|
611
|
+
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
612
|
+
const local = [];
|
|
383
613
|
for (const file of files) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
614
|
+
local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
|
|
615
|
+
}
|
|
616
|
+
const plan = planWorkspaceUpload(local, remote);
|
|
617
|
+
const counts = countsFromPlan(plan);
|
|
618
|
+
const pushBody = buildWorkspacePushBody(plan);
|
|
619
|
+
if (pushBody) {
|
|
620
|
+
await client.request(ENDPOINTS.workspaceCodePush(), {
|
|
621
|
+
method: "POST",
|
|
622
|
+
body: JSON.stringify(pushBody)
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
let sent = 0;
|
|
626
|
+
const bump = (file) => {
|
|
393
627
|
sent += file.size;
|
|
394
628
|
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
629
|
+
};
|
|
630
|
+
for (const c of plan.creates) bump(c.file);
|
|
631
|
+
for (const p of plan.updates) bump(p.file);
|
|
632
|
+
for (const u of plan.unchanged) bump(u);
|
|
633
|
+
if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
|
|
634
|
+
return { fileCount: files.length, byteCount: total, ...counts };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/workspace.ts
|
|
638
|
+
import { promises as fs4 } from "fs";
|
|
639
|
+
import path5 from "path";
|
|
640
|
+
|
|
641
|
+
// src/assetsPath.ts
|
|
642
|
+
import path4 from "path";
|
|
643
|
+
function resolveWorkspaceAssetPath(cwd, relPath) {
|
|
644
|
+
const root = path4.resolve(cwd);
|
|
645
|
+
const input = relPath.trim();
|
|
646
|
+
if (!input) throw new Error("assets \u8DEF\u5F84\u4E3A\u7A7A");
|
|
647
|
+
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}`);
|
|
648
|
+
const resolved = path4.resolve(root, input);
|
|
649
|
+
const rel = path4.relative(root, resolved);
|
|
650
|
+
if (rel.startsWith("..") || path4.isAbsolute(rel)) {
|
|
651
|
+
throw new Error(`assets \u8DEF\u5F84\u9003\u9038\u5DE5\u4F5C\u533A: ${relPath}`);
|
|
652
|
+
}
|
|
653
|
+
return resolved;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/workspace.ts
|
|
657
|
+
var DEFAULT_ENTRY_PATH = "src/main.ts";
|
|
658
|
+
var DEFAULT_MAIN_TS = `import { TwinApp, type TwinAppContext } from "@easytwin/apps";
|
|
659
|
+
|
|
660
|
+
export default class App extends TwinApp {
|
|
661
|
+
async init(ctx: TwinAppContext) {
|
|
662
|
+
// engine / container \u5DF2\u6709;scene / camera \u4E3A null
|
|
663
|
+
void ctx;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async onSceneLoaded(ctx: TwinAppContext) {
|
|
667
|
+
// \u573A\u666F\u5B57\u6BB5\u6709\u503C;\u5728\u6B64\u52A0\u7269\u4F53 / \u7ED1\u4E8B\u4EF6,\u5E76\u7528 ctx.sceneCleanup \u5BF9\u79F0\u62C6\u9664
|
|
668
|
+
void ctx;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
onUpdate(ctx: TwinAppContext, delta: number, elapsed: number) {
|
|
672
|
+
void ctx;
|
|
673
|
+
void delta;
|
|
674
|
+
void elapsed;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
onDispose(ctx: TwinAppContext) {
|
|
678
|
+
void ctx;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
`;
|
|
682
|
+
function defaultPullIgnore(relPath) {
|
|
683
|
+
return defaultWorkspaceIgnore(relPath);
|
|
684
|
+
}
|
|
685
|
+
function isSafeRelPath(relPath) {
|
|
686
|
+
const n = normalizePath(relPath);
|
|
687
|
+
if (!n) return false;
|
|
688
|
+
if (n.toLowerCase() === "easytwin.config.json") return false;
|
|
689
|
+
const base = n.split("/").pop() ?? "";
|
|
690
|
+
if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
|
|
691
|
+
if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
|
|
692
|
+
const parts = n.split("/");
|
|
693
|
+
if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
function normalizeEol(text) {
|
|
697
|
+
return text.replace(/\r\n/g, "\n");
|
|
698
|
+
}
|
|
699
|
+
function combineIgnore(extra) {
|
|
700
|
+
return (relPath) => defaultPullIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
701
|
+
}
|
|
702
|
+
async function readLocalText(absPath) {
|
|
703
|
+
try {
|
|
704
|
+
return await fs4.readFile(absPath, "utf8");
|
|
705
|
+
} catch (err) {
|
|
706
|
+
const code = err.code;
|
|
707
|
+
if (code === "ENOENT") return void 0;
|
|
708
|
+
throw err;
|
|
395
709
|
}
|
|
396
|
-
|
|
710
|
+
}
|
|
711
|
+
async function fetchRemoteFiles(client) {
|
|
712
|
+
if (client.mock) return [];
|
|
713
|
+
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
714
|
+
}
|
|
715
|
+
async function planWorkspacePull(client, dir, options = {}) {
|
|
716
|
+
const ignore = combineIgnore(options.ignore);
|
|
717
|
+
const remoteRaw = await fetchRemoteFiles(client);
|
|
718
|
+
const skippedRemotePaths = [];
|
|
719
|
+
const remoteByPath = /* @__PURE__ */ new Map();
|
|
720
|
+
for (const file of remoteRaw) {
|
|
721
|
+
const rel = normalizePath(file.filePath);
|
|
722
|
+
if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
|
|
723
|
+
skippedRemotePaths.push(rel || file.filePath);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
remoteByPath.set(rel, file.content);
|
|
727
|
+
}
|
|
728
|
+
let localFiles = [];
|
|
729
|
+
try {
|
|
730
|
+
localFiles = await collectFiles(dir, ignore);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
const code = err.code;
|
|
733
|
+
if (code !== "ENOENT") throw err;
|
|
734
|
+
}
|
|
735
|
+
const localByPath = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const file of localFiles) {
|
|
737
|
+
const rel = normalizePath(file.relPath);
|
|
738
|
+
if (!isWorkspaceCodeFile(rel)) continue;
|
|
739
|
+
const content = await readLocalText(file.absPath);
|
|
740
|
+
if (content !== void 0) localByPath.set(rel, content);
|
|
741
|
+
}
|
|
742
|
+
const changes = [];
|
|
743
|
+
const seen = /* @__PURE__ */ new Set();
|
|
744
|
+
for (const [rel, remoteContent] of remoteByPath) {
|
|
745
|
+
seen.add(rel);
|
|
746
|
+
const localContent = localByPath.get(rel);
|
|
747
|
+
if (localContent === void 0) {
|
|
748
|
+
changes.push({ path: rel, kind: "remote-only", remoteContent });
|
|
749
|
+
} else if (normalizeEol(localContent) === normalizeEol(remoteContent)) {
|
|
750
|
+
changes.push({ path: rel, kind: "identical", localContent, remoteContent });
|
|
751
|
+
} else {
|
|
752
|
+
changes.push({ path: rel, kind: "modified", localContent, remoteContent });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
for (const [rel, localContent] of localByPath) {
|
|
756
|
+
if (seen.has(rel)) continue;
|
|
757
|
+
changes.push({ path: rel, kind: "local-only", localContent });
|
|
758
|
+
}
|
|
759
|
+
const remoteEmpty = remoteByPath.size === 0;
|
|
760
|
+
let seededDefaults = false;
|
|
761
|
+
if (remoteEmpty) {
|
|
762
|
+
const localMain = localByPath.get(DEFAULT_ENTRY_PATH);
|
|
763
|
+
if (localMain === void 0) {
|
|
764
|
+
changes.push({ path: DEFAULT_ENTRY_PATH, kind: "seed", remoteContent: DEFAULT_MAIN_TS });
|
|
765
|
+
seededDefaults = true;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
769
|
+
return {
|
|
770
|
+
remoteEmpty,
|
|
771
|
+
seededDefaults,
|
|
772
|
+
mock: client.mock || void 0,
|
|
773
|
+
skippedRemotePaths,
|
|
774
|
+
changes
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
async function applyWorkspacePull(dir, plan, options = {}) {
|
|
778
|
+
const written = [];
|
|
779
|
+
const skippedConflicts = [];
|
|
780
|
+
for (const change of plan.changes) {
|
|
781
|
+
if (change.kind === "identical" || change.kind === "local-only") continue;
|
|
782
|
+
if (change.kind === "modified" && !options.force) {
|
|
783
|
+
skippedConflicts.push(change.path);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const content = change.remoteContent ?? "";
|
|
787
|
+
const abs = resolveWorkspaceAssetPath(dir, change.path);
|
|
788
|
+
await fs4.mkdir(path5.dirname(abs), { recursive: true });
|
|
789
|
+
await fs4.writeFile(abs, content, "utf8");
|
|
790
|
+
written.push(change.path);
|
|
791
|
+
}
|
|
792
|
+
return { written, skippedConflicts };
|
|
793
|
+
}
|
|
794
|
+
async function pullWorkspace(client, dir, options = {}) {
|
|
795
|
+
const plan = await planWorkspacePull(client, dir, { ignore: options.ignore });
|
|
796
|
+
if (options.dryRun) {
|
|
797
|
+
const skippedConflicts = plan.changes.filter((c) => c.kind === "modified" && !options.force).map((c) => c.path);
|
|
798
|
+
return { plan, written: [], skippedConflicts, dryRun: true, mock: plan.mock };
|
|
799
|
+
}
|
|
800
|
+
const applied = await applyWorkspacePull(dir, plan, { force: options.force });
|
|
801
|
+
return { plan, ...applied, mock: plan.mock };
|
|
802
|
+
}
|
|
803
|
+
var KIND_LABEL = {
|
|
804
|
+
identical: "same ",
|
|
805
|
+
"remote-only": "create",
|
|
806
|
+
"local-only": "keep ",
|
|
807
|
+
modified: "modify",
|
|
808
|
+
seed: "seed "
|
|
809
|
+
};
|
|
810
|
+
function formatWorkspacePullPlan(plan) {
|
|
811
|
+
const lines = [];
|
|
812
|
+
if (plan.mock) lines.push("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u6309\u8FDC\u7AEF\u4E3A\u7A7A\u5904\u7406");
|
|
813
|
+
const remoteCount = plan.changes.filter((c) => c.kind !== "local-only" && c.kind !== "seed").length;
|
|
814
|
+
if (plan.remoteEmpty) {
|
|
815
|
+
lines.push(
|
|
816
|
+
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"
|
|
817
|
+
);
|
|
818
|
+
} else {
|
|
819
|
+
lines.push(`\u8FDC\u7AEF ${remoteCount} \u4E2A\u6587\u4EF6`);
|
|
820
|
+
}
|
|
821
|
+
for (const change of plan.changes) {
|
|
822
|
+
let extra = "";
|
|
823
|
+
if (change.kind === "modified") extra = " (\u51B2\u7A81,\u9ED8\u8BA4\u4E0D\u8986\u76D6)";
|
|
824
|
+
if (change.kind === "local-only") extra = " (\u4EC5\u672C\u5730)";
|
|
825
|
+
if (change.kind === "seed") extra = " (\u9ED8\u8BA4 TwinApp)";
|
|
826
|
+
lines.push(` ${KIND_LABEL[change.kind]} ${change.path}${extra}`);
|
|
827
|
+
}
|
|
828
|
+
if (plan.skippedRemotePaths.length > 0) {
|
|
829
|
+
lines.push(`\u8DF3\u8FC7\u975E\u6CD5/\u51ED\u636E\u8DEF\u5F84: ${plan.skippedRemotePaths.join(", ")}`);
|
|
830
|
+
}
|
|
831
|
+
return lines.join("\n");
|
|
832
|
+
}
|
|
833
|
+
function formatWorkspacePullResult(result) {
|
|
834
|
+
const lines = [formatWorkspacePullPlan(result.plan)];
|
|
835
|
+
if (result.dryRun) {
|
|
836
|
+
lines.push("dry-run:\u672A\u5199\u76D8");
|
|
837
|
+
if (result.skippedConflicts.length > 0) {
|
|
838
|
+
lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force)`);
|
|
839
|
+
}
|
|
840
|
+
return lines.join("\n");
|
|
841
|
+
}
|
|
842
|
+
if (result.written.length > 0) {
|
|
843
|
+
lines.push(`\u5DF2\u5199\u5165 ${result.written.length} \u4E2A\u6587\u4EF6: ${result.written.join(", ")}`);
|
|
844
|
+
} else {
|
|
845
|
+
lines.push("\u672A\u5199\u5165\u6587\u4EF6");
|
|
846
|
+
}
|
|
847
|
+
if (result.skippedConflicts.length > 0) {
|
|
848
|
+
lines.push(`${result.skippedConflicts.length} \u4E2A\u51B2\u7A81\u672A\u8986\u76D6(\u52A0 --force): ${result.skippedConflicts.join(", ")}`);
|
|
849
|
+
}
|
|
850
|
+
return lines.join("\n");
|
|
397
851
|
}
|
|
398
852
|
|
|
399
853
|
// src/skills.ts
|
|
400
|
-
import { existsSync, promises as
|
|
401
|
-
import
|
|
854
|
+
import { existsSync, promises as fs5 } from "fs";
|
|
855
|
+
import path6 from "path";
|
|
402
856
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
403
857
|
var SKILL_NAMES = [
|
|
404
858
|
"easytwin-render",
|
|
@@ -406,7 +860,8 @@ var SKILL_NAMES = [
|
|
|
406
860
|
"easytwin-develop",
|
|
407
861
|
"easytwin-bootstrap",
|
|
408
862
|
"easytwin-scene",
|
|
409
|
-
"easytwin-upload"
|
|
863
|
+
"easytwin-upload",
|
|
864
|
+
"easytwin-test"
|
|
410
865
|
];
|
|
411
866
|
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
412
867
|
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
@@ -421,7 +876,8 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
|
421
876
|
skipLibCheck: true,
|
|
422
877
|
noEmit: true,
|
|
423
878
|
paths: {
|
|
424
|
-
"@easytwin/runtime": [".easytwin/types"]
|
|
879
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
880
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
425
881
|
}
|
|
426
882
|
},
|
|
427
883
|
include: ["src/**/*.ts"]
|
|
@@ -433,22 +889,55 @@ var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
|
433
889
|
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
434
890
|
"skipLibCheck": true,
|
|
435
891
|
"paths": {
|
|
436
|
-
"@easytwin/runtime": [".easytwin/types"]
|
|
892
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
893
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
437
894
|
}`;
|
|
438
|
-
var
|
|
439
|
-
|
|
440
|
-
|
|
895
|
+
var APPS_TYPES_FILE = "apps.d.ts";
|
|
896
|
+
var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
|
|
897
|
+
|
|
898
|
+
export type TwinAppContext = {
|
|
899
|
+
app: {
|
|
900
|
+
id: string;
|
|
901
|
+
mode: "preview" | "publish";
|
|
902
|
+
};
|
|
441
903
|
engine: RuntimeEngine;
|
|
442
|
-
|
|
443
|
-
|
|
904
|
+
container: HTMLElement;
|
|
905
|
+
runtimeScene: RuntimeScene | null;
|
|
906
|
+
scene: RuntimeScene["sceneObject"] | null;
|
|
907
|
+
camera: RuntimeScene["camera"]["main"] | null;
|
|
908
|
+
sceneManager: {
|
|
909
|
+
currentSceneId: string;
|
|
910
|
+
runtime: SceneManager | null;
|
|
911
|
+
loadScene(sceneId: string): Promise<void>;
|
|
912
|
+
};
|
|
913
|
+
logger: {
|
|
914
|
+
log(...args: unknown[]): void;
|
|
915
|
+
info(...args: unknown[]): void;
|
|
916
|
+
warn(...args: unknown[]): void;
|
|
917
|
+
error(...args: unknown[]): void;
|
|
918
|
+
};
|
|
919
|
+
assets: {
|
|
920
|
+
text(path: string): Promise<string>;
|
|
921
|
+
json<T = unknown>(path: string): Promise<T>;
|
|
922
|
+
};
|
|
923
|
+
cleanup(fn: () => void | Promise<void>): void;
|
|
924
|
+
sceneCleanup(fn: () => void | Promise<void>): void;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
export declare abstract class TwinApp {
|
|
928
|
+
init?(ctx: TwinAppContext): void | Promise<void>;
|
|
929
|
+
onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
|
|
930
|
+
onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
|
|
931
|
+
onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
|
|
932
|
+
onDispose?(ctx: TwinAppContext): void | Promise<void>;
|
|
933
|
+
onError?(ctx: TwinAppContext, error: unknown): boolean | void;
|
|
444
934
|
}
|
|
935
|
+
|
|
936
|
+
export declare function defineApp<T>(app: T): T;
|
|
445
937
|
`;
|
|
446
938
|
function buildRuntimeTypesContent(sourceDts) {
|
|
447
|
-
|
|
448
|
-
if (trimmed.includes("export interface EasyTwinRunContext")) return `${trimmed}
|
|
939
|
+
return `${sourceDts.replace(/\s+$/, "")}
|
|
449
940
|
`;
|
|
450
|
-
return `${trimmed}
|
|
451
|
-
${RUN_CONTEXT_DECL}`;
|
|
452
941
|
}
|
|
453
942
|
function normalizeTargets(target = "all") {
|
|
454
943
|
if (target === "all") return ["cursor", "claude", "codex", "qoder"];
|
|
@@ -459,69 +948,69 @@ function resolveSkillsSourceDir() {
|
|
|
459
948
|
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
460
949
|
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
|
|
461
950
|
}
|
|
462
|
-
const here =
|
|
463
|
-
return
|
|
951
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
952
|
+
return path6.resolve(here, "..", "skills");
|
|
464
953
|
}
|
|
465
954
|
function resolveRuntimeTypesSourceFile() {
|
|
466
955
|
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
467
956
|
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
|
|
468
957
|
}
|
|
469
|
-
const here =
|
|
470
|
-
const fromDist =
|
|
471
|
-
const fromSrc =
|
|
958
|
+
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
959
|
+
const fromDist = path6.join(here, "runtime-types", "index.d.ts");
|
|
960
|
+
const fromSrc = path6.resolve(here, "lib", "index.d.ts");
|
|
472
961
|
if (existsSync(fromDist)) return fromDist;
|
|
473
962
|
if (existsSync(fromSrc)) return fromSrc;
|
|
474
963
|
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
|
|
475
964
|
}
|
|
476
965
|
async function readJson(file) {
|
|
477
|
-
return JSON.parse(await
|
|
966
|
+
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
478
967
|
}
|
|
479
968
|
async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
|
|
480
|
-
const marker =
|
|
969
|
+
const marker = path6.join(skillsDir, ".easytwin-source.json");
|
|
481
970
|
try {
|
|
482
971
|
const meta = await readJson(marker);
|
|
483
972
|
if (typeof meta.version === "string") return meta.version;
|
|
484
973
|
} catch {
|
|
485
974
|
}
|
|
486
975
|
try {
|
|
487
|
-
const pkg = await readJson(
|
|
976
|
+
const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
|
|
488
977
|
return pkg.version;
|
|
489
978
|
} catch {
|
|
490
979
|
return "0.0.0";
|
|
491
980
|
}
|
|
492
981
|
}
|
|
493
982
|
async function copyDir(src, dest) {
|
|
494
|
-
await
|
|
495
|
-
const entries = await
|
|
983
|
+
await fs5.mkdir(dest, { recursive: true });
|
|
984
|
+
const entries = await fs5.readdir(src, { withFileTypes: true });
|
|
496
985
|
for (const entry of entries) {
|
|
497
|
-
const s =
|
|
498
|
-
const d =
|
|
986
|
+
const s = path6.join(src, entry.name);
|
|
987
|
+
const d = path6.join(dest, entry.name);
|
|
499
988
|
if (entry.isDirectory()) await copyDir(s, d);
|
|
500
|
-
else if (entry.isFile()) await
|
|
989
|
+
else if (entry.isFile()) await fs5.copyFile(s, d);
|
|
501
990
|
}
|
|
502
991
|
}
|
|
503
992
|
async function dirMatches(src, dest) {
|
|
504
993
|
let sourceEntries;
|
|
505
994
|
try {
|
|
506
|
-
sourceEntries = await
|
|
995
|
+
sourceEntries = await fs5.readdir(src);
|
|
507
996
|
} catch {
|
|
508
997
|
return false;
|
|
509
998
|
}
|
|
510
999
|
for (const name of sourceEntries) {
|
|
511
|
-
const s =
|
|
512
|
-
const d =
|
|
513
|
-
const sStat = await
|
|
1000
|
+
const s = path6.join(src, name);
|
|
1001
|
+
const d = path6.join(dest, name);
|
|
1002
|
+
const sStat = await fs5.stat(s);
|
|
514
1003
|
if (sStat.isDirectory()) {
|
|
515
1004
|
if (!await dirMatches(s, d)) return false;
|
|
516
1005
|
} else {
|
|
517
1006
|
let dStat;
|
|
518
1007
|
try {
|
|
519
|
-
dStat = await
|
|
1008
|
+
dStat = await fs5.stat(d);
|
|
520
1009
|
} catch {
|
|
521
1010
|
return false;
|
|
522
1011
|
}
|
|
523
1012
|
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
524
|
-
if (!(await
|
|
1013
|
+
if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
|
|
525
1014
|
}
|
|
526
1015
|
}
|
|
527
1016
|
return true;
|
|
@@ -532,18 +1021,18 @@ function actionFor(exists, matches) {
|
|
|
532
1021
|
}
|
|
533
1022
|
async function writeMeta(destDir, version) {
|
|
534
1023
|
const meta = { name: "@easytwin/devkit", version };
|
|
535
|
-
await
|
|
1024
|
+
await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
536
1025
|
}
|
|
537
1026
|
async function syncToDir(target, sourceRoot, targetRoot, version) {
|
|
538
1027
|
const entries = [];
|
|
539
1028
|
for (const name of SKILL_NAMES) {
|
|
540
|
-
const src =
|
|
541
|
-
const dest =
|
|
542
|
-
const exists = await
|
|
1029
|
+
const src = path6.join(sourceRoot, name);
|
|
1030
|
+
const dest = path6.join(targetRoot, name);
|
|
1031
|
+
const exists = await fs5.stat(dest).then(() => true).catch(() => false);
|
|
543
1032
|
const matches = await dirMatches(src, dest);
|
|
544
1033
|
const action = actionFor(exists, matches);
|
|
545
1034
|
if (action !== "unchanged") {
|
|
546
|
-
await
|
|
1035
|
+
await fs5.rm(dest, { recursive: true, force: true });
|
|
547
1036
|
await copyDir(src, dest);
|
|
548
1037
|
}
|
|
549
1038
|
entries.push({ name, action });
|
|
@@ -562,19 +1051,20 @@ function buildCodexSegment(version) {
|
|
|
562
1051
|
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
|
|
563
1052
|
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
564
1053
|
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
565
|
-
"- `easytwin-upload`:\
|
|
1054
|
+
"- `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",
|
|
1055
|
+
"- `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",
|
|
566
1056
|
"",
|
|
567
1057
|
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
568
1058
|
CODEX_MARKER_END
|
|
569
1059
|
].join("\n");
|
|
570
1060
|
}
|
|
571
1061
|
async function syncToCodex(sourceRoot, cwd, version) {
|
|
572
|
-
const agentsFile =
|
|
1062
|
+
const agentsFile = path6.join(cwd, "AGENTS.md");
|
|
573
1063
|
const segment = buildCodexSegment(version);
|
|
574
1064
|
let content = "";
|
|
575
1065
|
let exists = true;
|
|
576
1066
|
try {
|
|
577
|
-
content = await
|
|
1067
|
+
content = await fs5.readFile(agentsFile, "utf8");
|
|
578
1068
|
} catch {
|
|
579
1069
|
exists = false;
|
|
580
1070
|
}
|
|
@@ -592,39 +1082,50 @@ async function syncToCodex(sourceRoot, cwd, version) {
|
|
|
592
1082
|
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
593
1083
|
}
|
|
594
1084
|
if (action !== "unchanged") {
|
|
595
|
-
await
|
|
596
|
-
await
|
|
1085
|
+
await fs5.mkdir(cwd, { recursive: true });
|
|
1086
|
+
await fs5.writeFile(agentsFile, next, "utf8");
|
|
597
1087
|
}
|
|
598
1088
|
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
599
1089
|
}
|
|
600
1090
|
async function syncRuntimeTypes(cwd, typesSourceFile) {
|
|
601
|
-
const destDir =
|
|
602
|
-
const destFile =
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
let
|
|
1091
|
+
const destDir = path6.join(cwd, ".easytwin", "types");
|
|
1092
|
+
const destFile = path6.join(destDir, "index.d.ts");
|
|
1093
|
+
const appsFile = path6.join(destDir, APPS_TYPES_FILE);
|
|
1094
|
+
const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
|
|
1095
|
+
let runtimeCurrent = "";
|
|
1096
|
+
let appsCurrent = "";
|
|
1097
|
+
let runtimeExists = true;
|
|
1098
|
+
let appsExists = true;
|
|
606
1099
|
try {
|
|
607
|
-
|
|
1100
|
+
runtimeCurrent = await fs5.readFile(destFile, "utf8");
|
|
608
1101
|
} catch {
|
|
609
|
-
|
|
1102
|
+
runtimeExists = false;
|
|
610
1103
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1104
|
+
try {
|
|
1105
|
+
appsCurrent = await fs5.readFile(appsFile, "utf8");
|
|
1106
|
+
} catch {
|
|
1107
|
+
appsExists = false;
|
|
1108
|
+
}
|
|
1109
|
+
const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
|
|
1110
|
+
const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
|
|
1111
|
+
const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
|
|
1112
|
+
if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
|
|
1113
|
+
await fs5.mkdir(destDir, { recursive: true });
|
|
1114
|
+
if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
|
|
1115
|
+
if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
|
|
615
1116
|
}
|
|
616
|
-
const tsconfigPath =
|
|
1117
|
+
const tsconfigPath = path6.join(cwd, "tsconfig.json");
|
|
617
1118
|
let tsconfig;
|
|
618
1119
|
let pathsHint;
|
|
619
1120
|
try {
|
|
620
|
-
const existing = await
|
|
1121
|
+
const existing = await fs5.readFile(tsconfigPath, "utf8");
|
|
621
1122
|
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
622
1123
|
else {
|
|
623
1124
|
tsconfig = "manual-paths";
|
|
624
1125
|
pathsHint = TSCONFIG_PATHS_HINT;
|
|
625
1126
|
}
|
|
626
1127
|
} catch {
|
|
627
|
-
await
|
|
1128
|
+
await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
628
1129
|
tsconfig = "created";
|
|
629
1130
|
}
|
|
630
1131
|
return { action, tsconfig, pathsHint };
|
|
@@ -635,9 +1136,9 @@ async function syncSkills(options) {
|
|
|
635
1136
|
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
636
1137
|
const summaries = [];
|
|
637
1138
|
for (const target of targets) {
|
|
638
|
-
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot,
|
|
639
|
-
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot,
|
|
640
|
-
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot,
|
|
1139
|
+
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
|
|
1140
|
+
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
|
|
1141
|
+
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
|
|
641
1142
|
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
642
1143
|
}
|
|
643
1144
|
const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
|
|
@@ -646,11 +1147,33 @@ async function syncSkills(options) {
|
|
|
646
1147
|
|
|
647
1148
|
// src/bundle.ts
|
|
648
1149
|
import * as esbuild from "esbuild-wasm";
|
|
649
|
-
import { promises as
|
|
650
|
-
import
|
|
1150
|
+
import { promises as fs6 } from "fs";
|
|
1151
|
+
import path7 from "path";
|
|
1152
|
+
|
|
1153
|
+
// src/apps.ts
|
|
1154
|
+
var PORTABLE_RUNTIME_EXPORTS = [
|
|
1155
|
+
"THREE",
|
|
1156
|
+
"RuntimeEngine",
|
|
1157
|
+
"SceneManager",
|
|
1158
|
+
"LoadSceneMode",
|
|
1159
|
+
"convertObjToComponentJson"
|
|
1160
|
+
];
|
|
1161
|
+
function portableRuntimeWarning(names) {
|
|
1162
|
+
const extra = [...new Set(names)].filter(
|
|
1163
|
+
(n) => n !== "*" && !PORTABLE_RUNTIME_EXPORTS.includes(n)
|
|
1164
|
+
);
|
|
1165
|
+
const ns = [...names].includes("*");
|
|
1166
|
+
if (!ns && extra.length === 0) return void 0;
|
|
1167
|
+
const detail = ns ? "namespace import *" : extra.sort().join(", ");
|
|
1168
|
+
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`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/bundle.ts
|
|
651
1172
|
var USER_ENTRY = "src/main.ts";
|
|
652
1173
|
var DEFAULT_BUNDLE_OUT = "dist/main.js";
|
|
653
1174
|
var RUNTIME_MODULE = "@easytwin/runtime";
|
|
1175
|
+
var APPS_MODULE = "@easytwin/apps";
|
|
1176
|
+
var ALLOWED_BARE_MODULES = /* @__PURE__ */ new Set([RUNTIME_MODULE, APPS_MODULE]);
|
|
654
1177
|
var BundleError = class extends Error {
|
|
655
1178
|
constructor(message) {
|
|
656
1179
|
super(message);
|
|
@@ -658,7 +1181,7 @@ var BundleError = class extends Error {
|
|
|
658
1181
|
}
|
|
659
1182
|
};
|
|
660
1183
|
function isRelativeOrAbsolute(spec) {
|
|
661
|
-
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") ||
|
|
1184
|
+
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path7.isAbsolute(spec);
|
|
662
1185
|
}
|
|
663
1186
|
function whitelistPlugin() {
|
|
664
1187
|
return {
|
|
@@ -667,11 +1190,11 @@ function whitelistPlugin() {
|
|
|
667
1190
|
build2.onResolve({ filter: /.*/ }, (args) => {
|
|
668
1191
|
if (args.kind === "entry-point") return void 0;
|
|
669
1192
|
if (isRelativeOrAbsolute(args.path)) return void 0;
|
|
670
|
-
if (args.path
|
|
1193
|
+
if (ALLOWED_BARE_MODULES.has(args.path)) return { path: args.path, external: true };
|
|
671
1194
|
return {
|
|
672
1195
|
errors: [
|
|
673
1196
|
{
|
|
674
|
-
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`
|
|
1197
|
+
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`
|
|
675
1198
|
}
|
|
676
1199
|
]
|
|
677
1200
|
};
|
|
@@ -685,14 +1208,29 @@ function formatEsbuildMessages(messages) {
|
|
|
685
1208
|
return `${loc}${m.text}`;
|
|
686
1209
|
}).join("\n");
|
|
687
1210
|
}
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
|
|
1211
|
+
function collectBundledRuntimeExports(code) {
|
|
1212
|
+
const names = [];
|
|
1213
|
+
if (/import\s+\*\s+as\s+[\w$]+\s+from\s*["']@easytwin\/runtime["']/.test(code)) names.push("*");
|
|
1214
|
+
const named = /import\s*\{([^}]+)\}\s*from\s*["']@easytwin\/runtime["']/g;
|
|
1215
|
+
for (const match of code.matchAll(named)) {
|
|
1216
|
+
const body = match[1] ?? "";
|
|
1217
|
+
for (const part of body.split(",")) {
|
|
1218
|
+
const token = part.replace(/\btype\b/g, "").trim();
|
|
1219
|
+
if (!token) continue;
|
|
1220
|
+
const id = token.split(/\s+as\s+/)[0]?.trim();
|
|
1221
|
+
if (id) names.push(id);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return names;
|
|
1225
|
+
}
|
|
1226
|
+
async function bundleWorkspaceModule(options) {
|
|
1227
|
+
const cwd = path7.resolve(options.cwd);
|
|
1228
|
+
const entry = path7.isAbsolute(options.entry) ? options.entry : path7.join(cwd, options.entry);
|
|
691
1229
|
try {
|
|
692
|
-
await
|
|
1230
|
+
await fs6.access(entry);
|
|
693
1231
|
} catch {
|
|
694
1232
|
throw new BundleError(
|
|
695
|
-
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002
|
|
1233
|
+
options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
|
|
696
1234
|
);
|
|
697
1235
|
}
|
|
698
1236
|
let result;
|
|
@@ -723,21 +1261,1167 @@ async function bundleUserCode(options) {
|
|
|
723
1261
|
if (!file) throw new BundleError("\u6253\u5305\u672A\u4EA7\u51FA\u6587\u4EF6");
|
|
724
1262
|
const code = file.text;
|
|
725
1263
|
const warnings = result.warnings.map((w) => formatEsbuildMessages([w]));
|
|
1264
|
+
const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
|
|
1265
|
+
if (portable) warnings.push(portable);
|
|
726
1266
|
if (options.outFile) {
|
|
727
|
-
const outFile =
|
|
728
|
-
await
|
|
729
|
-
await
|
|
1267
|
+
const outFile = path7.isAbsolute(options.outFile) ? options.outFile : path7.join(cwd, options.outFile);
|
|
1268
|
+
await fs6.mkdir(path7.dirname(outFile), { recursive: true });
|
|
1269
|
+
await fs6.writeFile(outFile, code, "utf8");
|
|
730
1270
|
}
|
|
731
1271
|
return { code, warnings };
|
|
732
1272
|
}
|
|
1273
|
+
async function bundleUserCode(options) {
|
|
1274
|
+
const cwd = path7.resolve(options.cwd);
|
|
1275
|
+
const entry = path7.join(cwd, USER_ENTRY);
|
|
1276
|
+
return bundleWorkspaceModule({
|
|
1277
|
+
cwd: options.cwd,
|
|
1278
|
+
entry: USER_ENTRY,
|
|
1279
|
+
outFile: options.outFile,
|
|
1280
|
+
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`
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
733
1283
|
async function typesMissingHint(cwd) {
|
|
734
1284
|
try {
|
|
735
|
-
await
|
|
1285
|
+
await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
736
1286
|
return void 0;
|
|
737
1287
|
} catch {
|
|
738
1288
|
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
739
1289
|
}
|
|
740
1290
|
}
|
|
1291
|
+
var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
|
|
1292
|
+
var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1293
|
+
function decodeVLQValues(str) {
|
|
1294
|
+
const values = [];
|
|
1295
|
+
let i = 0;
|
|
1296
|
+
while (i < str.length) {
|
|
1297
|
+
let result = 0;
|
|
1298
|
+
let shift = 0;
|
|
1299
|
+
let continuation = true;
|
|
1300
|
+
while (continuation) {
|
|
1301
|
+
if (i >= str.length) return values;
|
|
1302
|
+
const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
|
|
1303
|
+
if (digit < 0) return values;
|
|
1304
|
+
continuation = (digit & 32) !== 0;
|
|
1305
|
+
result += (digit & 31) << shift;
|
|
1306
|
+
shift += 5;
|
|
1307
|
+
}
|
|
1308
|
+
values.push(result & 1 ? -(result >> 1) : result >> 1);
|
|
1309
|
+
}
|
|
1310
|
+
return values;
|
|
1311
|
+
}
|
|
1312
|
+
function decodeMappings(map) {
|
|
1313
|
+
const sources = map.sources ?? [];
|
|
1314
|
+
const lines = (map.mappings ?? "").split(";");
|
|
1315
|
+
let sourceIndex = 0;
|
|
1316
|
+
let originalLine = 0;
|
|
1317
|
+
let originalColumn = 0;
|
|
1318
|
+
const decoded = [];
|
|
1319
|
+
for (const line of lines) {
|
|
1320
|
+
let generatedColumn = 0;
|
|
1321
|
+
const segs = [];
|
|
1322
|
+
if (line) {
|
|
1323
|
+
for (const raw of line.split(",")) {
|
|
1324
|
+
if (!raw) continue;
|
|
1325
|
+
const nums = decodeVLQValues(raw);
|
|
1326
|
+
if (nums[0] === void 0) continue;
|
|
1327
|
+
generatedColumn += nums[0];
|
|
1328
|
+
if (nums.length >= 4) {
|
|
1329
|
+
sourceIndex += nums[1] ?? 0;
|
|
1330
|
+
originalLine += nums[2] ?? 0;
|
|
1331
|
+
originalColumn += nums[3] ?? 0;
|
|
1332
|
+
segs.push({
|
|
1333
|
+
generatedColumn,
|
|
1334
|
+
source: sources[sourceIndex] ?? USER_ENTRY,
|
|
1335
|
+
originalLine,
|
|
1336
|
+
originalColumn
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
decoded.push(segs);
|
|
1342
|
+
}
|
|
1343
|
+
return decoded;
|
|
1344
|
+
}
|
|
1345
|
+
function originalPositionFor(map, line, column) {
|
|
1346
|
+
const decoded = decodeMappings(map);
|
|
1347
|
+
for (let i = line - 1; i >= 0; i--) {
|
|
1348
|
+
const segs = decoded[i];
|
|
1349
|
+
if (!segs || segs.length === 0) continue;
|
|
1350
|
+
const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
|
|
1351
|
+
let best = segs[0];
|
|
1352
|
+
for (const seg of segs) {
|
|
1353
|
+
if (seg.generatedColumn <= col) best = seg;
|
|
1354
|
+
else break;
|
|
1355
|
+
}
|
|
1356
|
+
if (!best) continue;
|
|
1357
|
+
return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
|
|
1358
|
+
}
|
|
1359
|
+
return void 0;
|
|
1360
|
+
}
|
|
1361
|
+
function extractInlineSourceMap(code) {
|
|
1362
|
+
const m = code.match(SOURCEMAP_RE);
|
|
1363
|
+
if (!m?.[1]) return void 0;
|
|
1364
|
+
try {
|
|
1365
|
+
return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
|
|
1366
|
+
} catch {
|
|
1367
|
+
return void 0;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function remapErrorStack(stack, bundledCode) {
|
|
1371
|
+
const map = extractInlineSourceMap(bundledCode);
|
|
1372
|
+
if (!map?.mappings) return stack;
|
|
1373
|
+
return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
|
|
1374
|
+
const orig = originalPositionFor(map, Number(line), Number(col));
|
|
1375
|
+
if (!orig) return full;
|
|
1376
|
+
return `${orig.source}:${orig.line}:${orig.column}`;
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/previewServer.ts
|
|
1381
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1382
|
+
import { promises as fs8 } from "fs";
|
|
1383
|
+
import http2 from "http";
|
|
1384
|
+
import path9 from "path";
|
|
1385
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1386
|
+
|
|
1387
|
+
// src/previewHtml.ts
|
|
1388
|
+
function escapeJsonForScript(json) {
|
|
1389
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1390
|
+
}
|
|
1391
|
+
var PREVIEW_STYLE = `
|
|
1392
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
|
|
1393
|
+
body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
|
|
1394
|
+
#stage { position: relative; flex: 1; min-height: 0; }
|
|
1395
|
+
#twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
|
|
1396
|
+
#twin-root canvas { display: block; }
|
|
1397
|
+
#status {
|
|
1398
|
+
position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
|
|
1399
|
+
padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
|
|
1400
|
+
}
|
|
1401
|
+
#status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
|
|
1402
|
+
/* \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 */
|
|
1403
|
+
#status[hidden] { display: none; }
|
|
1404
|
+
#mock-banner {
|
|
1405
|
+
position: absolute; top: 0; left: 0; right: 0; z-index: 2;
|
|
1406
|
+
padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
|
|
1407
|
+
border-bottom: 1px solid #f0c36d; pointer-events: none;
|
|
1408
|
+
}
|
|
1409
|
+
#debug-log {
|
|
1410
|
+
display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
|
|
1411
|
+
max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
|
|
1412
|
+
background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
|
|
1413
|
+
white-space: pre-wrap; word-break: break-all; pointer-events: auto;
|
|
1414
|
+
}
|
|
1415
|
+
#debug-log.open { display: block; }
|
|
1416
|
+
#run-bar {
|
|
1417
|
+
flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
|
|
1418
|
+
padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
|
|
1419
|
+
}
|
|
1420
|
+
#run-bar button {
|
|
1421
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
1422
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
1423
|
+
}
|
|
1424
|
+
#run-bar button:disabled { opacity: .55; cursor: default; }
|
|
1425
|
+
#btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
|
|
1426
|
+
#run-status { color: #bbb; min-width: 4em; }
|
|
1427
|
+
#run-bar .spacer { flex: 1; }
|
|
1428
|
+
#test-panel {
|
|
1429
|
+
flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
|
|
1430
|
+
padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
|
|
1431
|
+
max-height: 30%; overflow: auto;
|
|
1432
|
+
}
|
|
1433
|
+
#test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
|
|
1434
|
+
#test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
|
|
1435
|
+
#test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
|
|
1436
|
+
#test-panel .test-empty { color: #777; }
|
|
1437
|
+
#test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
1438
|
+
#test-panel input.test-input {
|
|
1439
|
+
width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
|
|
1440
|
+
padding: 3px 6px; border-radius: 4px; font: 12px inherit;
|
|
1441
|
+
}
|
|
1442
|
+
#test-panel button {
|
|
1443
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
1444
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
1445
|
+
}
|
|
1446
|
+
#test-panel button:disabled { opacity: .55; cursor: default; }
|
|
1447
|
+
`;
|
|
1448
|
+
var RENDER_SCRIPT = `
|
|
1449
|
+
var statusEl = document.getElementById("status");
|
|
1450
|
+
var logEl = document.getElementById("debug-log");
|
|
1451
|
+
var engine = null;
|
|
1452
|
+
var runtime = null;
|
|
1453
|
+
var sceneJson = null;
|
|
1454
|
+
var host = null;
|
|
1455
|
+
var running = false;
|
|
1456
|
+
var runningTest = false;
|
|
1457
|
+
var testsReady = false;
|
|
1458
|
+
var hostKind = __HOST_KIND__;
|
|
1459
|
+
var vscodeApi = null;
|
|
1460
|
+
if (hostKind === "vscode") {
|
|
1461
|
+
try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
|
|
1462
|
+
}
|
|
1463
|
+
var pending = {};
|
|
1464
|
+
var seq = 0;
|
|
1465
|
+
var origFetch = window.fetch.bind(window);
|
|
1466
|
+
function now() { return new Date().toISOString().slice(11, 23); }
|
|
1467
|
+
function log(line) {
|
|
1468
|
+
var text = "[" + now() + "] " + line;
|
|
1469
|
+
if (logEl) {
|
|
1470
|
+
logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
|
|
1471
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1472
|
+
}
|
|
1473
|
+
if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
|
|
1474
|
+
console.log("[EasyTwin]", line);
|
|
1475
|
+
}
|
|
1476
|
+
function nextId() { return String(++seq); }
|
|
1477
|
+
function handleHostMessage(msg) {
|
|
1478
|
+
if (!msg) return;
|
|
1479
|
+
if (msg.type === "proxy-fetch-result") {
|
|
1480
|
+
var p = pending[msg.id];
|
|
1481
|
+
if (!p) return;
|
|
1482
|
+
delete pending[msg.id];
|
|
1483
|
+
if (msg.error) p.reject(new Error(msg.error));
|
|
1484
|
+
else p.resolve(msg);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (msg.type === "bundle-result") {
|
|
1488
|
+
if (!msg.ok) {
|
|
1489
|
+
log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
1490
|
+
setRunStatus("\u7F16\u8BD1\u5931\u8D25");
|
|
1491
|
+
setRunBusy(false);
|
|
1492
|
+
if (logEl) logEl.classList.add("open");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
|
|
1496
|
+
runUserCode(msg.code);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (msg.type === "read-asset-result") {
|
|
1500
|
+
var ap = pending[msg.id];
|
|
1501
|
+
if (!ap) return;
|
|
1502
|
+
delete pending[msg.id];
|
|
1503
|
+
if (msg.error) ap.reject(new Error(msg.error));
|
|
1504
|
+
else ap.resolve(msg.text);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
if (msg.type === "run-error-mapped") {
|
|
1508
|
+
log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (msg.type === "tests") {
|
|
1512
|
+
renderTests(msg.tests || []);
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if (msg.type === "test-bundle") {
|
|
1516
|
+
if (!msg.ok) {
|
|
1517
|
+
log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
1518
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
1519
|
+
runningTest = false;
|
|
1520
|
+
setRunBusy(running);
|
|
1521
|
+
if (logEl) logEl.classList.add("open");
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
|
|
1525
|
+
runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
|
|
1526
|
+
log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
|
|
1527
|
+
setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
|
|
1528
|
+
}).catch(function (err) {
|
|
1529
|
+
log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
1530
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
1531
|
+
if (logEl) logEl.classList.add("open");
|
|
1532
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
1533
|
+
}).then(function () {
|
|
1534
|
+
runningTest = false;
|
|
1535
|
+
setRunBusy(running);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
function httpJson(url, init) {
|
|
1540
|
+
return origFetch(url, init).then(function (res) {
|
|
1541
|
+
return res.json().then(function (body) {
|
|
1542
|
+
if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
|
|
1543
|
+
return body;
|
|
1544
|
+
});
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function postToHost(msg) {
|
|
1548
|
+
if (vscodeApi) {
|
|
1549
|
+
vscodeApi.postMessage(msg);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
|
|
1553
|
+
if (msg.type === "run") {
|
|
1554
|
+
httpJson("/api/bundle", { method: "POST" }).then(function (body) {
|
|
1555
|
+
handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
|
|
1556
|
+
}).catch(function (err) {
|
|
1557
|
+
handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
|
|
1558
|
+
});
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (msg.type === "run-error") {
|
|
1562
|
+
httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
|
|
1563
|
+
handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
|
|
1564
|
+
}).catch(function (err) {
|
|
1565
|
+
log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (msg.type === "read-asset") {
|
|
1570
|
+
httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
|
|
1571
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
|
|
1572
|
+
}).catch(function (err) {
|
|
1573
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
|
|
1574
|
+
});
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (msg.type === "list-tests") {
|
|
1578
|
+
httpJson("/api/tests").then(function (body) {
|
|
1579
|
+
handleHostMessage({ type: "tests", tests: body.tests || [] });
|
|
1580
|
+
}).catch(function (err) {
|
|
1581
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (msg.type === "run-test") {
|
|
1586
|
+
httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
|
|
1587
|
+
handleHostMessage(Object.assign({ type: "test-bundle" }, body));
|
|
1588
|
+
}).catch(function (err) {
|
|
1589
|
+
handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function showStatus(text, isError) {
|
|
1594
|
+
if (!statusEl) return;
|
|
1595
|
+
statusEl.textContent = text;
|
|
1596
|
+
statusEl.className = isError ? "error" : "";
|
|
1597
|
+
statusEl.hidden = false;
|
|
1598
|
+
if (isError && logEl) logEl.classList.add("open");
|
|
1599
|
+
}
|
|
1600
|
+
function hideStatus() { if (statusEl) statusEl.hidden = true; }
|
|
1601
|
+
function formatError(err) {
|
|
1602
|
+
var msg = err && err.message ? err.message : String(err);
|
|
1603
|
+
var stack = err && err.stack ? "\\n" + err.stack : "";
|
|
1604
|
+
return msg + stack;
|
|
1605
|
+
}
|
|
1606
|
+
window.addEventListener("message", function (ev) {
|
|
1607
|
+
handleHostMessage(ev.data);
|
|
1608
|
+
});
|
|
1609
|
+
function b64ToBuf(b64) {
|
|
1610
|
+
var bin = atob(b64);
|
|
1611
|
+
var bytes = new Uint8Array(bin.length);
|
|
1612
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1613
|
+
return bytes.buffer;
|
|
1614
|
+
}
|
|
1615
|
+
function proxyFetch(url, method) {
|
|
1616
|
+
return new Promise(function (resolve, reject) {
|
|
1617
|
+
if (!vscodeApi) {
|
|
1618
|
+
reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
var id = nextId();
|
|
1622
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
1623
|
+
vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
|
|
1624
|
+
}).then(function (msg) {
|
|
1625
|
+
var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
|
|
1626
|
+
return new Response(buf, {
|
|
1627
|
+
status: msg.status || 0,
|
|
1628
|
+
statusText: msg.statusText || "",
|
|
1629
|
+
headers: msg.headers || {}
|
|
1630
|
+
});
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
function isHttpUrl(url) {
|
|
1634
|
+
return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
|
|
1635
|
+
}
|
|
1636
|
+
function isPlainHttp(url) {
|
|
1637
|
+
return typeof url === "string" && url.indexOf("http://") === 0;
|
|
1638
|
+
}
|
|
1639
|
+
if (hostKind === "vscode") {
|
|
1640
|
+
window.fetch = function (input, init) {
|
|
1641
|
+
var url = typeof input === "string" ? input : (input && input.url);
|
|
1642
|
+
log("fetch " + url);
|
|
1643
|
+
if (isPlainHttp(url)) {
|
|
1644
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
1645
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
1646
|
+
return res;
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
return origFetch(input, init).catch(function (err) {
|
|
1650
|
+
log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
|
|
1651
|
+
if (!isHttpUrl(url)) throw err;
|
|
1652
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
1653
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
1654
|
+
return res;
|
|
1655
|
+
});
|
|
1656
|
+
});
|
|
1657
|
+
};
|
|
1658
|
+
var origOpen = XMLHttpRequest.prototype.open;
|
|
1659
|
+
var origSend = XMLHttpRequest.prototype.send;
|
|
1660
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
1661
|
+
this.__etMethod = method;
|
|
1662
|
+
this.__etUrl = String(url);
|
|
1663
|
+
return origOpen.apply(this, arguments);
|
|
1664
|
+
};
|
|
1665
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
1666
|
+
var xhr = this;
|
|
1667
|
+
var url = xhr.__etUrl;
|
|
1668
|
+
if (!isPlainHttp(url)) return origSend.call(this, body);
|
|
1669
|
+
log("xhr proxy " + xhr.__etMethod + " " + url);
|
|
1670
|
+
proxyFetch(url, xhr.__etMethod).then(function (res) {
|
|
1671
|
+
return res.arrayBuffer().then(function (buf) {
|
|
1672
|
+
var text = "";
|
|
1673
|
+
try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
|
|
1674
|
+
Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
|
|
1675
|
+
Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
|
|
1676
|
+
Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
|
|
1677
|
+
Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
|
|
1678
|
+
var rt = xhr.responseType;
|
|
1679
|
+
var response = buf;
|
|
1680
|
+
if (rt === "" || rt === "text") response = text;
|
|
1681
|
+
else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
|
|
1682
|
+
Object.defineProperty(xhr, "response", { configurable: true, value: response });
|
|
1683
|
+
Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
|
|
1684
|
+
if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
|
|
1685
|
+
xhr.dispatchEvent(new Event("load"));
|
|
1686
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
1687
|
+
});
|
|
1688
|
+
}).catch(function (err) {
|
|
1689
|
+
log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
1690
|
+
if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
|
|
1691
|
+
xhr.dispatchEvent(new Event("error"));
|
|
1692
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
1693
|
+
});
|
|
1694
|
+
};
|
|
1695
|
+
function patchHttpSrc(proto, prop) {
|
|
1696
|
+
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
1697
|
+
if (!desc || typeof desc.set !== "function") return;
|
|
1698
|
+
Object.defineProperty(proto, prop, {
|
|
1699
|
+
configurable: true,
|
|
1700
|
+
enumerable: desc.enumerable,
|
|
1701
|
+
get: function () { return desc.get.call(this); },
|
|
1702
|
+
set: function (value) {
|
|
1703
|
+
var el = this;
|
|
1704
|
+
var url = String(value);
|
|
1705
|
+
if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
|
|
1706
|
+
log("media proxy " + url);
|
|
1707
|
+
proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
|
|
1708
|
+
desc.set.call(el, URL.createObjectURL(blob));
|
|
1709
|
+
}).catch(function (err) {
|
|
1710
|
+
log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
1711
|
+
try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
patchHttpSrc(HTMLImageElement.prototype, "src");
|
|
1717
|
+
patchHttpSrc(HTMLMediaElement.prototype, "src");
|
|
1718
|
+
}
|
|
1719
|
+
document.getElementById("btn-debug").addEventListener("click", function () {
|
|
1720
|
+
if (logEl) logEl.classList.toggle("open");
|
|
1721
|
+
});
|
|
1722
|
+
var btnDevtools = document.getElementById("btn-devtools");
|
|
1723
|
+
var btnOutput = document.getElementById("btn-output");
|
|
1724
|
+
if (hostKind === "http") {
|
|
1725
|
+
if (btnDevtools) btnDevtools.hidden = true;
|
|
1726
|
+
if (btnOutput) btnOutput.hidden = true;
|
|
1727
|
+
}
|
|
1728
|
+
if (btnDevtools) btnDevtools.addEventListener("click", function () {
|
|
1729
|
+
postToHost({ type: "open-devtools" });
|
|
1730
|
+
});
|
|
1731
|
+
if (btnOutput) btnOutput.addEventListener("click", function () {
|
|
1732
|
+
postToHost({ type: "show-output" });
|
|
1733
|
+
});
|
|
1734
|
+
function setRunStatus(text) {
|
|
1735
|
+
var el = document.getElementById("run-status");
|
|
1736
|
+
if (el) el.textContent = text;
|
|
1737
|
+
}
|
|
1738
|
+
function setRunBusy(busy) {
|
|
1739
|
+
running = busy;
|
|
1740
|
+
var btn = document.getElementById("btn-run");
|
|
1741
|
+
if (btn) btn.disabled = !!busy || runningTest;
|
|
1742
|
+
syncTestControls();
|
|
1743
|
+
}
|
|
1744
|
+
function syncTestControls() {
|
|
1745
|
+
var panel = document.getElementById("test-panel");
|
|
1746
|
+
if (!panel) return;
|
|
1747
|
+
var disabled = !testsReady || running || runningTest;
|
|
1748
|
+
var nodes = panel.querySelectorAll("button.test-run, input.test-input");
|
|
1749
|
+
for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
|
|
1750
|
+
}
|
|
1751
|
+
function renderTests(tests) {
|
|
1752
|
+
var list = document.getElementById("test-list");
|
|
1753
|
+
if (!list) return;
|
|
1754
|
+
list.textContent = "";
|
|
1755
|
+
if (!tests || !tests.length) {
|
|
1756
|
+
var empty = document.createElement("span");
|
|
1757
|
+
empty.className = "test-empty";
|
|
1758
|
+
empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
|
|
1759
|
+
list.appendChild(empty);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
var groups = {};
|
|
1763
|
+
var order = [];
|
|
1764
|
+
for (var i = 0; i < tests.length; i++) {
|
|
1765
|
+
var t = tests[i];
|
|
1766
|
+
if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
|
|
1767
|
+
groups[t.file].push(t);
|
|
1768
|
+
}
|
|
1769
|
+
for (var g = 0; g < order.length; g++) {
|
|
1770
|
+
var file = order[g];
|
|
1771
|
+
var heading = document.createElement("span");
|
|
1772
|
+
heading.className = "test-file";
|
|
1773
|
+
heading.textContent = file;
|
|
1774
|
+
list.appendChild(heading);
|
|
1775
|
+
var items = groups[file];
|
|
1776
|
+
for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
|
|
1777
|
+
}
|
|
1778
|
+
syncTestControls();
|
|
1779
|
+
}
|
|
1780
|
+
function makeTestControl(t) {
|
|
1781
|
+
var wrap = document.createElement("span");
|
|
1782
|
+
wrap.className = "test-item";
|
|
1783
|
+
if (t.hasInput) {
|
|
1784
|
+
var input = document.createElement("input");
|
|
1785
|
+
input.type = "text";
|
|
1786
|
+
input.className = "test-input";
|
|
1787
|
+
input.placeholder = t.inputName || "input";
|
|
1788
|
+
var btn = document.createElement("button");
|
|
1789
|
+
btn.type = "button";
|
|
1790
|
+
btn.className = "test-run";
|
|
1791
|
+
btn.textContent = t.name;
|
|
1792
|
+
btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
|
|
1793
|
+
input.addEventListener("keydown", function (ev) {
|
|
1794
|
+
if (ev.key === "Enter") requestRunTest(t.id, input.value);
|
|
1795
|
+
});
|
|
1796
|
+
wrap.appendChild(input);
|
|
1797
|
+
wrap.appendChild(btn);
|
|
1798
|
+
} else {
|
|
1799
|
+
var only = document.createElement("button");
|
|
1800
|
+
only.type = "button";
|
|
1801
|
+
only.className = "test-run";
|
|
1802
|
+
only.textContent = t.name;
|
|
1803
|
+
only.addEventListener("click", function () { requestRunTest(t.id); });
|
|
1804
|
+
wrap.appendChild(only);
|
|
1805
|
+
}
|
|
1806
|
+
return wrap;
|
|
1807
|
+
}
|
|
1808
|
+
function requestRunTest(id, input) {
|
|
1809
|
+
if (!testsReady || running || runningTest) return;
|
|
1810
|
+
if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
|
|
1811
|
+
runningTest = true;
|
|
1812
|
+
setRunBusy(running);
|
|
1813
|
+
setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
|
|
1814
|
+
log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
|
|
1815
|
+
postToHost({ type: "run-test", id: id, input: input });
|
|
1816
|
+
}
|
|
1817
|
+
async function runExportedTest(code, exportName, input, hasInput) {
|
|
1818
|
+
var blob = new Blob([code], { type: "text/javascript" });
|
|
1819
|
+
var url = URL.createObjectURL(blob);
|
|
1820
|
+
try {
|
|
1821
|
+
var mod = await import(url);
|
|
1822
|
+
var fn = mod[exportName];
|
|
1823
|
+
if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
|
|
1824
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
|
|
1825
|
+
var ctx = host.getTestContext();
|
|
1826
|
+
var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
|
|
1827
|
+
await Promise.resolve(result);
|
|
1828
|
+
} finally {
|
|
1829
|
+
URL.revokeObjectURL(url);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function applyRenderPatch() {
|
|
1833
|
+
// 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,
|
|
1834
|
+
// \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
|
|
1835
|
+
var scene = engine && engine.mainScene;
|
|
1836
|
+
if (
|
|
1837
|
+
scene && scene.rootComponent && scene.rootComponent.version &&
|
|
1838
|
+
!scene.postprocessingComponent &&
|
|
1839
|
+
runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
|
|
1840
|
+
) {
|
|
1841
|
+
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");
|
|
1842
|
+
scene.registerRenderCallback(function () {
|
|
1843
|
+
scene.renderer.render(scene.sceneObject, scene.camera.main);
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
async function loadHostScene(nextEngine, nextJson) {
|
|
1848
|
+
engine = nextEngine;
|
|
1849
|
+
var sceneManager = engine.getManager(runtime.SceneManager);
|
|
1850
|
+
await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
|
|
1851
|
+
applyRenderPatch();
|
|
1852
|
+
}
|
|
1853
|
+
function readAsset(relPath) {
|
|
1854
|
+
return new Promise(function (resolve, reject) {
|
|
1855
|
+
var id = nextId();
|
|
1856
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
1857
|
+
postToHost({ type: "read-asset", id: id, path: relPath });
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
async function runUserCode(code) {
|
|
1861
|
+
setRunStatus("\u8FD0\u884C\u4E2D\u2026");
|
|
1862
|
+
try {
|
|
1863
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
1864
|
+
await host.run(code);
|
|
1865
|
+
log("Run \u5B8C\u6210");
|
|
1866
|
+
setRunStatus("\u8FD0\u884C\u4E2D");
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
log("Run \u5931\u8D25 " + formatError(err));
|
|
1869
|
+
setRunStatus("\u5931\u8D25");
|
|
1870
|
+
if (logEl) logEl.classList.add("open");
|
|
1871
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
1872
|
+
} finally {
|
|
1873
|
+
setRunBusy(false);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
document.getElementById("btn-run").addEventListener("click", function () {
|
|
1877
|
+
if (running || runningTest) return;
|
|
1878
|
+
if (!engine) {
|
|
1879
|
+
log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
setRunBusy(true);
|
|
1883
|
+
setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
|
|
1884
|
+
log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
|
|
1885
|
+
postToHost({ type: "run" });
|
|
1886
|
+
});
|
|
1887
|
+
document.getElementById("btn-refresh-tests").addEventListener("click", function () {
|
|
1888
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
|
|
1889
|
+
postToHost({ type: "list-tests" });
|
|
1890
|
+
});
|
|
1891
|
+
function readEmbeddedTests() {
|
|
1892
|
+
var el = document.getElementById("workspace-tests");
|
|
1893
|
+
if (!el || !el.textContent) return [];
|
|
1894
|
+
try { return JSON.parse(el.textContent); } catch (e) { return []; }
|
|
1895
|
+
}
|
|
1896
|
+
renderTests(readEmbeddedTests());
|
|
1897
|
+
function normalizeHierarchyConfig(vo) {
|
|
1898
|
+
var objs = vo && vo.objs ? vo.objs : [];
|
|
1899
|
+
for (var i = 0; i < objs.length; i++) {
|
|
1900
|
+
var hc = objs[i].hierarchyConfig || {};
|
|
1901
|
+
if (typeof hc.active !== "boolean") {
|
|
1902
|
+
hc.active = hc.inActive === true ? false : hc.visible !== false;
|
|
1903
|
+
}
|
|
1904
|
+
if (typeof hc.lock !== "boolean") hc.lock = false;
|
|
1905
|
+
if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
|
|
1906
|
+
objs[i].hierarchyConfig = hc;
|
|
1907
|
+
}
|
|
1908
|
+
return vo;
|
|
1909
|
+
}
|
|
1910
|
+
function toSceneJson(runtime, raw) {
|
|
1911
|
+
var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
|
|
1912
|
+
if (vo && vo.sceneComponent) {
|
|
1913
|
+
return {
|
|
1914
|
+
id: vo.id || "preview",
|
|
1915
|
+
name: vo.name || "\u573A\u666F\u9884\u89C8",
|
|
1916
|
+
sceneComponent: vo.sceneComponent
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
vo = normalizeHierarchyConfig(vo);
|
|
1920
|
+
var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
|
|
1921
|
+
var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
|
|
1922
|
+
return {
|
|
1923
|
+
id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
|
|
1924
|
+
name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
|
|
1925
|
+
sceneComponent: runtime.convertObjToComponentJson(vo)
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
try {
|
|
1929
|
+
showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
|
|
1930
|
+
log("runtimeUri=" + __RUNTIME_URI__);
|
|
1931
|
+
log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
|
|
1932
|
+
log("1/4 import twin-runtime / TwinApp host");
|
|
1933
|
+
runtime = await import("@easytwin/runtime");
|
|
1934
|
+
var hostMod = await import(__HOST_URI__);
|
|
1935
|
+
log("2/4 parse scene JSON");
|
|
1936
|
+
var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
|
|
1937
|
+
sceneJson = toSceneJson(runtime, sceneVo);
|
|
1938
|
+
log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
|
|
1939
|
+
log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
|
|
1940
|
+
host = new hostMod.TwinAppPreviewHost({
|
|
1941
|
+
runtime: runtime,
|
|
1942
|
+
containerId: "twin-root",
|
|
1943
|
+
ossUrl: __BASE_OSS_URL__,
|
|
1944
|
+
appId: __APP_ID__,
|
|
1945
|
+
sceneId: sceneJson.id,
|
|
1946
|
+
sceneJson: sceneJson,
|
|
1947
|
+
customComponentDeps: {
|
|
1948
|
+
"@easytwin/runtime": runtime,
|
|
1949
|
+
"@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
|
|
1950
|
+
react: { createElement: function () { return null; }, Fragment: "div" }
|
|
1951
|
+
},
|
|
1952
|
+
loadScene: loadHostScene,
|
|
1953
|
+
readAsset: readAsset,
|
|
1954
|
+
log: log
|
|
1955
|
+
});
|
|
1956
|
+
await host.boot();
|
|
1957
|
+
engine = host.getEngine();
|
|
1958
|
+
log("4/4 loadScene");
|
|
1959
|
+
log("\u5B8C\u6210");
|
|
1960
|
+
hideStatus();
|
|
1961
|
+
testsReady = true;
|
|
1962
|
+
setRunBusy(false);
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
log("\u5931\u8D25 " + formatError(err));
|
|
1965
|
+
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);
|
|
1966
|
+
}
|
|
1967
|
+
window.addEventListener("pagehide", function () {
|
|
1968
|
+
if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
|
|
1969
|
+
});
|
|
1970
|
+
`;
|
|
1971
|
+
function originOf(url) {
|
|
1972
|
+
return new URL(url).origin;
|
|
1973
|
+
}
|
|
1974
|
+
function buildPreviewHtml(options) {
|
|
1975
|
+
const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
|
|
1976
|
+
const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
|
|
1977
|
+
const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
|
|
1978
|
+
const appId = options.appId ?? "preview";
|
|
1979
|
+
const hostKind = options.host ?? "vscode";
|
|
1980
|
+
const apiOrigin = originOf(baseUrl);
|
|
1981
|
+
const ossOrigin = originOf(ossUrl);
|
|
1982
|
+
const runtimeOrigin = originOf(runtimeUri);
|
|
1983
|
+
const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
|
|
1984
|
+
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));
|
|
1985
|
+
const importMap = JSON.stringify({
|
|
1986
|
+
imports: {
|
|
1987
|
+
"@easytwin/runtime": runtimeUri,
|
|
1988
|
+
"@easytwin/apps": appsUri
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
const testsJson = JSON.stringify(options.tests ?? []);
|
|
1992
|
+
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>` : "";
|
|
1993
|
+
return `<!DOCTYPE html>
|
|
1994
|
+
<html lang="zh-CN">
|
|
1995
|
+
<head>
|
|
1996
|
+
<meta charset="UTF-8">
|
|
1997
|
+
<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:;">
|
|
1998
|
+
<title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
|
|
1999
|
+
<style>${PREVIEW_STYLE}</style>
|
|
2000
|
+
<script type="importmap">${importMap}</script>
|
|
2001
|
+
</head>
|
|
2002
|
+
<body>
|
|
2003
|
+
<div id="stage">
|
|
2004
|
+
<div id="twin-root"></div>
|
|
2005
|
+
${mockBanner}
|
|
2006
|
+
<div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
|
|
2007
|
+
<pre id="debug-log"></pre>
|
|
2008
|
+
</div>
|
|
2009
|
+
<div id="test-panel">
|
|
2010
|
+
<span class="test-label">\u6D4B\u8BD5</span>
|
|
2011
|
+
<div id="test-list"></div>
|
|
2012
|
+
<button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
|
|
2013
|
+
</div>
|
|
2014
|
+
<div id="run-bar">
|
|
2015
|
+
<button type="button" id="btn-run">Run</button>
|
|
2016
|
+
<span id="run-status">\u5C31\u7EEA</span>
|
|
2017
|
+
<span class="spacer"></span>
|
|
2018
|
+
<button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
|
|
2019
|
+
<button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
|
|
2020
|
+
<button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
|
|
2021
|
+
</div>
|
|
2022
|
+
<script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
|
|
2023
|
+
<script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
|
|
2024
|
+
<script type="module">
|
|
2025
|
+
${renderScript}
|
|
2026
|
+
</script>
|
|
2027
|
+
</body>
|
|
2028
|
+
</html>`;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/workspaceTests.ts
|
|
2032
|
+
import { promises as fs7 } from "fs";
|
|
2033
|
+
import path8 from "path";
|
|
2034
|
+
var IDENT = "[A-Za-z_$][\\w$]*";
|
|
2035
|
+
var FUNCTION_EXPORT = new RegExp(
|
|
2036
|
+
`export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
|
|
2037
|
+
"g"
|
|
2038
|
+
);
|
|
2039
|
+
var CONST_EXPORT = new RegExp(
|
|
2040
|
+
`export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
|
|
2041
|
+
"g"
|
|
2042
|
+
);
|
|
2043
|
+
function stripTsCommentsAndStringsKeepCode(source) {
|
|
2044
|
+
let out = "";
|
|
2045
|
+
let i = 0;
|
|
2046
|
+
const n = source.length;
|
|
2047
|
+
while (i < n) {
|
|
2048
|
+
const c = source[i];
|
|
2049
|
+
const next = source[i + 1];
|
|
2050
|
+
if (c === "/" && next === "/") {
|
|
2051
|
+
i += 2;
|
|
2052
|
+
while (i < n && source[i] !== "\n") i++;
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
if (c === "/" && next === "*") {
|
|
2056
|
+
i += 2;
|
|
2057
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
2058
|
+
i = Math.min(n, i + 2);
|
|
2059
|
+
out += " ";
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
2063
|
+
const quote = c;
|
|
2064
|
+
out += " ";
|
|
2065
|
+
i++;
|
|
2066
|
+
while (i < n) {
|
|
2067
|
+
const ch = source[i];
|
|
2068
|
+
if (ch === "\\") {
|
|
2069
|
+
i += 2;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
if (ch === quote) {
|
|
2073
|
+
i++;
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
i++;
|
|
2077
|
+
}
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
out += c;
|
|
2081
|
+
i++;
|
|
2082
|
+
}
|
|
2083
|
+
return out;
|
|
2084
|
+
}
|
|
2085
|
+
function matchingClose(src, openIndex, open, close) {
|
|
2086
|
+
let depth = 0;
|
|
2087
|
+
let angle = 0;
|
|
2088
|
+
let brace = 0;
|
|
2089
|
+
for (let i = openIndex; i < src.length; i++) {
|
|
2090
|
+
const c = src[i];
|
|
2091
|
+
if (c === open) depth++;
|
|
2092
|
+
else if (c === close) {
|
|
2093
|
+
depth--;
|
|
2094
|
+
if (depth === 0 && angle <= 0 && brace <= 0) return i;
|
|
2095
|
+
} else if (c === "<") angle++;
|
|
2096
|
+
else if (c === ">" && angle > 0) angle--;
|
|
2097
|
+
else if (c === "{") brace++;
|
|
2098
|
+
else if (c === "}" && brace > 0) brace--;
|
|
2099
|
+
}
|
|
2100
|
+
return -1;
|
|
2101
|
+
}
|
|
2102
|
+
function splitTopLevelParams(list) {
|
|
2103
|
+
const params = [];
|
|
2104
|
+
let current = "";
|
|
2105
|
+
let paren = 0;
|
|
2106
|
+
let angle = 0;
|
|
2107
|
+
let brace = 0;
|
|
2108
|
+
let bracket = 0;
|
|
2109
|
+
for (const c of list) {
|
|
2110
|
+
if (c === "(") paren++;
|
|
2111
|
+
else if (c === ")") paren--;
|
|
2112
|
+
else if (c === "<") angle++;
|
|
2113
|
+
else if (c === ">" && angle > 0) angle--;
|
|
2114
|
+
else if (c === "{") brace++;
|
|
2115
|
+
else if (c === "}" && brace > 0) brace--;
|
|
2116
|
+
else if (c === "[") bracket++;
|
|
2117
|
+
else if (c === "]" && bracket > 0) bracket--;
|
|
2118
|
+
if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
|
|
2119
|
+
if (current.trim()) params.push(current.trim());
|
|
2120
|
+
current = "";
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
current += c;
|
|
2124
|
+
}
|
|
2125
|
+
if (current.trim()) params.push(current.trim());
|
|
2126
|
+
return params;
|
|
2127
|
+
}
|
|
2128
|
+
function paramName(raw) {
|
|
2129
|
+
let s = raw.trim();
|
|
2130
|
+
if (!s || s === "this") return void 0;
|
|
2131
|
+
if (s.startsWith("{") || s.startsWith("[")) return "input";
|
|
2132
|
+
s = s.replace(/^\.\.\./, "");
|
|
2133
|
+
const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
|
|
2134
|
+
if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
|
|
2135
|
+
return token;
|
|
2136
|
+
}
|
|
2137
|
+
function collectFromPattern(source, pattern) {
|
|
2138
|
+
const found = [];
|
|
2139
|
+
pattern.lastIndex = 0;
|
|
2140
|
+
let match;
|
|
2141
|
+
while (match = pattern.exec(source)) {
|
|
2142
|
+
const name = match[1];
|
|
2143
|
+
if (!name) continue;
|
|
2144
|
+
const openIndex = match.index + match[0].length - 1;
|
|
2145
|
+
const closeIndex = matchingClose(source, openIndex, "(", ")");
|
|
2146
|
+
if (closeIndex < 0) continue;
|
|
2147
|
+
const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
|
|
2148
|
+
const hasInput = params.length >= 2;
|
|
2149
|
+
const second = hasInput ? paramName(params[1] ?? "") : void 0;
|
|
2150
|
+
found.push({
|
|
2151
|
+
id: name,
|
|
2152
|
+
file: "",
|
|
2153
|
+
name,
|
|
2154
|
+
hasInput,
|
|
2155
|
+
inputName: hasInput ? second : void 0
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
return found;
|
|
2159
|
+
}
|
|
2160
|
+
function parseExportedTestFunctions(source) {
|
|
2161
|
+
const text = stripTsCommentsAndStringsKeepCode(source);
|
|
2162
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2163
|
+
const out = [];
|
|
2164
|
+
for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
|
|
2165
|
+
if (seen.has(item.name)) continue;
|
|
2166
|
+
seen.add(item.name);
|
|
2167
|
+
out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
|
|
2168
|
+
}
|
|
2169
|
+
return out;
|
|
2170
|
+
}
|
|
2171
|
+
function workspaceTestId(file, name) {
|
|
2172
|
+
return `${normalizePath(file)}:${name}`;
|
|
2173
|
+
}
|
|
2174
|
+
async function listWorkspaceTests(cwd) {
|
|
2175
|
+
const root = path8.resolve(cwd);
|
|
2176
|
+
let files;
|
|
2177
|
+
try {
|
|
2178
|
+
files = await collectFiles(root, isIgnoredWorkspacePath);
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
const code = err.code;
|
|
2181
|
+
if (code === "ENOENT") return [];
|
|
2182
|
+
throw err;
|
|
2183
|
+
}
|
|
2184
|
+
const tests = [];
|
|
2185
|
+
for (const file of files) {
|
|
2186
|
+
if (!isWorkspaceSpecFile(file.relPath)) continue;
|
|
2187
|
+
const posix = normalizePath(file.relPath);
|
|
2188
|
+
const source = await fs7.readFile(file.absPath, "utf8");
|
|
2189
|
+
for (const fn of parseExportedTestFunctions(source)) {
|
|
2190
|
+
tests.push({
|
|
2191
|
+
id: workspaceTestId(posix, fn.name),
|
|
2192
|
+
file: posix,
|
|
2193
|
+
name: fn.name,
|
|
2194
|
+
hasInput: fn.hasInput,
|
|
2195
|
+
inputName: fn.inputName
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
tests.sort((a, b) => a.id.localeCompare(b.id));
|
|
2200
|
+
return tests;
|
|
2201
|
+
}
|
|
2202
|
+
async function bundleWorkspaceTestFile(options) {
|
|
2203
|
+
const file = normalizePath(options.file);
|
|
2204
|
+
return bundleWorkspaceModule({
|
|
2205
|
+
cwd: options.cwd,
|
|
2206
|
+
entry: file,
|
|
2207
|
+
missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/previewServer.ts
|
|
2212
|
+
var DEFAULT_PREVIEW_PORT = 4173;
|
|
2213
|
+
var DEFAULT_PREVIEW_HOST = "127.0.0.1";
|
|
2214
|
+
var RUNTIME_FILES = /* @__PURE__ */ new Set(["twin-runtime.js", "twin-apps.js", "twin-app-host.js"]);
|
|
2215
|
+
function formatPreviewReadyMessage(info) {
|
|
2216
|
+
const lines = [
|
|
2217
|
+
`EasyTwin \u9884\u89C8: ${info.url}`,
|
|
2218
|
+
`\u573A\u666F: ${info.sceneId}`,
|
|
2219
|
+
"\u7528\u6D4F\u89C8\u5668\u6253\u5F00\u4E0A\u8FF0\u5730\u5740\u3002\u70B9 Run \u8FD0\u884C TwinApp;\u6D4B\u8BD5\u6309\u94AE\u5728 Run \u680F\u4E0A\u65B9\u3002Ctrl+C \u505C\u6B62\u3002"
|
|
2220
|
+
];
|
|
2221
|
+
if (info.mock) lines.unshift("[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u573A\u666F\u6570\u636E\u6765\u81EA\u672C\u5730\u793A\u4F8B,\u672A\u8BF7\u6C42\u573A\u666F\u63A5\u53E3");
|
|
2222
|
+
return lines.join("\n");
|
|
2223
|
+
}
|
|
2224
|
+
function resolvePreviewSceneId(scenes, requested) {
|
|
2225
|
+
const id = requested?.trim() ?? "";
|
|
2226
|
+
if (id.length > 0) return id;
|
|
2227
|
+
const list = scenes ?? [];
|
|
2228
|
+
const preferred = list.find((s) => s.defaultLoading === true) ?? list[0];
|
|
2229
|
+
if (preferred) return preferred.id;
|
|
2230
|
+
throw new Error("\u672A\u6307\u5B9A\u573A\u666F\u3002\u8BF7\u4F20\u5165 Scene Key,\u6216\u5148\u8FD0\u884C easytwin scene list");
|
|
2231
|
+
}
|
|
2232
|
+
function resolvePreviewRuntimeDir() {
|
|
2233
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
2234
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u9884\u89C8 runtime:\u63D2\u4EF6/CJS \u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 runtimeDir");
|
|
2235
|
+
}
|
|
2236
|
+
const here = path9.dirname(fileURLToPath3(import.meta.url));
|
|
2237
|
+
const candidates = [
|
|
2238
|
+
path9.join(here, "runtime"),
|
|
2239
|
+
path9.resolve(here, "..", "dist", "runtime"),
|
|
2240
|
+
path9.resolve(here, "runtime")
|
|
2241
|
+
];
|
|
2242
|
+
for (const dir of candidates) {
|
|
2243
|
+
if (existsSync2(path9.join(dir, "twin-runtime.js"))) return dir;
|
|
2244
|
+
}
|
|
2245
|
+
throw new Error(
|
|
2246
|
+
`\u672A\u627E\u5230\u9884\u89C8 runtime(twin-runtime.js)\u3002\u5DF2\u5C1D\u8BD5:${candidates.join(", ")}\u3002\u8BF7\u5148\u6784\u5EFA @easytwin/devkit\u3002`
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
function mimeFor(file) {
|
|
2250
|
+
if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
|
|
2251
|
+
return "application/octet-stream";
|
|
2252
|
+
}
|
|
2253
|
+
function sendJson(res, body, status = 200) {
|
|
2254
|
+
res.statusCode = status;
|
|
2255
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
2256
|
+
res.setHeader("cache-control", "no-store");
|
|
2257
|
+
res.end(JSON.stringify(body));
|
|
2258
|
+
}
|
|
2259
|
+
function sendText(res, body, status, contentType) {
|
|
2260
|
+
res.statusCode = status;
|
|
2261
|
+
res.setHeader("content-type", contentType);
|
|
2262
|
+
res.end(body);
|
|
2263
|
+
}
|
|
2264
|
+
async function readJsonBody(req) {
|
|
2265
|
+
const chunks = [];
|
|
2266
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
2267
|
+
if (chunks.length === 0) return {};
|
|
2268
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
2269
|
+
if (raw.length === 0) return {};
|
|
2270
|
+
return JSON.parse(raw);
|
|
2271
|
+
}
|
|
2272
|
+
function asRecord3(value) {
|
|
2273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2274
|
+
}
|
|
2275
|
+
async function startPreviewServer(options) {
|
|
2276
|
+
const cwd = path9.resolve(options.cwd);
|
|
2277
|
+
const config = await loadConfig(cwd);
|
|
2278
|
+
const fileConfig = await readConfigFile(cwd);
|
|
2279
|
+
const runtimeDir = options.runtimeDir ?? resolvePreviewRuntimeDir();
|
|
2280
|
+
if (!existsSync2(path9.join(runtimeDir, "twin-runtime.js"))) {
|
|
2281
|
+
throw new Error(`\u9884\u89C8 runtime \u76EE\u5F55\u7F3A\u5C11 twin-runtime.js:${runtimeDir}`);
|
|
2282
|
+
}
|
|
2283
|
+
let sceneId;
|
|
2284
|
+
try {
|
|
2285
|
+
sceneId = resolvePreviewSceneId(fileConfig.scenes, options.sceneId);
|
|
2286
|
+
} catch (err) {
|
|
2287
|
+
if (config.mock) {
|
|
2288
|
+
sceneId = deriveExampleSceneId(await loadExampleScene());
|
|
2289
|
+
} else {
|
|
2290
|
+
throw err;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
const client = new EasyTwinClient(config);
|
|
2294
|
+
const scene = await pullScene(client, sceneId);
|
|
2295
|
+
const tests = await listWorkspaceTests(cwd);
|
|
2296
|
+
const hostname = options.hostname ?? DEFAULT_PREVIEW_HOST;
|
|
2297
|
+
const log = options.log ?? ((line) => process.stderr.write(`${line}
|
|
2298
|
+
`));
|
|
2299
|
+
let lastBundledCode;
|
|
2300
|
+
const originRef = { url: "" };
|
|
2301
|
+
const html = () => buildPreviewHtml({
|
|
2302
|
+
baseUrl: config.baseUrl,
|
|
2303
|
+
ossUrl: config.ossUrl,
|
|
2304
|
+
sceneJson: JSON.stringify(scene.payload ?? scene, null, 2),
|
|
2305
|
+
mock: config.mock,
|
|
2306
|
+
runtimeUri: `${originRef.url}/runtime/twin-runtime.js`,
|
|
2307
|
+
appsUri: `${originRef.url}/runtime/twin-apps.js`,
|
|
2308
|
+
hostUri: `${originRef.url}/runtime/twin-app-host.js`,
|
|
2309
|
+
appId: config.appId,
|
|
2310
|
+
host: "http",
|
|
2311
|
+
tests
|
|
2312
|
+
});
|
|
2313
|
+
const server = http2.createServer((req, res) => {
|
|
2314
|
+
void handle(req, res);
|
|
2315
|
+
});
|
|
2316
|
+
async function handle(req, res) {
|
|
2317
|
+
try {
|
|
2318
|
+
const url = new URL(req.url ?? "/", originRef.url || `http://${hostname}`);
|
|
2319
|
+
const method = req.method ?? "GET";
|
|
2320
|
+
if (method === "GET" && url.pathname === "/") {
|
|
2321
|
+
sendText(res, html(), 200, "text/html; charset=utf-8");
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
if (method === "GET" && url.pathname.startsWith("/runtime/")) {
|
|
2325
|
+
const name = path9.posix.basename(url.pathname);
|
|
2326
|
+
if (!RUNTIME_FILES.has(name)) {
|
|
2327
|
+
sendText(res, "not found", 404, "text/plain; charset=utf-8");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const file = path9.join(runtimeDir, name);
|
|
2331
|
+
const buf = await fs8.readFile(file);
|
|
2332
|
+
res.statusCode = 200;
|
|
2333
|
+
res.setHeader("content-type", mimeFor(name));
|
|
2334
|
+
res.setHeader("cache-control", "no-store");
|
|
2335
|
+
res.end(buf);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
if (method === "POST" && url.pathname === "/api/bundle") {
|
|
2339
|
+
try {
|
|
2340
|
+
const result = await bundleUserCode({ cwd });
|
|
2341
|
+
lastBundledCode = result.code;
|
|
2342
|
+
for (const warning of result.warnings) log(`[run] \u8B66\u544A ${warning}`);
|
|
2343
|
+
sendJson(res, { ok: true, code: result.code, warnings: result.warnings });
|
|
2344
|
+
} catch (err) {
|
|
2345
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2346
|
+
log(`[run] \u7F16\u8BD1\u5931\u8D25 ${error}`);
|
|
2347
|
+
sendJson(res, { ok: false, error });
|
|
2348
|
+
}
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (method === "GET" && url.pathname === "/api/tests") {
|
|
2352
|
+
sendJson(res, { tests: await listWorkspaceTests(cwd) });
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (method === "POST" && url.pathname === "/api/test-bundle") {
|
|
2356
|
+
const body = asRecord3(await readJsonBody(req));
|
|
2357
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
2358
|
+
const input = typeof body.input === "string" ? body.input : void 0;
|
|
2359
|
+
try {
|
|
2360
|
+
const listed = await listWorkspaceTests(cwd);
|
|
2361
|
+
const item = listed.find((t) => t.id === id);
|
|
2362
|
+
if (!item) throw new Error(`\u672A\u627E\u5230\u6D4B\u8BD5 ${id}`);
|
|
2363
|
+
log(`[test] bundle ${item.file} :: ${item.name}`);
|
|
2364
|
+
const result = await bundleWorkspaceTestFile({ cwd, file: item.file });
|
|
2365
|
+
lastBundledCode = result.code;
|
|
2366
|
+
for (const warning of result.warnings) log(`[test] \u8B66\u544A ${warning}`);
|
|
2367
|
+
sendJson(res, {
|
|
2368
|
+
ok: true,
|
|
2369
|
+
code: result.code,
|
|
2370
|
+
exportName: item.name,
|
|
2371
|
+
input,
|
|
2372
|
+
hasInput: item.hasInput,
|
|
2373
|
+
warnings: result.warnings
|
|
2374
|
+
});
|
|
2375
|
+
} catch (err) {
|
|
2376
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2377
|
+
log(`[test] \u7F16\u8BD1\u5931\u8D25 ${error}`);
|
|
2378
|
+
sendJson(res, { ok: false, error });
|
|
2379
|
+
}
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (method === "GET" && url.pathname === "/api/assets") {
|
|
2383
|
+
const rel = url.searchParams.get("path") ?? "";
|
|
2384
|
+
try {
|
|
2385
|
+
const file = resolveWorkspaceAssetPath(cwd, rel);
|
|
2386
|
+
sendJson(res, { text: await fs8.readFile(file, "utf8") });
|
|
2387
|
+
} catch (err) {
|
|
2388
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2389
|
+
log(`[assets] FAIL ${rel} ${error}`);
|
|
2390
|
+
sendJson(res, { error });
|
|
2391
|
+
}
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (method === "POST" && url.pathname === "/api/remap-error") {
|
|
2395
|
+
const body = asRecord3(await readJsonBody(req));
|
|
2396
|
+
const stack = typeof body.stack === "string" ? body.stack : "";
|
|
2397
|
+
sendJson(res, { stack: lastBundledCode ? remapErrorStack(stack, lastBundledCode) : stack });
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
sendText(res, "not found", 404, "text/plain; charset=utf-8");
|
|
2401
|
+
} catch (err) {
|
|
2402
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2403
|
+
log(`[preview] ${error}`);
|
|
2404
|
+
if (!res.headersSent) sendJson(res, { error }, 500);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
const port = options.port ?? DEFAULT_PREVIEW_PORT;
|
|
2408
|
+
await new Promise((resolve, reject) => {
|
|
2409
|
+
server.once("error", reject);
|
|
2410
|
+
server.listen(port, hostname, () => resolve());
|
|
2411
|
+
});
|
|
2412
|
+
const address = server.address();
|
|
2413
|
+
const bound = typeof address === "object" && address !== null ? address.port : port;
|
|
2414
|
+
originRef.url = `http://${hostname}:${bound}`;
|
|
2415
|
+
return {
|
|
2416
|
+
url: originRef.url,
|
|
2417
|
+
port: bound,
|
|
2418
|
+
sceneId,
|
|
2419
|
+
mock: config.mock,
|
|
2420
|
+
close: () => new Promise((resolve, reject) => {
|
|
2421
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
2422
|
+
})
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
741
2425
|
|
|
742
2426
|
// src/bin.ts
|
|
743
2427
|
async function readVersion() {
|
|
@@ -748,7 +2432,7 @@ async function readVersion() {
|
|
|
748
2432
|
return "0.0.0";
|
|
749
2433
|
}
|
|
750
2434
|
}
|
|
751
|
-
var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20\u8D70\u672C\u5730 mock";
|
|
2435
|
+
var MOCK_MODE_HINT = "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] appId/appSecret \u5747\u4E3A test:\u672A\u8BF7\u6C42\u670D\u52A1\u7AEF,\u573A\u666F/\u4E0A\u4F20/\u5DE5\u4F5C\u533A\u62C9\u53D6\u8D70\u672C\u5730 mock";
|
|
752
2436
|
async function prompt(question) {
|
|
753
2437
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
754
2438
|
try {
|
|
@@ -783,31 +2467,41 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
783
2467
|
program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
|
|
784
2468
|
program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 easytwin.config.json,\u5E76\u8FFD\u52A0\u8FDB .gitignore").option("--app-id <id>", "App ID(\u8DF3\u8FC7\u4EA4\u4E92)").option("--app-secret <secret>", "App Secret(\u8DF3\u8FC7\u4EA4\u4E92)").option("--env <prod|test>", "\u670D\u52A1\u73AF\u5883(\u8DF3\u8FC7\u4EA4\u4E92)").option("--base-url <url>", "Base URL(\u8DF3\u8FC7\u4EA4\u4E92)").action((opts) => runInit(cwd, opts));
|
|
785
2469
|
const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
|
|
786
|
-
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F").action(async () => {
|
|
2470
|
+
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
|
|
787
2471
|
const config = await loadConfig(cwd);
|
|
788
2472
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
789
2473
|
const client = new EasyTwinClient(config);
|
|
790
|
-
const scenes = await listScenes(client);
|
|
2474
|
+
const scenes = await listScenes(client, { cwd });
|
|
791
2475
|
if (scenes.length === 0) {
|
|
792
2476
|
console.log("(\u65E0\u573A\u666F)");
|
|
793
|
-
|
|
2477
|
+
} else {
|
|
2478
|
+
for (const s of scenes) console.log(`${s.id} ${s.name}`);
|
|
794
2479
|
}
|
|
795
|
-
|
|
2480
|
+
console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
|
|
796
2481
|
});
|
|
797
2482
|
scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ./<id>.scene.json)").action(async (id, opts) => {
|
|
798
2483
|
const config = await loadConfig(cwd);
|
|
799
2484
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
800
2485
|
const client = new EasyTwinClient(config);
|
|
801
2486
|
const scene2 = await pullScene(client, id);
|
|
802
|
-
const out = opts.out ??
|
|
2487
|
+
const out = opts.out ?? path10.join(cwd, `${id}.scene.json`);
|
|
803
2488
|
const file = await saveSceneJson(scene2, out);
|
|
804
2489
|
console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
|
|
805
2490
|
});
|
|
806
|
-
program.command("
|
|
2491
|
+
program.command("pull").description("\u62C9\u53D6\u5DE5\u4F5C\u533A\u4EE3\u7801(\u8FDC\u7AEF\u4E3A\u7A7A\u5219\u5199\u5165\u9ED8\u8BA4 src/main.ts;\u51B2\u7A81\u9ED8\u8BA4\u4E0D\u8986\u76D6)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").option("--force", "\u7528\u8FDC\u7AEF\u5185\u5BB9\u8986\u76D6\u672C\u5730\u51B2\u7A81\u6587\u4EF6").option("--dry-run", "\u53EA\u6253\u5370 diff,\u4E0D\u5199\u76D8").action(async (dirArg, opts) => {
|
|
2492
|
+
const config = await loadConfig(cwd);
|
|
2493
|
+
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
2494
|
+
const client = new EasyTwinClient(config);
|
|
2495
|
+
const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
|
|
2496
|
+
const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
|
|
2497
|
+
console.log(formatWorkspacePullResult(result));
|
|
2498
|
+
});
|
|
2499
|
+
program.command("upload").description("\u5BF9\u7167\u8FDC\u7AEF\u5168\u91CF\u5BF9\u9F50\u5DE5\u4F5C\u533A(\u8DF3\u8FC7\u63D2\u4EF6\u4EA7\u7269,\u4E0D\u53EF\u9006)").argument("[dir]", "\u5DE5\u4F5C\u533A\u76EE\u5F55(\u7F3A\u7701\u5F53\u524D\u76EE\u5F55)").action(async (dirArg) => {
|
|
807
2500
|
const config = await loadConfig(cwd);
|
|
808
2501
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
809
2502
|
const client = new EasyTwinClient(config);
|
|
810
|
-
|
|
2503
|
+
const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
|
|
2504
|
+
const result = await uploadDirectory(client, target, {
|
|
811
2505
|
onProgress: (p) => {
|
|
812
2506
|
if (p.phase === "upload") {
|
|
813
2507
|
process.stderr.write(`\r\u5DF2\u4E0A\u4F20 ${p.current}/${p.total} bytes${p.file ? ` (${p.file})` : ""}`);
|
|
@@ -815,14 +2509,29 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
815
2509
|
}
|
|
816
2510
|
});
|
|
817
2511
|
process.stderr.write("\n");
|
|
2512
|
+
console.log(formatUploadResult(result));
|
|
818
2513
|
});
|
|
819
|
-
program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8\
|
|
2514
|
+
program.command("bundle").description("\u6253\u5305\u5DE5\u4F5C\u533A src/main.ts(\u4EC5\u5141\u8BB8 @easytwin/runtime \u4E0E @easytwin/apps)").option("-o, --out <path>", `\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84(\u7F3A\u7701 ${DEFAULT_BUNDLE_OUT})`).action(async (opts) => {
|
|
820
2515
|
const hint = await typesMissingHint(cwd);
|
|
821
2516
|
if (hint) console.warn(hint);
|
|
822
|
-
const outFile =
|
|
823
|
-
await bundleUserCode({ cwd, outFile });
|
|
2517
|
+
const outFile = path10.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
|
|
2518
|
+
const result = await bundleUserCode({ cwd, outFile });
|
|
2519
|
+
for (const warning of result.warnings) console.warn(warning);
|
|
824
2520
|
console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
|
|
825
2521
|
});
|
|
2522
|
+
program.command("preview").description("\u542F\u52A8\u672C\u5730\u9884\u89C8\u9875(\u4E09\u7EF4\u573A\u666F + Run + \u6D4B\u8BD5\u63A7\u4EF6),\u6253\u5370 URL \u4F9B\u6D4F\u89C8\u5668\u6253\u5F00").argument("[sceneId]", "\u573A\u666F Scene Key(\u7F3A\u7701\u914D\u7F6E scenes \u4E2D defaultLoading \u6216\u9996\u9879)").option("-p, --port <port>", "\u7AEF\u53E3(\u7F3A\u7701 4173,\u53EA\u7ED1 127.0.0.1)", "4173").action(async (sceneId, opts) => {
|
|
2523
|
+
const port = Number.parseInt(opts.port, 10);
|
|
2524
|
+
if (!Number.isFinite(port) || port < 0) throw new Error(`\u65E0\u6548\u7AEF\u53E3:${opts.port}`);
|
|
2525
|
+
const server = await startPreviewServer({ cwd, sceneId, port });
|
|
2526
|
+
console.log(formatPreviewReadyMessage(server));
|
|
2527
|
+
await new Promise((resolve, reject) => {
|
|
2528
|
+
const stop = () => {
|
|
2529
|
+
server.close().then(resolve, reject);
|
|
2530
|
+
};
|
|
2531
|
+
process.once("SIGINT", stop);
|
|
2532
|
+
process.once("SIGTERM", stop);
|
|
2533
|
+
});
|
|
2534
|
+
});
|
|
826
2535
|
const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
|
|
827
2536
|
skills.command("sync").description("\u540C\u6B65 skills \u5230\u7528\u6237\u9879\u76EE(cursor/claude/codex/qoder),\u7F3A\u7701 all").option("--target <target>", "cursor|claude|codex|qoder|all", "all").action(async (opts) => {
|
|
828
2537
|
const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
|
|
@@ -856,4 +2565,3 @@ export {
|
|
|
856
2565
|
main,
|
|
857
2566
|
runInit
|
|
858
2567
|
};
|
|
859
|
-
//# sourceMappingURL=bin.js.map
|