@easytwin/devkit 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/dist/bin.js +1308 -208
- package/dist/index.d.ts +175 -78
- package/dist/index.js +1510 -648
- 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/package.json +5 -3
- package/skills/easytwin-bootstrap/SKILL.md +1 -4
- package/skills/easytwin-develop/SKILL.md +6 -4
- package/skills/easytwin-render/SKILL.md +3 -3
- package/skills/easytwin-scene/SKILL.md +57 -54
- package/skills/easytwin-test/SKILL.md +47 -0
- package/skills/easytwin-upload/SKILL.md +39 -38
- package/dist/bin.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -7,7 +7,7 @@ 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";
|
|
@@ -15,9 +15,6 @@ 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
17
|
var TEST_BASE_URL = "http://172.16.125.3:10100/";
|
|
18
|
-
var TEST_OP_ACCOUNT_ID = "25";
|
|
19
|
-
var TEST_OP_USER_ID = "25";
|
|
20
|
-
var TEST_SPACE_ID = "54";
|
|
21
18
|
var DEFAULT_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
22
19
|
var MOCK_APP_ID = "test";
|
|
23
20
|
var MOCK_APP_SECRET = "test";
|
|
@@ -62,29 +59,14 @@ function validateConfigShape(value) {
|
|
|
62
59
|
if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
|
|
63
60
|
throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
|
|
64
61
|
}
|
|
65
|
-
const opAccountId = optionalGatewayId(v.opAccountId);
|
|
66
|
-
const opUserId = optionalGatewayId(v.opUserId);
|
|
67
|
-
const spaceId = optionalGatewayId(v.spaceId);
|
|
68
62
|
const config = { appId: v.appId, appSecret: v.appSecret };
|
|
69
63
|
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
70
64
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
71
65
|
if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
|
|
72
|
-
if (opAccountId) config.opAccountId = opAccountId;
|
|
73
|
-
if (opUserId) config.opUserId = opUserId;
|
|
74
|
-
if (spaceId) config.spaceId = spaceId;
|
|
75
66
|
const scenes = parseConfigScenes(v.scenes);
|
|
76
67
|
if (scenes) config.scenes = scenes;
|
|
77
68
|
return config;
|
|
78
69
|
}
|
|
79
|
-
function optionalGatewayId(value) {
|
|
80
|
-
if (typeof value === "string") {
|
|
81
|
-
const t = value.trim();
|
|
82
|
-
return t.length > 0 ? t : void 0;
|
|
83
|
-
}
|
|
84
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
85
|
-
if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
|
|
86
|
-
return void 0;
|
|
87
|
-
}
|
|
88
70
|
function parseConfigScenes(value) {
|
|
89
71
|
if (value === void 0) return void 0;
|
|
90
72
|
if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
@@ -134,30 +116,11 @@ function resolveConfig(file, env = process.env) {
|
|
|
134
116
|
if (!appId) throw new ConfigError("\u7F3A\u5C11 appId:\u914D\u7F6E\u6587\u4EF6\u4E2D\u672A\u63D0\u4F9B,\u4E14\u672A\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EASYTWIN_APP_ID");
|
|
135
117
|
if (!appSecret) throw new ConfigError("\u7F3A\u5C11 appSecret:\u914D\u7F6E\u6587\u4EF6\u4E2D\u672A\u63D0\u4F9B,\u4E14\u672A\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF EASYTWIN_APP_SECRET");
|
|
136
118
|
const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
|
|
137
|
-
|
|
138
|
-
const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
|
|
139
|
-
const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
|
|
140
|
-
const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
|
|
141
|
-
const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
142
|
-
if (opAccountId) resolved.opAccountId = opAccountId;
|
|
143
|
-
if (opUserId) resolved.opUserId = opUserId;
|
|
144
|
-
if (spaceId) resolved.spaceId = spaceId;
|
|
145
|
-
return resolved;
|
|
119
|
+
return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
146
120
|
}
|
|
147
121
|
async function loadConfig(cwd, env = process.env) {
|
|
148
122
|
const file = await readConfigFile(cwd);
|
|
149
|
-
|
|
150
|
-
if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
|
|
151
|
-
if (!file.opAccountId || !file.opUserId || !file.spaceId) {
|
|
152
|
-
await writeConfigFile(cwd, {
|
|
153
|
-
...file,
|
|
154
|
-
opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
|
|
155
|
-
opUserId: file.opUserId ?? TEST_OP_USER_ID,
|
|
156
|
-
spaceId: file.spaceId ?? TEST_SPACE_ID
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return resolved;
|
|
123
|
+
return resolveConfig(file, env);
|
|
161
124
|
}
|
|
162
125
|
async function writeConfigFile(cwd, config) {
|
|
163
126
|
const file = configFilePath(cwd);
|
|
@@ -165,12 +128,6 @@ async function writeConfigFile(cwd, config) {
|
|
|
165
128
|
if (config.env) body.env = config.env;
|
|
166
129
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
167
130
|
if (config.ossUrl) body.ossUrl = config.ossUrl;
|
|
168
|
-
const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
|
|
169
|
-
const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
|
|
170
|
-
const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
|
|
171
|
-
if (opAccountId) body.opAccountId = opAccountId;
|
|
172
|
-
if (opUserId) body.opUserId = opUserId;
|
|
173
|
-
if (spaceId) body.spaceId = spaceId;
|
|
174
131
|
if (config.scenes !== void 0) body.scenes = config.scenes;
|
|
175
132
|
await fs.mkdir(cwd, { recursive: true });
|
|
176
133
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
@@ -208,22 +165,20 @@ async function writeConfigScenes(cwd, scenes) {
|
|
|
208
165
|
import http from "http";
|
|
209
166
|
import https from "https";
|
|
210
167
|
import { URL as URL2 } from "url";
|
|
168
|
+
var APP_ID_HEADER = "x-app-id";
|
|
211
169
|
var AUTH_HEADER = "x-app-secret";
|
|
212
|
-
var OP_ACCOUNT_ID_HEADER = "op-account-id";
|
|
213
|
-
var OP_USER_ID_HEADER = "op-user-id";
|
|
214
|
-
var SPACE_ID_HEADER = "space-id";
|
|
215
170
|
function enc(id) {
|
|
216
171
|
return encodeURIComponent(id);
|
|
217
172
|
}
|
|
218
173
|
var ENDPOINTS = {
|
|
219
|
-
/**
|
|
220
|
-
linkedScenes: (
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
|
|
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`
|
|
227
182
|
};
|
|
228
183
|
var EasyTwinApiError = class extends Error {
|
|
229
184
|
status;
|
|
@@ -266,35 +221,25 @@ var EasyTwinClient = class {
|
|
|
266
221
|
baseUrl;
|
|
267
222
|
/** twin runtime / 场景快照资产根(baseOSSUrl)。 */
|
|
268
223
|
ossUrl;
|
|
269
|
-
/** 接入凭证 App ID
|
|
224
|
+
/** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
|
|
270
225
|
appId;
|
|
271
226
|
/** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
272
227
|
mock;
|
|
273
228
|
appSecret;
|
|
274
|
-
opAccountId;
|
|
275
|
-
opUserId;
|
|
276
|
-
spaceId;
|
|
277
229
|
constructor(config) {
|
|
278
230
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
279
231
|
this.ossUrl = config.ossUrl.replace(/\/+$/, "");
|
|
280
232
|
this.mock = config.mock;
|
|
281
233
|
this.appId = config.appId;
|
|
282
234
|
this.appSecret = config.appSecret;
|
|
283
|
-
this.opAccountId = config.opAccountId;
|
|
284
|
-
this.opUserId = config.opUserId;
|
|
285
|
-
this.spaceId = config.spaceId;
|
|
286
235
|
}
|
|
287
236
|
/** 全仓唯一认证头注入点。 */
|
|
288
237
|
authHeaders() {
|
|
289
|
-
|
|
290
|
-
if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
|
|
291
|
-
if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
|
|
292
|
-
if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
|
|
293
|
-
return headers;
|
|
238
|
+
return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
|
|
294
239
|
}
|
|
295
240
|
/** JSON 请求(原生 fetch)。 */
|
|
296
|
-
async request(
|
|
297
|
-
const url = `${this.baseUrl}${
|
|
241
|
+
async request(path11, options = {}) {
|
|
242
|
+
const url = `${this.baseUrl}${path11}`;
|
|
298
243
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
299
244
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
300
245
|
headers["Content-Type"] = "application/json";
|
|
@@ -316,8 +261,8 @@ var EasyTwinClient = class {
|
|
|
316
261
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
317
262
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
318
263
|
*/
|
|
319
|
-
async upload(
|
|
320
|
-
const url = new URL2(`${this.baseUrl}${
|
|
264
|
+
async upload(path11, options) {
|
|
265
|
+
const url = new URL2(`${this.baseUrl}${path11}`);
|
|
321
266
|
const mod = url.protocol === "https:" ? https : http;
|
|
322
267
|
const headers = {
|
|
323
268
|
...this.authHeaders(),
|
|
@@ -474,8 +419,11 @@ function deriveExampleSceneId(payload) {
|
|
|
474
419
|
async function exampleSceneSummary(exampleFile) {
|
|
475
420
|
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
476
421
|
}
|
|
422
|
+
async function fetchLinkedScenes(client) {
|
|
423
|
+
return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
|
|
424
|
+
}
|
|
477
425
|
async function listScenes(client, options = {}) {
|
|
478
|
-
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await
|
|
426
|
+
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
|
|
479
427
|
if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
|
|
480
428
|
return scenes;
|
|
481
429
|
}
|
|
@@ -488,7 +436,7 @@ async function pullScene(client, id, options = {}) {
|
|
|
488
436
|
}
|
|
489
437
|
return { id: sceneId, name: MOCK_SCENE_NAME, payload: payload2 };
|
|
490
438
|
}
|
|
491
|
-
const data = await
|
|
439
|
+
const data = await fetchLinkedScenes(client);
|
|
492
440
|
const scenes = normalizeLinkedScenes(data);
|
|
493
441
|
const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
|
|
494
442
|
if (!hit) {
|
|
@@ -509,7 +457,43 @@ async function saveSceneJson(scene, out) {
|
|
|
509
457
|
// src/upload.ts
|
|
510
458
|
import { promises as fs3 } from "fs";
|
|
511
459
|
import path3 from "path";
|
|
512
|
-
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
|
+
}
|
|
513
497
|
async function collectFiles(dir, ignore = () => false) {
|
|
514
498
|
const files = [];
|
|
515
499
|
async function walk(current, rel) {
|
|
@@ -518,7 +502,7 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
518
502
|
const relPath = path3.posix.join(rel, entry.name);
|
|
519
503
|
if (ignore(relPath)) continue;
|
|
520
504
|
if (entry.isDirectory()) {
|
|
521
|
-
if (
|
|
505
|
+
if (entry.name === ".git") continue;
|
|
522
506
|
await walk(path3.join(current, entry.name), relPath);
|
|
523
507
|
} else if (entry.isFile()) {
|
|
524
508
|
const absPath = path3.join(current, entry.name);
|
|
@@ -537,26 +521,9 @@ function asRecord2(value) {
|
|
|
537
521
|
function asString2(value) {
|
|
538
522
|
return typeof value === "string" ? value : "";
|
|
539
523
|
}
|
|
540
|
-
function asNumber(value) {
|
|
541
|
-
return typeof value === "number" && Number.isFinite(value) ? value : NaN;
|
|
542
|
-
}
|
|
543
524
|
function normalizePath(relPath) {
|
|
544
525
|
return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
545
526
|
}
|
|
546
|
-
function normalizeWorkspaceConfig(data) {
|
|
547
|
-
const it = asRecord2(data);
|
|
548
|
-
if (!Array.isArray(it.allowedFileExtensions)) throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
549
|
-
const maxCodeFiles = asNumber(it.maxCodeFiles);
|
|
550
|
-
const maxCodeDirectoryDepth = asNumber(it.maxCodeDirectoryDepth);
|
|
551
|
-
if (!Number.isFinite(maxCodeFiles) || !Number.isFinite(maxCodeDirectoryDepth)) {
|
|
552
|
-
throw new Error("workspace-config \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
553
|
-
}
|
|
554
|
-
return {
|
|
555
|
-
allowedFileExtensions: it.allowedFileExtensions.filter((e) => typeof e === "string"),
|
|
556
|
-
maxCodeFiles,
|
|
557
|
-
maxCodeDirectoryDepth
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
527
|
function normalizeWorkspaceFiles(data) {
|
|
561
528
|
if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
562
529
|
return data.map((item) => {
|
|
@@ -573,91 +540,98 @@ function extensionOf(relPath) {
|
|
|
573
540
|
function allowedExtensionSet(list) {
|
|
574
541
|
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
575
542
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return
|
|
543
|
+
var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
|
|
544
|
+
function isWorkspaceCodeFile(relPath) {
|
|
545
|
+
return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
|
|
579
546
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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);
|
|
598
569
|
}
|
|
599
570
|
}
|
|
600
|
-
|
|
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}`;
|
|
601
576
|
}
|
|
602
577
|
async function mockUpload(dir, options) {
|
|
603
|
-
const ignore = options.ignore
|
|
604
|
-
const files = await
|
|
578
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
579
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
605
580
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
606
581
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
607
582
|
options.onProgress?.({ phase: "upload", current: total, total });
|
|
608
|
-
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;
|
|
609
604
|
}
|
|
610
605
|
async function uploadDirectory(client, dir, options = {}) {
|
|
611
606
|
if (client.mock) return mockUpload(dir, options);
|
|
612
|
-
const ignore = options.ignore
|
|
613
|
-
const files = await
|
|
607
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
608
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
614
609
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
615
610
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
616
|
-
const
|
|
617
|
-
const
|
|
618
|
-
assertUploadable(files, wsConfig);
|
|
619
|
-
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
|
|
620
|
-
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
621
|
-
const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
|
|
622
|
-
const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
|
|
623
|
-
if (toDelete.length > 0) {
|
|
624
|
-
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
625
|
-
method: "DELETE",
|
|
626
|
-
body: JSON.stringify({ ids: toDelete })
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
const creates = [];
|
|
630
|
-
const patches = [];
|
|
631
|
-
const unchanged = [];
|
|
611
|
+
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
612
|
+
const local = [];
|
|
632
613
|
for (const file of files) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
+
});
|
|
638
624
|
}
|
|
639
625
|
let sent = 0;
|
|
640
626
|
const bump = (file) => {
|
|
641
627
|
sent += file.size;
|
|
642
628
|
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
643
629
|
};
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
|
|
648
|
-
});
|
|
649
|
-
for (const p of patches) bump(p.file);
|
|
650
|
-
}
|
|
651
|
-
for (const c of creates) {
|
|
652
|
-
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
653
|
-
method: "POST",
|
|
654
|
-
body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
|
|
655
|
-
});
|
|
656
|
-
bump(c.file);
|
|
657
|
-
}
|
|
658
|
-
for (const u of unchanged) bump(u);
|
|
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);
|
|
659
633
|
if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
|
|
660
|
-
return { fileCount: files.length, byteCount: total };
|
|
634
|
+
return { fileCount: files.length, byteCount: total, ...counts };
|
|
661
635
|
}
|
|
662
636
|
|
|
663
637
|
// src/workspace.ts
|
|
@@ -705,42 +679,20 @@ export default class App extends TwinApp {
|
|
|
705
679
|
}
|
|
706
680
|
}
|
|
707
681
|
`;
|
|
708
|
-
var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
709
|
-
".git",
|
|
710
|
-
"node_modules",
|
|
711
|
-
"dist",
|
|
712
|
-
".easytwin",
|
|
713
|
-
".cursor",
|
|
714
|
-
".claude",
|
|
715
|
-
".qoder",
|
|
716
|
-
".vscode"
|
|
717
|
-
]);
|
|
718
|
-
var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
|
|
719
|
-
var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
|
|
720
682
|
function defaultPullIgnore(relPath) {
|
|
721
|
-
|
|
722
|
-
if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
|
|
723
|
-
const base = parts[parts.length - 1];
|
|
724
|
-
return base !== void 0 && PULL_IGNORED_FILES.has(base);
|
|
683
|
+
return defaultWorkspaceIgnore(relPath);
|
|
725
684
|
}
|
|
726
685
|
function isSafeRelPath(relPath) {
|
|
727
686
|
const n = normalizePath(relPath);
|
|
728
687
|
if (!n) return false;
|
|
729
688
|
if (n.toLowerCase() === "easytwin.config.json") return false;
|
|
689
|
+
const base = n.split("/").pop() ?? "";
|
|
690
|
+
if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
|
|
730
691
|
if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
|
|
731
692
|
const parts = n.split("/");
|
|
732
693
|
if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
|
|
733
694
|
return true;
|
|
734
695
|
}
|
|
735
|
-
function extensionOf2(relPath) {
|
|
736
|
-
const base = relPath.split("/").pop() ?? "";
|
|
737
|
-
const i = base.lastIndexOf(".");
|
|
738
|
-
if (i <= 0) return "";
|
|
739
|
-
return base.slice(i).toLowerCase();
|
|
740
|
-
}
|
|
741
|
-
function allowedExtensionSet2(list) {
|
|
742
|
-
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
743
|
-
}
|
|
744
696
|
function normalizeEol(text) {
|
|
745
697
|
return text.replace(/\r\n/g, "\n");
|
|
746
698
|
}
|
|
@@ -756,30 +708,18 @@ async function readLocalText(absPath) {
|
|
|
756
708
|
throw err;
|
|
757
709
|
}
|
|
758
710
|
}
|
|
759
|
-
async function loadAllowedExtensions(client) {
|
|
760
|
-
if (client.mock) return DEFAULT_PULL_EXTENSIONS;
|
|
761
|
-
try {
|
|
762
|
-
const cfg = normalizeWorkspaceConfig(
|
|
763
|
-
await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
|
|
764
|
-
);
|
|
765
|
-
return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
|
|
766
|
-
} catch {
|
|
767
|
-
return DEFAULT_PULL_EXTENSIONS;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
711
|
async function fetchRemoteFiles(client) {
|
|
771
712
|
if (client.mock) return [];
|
|
772
|
-
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.
|
|
713
|
+
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
773
714
|
}
|
|
774
715
|
async function planWorkspacePull(client, dir, options = {}) {
|
|
775
716
|
const ignore = combineIgnore(options.ignore);
|
|
776
|
-
const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
|
|
777
717
|
const remoteRaw = await fetchRemoteFiles(client);
|
|
778
718
|
const skippedRemotePaths = [];
|
|
779
719
|
const remoteByPath = /* @__PURE__ */ new Map();
|
|
780
720
|
for (const file of remoteRaw) {
|
|
781
721
|
const rel = normalizePath(file.filePath);
|
|
782
|
-
if (!isSafeRelPath(rel) || ignore(rel)) {
|
|
722
|
+
if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
|
|
783
723
|
skippedRemotePaths.push(rel || file.filePath);
|
|
784
724
|
continue;
|
|
785
725
|
}
|
|
@@ -795,7 +735,7 @@ async function planWorkspacePull(client, dir, options = {}) {
|
|
|
795
735
|
const localByPath = /* @__PURE__ */ new Map();
|
|
796
736
|
for (const file of localFiles) {
|
|
797
737
|
const rel = normalizePath(file.relPath);
|
|
798
|
-
if (!
|
|
738
|
+
if (!isWorkspaceCodeFile(rel)) continue;
|
|
799
739
|
const content = await readLocalText(file.absPath);
|
|
800
740
|
if (content !== void 0) localByPath.set(rel, content);
|
|
801
741
|
}
|
|
@@ -920,7 +860,8 @@ var SKILL_NAMES = [
|
|
|
920
860
|
"easytwin-develop",
|
|
921
861
|
"easytwin-bootstrap",
|
|
922
862
|
"easytwin-scene",
|
|
923
|
-
"easytwin-upload"
|
|
863
|
+
"easytwin-upload",
|
|
864
|
+
"easytwin-test"
|
|
924
865
|
];
|
|
925
866
|
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
926
867
|
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
@@ -1111,6 +1052,7 @@ function buildCodexSegment(version) {
|
|
|
1111
1052
|
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
1112
1053
|
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
1113
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",
|
|
1114
1056
|
"",
|
|
1115
1057
|
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
1116
1058
|
CODEX_MARKER_END
|
|
@@ -1281,14 +1223,14 @@ function collectBundledRuntimeExports(code) {
|
|
|
1281
1223
|
}
|
|
1282
1224
|
return names;
|
|
1283
1225
|
}
|
|
1284
|
-
async function
|
|
1226
|
+
async function bundleWorkspaceModule(options) {
|
|
1285
1227
|
const cwd = path7.resolve(options.cwd);
|
|
1286
|
-
const entry = path7.join(cwd,
|
|
1228
|
+
const entry = path7.isAbsolute(options.entry) ? options.entry : path7.join(cwd, options.entry);
|
|
1287
1229
|
try {
|
|
1288
1230
|
await fs6.access(entry);
|
|
1289
1231
|
} catch {
|
|
1290
1232
|
throw new BundleError(
|
|
1291
|
-
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002
|
|
1233
|
+
options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
|
|
1292
1234
|
);
|
|
1293
1235
|
}
|
|
1294
1236
|
let result;
|
|
@@ -1328,6 +1270,16 @@ async function bundleUserCode(options) {
|
|
|
1328
1270
|
}
|
|
1329
1271
|
return { code, warnings };
|
|
1330
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
|
+
}
|
|
1331
1283
|
async function typesMissingHint(cwd) {
|
|
1332
1284
|
try {
|
|
1333
1285
|
await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
@@ -1336,6 +1288,1140 @@ async function typesMissingHint(cwd) {
|
|
|
1336
1288
|
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
1337
1289
|
}
|
|
1338
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
|
+
}
|
|
1339
2425
|
|
|
1340
2426
|
// src/bin.ts
|
|
1341
2427
|
async function readVersion() {
|
|
@@ -1398,7 +2484,7 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
1398
2484
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1399
2485
|
const client = new EasyTwinClient(config);
|
|
1400
2486
|
const scene2 = await pullScene(client, id);
|
|
1401
|
-
const out = opts.out ??
|
|
2487
|
+
const out = opts.out ?? path10.join(cwd, `${id}.scene.json`);
|
|
1402
2488
|
const file = await saveSceneJson(scene2, out);
|
|
1403
2489
|
console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
|
|
1404
2490
|
});
|
|
@@ -1406,15 +2492,16 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
1406
2492
|
const config = await loadConfig(cwd);
|
|
1407
2493
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1408
2494
|
const client = new EasyTwinClient(config);
|
|
1409
|
-
const target = dirArg ?
|
|
2495
|
+
const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
|
|
1410
2496
|
const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
|
|
1411
2497
|
console.log(formatWorkspacePullResult(result));
|
|
1412
2498
|
});
|
|
1413
|
-
program.command("upload
|
|
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) => {
|
|
1414
2500
|
const config = await loadConfig(cwd);
|
|
1415
2501
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1416
2502
|
const client = new EasyTwinClient(config);
|
|
1417
|
-
|
|
2503
|
+
const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
|
|
2504
|
+
const result = await uploadDirectory(client, target, {
|
|
1418
2505
|
onProgress: (p) => {
|
|
1419
2506
|
if (p.phase === "upload") {
|
|
1420
2507
|
process.stderr.write(`\r\u5DF2\u4E0A\u4F20 ${p.current}/${p.total} bytes${p.file ? ` (${p.file})` : ""}`);
|
|
@@ -1422,15 +2509,29 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
1422
2509
|
}
|
|
1423
2510
|
});
|
|
1424
2511
|
process.stderr.write("\n");
|
|
2512
|
+
console.log(formatUploadResult(result));
|
|
1425
2513
|
});
|
|
1426
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) => {
|
|
1427
2515
|
const hint = await typesMissingHint(cwd);
|
|
1428
2516
|
if (hint) console.warn(hint);
|
|
1429
|
-
const outFile =
|
|
2517
|
+
const outFile = path10.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
|
|
1430
2518
|
const result = await bundleUserCode({ cwd, outFile });
|
|
1431
2519
|
for (const warning of result.warnings) console.warn(warning);
|
|
1432
2520
|
console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
|
|
1433
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
|
+
});
|
|
1434
2535
|
const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
|
|
1435
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) => {
|
|
1436
2537
|
const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
|
|
@@ -1464,4 +2565,3 @@ export {
|
|
|
1464
2565
|
main,
|
|
1465
2566
|
runInit
|
|
1466
2567
|
};
|
|
1467
|
-
//# sourceMappingURL=bin.js.map
|