@easytwin/devkit 0.1.2 → 0.1.4
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 +78 -79
- package/dist/bin.js +1567 -254
- package/dist/index.d.ts +256 -85
- package/dist/index.js +1699 -652
- 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 +37 -36
- package/skills/easytwin-develop/SKILL.md +32 -30
- package/skills/easytwin-render/SKILL.md +3 -3
- package/skills/easytwin-render/references/intro.md +1 -1
- package/skills/easytwin-scene/SKILL.md +37 -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/runtime-types/index.d.ts +0 -8248
package/dist/bin.js
CHANGED
|
@@ -7,18 +7,20 @@ 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
|
+
var EASYTWIN_DIR = ".easytwin";
|
|
15
16
|
var CONFIG_FILE_NAME = "easytwin.config.json";
|
|
17
|
+
var EASYTWIN_SCENES_DIRNAME = "scenes";
|
|
16
18
|
var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
|
|
17
19
|
var TEST_BASE_URL = "http://172.16.125.3:10100/";
|
|
18
|
-
var
|
|
19
|
-
var
|
|
20
|
-
var
|
|
21
|
-
var
|
|
20
|
+
var TEST_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
21
|
+
var PROD_OSS_URL = "https://dt-easyv-prod.oss-cn-hangzhou.aliyuncs.com/";
|
|
22
|
+
var DEFAULT_OSS_URL = TEST_OSS_URL;
|
|
23
|
+
var RUNTIME_TYPES_RELPATH = "easytwin/system/libs/runtime/types/index.d.ts";
|
|
22
24
|
var MOCK_APP_ID = "test";
|
|
23
25
|
var MOCK_APP_SECRET = "test";
|
|
24
26
|
function isMockCredentials(appId, appSecret) {
|
|
@@ -31,8 +33,31 @@ var ConfigError = class extends Error {
|
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
35
|
function configFilePath(cwd) {
|
|
36
|
+
return path.join(cwd, EASYTWIN_DIR, CONFIG_FILE_NAME);
|
|
37
|
+
}
|
|
38
|
+
function joinOssPath(ossUrl, relPath) {
|
|
39
|
+
return `${ossUrl.replace(/\/+$/, "")}/${relPath.replace(/^\/+/, "")}`;
|
|
40
|
+
}
|
|
41
|
+
function resolveRuntimeTypesUrl(options = {}) {
|
|
42
|
+
const root = options.ossUrl ?? (options.env === "prod" ? PROD_OSS_URL : TEST_OSS_URL);
|
|
43
|
+
return joinOssPath(root, RUNTIME_TYPES_RELPATH);
|
|
44
|
+
}
|
|
45
|
+
async function resolveRuntimeTypesUrlForCwd(cwd, env = process.env) {
|
|
46
|
+
let file;
|
|
47
|
+
try {
|
|
48
|
+
file = await readConfigFile(cwd);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file?.env;
|
|
52
|
+
const ossUrl = env.EASYTWIN_OSS_URL ?? file?.ossUrl;
|
|
53
|
+
return resolveRuntimeTypesUrl({ env: easyEnv, ossUrl });
|
|
54
|
+
}
|
|
55
|
+
function legacyConfigFilePath(cwd) {
|
|
34
56
|
return path.join(cwd, CONFIG_FILE_NAME);
|
|
35
57
|
}
|
|
58
|
+
function defaultSceneOutPath(cwd, id) {
|
|
59
|
+
return path.join(cwd, EASYTWIN_DIR, EASYTWIN_SCENES_DIRNAME, `${path.basename(id)}.scene.json`);
|
|
60
|
+
}
|
|
36
61
|
function parseConfig(raw) {
|
|
37
62
|
let parsed;
|
|
38
63
|
try {
|
|
@@ -62,29 +87,14 @@ function validateConfigShape(value) {
|
|
|
62
87
|
if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
|
|
63
88
|
throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
|
|
64
89
|
}
|
|
65
|
-
const opAccountId = optionalGatewayId(v.opAccountId);
|
|
66
|
-
const opUserId = optionalGatewayId(v.opUserId);
|
|
67
|
-
const spaceId = optionalGatewayId(v.spaceId);
|
|
68
90
|
const config = { appId: v.appId, appSecret: v.appSecret };
|
|
69
91
|
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
70
92
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
71
93
|
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
94
|
const scenes = parseConfigScenes(v.scenes);
|
|
76
95
|
if (scenes) config.scenes = scenes;
|
|
77
96
|
return config;
|
|
78
97
|
}
|
|
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
98
|
function parseConfigScenes(value) {
|
|
89
99
|
if (value === void 0) return void 0;
|
|
90
100
|
if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
@@ -115,15 +125,39 @@ function parseConfigScenes(value) {
|
|
|
115
125
|
return scene;
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
|
-
async function
|
|
119
|
-
const file = configFilePath(cwd);
|
|
120
|
-
let raw;
|
|
128
|
+
async function tryReadText(file) {
|
|
121
129
|
try {
|
|
122
|
-
|
|
130
|
+
return await fs.readFile(file, "utf8");
|
|
123
131
|
} catch {
|
|
124
|
-
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function removeLegacyConfigFile(cwd) {
|
|
136
|
+
try {
|
|
137
|
+
await fs.unlink(legacyConfigFilePath(cwd));
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function migrateLegacyConfigIfPresent(cwd) {
|
|
142
|
+
const raw = await tryReadText(legacyConfigFilePath(cwd));
|
|
143
|
+
if (raw === void 0) return void 0;
|
|
144
|
+
const config = parseConfig(raw);
|
|
145
|
+
await writeConfigFile(cwd, config);
|
|
146
|
+
await removeLegacyConfigFile(cwd);
|
|
147
|
+
await appendGitignore(cwd);
|
|
148
|
+
return config;
|
|
149
|
+
}
|
|
150
|
+
async function readConfigFile(cwd) {
|
|
151
|
+
const file = configFilePath(cwd);
|
|
152
|
+
const raw = await tryReadText(file);
|
|
153
|
+
if (raw !== void 0) {
|
|
154
|
+
const config = parseConfig(raw);
|
|
155
|
+
await removeLegacyConfigFile(cwd);
|
|
156
|
+
return config;
|
|
125
157
|
}
|
|
126
|
-
|
|
158
|
+
const migrated = await migrateLegacyConfigIfPresent(cwd);
|
|
159
|
+
if (migrated) return migrated;
|
|
160
|
+
throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
|
|
127
161
|
}
|
|
128
162
|
function resolveConfig(file, env = process.env) {
|
|
129
163
|
const appId = env.EASYTWIN_APP_ID ?? file.appId;
|
|
@@ -134,30 +168,11 @@ function resolveConfig(file, env = process.env) {
|
|
|
134
168
|
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
169
|
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
170
|
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;
|
|
171
|
+
return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
146
172
|
}
|
|
147
173
|
async function loadConfig(cwd, env = process.env) {
|
|
148
174
|
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;
|
|
175
|
+
return resolveConfig(file, env);
|
|
161
176
|
}
|
|
162
177
|
async function writeConfigFile(cwd, config) {
|
|
163
178
|
const file = configFilePath(cwd);
|
|
@@ -165,19 +180,15 @@ async function writeConfigFile(cwd, config) {
|
|
|
165
180
|
if (config.env) body.env = config.env;
|
|
166
181
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
167
182
|
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
183
|
if (config.scenes !== void 0) body.scenes = config.scenes;
|
|
175
|
-
await fs.mkdir(
|
|
184
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
176
185
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
186
|
+
await removeLegacyConfigFile(cwd);
|
|
177
187
|
return file;
|
|
178
188
|
}
|
|
179
189
|
var GITIGNORE_FILE_NAME = ".gitignore";
|
|
180
|
-
var GITIGNORE_ENTRY = "easytwin.config.json";
|
|
190
|
+
var GITIGNORE_ENTRY = ".easytwin/easytwin.config.json";
|
|
191
|
+
var GITIGNORE_ENTRIES = [GITIGNORE_ENTRY, ".easytwin/scenes/"];
|
|
181
192
|
async function appendGitignore(cwd) {
|
|
182
193
|
const file = path.join(cwd, GITIGNORE_FILE_NAME);
|
|
183
194
|
let content = "";
|
|
@@ -188,9 +199,10 @@ async function appendGitignore(cwd) {
|
|
|
188
199
|
created = true;
|
|
189
200
|
}
|
|
190
201
|
const lines = content.split(/\r?\n/);
|
|
191
|
-
|
|
202
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
|
|
203
|
+
if (missing.length === 0) return { created, added: false };
|
|
192
204
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
193
|
-
await fs.writeFile(file, content + prefix +
|
|
205
|
+
await fs.writeFile(file, content + prefix + missing.join("\n") + "\n", "utf8");
|
|
194
206
|
return { created, added: true };
|
|
195
207
|
}
|
|
196
208
|
async function initConfig(input, cwd) {
|
|
@@ -208,22 +220,20 @@ async function writeConfigScenes(cwd, scenes) {
|
|
|
208
220
|
import http from "http";
|
|
209
221
|
import https from "https";
|
|
210
222
|
import { URL as URL2 } from "url";
|
|
223
|
+
var APP_ID_HEADER = "x-app-id";
|
|
211
224
|
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
225
|
function enc(id) {
|
|
216
226
|
return encodeURIComponent(id);
|
|
217
227
|
}
|
|
218
228
|
var ENDPOINTS = {
|
|
219
|
-
/**
|
|
220
|
-
linkedScenes: (
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
|
|
229
|
+
/** POST 已关联场景列表(分享路径,空 body)。 */
|
|
230
|
+
linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
|
|
231
|
+
/** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
|
|
232
|
+
workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
|
|
233
|
+
/** POST 一次推送 create/update/delete(分享路径)。 */
|
|
234
|
+
workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
|
|
235
|
+
/** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
|
|
236
|
+
workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
|
|
227
237
|
};
|
|
228
238
|
var EasyTwinApiError = class extends Error {
|
|
229
239
|
status;
|
|
@@ -266,35 +276,25 @@ var EasyTwinClient = class {
|
|
|
266
276
|
baseUrl;
|
|
267
277
|
/** twin runtime / 场景快照资产根(baseOSSUrl)。 */
|
|
268
278
|
ossUrl;
|
|
269
|
-
/** 接入凭证 App ID
|
|
279
|
+
/** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
|
|
270
280
|
appId;
|
|
271
281
|
/** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
272
282
|
mock;
|
|
273
283
|
appSecret;
|
|
274
|
-
opAccountId;
|
|
275
|
-
opUserId;
|
|
276
|
-
spaceId;
|
|
277
284
|
constructor(config) {
|
|
278
285
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
279
286
|
this.ossUrl = config.ossUrl.replace(/\/+$/, "");
|
|
280
287
|
this.mock = config.mock;
|
|
281
288
|
this.appId = config.appId;
|
|
282
289
|
this.appSecret = config.appSecret;
|
|
283
|
-
this.opAccountId = config.opAccountId;
|
|
284
|
-
this.opUserId = config.opUserId;
|
|
285
|
-
this.spaceId = config.spaceId;
|
|
286
290
|
}
|
|
287
291
|
/** 全仓唯一认证头注入点。 */
|
|
288
292
|
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;
|
|
293
|
+
return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
|
|
294
294
|
}
|
|
295
295
|
/** JSON 请求(原生 fetch)。 */
|
|
296
|
-
async request(
|
|
297
|
-
const url = `${this.baseUrl}${
|
|
296
|
+
async request(path11, options = {}) {
|
|
297
|
+
const url = `${this.baseUrl}${path11}`;
|
|
298
298
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
299
299
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
300
300
|
headers["Content-Type"] = "application/json";
|
|
@@ -316,8 +316,8 @@ var EasyTwinClient = class {
|
|
|
316
316
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
317
317
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
318
318
|
*/
|
|
319
|
-
async upload(
|
|
320
|
-
const url = new URL2(`${this.baseUrl}${
|
|
319
|
+
async upload(path11, options) {
|
|
320
|
+
const url = new URL2(`${this.baseUrl}${path11}`);
|
|
321
321
|
const mod = url.protocol === "https:" ? https : http;
|
|
322
322
|
const headers = {
|
|
323
323
|
...this.authHeaders(),
|
|
@@ -474,42 +474,215 @@ function deriveExampleSceneId(payload) {
|
|
|
474
474
|
async function exampleSceneSummary(exampleFile) {
|
|
475
475
|
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
476
476
|
}
|
|
477
|
+
async function fetchLinkedScenes(client) {
|
|
478
|
+
return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
|
|
479
|
+
}
|
|
477
480
|
async function listScenes(client, options = {}) {
|
|
478
|
-
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await
|
|
481
|
+
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
|
|
479
482
|
if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
|
|
480
483
|
return scenes;
|
|
481
484
|
}
|
|
482
485
|
async function pullScene(client, id, options = {}) {
|
|
486
|
+
let scene;
|
|
483
487
|
if (client.mock) {
|
|
484
|
-
const
|
|
485
|
-
const sceneId = deriveExampleSceneId(
|
|
488
|
+
const payload = await loadExampleScene(options.exampleFile);
|
|
489
|
+
const sceneId = deriveExampleSceneId(payload);
|
|
486
490
|
if (sceneId !== id) {
|
|
487
491
|
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)`);
|
|
488
492
|
}
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
493
|
+
scene = { id: sceneId, name: MOCK_SCENE_NAME, payload };
|
|
494
|
+
} else {
|
|
495
|
+
const data = await fetchLinkedScenes(client);
|
|
496
|
+
const scenes = normalizeLinkedScenes(data);
|
|
497
|
+
const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
|
|
498
|
+
if (!hit) {
|
|
499
|
+
const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
|
|
500
|
+
throw new Error(
|
|
501
|
+
available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
|
|
505
|
+
scene = { id: hit.sceneKey, name: hit.name, payload };
|
|
499
506
|
}
|
|
500
|
-
|
|
501
|
-
return
|
|
507
|
+
if (options.cwd) await saveSceneJson(scene, defaultSceneOutPath(options.cwd, scene.id));
|
|
508
|
+
return scene;
|
|
502
509
|
}
|
|
503
510
|
async function saveSceneJson(scene, out) {
|
|
504
511
|
await fs2.mkdir(path2.dirname(out), { recursive: true });
|
|
505
512
|
await fs2.writeFile(out, JSON.stringify(scene.payload ?? scene, null, 2) + "\n", "utf8");
|
|
506
513
|
return out;
|
|
507
514
|
}
|
|
515
|
+
function parseSceneStructure(scene) {
|
|
516
|
+
let data = scene;
|
|
517
|
+
if (typeof data === "string") {
|
|
518
|
+
try {
|
|
519
|
+
data = JSON.parse(data);
|
|
520
|
+
} catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const root = data ?? {};
|
|
525
|
+
const objs = Array.isArray(root.objs) ? root.objs : [];
|
|
526
|
+
const hierarchy = Array.isArray(root.hierarchy) ? root.hierarchy : [];
|
|
527
|
+
const items = objs.length > 0 ? objs : hierarchy;
|
|
528
|
+
const typeById = /* @__PURE__ */ new Map();
|
|
529
|
+
for (const o of objs) {
|
|
530
|
+
if (typeof o.id === "string" && typeof o.type === "string") typeById.set(o.id, o.type);
|
|
531
|
+
}
|
|
532
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
533
|
+
const order = [];
|
|
534
|
+
for (const item of items) {
|
|
535
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
536
|
+
if (id.length === 0 || nodes.has(id)) continue;
|
|
537
|
+
const name = typeof item.name === "string" && item.name.length > 0 ? item.name : id;
|
|
538
|
+
nodes.set(id, { id, name, type: typeById.get(id), children: [] });
|
|
539
|
+
order.push(id);
|
|
540
|
+
}
|
|
541
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const item of items) {
|
|
543
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
544
|
+
if (id.length === 0 || !nodes.has(id)) continue;
|
|
545
|
+
const parentId = typeof item.parentObjId === "string" && item.parentObjId.length > 0 ? item.parentObjId : "";
|
|
546
|
+
if (parentId.length === 0 || parentId === id || !nodes.has(parentId)) continue;
|
|
547
|
+
const kids = childrenOf.get(parentId) ?? [];
|
|
548
|
+
kids.push(id);
|
|
549
|
+
childrenOf.set(parentId, kids);
|
|
550
|
+
}
|
|
551
|
+
const isChild = /* @__PURE__ */ new Set();
|
|
552
|
+
for (const kids of childrenOf.values()) for (const k of kids) isChild.add(k);
|
|
553
|
+
const build2 = (id, seen) => {
|
|
554
|
+
const node = nodes.get(id);
|
|
555
|
+
const children = [];
|
|
556
|
+
if (!seen.has(id)) {
|
|
557
|
+
seen.add(id);
|
|
558
|
+
for (const kid of childrenOf.get(id) ?? []) children.push(build2(kid, seen));
|
|
559
|
+
seen.delete(id);
|
|
560
|
+
}
|
|
561
|
+
return { ...node, children };
|
|
562
|
+
};
|
|
563
|
+
return order.filter((id) => !isChild.has(id)).map((id) => build2(id, /* @__PURE__ */ new Set()));
|
|
564
|
+
}
|
|
565
|
+
var INSPECT_NODE_LIMIT = 200;
|
|
566
|
+
var INSPECT_INDENT = " ";
|
|
567
|
+
function nodeMatchesInspect(node, filter) {
|
|
568
|
+
if (filter.name !== void 0 && filter.name.length > 0) {
|
|
569
|
+
if (!node.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
|
|
570
|
+
}
|
|
571
|
+
if (filter.type !== void 0 && filter.type.length > 0) {
|
|
572
|
+
if (node.type !== filter.type) return false;
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
function filterSceneTree(roots, filter = {}) {
|
|
577
|
+
const hasName = filter.name !== void 0 && filter.name.length > 0;
|
|
578
|
+
const hasType = filter.type !== void 0 && filter.type.length > 0;
|
|
579
|
+
if (!hasName && !hasType) return roots;
|
|
580
|
+
const walk = (node) => {
|
|
581
|
+
const children = [];
|
|
582
|
+
for (const child of node.children) {
|
|
583
|
+
const kept = walk(child);
|
|
584
|
+
if (kept) children.push(kept);
|
|
585
|
+
}
|
|
586
|
+
if (nodeMatchesInspect(node, filter) || children.length > 0) {
|
|
587
|
+
return { ...node, children };
|
|
588
|
+
}
|
|
589
|
+
return void 0;
|
|
590
|
+
};
|
|
591
|
+
return roots.flatMap((node) => {
|
|
592
|
+
const kept = walk(node);
|
|
593
|
+
return kept ? [kept] : [];
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function countSceneTreeNodes(roots) {
|
|
597
|
+
let n = 0;
|
|
598
|
+
const walk = (nodes) => {
|
|
599
|
+
for (const node of nodes) {
|
|
600
|
+
n += 1;
|
|
601
|
+
walk(node.children);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
walk(roots);
|
|
605
|
+
return n;
|
|
606
|
+
}
|
|
607
|
+
function formatSceneTree(roots, limit = INSPECT_NODE_LIMIT) {
|
|
608
|
+
const total = countSceneTreeNodes(roots);
|
|
609
|
+
if (total === 0) {
|
|
610
|
+
return { text: "(\u65E0\u5BF9\u8C61\u7ED3\u6784)\n", printed: 0, total: 0, truncated: false };
|
|
611
|
+
}
|
|
612
|
+
const lines = [];
|
|
613
|
+
let printed = 0;
|
|
614
|
+
const walk = (nodes, depth) => {
|
|
615
|
+
for (const node of nodes) {
|
|
616
|
+
if (printed >= limit) return;
|
|
617
|
+
lines.push(`${INSPECT_INDENT.repeat(depth)}${node.id} ${node.name} ${node.type ?? ""}`);
|
|
618
|
+
printed += 1;
|
|
619
|
+
walk(node.children, depth + 1);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
walk(roots, 0);
|
|
623
|
+
const truncated = printed < total;
|
|
624
|
+
if (truncated) lines.push(`\u5176\u4F59 ${total - printed} \u4E2A,\u8BF7\u52A0 --name/--type`);
|
|
625
|
+
return { text: `${lines.join("\n")}
|
|
626
|
+
`, printed, total, truncated };
|
|
627
|
+
}
|
|
628
|
+
async function inspectScene(options) {
|
|
629
|
+
const file = defaultSceneOutPath(options.cwd, options.id);
|
|
630
|
+
let raw;
|
|
631
|
+
try {
|
|
632
|
+
raw = await fs2.readFile(file, "utf8");
|
|
633
|
+
} catch {
|
|
634
|
+
throw new Error(`\u672A\u627E\u5230\u672C\u5730\u573A\u666F\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin scene pull ${options.id}\``);
|
|
635
|
+
}
|
|
636
|
+
let payload;
|
|
637
|
+
try {
|
|
638
|
+
payload = JSON.parse(raw);
|
|
639
|
+
} catch {
|
|
640
|
+
throw new Error(`\u573A\u666F\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
|
|
641
|
+
}
|
|
642
|
+
const roots = filterSceneTree(parseSceneStructure(payload), { name: options.name, type: options.type });
|
|
643
|
+
return { file, roots, ...formatSceneTree(roots, options.limit ?? INSPECT_NODE_LIMIT) };
|
|
644
|
+
}
|
|
508
645
|
|
|
509
646
|
// src/upload.ts
|
|
510
647
|
import { promises as fs3 } from "fs";
|
|
511
648
|
import path3 from "path";
|
|
512
|
-
var
|
|
649
|
+
var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
650
|
+
".git",
|
|
651
|
+
"node_modules",
|
|
652
|
+
"dist",
|
|
653
|
+
".easytwin",
|
|
654
|
+
".cursor",
|
|
655
|
+
".claude",
|
|
656
|
+
".qoder",
|
|
657
|
+
".vscode"
|
|
658
|
+
]);
|
|
659
|
+
var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
|
|
660
|
+
function isTsconfigFile(base) {
|
|
661
|
+
return /^tsconfig(\..+)?\.json$/i.test(base);
|
|
662
|
+
}
|
|
663
|
+
var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
|
|
664
|
+
function isWorkspaceSpecFile(relPath) {
|
|
665
|
+
const base = normalizePath(relPath).split("/").pop() ?? "";
|
|
666
|
+
return /\.spec\.ts$/i.test(base);
|
|
667
|
+
}
|
|
668
|
+
function isIgnoredWorkspacePath(relPath) {
|
|
669
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
670
|
+
if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
|
|
671
|
+
const base = parts[parts.length - 1];
|
|
672
|
+
if (base === void 0) return false;
|
|
673
|
+
if (WORKSPACE_IGNORED_FILES.has(base)) return true;
|
|
674
|
+
if (isTsconfigFile(base)) return true;
|
|
675
|
+
return base.toLowerCase().endsWith(".scene.json");
|
|
676
|
+
}
|
|
677
|
+
function defaultWorkspaceIgnore(relPath) {
|
|
678
|
+
return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
|
|
679
|
+
}
|
|
680
|
+
function combineUploadIgnore(extra) {
|
|
681
|
+
return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
682
|
+
}
|
|
683
|
+
async function collectWorkspaceCodeFiles(dir, ignore) {
|
|
684
|
+
return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
|
|
685
|
+
}
|
|
513
686
|
async function collectFiles(dir, ignore = () => false) {
|
|
514
687
|
const files = [];
|
|
515
688
|
async function walk(current, rel) {
|
|
@@ -518,7 +691,7 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
518
691
|
const relPath = path3.posix.join(rel, entry.name);
|
|
519
692
|
if (ignore(relPath)) continue;
|
|
520
693
|
if (entry.isDirectory()) {
|
|
521
|
-
if (
|
|
694
|
+
if (entry.name === ".git") continue;
|
|
522
695
|
await walk(path3.join(current, entry.name), relPath);
|
|
523
696
|
} else if (entry.isFile()) {
|
|
524
697
|
const absPath = path3.join(current, entry.name);
|
|
@@ -537,26 +710,9 @@ function asRecord2(value) {
|
|
|
537
710
|
function asString2(value) {
|
|
538
711
|
return typeof value === "string" ? value : "";
|
|
539
712
|
}
|
|
540
|
-
function asNumber(value) {
|
|
541
|
-
return typeof value === "number" && Number.isFinite(value) ? value : NaN;
|
|
542
|
-
}
|
|
543
713
|
function normalizePath(relPath) {
|
|
544
714
|
return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
545
715
|
}
|
|
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
716
|
function normalizeWorkspaceFiles(data) {
|
|
561
717
|
if (!Array.isArray(data)) throw new Error("workspace code \u54CD\u5E94\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B");
|
|
562
718
|
return data.map((item) => {
|
|
@@ -573,91 +729,98 @@ function extensionOf(relPath) {
|
|
|
573
729
|
function allowedExtensionSet(list) {
|
|
574
730
|
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
575
731
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return
|
|
732
|
+
var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
|
|
733
|
+
function isWorkspaceCodeFile(relPath) {
|
|
734
|
+
return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
|
|
579
735
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
736
|
+
function countsFromPlan(plan) {
|
|
737
|
+
return {
|
|
738
|
+
created: plan.creates.length,
|
|
739
|
+
updated: plan.updates.length,
|
|
740
|
+
deleted: plan.deletes.length,
|
|
741
|
+
unchanged: plan.unchanged.length
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function planWorkspaceUpload(local, remote) {
|
|
745
|
+
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
746
|
+
const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
|
|
747
|
+
const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
|
|
748
|
+
const creates = [];
|
|
749
|
+
const updates = [];
|
|
750
|
+
const unchanged = [];
|
|
751
|
+
for (const item of local) {
|
|
752
|
+
const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
|
|
753
|
+
if (!remoteFile) creates.push(item);
|
|
754
|
+
else if (remoteFile.content !== item.content) {
|
|
755
|
+
updates.push({ file: item.file, id: remoteFile.id, content: item.content });
|
|
756
|
+
} else {
|
|
757
|
+
unchanged.push(item.file);
|
|
598
758
|
}
|
|
599
759
|
}
|
|
600
|
-
|
|
760
|
+
return { creates, updates, deletes, unchanged };
|
|
761
|
+
}
|
|
762
|
+
function formatUploadResult(result) {
|
|
763
|
+
const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
|
|
764
|
+
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
765
|
}
|
|
602
766
|
async function mockUpload(dir, options) {
|
|
603
|
-
const ignore = options.ignore
|
|
604
|
-
const files = await
|
|
767
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
768
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
605
769
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
606
770
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
607
771
|
options.onProgress?.({ phase: "upload", current: total, total });
|
|
608
|
-
return { fileCount: files.length, byteCount: total, mock: true };
|
|
772
|
+
return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
|
|
773
|
+
}
|
|
774
|
+
function buildWorkspacePushBody(plan) {
|
|
775
|
+
const body = {};
|
|
776
|
+
if (plan.creates.length > 0) {
|
|
777
|
+
body.create = plan.creates.map((c) => ({
|
|
778
|
+
filePath: normalizePath(c.file.relPath),
|
|
779
|
+
content: c.content
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
if (plan.updates.length > 0) {
|
|
783
|
+
body.update = plan.updates.map((p) => ({
|
|
784
|
+
id: p.id,
|
|
785
|
+
content: p.content,
|
|
786
|
+
filePath: normalizePath(p.file.relPath)
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
if (plan.deletes.length > 0) {
|
|
790
|
+
body.delete = plan.deletes.map((d) => d.id);
|
|
791
|
+
}
|
|
792
|
+
return Object.keys(body).length > 0 ? body : null;
|
|
609
793
|
}
|
|
610
794
|
async function uploadDirectory(client, dir, options = {}) {
|
|
611
795
|
if (client.mock) return mockUpload(dir, options);
|
|
612
|
-
const ignore = options.ignore
|
|
613
|
-
const files = await
|
|
796
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
797
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
614
798
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
615
799
|
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 = [];
|
|
800
|
+
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
801
|
+
const local = [];
|
|
632
802
|
for (const file of files) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
803
|
+
local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
|
|
804
|
+
}
|
|
805
|
+
const plan = planWorkspaceUpload(local, remote);
|
|
806
|
+
const counts = countsFromPlan(plan);
|
|
807
|
+
const pushBody = buildWorkspacePushBody(plan);
|
|
808
|
+
if (pushBody) {
|
|
809
|
+
await client.request(ENDPOINTS.workspaceCodePush(), {
|
|
810
|
+
method: "POST",
|
|
811
|
+
body: JSON.stringify(pushBody)
|
|
812
|
+
});
|
|
638
813
|
}
|
|
639
814
|
let sent = 0;
|
|
640
815
|
const bump = (file) => {
|
|
641
816
|
sent += file.size;
|
|
642
817
|
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
643
818
|
};
|
|
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);
|
|
819
|
+
for (const c of plan.creates) bump(c.file);
|
|
820
|
+
for (const p of plan.updates) bump(p.file);
|
|
821
|
+
for (const u of plan.unchanged) bump(u);
|
|
659
822
|
if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
|
|
660
|
-
return { fileCount: files.length, byteCount: total };
|
|
823
|
+
return { fileCount: files.length, byteCount: total, ...counts };
|
|
661
824
|
}
|
|
662
825
|
|
|
663
826
|
// src/workspace.ts
|
|
@@ -705,42 +868,22 @@ export default class App extends TwinApp {
|
|
|
705
868
|
}
|
|
706
869
|
}
|
|
707
870
|
`;
|
|
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
871
|
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);
|
|
872
|
+
return defaultWorkspaceIgnore(relPath);
|
|
725
873
|
}
|
|
726
874
|
function isSafeRelPath(relPath) {
|
|
727
875
|
const n = normalizePath(relPath);
|
|
728
876
|
if (!n) return false;
|
|
729
|
-
|
|
877
|
+
const lower = n.toLowerCase();
|
|
878
|
+
if (lower === "easytwin.config.json" || lower.endsWith("/easytwin.config.json")) return false;
|
|
879
|
+
if (lower.split("/")[0] === ".easytwin") return false;
|
|
880
|
+
const base = n.split("/").pop() ?? "";
|
|
881
|
+
if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
|
|
730
882
|
if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
|
|
731
883
|
const parts = n.split("/");
|
|
732
884
|
if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
|
|
733
885
|
return true;
|
|
734
886
|
}
|
|
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
887
|
function normalizeEol(text) {
|
|
745
888
|
return text.replace(/\r\n/g, "\n");
|
|
746
889
|
}
|
|
@@ -756,30 +899,18 @@ async function readLocalText(absPath) {
|
|
|
756
899
|
throw err;
|
|
757
900
|
}
|
|
758
901
|
}
|
|
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
902
|
async function fetchRemoteFiles(client) {
|
|
771
903
|
if (client.mock) return [];
|
|
772
|
-
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.
|
|
904
|
+
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
773
905
|
}
|
|
774
906
|
async function planWorkspacePull(client, dir, options = {}) {
|
|
775
907
|
const ignore = combineIgnore(options.ignore);
|
|
776
|
-
const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
|
|
777
908
|
const remoteRaw = await fetchRemoteFiles(client);
|
|
778
909
|
const skippedRemotePaths = [];
|
|
779
910
|
const remoteByPath = /* @__PURE__ */ new Map();
|
|
780
911
|
for (const file of remoteRaw) {
|
|
781
912
|
const rel = normalizePath(file.filePath);
|
|
782
|
-
if (!isSafeRelPath(rel) || ignore(rel)) {
|
|
913
|
+
if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
|
|
783
914
|
skippedRemotePaths.push(rel || file.filePath);
|
|
784
915
|
continue;
|
|
785
916
|
}
|
|
@@ -795,7 +926,7 @@ async function planWorkspacePull(client, dir, options = {}) {
|
|
|
795
926
|
const localByPath = /* @__PURE__ */ new Map();
|
|
796
927
|
for (const file of localFiles) {
|
|
797
928
|
const rel = normalizePath(file.relPath);
|
|
798
|
-
if (!
|
|
929
|
+
if (!isWorkspaceCodeFile(rel)) continue;
|
|
799
930
|
const content = await readLocalText(file.absPath);
|
|
800
931
|
if (content !== void 0) localByPath.set(rel, content);
|
|
801
932
|
}
|
|
@@ -920,7 +1051,8 @@ var SKILL_NAMES = [
|
|
|
920
1051
|
"easytwin-develop",
|
|
921
1052
|
"easytwin-bootstrap",
|
|
922
1053
|
"easytwin-scene",
|
|
923
|
-
"easytwin-upload"
|
|
1054
|
+
"easytwin-upload",
|
|
1055
|
+
"easytwin-test"
|
|
924
1056
|
];
|
|
925
1057
|
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
926
1058
|
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
@@ -998,6 +1130,12 @@ function buildRuntimeTypesContent(sourceDts) {
|
|
|
998
1130
|
return `${sourceDts.replace(/\s+$/, "")}
|
|
999
1131
|
`;
|
|
1000
1132
|
}
|
|
1133
|
+
var SkillsError = class extends Error {
|
|
1134
|
+
constructor(message) {
|
|
1135
|
+
super(message);
|
|
1136
|
+
this.name = "SkillsError";
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1001
1139
|
function normalizeTargets(target = "all") {
|
|
1002
1140
|
if (target === "all") return ["cursor", "claude", "codex", "qoder"];
|
|
1003
1141
|
if (Array.isArray(target)) return [...new Set(target)];
|
|
@@ -1010,16 +1148,23 @@ function resolveSkillsSourceDir() {
|
|
|
1010
1148
|
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1011
1149
|
return path6.resolve(here, "..", "skills");
|
|
1012
1150
|
}
|
|
1013
|
-
function
|
|
1014
|
-
|
|
1015
|
-
|
|
1151
|
+
async function fetchRuntimeTypesDts(url, fetchImpl) {
|
|
1152
|
+
let res;
|
|
1153
|
+
try {
|
|
1154
|
+
res = await fetchImpl(url);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
throw new SkillsError(
|
|
1157
|
+
`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:${url} (${err instanceof Error ? err.message : String(err)})`
|
|
1158
|
+
);
|
|
1016
1159
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
1160
|
+
if (!res.ok) {
|
|
1161
|
+
throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:HTTP ${res.status} ${url}`);
|
|
1162
|
+
}
|
|
1163
|
+
const text = await res.text();
|
|
1164
|
+
if (text.trim().length === 0) {
|
|
1165
|
+
throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:\u7A7A\u54CD\u5E94 ${url}`);
|
|
1166
|
+
}
|
|
1167
|
+
return text;
|
|
1023
1168
|
}
|
|
1024
1169
|
async function readJson(file) {
|
|
1025
1170
|
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
@@ -1106,11 +1251,12 @@ function buildCodexSegment(version) {
|
|
|
1106
1251
|
`EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
|
|
1107
1252
|
"",
|
|
1108
1253
|
"- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
|
|
1109
|
-
"- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
|
|
1110
|
-
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
|
|
1254
|
+
"- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(`.easytwin/easytwin.config.json`)\u3002",
|
|
1255
|
+
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3001inspect \u672C\u5730\u5BF9\u8C61\u6811\u3002",
|
|
1111
1256
|
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
1112
1257
|
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
1113
1258
|
"- `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",
|
|
1259
|
+
"- `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
1260
|
"",
|
|
1115
1261
|
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
1116
1262
|
CODEX_MARKER_END
|
|
@@ -1145,11 +1291,11 @@ async function syncToCodex(sourceRoot, cwd, version) {
|
|
|
1145
1291
|
}
|
|
1146
1292
|
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
1147
1293
|
}
|
|
1148
|
-
async function syncRuntimeTypes(cwd,
|
|
1294
|
+
async function syncRuntimeTypes(cwd, sourceDts) {
|
|
1149
1295
|
const destDir = path6.join(cwd, ".easytwin", "types");
|
|
1150
1296
|
const destFile = path6.join(destDir, "index.d.ts");
|
|
1151
1297
|
const appsFile = path6.join(destDir, APPS_TYPES_FILE);
|
|
1152
|
-
const content = buildRuntimeTypesContent(
|
|
1298
|
+
const content = buildRuntimeTypesContent(sourceDts);
|
|
1153
1299
|
let runtimeCurrent = "";
|
|
1154
1300
|
let appsCurrent = "";
|
|
1155
1301
|
let runtimeExists = true;
|
|
@@ -1199,7 +1345,11 @@ async function syncSkills(options) {
|
|
|
1199
1345
|
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
|
|
1200
1346
|
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
1201
1347
|
}
|
|
1202
|
-
const
|
|
1348
|
+
const typesSource = options.typesSourceFile ? await fs5.readFile(options.typesSourceFile, "utf8") : await fetchRuntimeTypesDts(
|
|
1349
|
+
await resolveRuntimeTypesUrlForCwd(options.cwd, options.env ?? process.env),
|
|
1350
|
+
options.fetch ?? fetch
|
|
1351
|
+
);
|
|
1352
|
+
const types = await syncRuntimeTypes(options.cwd, typesSource);
|
|
1203
1353
|
return { summaries, types };
|
|
1204
1354
|
}
|
|
1205
1355
|
|
|
@@ -1281,14 +1431,14 @@ function collectBundledRuntimeExports(code) {
|
|
|
1281
1431
|
}
|
|
1282
1432
|
return names;
|
|
1283
1433
|
}
|
|
1284
|
-
async function
|
|
1434
|
+
async function bundleWorkspaceModule(options) {
|
|
1285
1435
|
const cwd = path7.resolve(options.cwd);
|
|
1286
|
-
const entry = path7.join(cwd,
|
|
1436
|
+
const entry = path7.isAbsolute(options.entry) ? options.entry : path7.join(cwd, options.entry);
|
|
1287
1437
|
try {
|
|
1288
1438
|
await fs6.access(entry);
|
|
1289
1439
|
} catch {
|
|
1290
1440
|
throw new BundleError(
|
|
1291
|
-
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002
|
|
1441
|
+
options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
|
|
1292
1442
|
);
|
|
1293
1443
|
}
|
|
1294
1444
|
let result;
|
|
@@ -1328,6 +1478,16 @@ async function bundleUserCode(options) {
|
|
|
1328
1478
|
}
|
|
1329
1479
|
return { code, warnings };
|
|
1330
1480
|
}
|
|
1481
|
+
async function bundleUserCode(options) {
|
|
1482
|
+
const cwd = path7.resolve(options.cwd);
|
|
1483
|
+
const entry = path7.join(cwd, USER_ENTRY);
|
|
1484
|
+
return bundleWorkspaceModule({
|
|
1485
|
+
cwd: options.cwd,
|
|
1486
|
+
entry: USER_ENTRY,
|
|
1487
|
+
outFile: options.outFile,
|
|
1488
|
+
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`
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1331
1491
|
async function typesMissingHint(cwd) {
|
|
1332
1492
|
try {
|
|
1333
1493
|
await fs6.access(path7.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
@@ -1336,6 +1496,1140 @@ async function typesMissingHint(cwd) {
|
|
|
1336
1496
|
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
1337
1497
|
}
|
|
1338
1498
|
}
|
|
1499
|
+
var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
|
|
1500
|
+
var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1501
|
+
function decodeVLQValues(str) {
|
|
1502
|
+
const values = [];
|
|
1503
|
+
let i = 0;
|
|
1504
|
+
while (i < str.length) {
|
|
1505
|
+
let result = 0;
|
|
1506
|
+
let shift = 0;
|
|
1507
|
+
let continuation = true;
|
|
1508
|
+
while (continuation) {
|
|
1509
|
+
if (i >= str.length) return values;
|
|
1510
|
+
const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
|
|
1511
|
+
if (digit < 0) return values;
|
|
1512
|
+
continuation = (digit & 32) !== 0;
|
|
1513
|
+
result += (digit & 31) << shift;
|
|
1514
|
+
shift += 5;
|
|
1515
|
+
}
|
|
1516
|
+
values.push(result & 1 ? -(result >> 1) : result >> 1);
|
|
1517
|
+
}
|
|
1518
|
+
return values;
|
|
1519
|
+
}
|
|
1520
|
+
function decodeMappings(map) {
|
|
1521
|
+
const sources = map.sources ?? [];
|
|
1522
|
+
const lines = (map.mappings ?? "").split(";");
|
|
1523
|
+
let sourceIndex = 0;
|
|
1524
|
+
let originalLine = 0;
|
|
1525
|
+
let originalColumn = 0;
|
|
1526
|
+
const decoded = [];
|
|
1527
|
+
for (const line of lines) {
|
|
1528
|
+
let generatedColumn = 0;
|
|
1529
|
+
const segs = [];
|
|
1530
|
+
if (line) {
|
|
1531
|
+
for (const raw of line.split(",")) {
|
|
1532
|
+
if (!raw) continue;
|
|
1533
|
+
const nums = decodeVLQValues(raw);
|
|
1534
|
+
if (nums[0] === void 0) continue;
|
|
1535
|
+
generatedColumn += nums[0];
|
|
1536
|
+
if (nums.length >= 4) {
|
|
1537
|
+
sourceIndex += nums[1] ?? 0;
|
|
1538
|
+
originalLine += nums[2] ?? 0;
|
|
1539
|
+
originalColumn += nums[3] ?? 0;
|
|
1540
|
+
segs.push({
|
|
1541
|
+
generatedColumn,
|
|
1542
|
+
source: sources[sourceIndex] ?? USER_ENTRY,
|
|
1543
|
+
originalLine,
|
|
1544
|
+
originalColumn
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
decoded.push(segs);
|
|
1550
|
+
}
|
|
1551
|
+
return decoded;
|
|
1552
|
+
}
|
|
1553
|
+
function originalPositionFor(map, line, column) {
|
|
1554
|
+
const decoded = decodeMappings(map);
|
|
1555
|
+
for (let i = line - 1; i >= 0; i--) {
|
|
1556
|
+
const segs = decoded[i];
|
|
1557
|
+
if (!segs || segs.length === 0) continue;
|
|
1558
|
+
const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
|
|
1559
|
+
let best = segs[0];
|
|
1560
|
+
for (const seg of segs) {
|
|
1561
|
+
if (seg.generatedColumn <= col) best = seg;
|
|
1562
|
+
else break;
|
|
1563
|
+
}
|
|
1564
|
+
if (!best) continue;
|
|
1565
|
+
return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
|
|
1566
|
+
}
|
|
1567
|
+
return void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function extractInlineSourceMap(code) {
|
|
1570
|
+
const m = code.match(SOURCEMAP_RE);
|
|
1571
|
+
if (!m?.[1]) return void 0;
|
|
1572
|
+
try {
|
|
1573
|
+
return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
|
|
1574
|
+
} catch {
|
|
1575
|
+
return void 0;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function remapErrorStack(stack, bundledCode) {
|
|
1579
|
+
const map = extractInlineSourceMap(bundledCode);
|
|
1580
|
+
if (!map?.mappings) return stack;
|
|
1581
|
+
return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
|
|
1582
|
+
const orig = originalPositionFor(map, Number(line), Number(col));
|
|
1583
|
+
if (!orig) return full;
|
|
1584
|
+
return `${orig.source}:${orig.line}:${orig.column}`;
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/previewServer.ts
|
|
1589
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1590
|
+
import { promises as fs8 } from "fs";
|
|
1591
|
+
import http2 from "http";
|
|
1592
|
+
import path9 from "path";
|
|
1593
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1594
|
+
|
|
1595
|
+
// src/previewHtml.ts
|
|
1596
|
+
function escapeJsonForScript(json) {
|
|
1597
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
1598
|
+
}
|
|
1599
|
+
var PREVIEW_STYLE = `
|
|
1600
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
|
|
1601
|
+
body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
|
|
1602
|
+
#stage { position: relative; flex: 1; min-height: 0; }
|
|
1603
|
+
#twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
|
|
1604
|
+
#twin-root canvas { display: block; }
|
|
1605
|
+
#status {
|
|
1606
|
+
position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
|
|
1607
|
+
padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
|
|
1608
|
+
}
|
|
1609
|
+
#status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
|
|
1610
|
+
/* \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 */
|
|
1611
|
+
#status[hidden] { display: none; }
|
|
1612
|
+
#mock-banner {
|
|
1613
|
+
position: absolute; top: 0; left: 0; right: 0; z-index: 2;
|
|
1614
|
+
padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
|
|
1615
|
+
border-bottom: 1px solid #f0c36d; pointer-events: none;
|
|
1616
|
+
}
|
|
1617
|
+
#debug-log {
|
|
1618
|
+
display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
|
|
1619
|
+
max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
|
|
1620
|
+
background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
|
|
1621
|
+
white-space: pre-wrap; word-break: break-all; pointer-events: auto;
|
|
1622
|
+
}
|
|
1623
|
+
#debug-log.open { display: block; }
|
|
1624
|
+
#run-bar {
|
|
1625
|
+
flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
|
|
1626
|
+
padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
|
|
1627
|
+
}
|
|
1628
|
+
#run-bar button {
|
|
1629
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
1630
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
1631
|
+
}
|
|
1632
|
+
#run-bar button:disabled { opacity: .55; cursor: default; }
|
|
1633
|
+
#btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
|
|
1634
|
+
#run-status { color: #bbb; min-width: 4em; }
|
|
1635
|
+
#run-bar .spacer { flex: 1; }
|
|
1636
|
+
#test-panel {
|
|
1637
|
+
flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
|
|
1638
|
+
padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
|
|
1639
|
+
max-height: 30%; overflow: auto;
|
|
1640
|
+
}
|
|
1641
|
+
#test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
|
|
1642
|
+
#test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
|
|
1643
|
+
#test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
|
|
1644
|
+
#test-panel .test-empty { color: #777; }
|
|
1645
|
+
#test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
1646
|
+
#test-panel input.test-input {
|
|
1647
|
+
width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
|
|
1648
|
+
padding: 3px 6px; border-radius: 4px; font: 12px inherit;
|
|
1649
|
+
}
|
|
1650
|
+
#test-panel button {
|
|
1651
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
1652
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
1653
|
+
}
|
|
1654
|
+
#test-panel button:disabled { opacity: .55; cursor: default; }
|
|
1655
|
+
`;
|
|
1656
|
+
var RENDER_SCRIPT = `
|
|
1657
|
+
var statusEl = document.getElementById("status");
|
|
1658
|
+
var logEl = document.getElementById("debug-log");
|
|
1659
|
+
var engine = null;
|
|
1660
|
+
var runtime = null;
|
|
1661
|
+
var sceneJson = null;
|
|
1662
|
+
var host = null;
|
|
1663
|
+
var running = false;
|
|
1664
|
+
var runningTest = false;
|
|
1665
|
+
var testsReady = false;
|
|
1666
|
+
var hostKind = __HOST_KIND__;
|
|
1667
|
+
var vscodeApi = null;
|
|
1668
|
+
if (hostKind === "vscode") {
|
|
1669
|
+
try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
|
|
1670
|
+
}
|
|
1671
|
+
var pending = {};
|
|
1672
|
+
var seq = 0;
|
|
1673
|
+
var origFetch = window.fetch.bind(window);
|
|
1674
|
+
function now() { return new Date().toISOString().slice(11, 23); }
|
|
1675
|
+
function log(line) {
|
|
1676
|
+
var text = "[" + now() + "] " + line;
|
|
1677
|
+
if (logEl) {
|
|
1678
|
+
logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
|
|
1679
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
1680
|
+
}
|
|
1681
|
+
if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
|
|
1682
|
+
console.log("[EasyTwin]", line);
|
|
1683
|
+
}
|
|
1684
|
+
function nextId() { return String(++seq); }
|
|
1685
|
+
function handleHostMessage(msg) {
|
|
1686
|
+
if (!msg) return;
|
|
1687
|
+
if (msg.type === "proxy-fetch-result") {
|
|
1688
|
+
var p = pending[msg.id];
|
|
1689
|
+
if (!p) return;
|
|
1690
|
+
delete pending[msg.id];
|
|
1691
|
+
if (msg.error) p.reject(new Error(msg.error));
|
|
1692
|
+
else p.resolve(msg);
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (msg.type === "bundle-result") {
|
|
1696
|
+
if (!msg.ok) {
|
|
1697
|
+
log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
1698
|
+
setRunStatus("\u7F16\u8BD1\u5931\u8D25");
|
|
1699
|
+
setRunBusy(false);
|
|
1700
|
+
if (logEl) logEl.classList.add("open");
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
|
|
1704
|
+
runUserCode(msg.code);
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (msg.type === "read-asset-result") {
|
|
1708
|
+
var ap = pending[msg.id];
|
|
1709
|
+
if (!ap) return;
|
|
1710
|
+
delete pending[msg.id];
|
|
1711
|
+
if (msg.error) ap.reject(new Error(msg.error));
|
|
1712
|
+
else ap.resolve(msg.text);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (msg.type === "run-error-mapped") {
|
|
1716
|
+
log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (msg.type === "tests") {
|
|
1720
|
+
renderTests(msg.tests || []);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (msg.type === "test-bundle") {
|
|
1724
|
+
if (!msg.ok) {
|
|
1725
|
+
log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
1726
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
1727
|
+
runningTest = false;
|
|
1728
|
+
setRunBusy(running);
|
|
1729
|
+
if (logEl) logEl.classList.add("open");
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
|
|
1733
|
+
runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
|
|
1734
|
+
log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
|
|
1735
|
+
setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
|
|
1736
|
+
}).catch(function (err) {
|
|
1737
|
+
log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
1738
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
1739
|
+
if (logEl) logEl.classList.add("open");
|
|
1740
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
1741
|
+
}).then(function () {
|
|
1742
|
+
runningTest = false;
|
|
1743
|
+
setRunBusy(running);
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function httpJson(url, init) {
|
|
1748
|
+
return origFetch(url, init).then(function (res) {
|
|
1749
|
+
return res.json().then(function (body) {
|
|
1750
|
+
if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
|
|
1751
|
+
return body;
|
|
1752
|
+
});
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
function postToHost(msg) {
|
|
1756
|
+
if (vscodeApi) {
|
|
1757
|
+
vscodeApi.postMessage(msg);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
|
|
1761
|
+
if (msg.type === "run") {
|
|
1762
|
+
httpJson("/api/bundle", { method: "POST" }).then(function (body) {
|
|
1763
|
+
handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
|
|
1764
|
+
}).catch(function (err) {
|
|
1765
|
+
handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
|
|
1766
|
+
});
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (msg.type === "run-error") {
|
|
1770
|
+
httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
|
|
1771
|
+
handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
|
|
1772
|
+
}).catch(function (err) {
|
|
1773
|
+
log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
|
|
1774
|
+
});
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (msg.type === "read-asset") {
|
|
1778
|
+
httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
|
|
1779
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
|
|
1780
|
+
}).catch(function (err) {
|
|
1781
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (msg.type === "list-tests") {
|
|
1786
|
+
httpJson("/api/tests").then(function (body) {
|
|
1787
|
+
handleHostMessage({ type: "tests", tests: body.tests || [] });
|
|
1788
|
+
}).catch(function (err) {
|
|
1789
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
1790
|
+
});
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (msg.type === "run-test") {
|
|
1794
|
+
httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
|
|
1795
|
+
handleHostMessage(Object.assign({ type: "test-bundle" }, body));
|
|
1796
|
+
}).catch(function (err) {
|
|
1797
|
+
handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function showStatus(text, isError) {
|
|
1802
|
+
if (!statusEl) return;
|
|
1803
|
+
statusEl.textContent = text;
|
|
1804
|
+
statusEl.className = isError ? "error" : "";
|
|
1805
|
+
statusEl.hidden = false;
|
|
1806
|
+
if (isError && logEl) logEl.classList.add("open");
|
|
1807
|
+
}
|
|
1808
|
+
function hideStatus() { if (statusEl) statusEl.hidden = true; }
|
|
1809
|
+
function formatError(err) {
|
|
1810
|
+
var msg = err && err.message ? err.message : String(err);
|
|
1811
|
+
var stack = err && err.stack ? "\\n" + err.stack : "";
|
|
1812
|
+
return msg + stack;
|
|
1813
|
+
}
|
|
1814
|
+
window.addEventListener("message", function (ev) {
|
|
1815
|
+
handleHostMessage(ev.data);
|
|
1816
|
+
});
|
|
1817
|
+
function b64ToBuf(b64) {
|
|
1818
|
+
var bin = atob(b64);
|
|
1819
|
+
var bytes = new Uint8Array(bin.length);
|
|
1820
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
1821
|
+
return bytes.buffer;
|
|
1822
|
+
}
|
|
1823
|
+
function proxyFetch(url, method) {
|
|
1824
|
+
return new Promise(function (resolve, reject) {
|
|
1825
|
+
if (!vscodeApi) {
|
|
1826
|
+
reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
var id = nextId();
|
|
1830
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
1831
|
+
vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
|
|
1832
|
+
}).then(function (msg) {
|
|
1833
|
+
var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
|
|
1834
|
+
return new Response(buf, {
|
|
1835
|
+
status: msg.status || 0,
|
|
1836
|
+
statusText: msg.statusText || "",
|
|
1837
|
+
headers: msg.headers || {}
|
|
1838
|
+
});
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
function isHttpUrl(url) {
|
|
1842
|
+
return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
|
|
1843
|
+
}
|
|
1844
|
+
function isPlainHttp(url) {
|
|
1845
|
+
return typeof url === "string" && url.indexOf("http://") === 0;
|
|
1846
|
+
}
|
|
1847
|
+
if (hostKind === "vscode") {
|
|
1848
|
+
window.fetch = function (input, init) {
|
|
1849
|
+
var url = typeof input === "string" ? input : (input && input.url);
|
|
1850
|
+
log("fetch " + url);
|
|
1851
|
+
if (isPlainHttp(url)) {
|
|
1852
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
1853
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
1854
|
+
return res;
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
return origFetch(input, init).catch(function (err) {
|
|
1858
|
+
log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
|
|
1859
|
+
if (!isHttpUrl(url)) throw err;
|
|
1860
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
1861
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
1862
|
+
return res;
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
};
|
|
1866
|
+
var origOpen = XMLHttpRequest.prototype.open;
|
|
1867
|
+
var origSend = XMLHttpRequest.prototype.send;
|
|
1868
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
1869
|
+
this.__etMethod = method;
|
|
1870
|
+
this.__etUrl = String(url);
|
|
1871
|
+
return origOpen.apply(this, arguments);
|
|
1872
|
+
};
|
|
1873
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
1874
|
+
var xhr = this;
|
|
1875
|
+
var url = xhr.__etUrl;
|
|
1876
|
+
if (!isPlainHttp(url)) return origSend.call(this, body);
|
|
1877
|
+
log("xhr proxy " + xhr.__etMethod + " " + url);
|
|
1878
|
+
proxyFetch(url, xhr.__etMethod).then(function (res) {
|
|
1879
|
+
return res.arrayBuffer().then(function (buf) {
|
|
1880
|
+
var text = "";
|
|
1881
|
+
try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
|
|
1882
|
+
Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
|
|
1883
|
+
Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
|
|
1884
|
+
Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
|
|
1885
|
+
Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
|
|
1886
|
+
var rt = xhr.responseType;
|
|
1887
|
+
var response = buf;
|
|
1888
|
+
if (rt === "" || rt === "text") response = text;
|
|
1889
|
+
else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
|
|
1890
|
+
Object.defineProperty(xhr, "response", { configurable: true, value: response });
|
|
1891
|
+
Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
|
|
1892
|
+
if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
|
|
1893
|
+
xhr.dispatchEvent(new Event("load"));
|
|
1894
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
1895
|
+
});
|
|
1896
|
+
}).catch(function (err) {
|
|
1897
|
+
log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
1898
|
+
if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
|
|
1899
|
+
xhr.dispatchEvent(new Event("error"));
|
|
1900
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
1901
|
+
});
|
|
1902
|
+
};
|
|
1903
|
+
function patchHttpSrc(proto, prop) {
|
|
1904
|
+
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
1905
|
+
if (!desc || typeof desc.set !== "function") return;
|
|
1906
|
+
Object.defineProperty(proto, prop, {
|
|
1907
|
+
configurable: true,
|
|
1908
|
+
enumerable: desc.enumerable,
|
|
1909
|
+
get: function () { return desc.get.call(this); },
|
|
1910
|
+
set: function (value) {
|
|
1911
|
+
var el = this;
|
|
1912
|
+
var url = String(value);
|
|
1913
|
+
if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
|
|
1914
|
+
log("media proxy " + url);
|
|
1915
|
+
proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
|
|
1916
|
+
desc.set.call(el, URL.createObjectURL(blob));
|
|
1917
|
+
}).catch(function (err) {
|
|
1918
|
+
log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
1919
|
+
try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
patchHttpSrc(HTMLImageElement.prototype, "src");
|
|
1925
|
+
patchHttpSrc(HTMLMediaElement.prototype, "src");
|
|
1926
|
+
}
|
|
1927
|
+
document.getElementById("btn-debug").addEventListener("click", function () {
|
|
1928
|
+
if (logEl) logEl.classList.toggle("open");
|
|
1929
|
+
});
|
|
1930
|
+
var btnDevtools = document.getElementById("btn-devtools");
|
|
1931
|
+
var btnOutput = document.getElementById("btn-output");
|
|
1932
|
+
if (hostKind === "http") {
|
|
1933
|
+
if (btnDevtools) btnDevtools.hidden = true;
|
|
1934
|
+
if (btnOutput) btnOutput.hidden = true;
|
|
1935
|
+
}
|
|
1936
|
+
if (btnDevtools) btnDevtools.addEventListener("click", function () {
|
|
1937
|
+
postToHost({ type: "open-devtools" });
|
|
1938
|
+
});
|
|
1939
|
+
if (btnOutput) btnOutput.addEventListener("click", function () {
|
|
1940
|
+
postToHost({ type: "show-output" });
|
|
1941
|
+
});
|
|
1942
|
+
function setRunStatus(text) {
|
|
1943
|
+
var el = document.getElementById("run-status");
|
|
1944
|
+
if (el) el.textContent = text;
|
|
1945
|
+
}
|
|
1946
|
+
function setRunBusy(busy) {
|
|
1947
|
+
running = busy;
|
|
1948
|
+
var btn = document.getElementById("btn-run");
|
|
1949
|
+
if (btn) btn.disabled = !!busy || runningTest;
|
|
1950
|
+
syncTestControls();
|
|
1951
|
+
}
|
|
1952
|
+
function syncTestControls() {
|
|
1953
|
+
var panel = document.getElementById("test-panel");
|
|
1954
|
+
if (!panel) return;
|
|
1955
|
+
var disabled = !testsReady || running || runningTest;
|
|
1956
|
+
var nodes = panel.querySelectorAll("button.test-run, input.test-input");
|
|
1957
|
+
for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
|
|
1958
|
+
}
|
|
1959
|
+
function renderTests(tests) {
|
|
1960
|
+
var list = document.getElementById("test-list");
|
|
1961
|
+
if (!list) return;
|
|
1962
|
+
list.textContent = "";
|
|
1963
|
+
if (!tests || !tests.length) {
|
|
1964
|
+
var empty = document.createElement("span");
|
|
1965
|
+
empty.className = "test-empty";
|
|
1966
|
+
empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
|
|
1967
|
+
list.appendChild(empty);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
var groups = {};
|
|
1971
|
+
var order = [];
|
|
1972
|
+
for (var i = 0; i < tests.length; i++) {
|
|
1973
|
+
var t = tests[i];
|
|
1974
|
+
if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
|
|
1975
|
+
groups[t.file].push(t);
|
|
1976
|
+
}
|
|
1977
|
+
for (var g = 0; g < order.length; g++) {
|
|
1978
|
+
var file = order[g];
|
|
1979
|
+
var heading = document.createElement("span");
|
|
1980
|
+
heading.className = "test-file";
|
|
1981
|
+
heading.textContent = file;
|
|
1982
|
+
list.appendChild(heading);
|
|
1983
|
+
var items = groups[file];
|
|
1984
|
+
for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
|
|
1985
|
+
}
|
|
1986
|
+
syncTestControls();
|
|
1987
|
+
}
|
|
1988
|
+
function makeTestControl(t) {
|
|
1989
|
+
var wrap = document.createElement("span");
|
|
1990
|
+
wrap.className = "test-item";
|
|
1991
|
+
if (t.hasInput) {
|
|
1992
|
+
var input = document.createElement("input");
|
|
1993
|
+
input.type = "text";
|
|
1994
|
+
input.className = "test-input";
|
|
1995
|
+
input.placeholder = t.inputName || "input";
|
|
1996
|
+
var btn = document.createElement("button");
|
|
1997
|
+
btn.type = "button";
|
|
1998
|
+
btn.className = "test-run";
|
|
1999
|
+
btn.textContent = t.name;
|
|
2000
|
+
btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
|
|
2001
|
+
input.addEventListener("keydown", function (ev) {
|
|
2002
|
+
if (ev.key === "Enter") requestRunTest(t.id, input.value);
|
|
2003
|
+
});
|
|
2004
|
+
wrap.appendChild(input);
|
|
2005
|
+
wrap.appendChild(btn);
|
|
2006
|
+
} else {
|
|
2007
|
+
var only = document.createElement("button");
|
|
2008
|
+
only.type = "button";
|
|
2009
|
+
only.className = "test-run";
|
|
2010
|
+
only.textContent = t.name;
|
|
2011
|
+
only.addEventListener("click", function () { requestRunTest(t.id); });
|
|
2012
|
+
wrap.appendChild(only);
|
|
2013
|
+
}
|
|
2014
|
+
return wrap;
|
|
2015
|
+
}
|
|
2016
|
+
function requestRunTest(id, input) {
|
|
2017
|
+
if (!testsReady || running || runningTest) return;
|
|
2018
|
+
if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
|
|
2019
|
+
runningTest = true;
|
|
2020
|
+
setRunBusy(running);
|
|
2021
|
+
setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
|
|
2022
|
+
log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
|
|
2023
|
+
postToHost({ type: "run-test", id: id, input: input });
|
|
2024
|
+
}
|
|
2025
|
+
async function runExportedTest(code, exportName, input, hasInput) {
|
|
2026
|
+
var blob = new Blob([code], { type: "text/javascript" });
|
|
2027
|
+
var url = URL.createObjectURL(blob);
|
|
2028
|
+
try {
|
|
2029
|
+
var mod = await import(url);
|
|
2030
|
+
var fn = mod[exportName];
|
|
2031
|
+
if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
|
|
2032
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
|
|
2033
|
+
var ctx = host.getTestContext();
|
|
2034
|
+
var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
|
|
2035
|
+
await Promise.resolve(result);
|
|
2036
|
+
} finally {
|
|
2037
|
+
URL.revokeObjectURL(url);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function applyRenderPatch() {
|
|
2041
|
+
// 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,
|
|
2042
|
+
// \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
|
|
2043
|
+
var scene = engine && engine.mainScene;
|
|
2044
|
+
if (
|
|
2045
|
+
scene && scene.rootComponent && scene.rootComponent.version &&
|
|
2046
|
+
!scene.postprocessingComponent &&
|
|
2047
|
+
runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
|
|
2048
|
+
) {
|
|
2049
|
+
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");
|
|
2050
|
+
scene.registerRenderCallback(function () {
|
|
2051
|
+
scene.renderer.render(scene.sceneObject, scene.camera.main);
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
async function loadHostScene(nextEngine, nextJson) {
|
|
2056
|
+
engine = nextEngine;
|
|
2057
|
+
var sceneManager = engine.getManager(runtime.SceneManager);
|
|
2058
|
+
await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
|
|
2059
|
+
applyRenderPatch();
|
|
2060
|
+
}
|
|
2061
|
+
function readAsset(relPath) {
|
|
2062
|
+
return new Promise(function (resolve, reject) {
|
|
2063
|
+
var id = nextId();
|
|
2064
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
2065
|
+
postToHost({ type: "read-asset", id: id, path: relPath });
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
async function runUserCode(code) {
|
|
2069
|
+
setRunStatus("\u8FD0\u884C\u4E2D\u2026");
|
|
2070
|
+
try {
|
|
2071
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
2072
|
+
await host.run(code);
|
|
2073
|
+
log("Run \u5B8C\u6210");
|
|
2074
|
+
setRunStatus("\u8FD0\u884C\u4E2D");
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
log("Run \u5931\u8D25 " + formatError(err));
|
|
2077
|
+
setRunStatus("\u5931\u8D25");
|
|
2078
|
+
if (logEl) logEl.classList.add("open");
|
|
2079
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
2080
|
+
} finally {
|
|
2081
|
+
setRunBusy(false);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
document.getElementById("btn-run").addEventListener("click", function () {
|
|
2085
|
+
if (running || runningTest) return;
|
|
2086
|
+
if (!engine) {
|
|
2087
|
+
log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
setRunBusy(true);
|
|
2091
|
+
setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
|
|
2092
|
+
log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
|
|
2093
|
+
postToHost({ type: "run" });
|
|
2094
|
+
});
|
|
2095
|
+
document.getElementById("btn-refresh-tests").addEventListener("click", function () {
|
|
2096
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
|
|
2097
|
+
postToHost({ type: "list-tests" });
|
|
2098
|
+
});
|
|
2099
|
+
function readEmbeddedTests() {
|
|
2100
|
+
var el = document.getElementById("workspace-tests");
|
|
2101
|
+
if (!el || !el.textContent) return [];
|
|
2102
|
+
try { return JSON.parse(el.textContent); } catch (e) { return []; }
|
|
2103
|
+
}
|
|
2104
|
+
renderTests(readEmbeddedTests());
|
|
2105
|
+
function normalizeHierarchyConfig(vo) {
|
|
2106
|
+
var objs = vo && vo.objs ? vo.objs : [];
|
|
2107
|
+
for (var i = 0; i < objs.length; i++) {
|
|
2108
|
+
var hc = objs[i].hierarchyConfig || {};
|
|
2109
|
+
if (typeof hc.active !== "boolean") {
|
|
2110
|
+
hc.active = hc.inActive === true ? false : hc.visible !== false;
|
|
2111
|
+
}
|
|
2112
|
+
if (typeof hc.lock !== "boolean") hc.lock = false;
|
|
2113
|
+
if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
|
|
2114
|
+
objs[i].hierarchyConfig = hc;
|
|
2115
|
+
}
|
|
2116
|
+
return vo;
|
|
2117
|
+
}
|
|
2118
|
+
function toSceneJson(runtime, raw) {
|
|
2119
|
+
var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
|
|
2120
|
+
if (vo && vo.sceneComponent) {
|
|
2121
|
+
return {
|
|
2122
|
+
id: vo.id || "preview",
|
|
2123
|
+
name: vo.name || "\u573A\u666F\u9884\u89C8",
|
|
2124
|
+
sceneComponent: vo.sceneComponent
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
vo = normalizeHierarchyConfig(vo);
|
|
2128
|
+
var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
|
|
2129
|
+
var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
|
|
2130
|
+
return {
|
|
2131
|
+
id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
|
|
2132
|
+
name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
|
|
2133
|
+
sceneComponent: runtime.convertObjToComponentJson(vo)
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
try {
|
|
2137
|
+
showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
|
|
2138
|
+
log("runtimeUri=" + __RUNTIME_URI__);
|
|
2139
|
+
log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
|
|
2140
|
+
log("1/4 import twin-runtime / TwinApp host");
|
|
2141
|
+
runtime = await import("@easytwin/runtime");
|
|
2142
|
+
var hostMod = await import(__HOST_URI__);
|
|
2143
|
+
log("2/4 parse scene JSON");
|
|
2144
|
+
var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
|
|
2145
|
+
sceneJson = toSceneJson(runtime, sceneVo);
|
|
2146
|
+
log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
|
|
2147
|
+
log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
|
|
2148
|
+
host = new hostMod.TwinAppPreviewHost({
|
|
2149
|
+
runtime: runtime,
|
|
2150
|
+
containerId: "twin-root",
|
|
2151
|
+
ossUrl: __BASE_OSS_URL__,
|
|
2152
|
+
appId: __APP_ID__,
|
|
2153
|
+
sceneId: sceneJson.id,
|
|
2154
|
+
sceneJson: sceneJson,
|
|
2155
|
+
customComponentDeps: {
|
|
2156
|
+
"@easytwin/runtime": runtime,
|
|
2157
|
+
"@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
|
|
2158
|
+
react: { createElement: function () { return null; }, Fragment: "div" }
|
|
2159
|
+
},
|
|
2160
|
+
loadScene: loadHostScene,
|
|
2161
|
+
readAsset: readAsset,
|
|
2162
|
+
log: log
|
|
2163
|
+
});
|
|
2164
|
+
await host.boot();
|
|
2165
|
+
engine = host.getEngine();
|
|
2166
|
+
log("4/4 loadScene");
|
|
2167
|
+
log("\u5B8C\u6210");
|
|
2168
|
+
hideStatus();
|
|
2169
|
+
testsReady = true;
|
|
2170
|
+
setRunBusy(false);
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
log("\u5931\u8D25 " + formatError(err));
|
|
2173
|
+
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);
|
|
2174
|
+
}
|
|
2175
|
+
window.addEventListener("pagehide", function () {
|
|
2176
|
+
if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
|
|
2177
|
+
});
|
|
2178
|
+
`;
|
|
2179
|
+
function originOf(url) {
|
|
2180
|
+
return new URL(url).origin;
|
|
2181
|
+
}
|
|
2182
|
+
function buildPreviewHtml(options) {
|
|
2183
|
+
const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
|
|
2184
|
+
const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
|
|
2185
|
+
const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
|
|
2186
|
+
const appId = options.appId ?? "preview";
|
|
2187
|
+
const hostKind = options.host ?? "vscode";
|
|
2188
|
+
const apiOrigin = originOf(baseUrl);
|
|
2189
|
+
const ossOrigin = originOf(ossUrl);
|
|
2190
|
+
const runtimeOrigin = originOf(runtimeUri);
|
|
2191
|
+
const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
|
|
2192
|
+
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));
|
|
2193
|
+
const importMap = JSON.stringify({
|
|
2194
|
+
imports: {
|
|
2195
|
+
"@easytwin/runtime": runtimeUri,
|
|
2196
|
+
"@easytwin/apps": appsUri
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
const testsJson = JSON.stringify(options.tests ?? []);
|
|
2200
|
+
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>` : "";
|
|
2201
|
+
return `<!DOCTYPE html>
|
|
2202
|
+
<html lang="zh-CN">
|
|
2203
|
+
<head>
|
|
2204
|
+
<meta charset="UTF-8">
|
|
2205
|
+
<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:;">
|
|
2206
|
+
<title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
|
|
2207
|
+
<style>${PREVIEW_STYLE}</style>
|
|
2208
|
+
<script type="importmap">${importMap}</script>
|
|
2209
|
+
</head>
|
|
2210
|
+
<body>
|
|
2211
|
+
<div id="stage">
|
|
2212
|
+
<div id="twin-root"></div>
|
|
2213
|
+
${mockBanner}
|
|
2214
|
+
<div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
|
|
2215
|
+
<pre id="debug-log"></pre>
|
|
2216
|
+
</div>
|
|
2217
|
+
<div id="test-panel">
|
|
2218
|
+
<span class="test-label">\u6D4B\u8BD5</span>
|
|
2219
|
+
<div id="test-list"></div>
|
|
2220
|
+
<button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
|
|
2221
|
+
</div>
|
|
2222
|
+
<div id="run-bar">
|
|
2223
|
+
<button type="button" id="btn-run">Run</button>
|
|
2224
|
+
<span id="run-status">\u5C31\u7EEA</span>
|
|
2225
|
+
<span class="spacer"></span>
|
|
2226
|
+
<button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
|
|
2227
|
+
<button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
|
|
2228
|
+
<button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
|
|
2229
|
+
</div>
|
|
2230
|
+
<script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
|
|
2231
|
+
<script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
|
|
2232
|
+
<script type="module">
|
|
2233
|
+
${renderScript}
|
|
2234
|
+
</script>
|
|
2235
|
+
</body>
|
|
2236
|
+
</html>`;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// src/workspaceTests.ts
|
|
2240
|
+
import { promises as fs7 } from "fs";
|
|
2241
|
+
import path8 from "path";
|
|
2242
|
+
var IDENT = "[A-Za-z_$][\\w$]*";
|
|
2243
|
+
var FUNCTION_EXPORT = new RegExp(
|
|
2244
|
+
`export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
|
|
2245
|
+
"g"
|
|
2246
|
+
);
|
|
2247
|
+
var CONST_EXPORT = new RegExp(
|
|
2248
|
+
`export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
|
|
2249
|
+
"g"
|
|
2250
|
+
);
|
|
2251
|
+
function stripTsCommentsAndStringsKeepCode(source) {
|
|
2252
|
+
let out = "";
|
|
2253
|
+
let i = 0;
|
|
2254
|
+
const n = source.length;
|
|
2255
|
+
while (i < n) {
|
|
2256
|
+
const c = source[i];
|
|
2257
|
+
const next = source[i + 1];
|
|
2258
|
+
if (c === "/" && next === "/") {
|
|
2259
|
+
i += 2;
|
|
2260
|
+
while (i < n && source[i] !== "\n") i++;
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
if (c === "/" && next === "*") {
|
|
2264
|
+
i += 2;
|
|
2265
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
2266
|
+
i = Math.min(n, i + 2);
|
|
2267
|
+
out += " ";
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
2271
|
+
const quote = c;
|
|
2272
|
+
out += " ";
|
|
2273
|
+
i++;
|
|
2274
|
+
while (i < n) {
|
|
2275
|
+
const ch = source[i];
|
|
2276
|
+
if (ch === "\\") {
|
|
2277
|
+
i += 2;
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
if (ch === quote) {
|
|
2281
|
+
i++;
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
i++;
|
|
2285
|
+
}
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
out += c;
|
|
2289
|
+
i++;
|
|
2290
|
+
}
|
|
2291
|
+
return out;
|
|
2292
|
+
}
|
|
2293
|
+
function matchingClose(src, openIndex, open, close) {
|
|
2294
|
+
let depth = 0;
|
|
2295
|
+
let angle = 0;
|
|
2296
|
+
let brace = 0;
|
|
2297
|
+
for (let i = openIndex; i < src.length; i++) {
|
|
2298
|
+
const c = src[i];
|
|
2299
|
+
if (c === open) depth++;
|
|
2300
|
+
else if (c === close) {
|
|
2301
|
+
depth--;
|
|
2302
|
+
if (depth === 0 && angle <= 0 && brace <= 0) return i;
|
|
2303
|
+
} else if (c === "<") angle++;
|
|
2304
|
+
else if (c === ">" && angle > 0) angle--;
|
|
2305
|
+
else if (c === "{") brace++;
|
|
2306
|
+
else if (c === "}" && brace > 0) brace--;
|
|
2307
|
+
}
|
|
2308
|
+
return -1;
|
|
2309
|
+
}
|
|
2310
|
+
function splitTopLevelParams(list) {
|
|
2311
|
+
const params = [];
|
|
2312
|
+
let current = "";
|
|
2313
|
+
let paren = 0;
|
|
2314
|
+
let angle = 0;
|
|
2315
|
+
let brace = 0;
|
|
2316
|
+
let bracket = 0;
|
|
2317
|
+
for (const c of list) {
|
|
2318
|
+
if (c === "(") paren++;
|
|
2319
|
+
else if (c === ")") paren--;
|
|
2320
|
+
else if (c === "<") angle++;
|
|
2321
|
+
else if (c === ">" && angle > 0) angle--;
|
|
2322
|
+
else if (c === "{") brace++;
|
|
2323
|
+
else if (c === "}" && brace > 0) brace--;
|
|
2324
|
+
else if (c === "[") bracket++;
|
|
2325
|
+
else if (c === "]" && bracket > 0) bracket--;
|
|
2326
|
+
if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
|
|
2327
|
+
if (current.trim()) params.push(current.trim());
|
|
2328
|
+
current = "";
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
current += c;
|
|
2332
|
+
}
|
|
2333
|
+
if (current.trim()) params.push(current.trim());
|
|
2334
|
+
return params;
|
|
2335
|
+
}
|
|
2336
|
+
function paramName(raw) {
|
|
2337
|
+
let s = raw.trim();
|
|
2338
|
+
if (!s || s === "this") return void 0;
|
|
2339
|
+
if (s.startsWith("{") || s.startsWith("[")) return "input";
|
|
2340
|
+
s = s.replace(/^\.\.\./, "");
|
|
2341
|
+
const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
|
|
2342
|
+
if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
|
|
2343
|
+
return token;
|
|
2344
|
+
}
|
|
2345
|
+
function collectFromPattern(source, pattern) {
|
|
2346
|
+
const found = [];
|
|
2347
|
+
pattern.lastIndex = 0;
|
|
2348
|
+
let match;
|
|
2349
|
+
while (match = pattern.exec(source)) {
|
|
2350
|
+
const name = match[1];
|
|
2351
|
+
if (!name) continue;
|
|
2352
|
+
const openIndex = match.index + match[0].length - 1;
|
|
2353
|
+
const closeIndex = matchingClose(source, openIndex, "(", ")");
|
|
2354
|
+
if (closeIndex < 0) continue;
|
|
2355
|
+
const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
|
|
2356
|
+
const hasInput = params.length >= 2;
|
|
2357
|
+
const second = hasInput ? paramName(params[1] ?? "") : void 0;
|
|
2358
|
+
found.push({
|
|
2359
|
+
id: name,
|
|
2360
|
+
file: "",
|
|
2361
|
+
name,
|
|
2362
|
+
hasInput,
|
|
2363
|
+
inputName: hasInput ? second : void 0
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
return found;
|
|
2367
|
+
}
|
|
2368
|
+
function parseExportedTestFunctions(source) {
|
|
2369
|
+
const text = stripTsCommentsAndStringsKeepCode(source);
|
|
2370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2371
|
+
const out = [];
|
|
2372
|
+
for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
|
|
2373
|
+
if (seen.has(item.name)) continue;
|
|
2374
|
+
seen.add(item.name);
|
|
2375
|
+
out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
|
|
2376
|
+
}
|
|
2377
|
+
return out;
|
|
2378
|
+
}
|
|
2379
|
+
function workspaceTestId(file, name) {
|
|
2380
|
+
return `${normalizePath(file)}:${name}`;
|
|
2381
|
+
}
|
|
2382
|
+
async function listWorkspaceTests(cwd) {
|
|
2383
|
+
const root = path8.resolve(cwd);
|
|
2384
|
+
let files;
|
|
2385
|
+
try {
|
|
2386
|
+
files = await collectFiles(root, isIgnoredWorkspacePath);
|
|
2387
|
+
} catch (err) {
|
|
2388
|
+
const code = err.code;
|
|
2389
|
+
if (code === "ENOENT") return [];
|
|
2390
|
+
throw err;
|
|
2391
|
+
}
|
|
2392
|
+
const tests = [];
|
|
2393
|
+
for (const file of files) {
|
|
2394
|
+
if (!isWorkspaceSpecFile(file.relPath)) continue;
|
|
2395
|
+
const posix = normalizePath(file.relPath);
|
|
2396
|
+
const source = await fs7.readFile(file.absPath, "utf8");
|
|
2397
|
+
for (const fn of parseExportedTestFunctions(source)) {
|
|
2398
|
+
tests.push({
|
|
2399
|
+
id: workspaceTestId(posix, fn.name),
|
|
2400
|
+
file: posix,
|
|
2401
|
+
name: fn.name,
|
|
2402
|
+
hasInput: fn.hasInput,
|
|
2403
|
+
inputName: fn.inputName
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
tests.sort((a, b) => a.id.localeCompare(b.id));
|
|
2408
|
+
return tests;
|
|
2409
|
+
}
|
|
2410
|
+
async function bundleWorkspaceTestFile(options) {
|
|
2411
|
+
const file = normalizePath(options.file);
|
|
2412
|
+
return bundleWorkspaceModule({
|
|
2413
|
+
cwd: options.cwd,
|
|
2414
|
+
entry: file,
|
|
2415
|
+
missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/previewServer.ts
|
|
2420
|
+
var DEFAULT_PREVIEW_PORT = 4173;
|
|
2421
|
+
var DEFAULT_PREVIEW_HOST = "127.0.0.1";
|
|
2422
|
+
var RUNTIME_FILES = /* @__PURE__ */ new Set(["twin-runtime.js", "twin-apps.js", "twin-app-host.js"]);
|
|
2423
|
+
function formatPreviewReadyMessage(info) {
|
|
2424
|
+
const lines = [
|
|
2425
|
+
`EasyTwin \u9884\u89C8: ${info.url}`,
|
|
2426
|
+
`\u573A\u666F: ${info.sceneId}`,
|
|
2427
|
+
"\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"
|
|
2428
|
+
];
|
|
2429
|
+
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");
|
|
2430
|
+
return lines.join("\n");
|
|
2431
|
+
}
|
|
2432
|
+
function resolvePreviewSceneId(scenes, requested) {
|
|
2433
|
+
const id = requested?.trim() ?? "";
|
|
2434
|
+
if (id.length > 0) return id;
|
|
2435
|
+
const list = scenes ?? [];
|
|
2436
|
+
const preferred = list.find((s) => s.defaultLoading === true) ?? list[0];
|
|
2437
|
+
if (preferred) return preferred.id;
|
|
2438
|
+
throw new Error("\u672A\u6307\u5B9A\u573A\u666F\u3002\u8BF7\u4F20\u5165 Scene Key,\u6216\u5148\u8FD0\u884C easytwin scene list");
|
|
2439
|
+
}
|
|
2440
|
+
function resolvePreviewRuntimeDir() {
|
|
2441
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
2442
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u9884\u89C8 runtime:\u63D2\u4EF6/CJS \u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 runtimeDir");
|
|
2443
|
+
}
|
|
2444
|
+
const here = path9.dirname(fileURLToPath3(import.meta.url));
|
|
2445
|
+
const candidates = [
|
|
2446
|
+
path9.join(here, "runtime"),
|
|
2447
|
+
path9.resolve(here, "..", "dist", "runtime"),
|
|
2448
|
+
path9.resolve(here, "runtime")
|
|
2449
|
+
];
|
|
2450
|
+
for (const dir of candidates) {
|
|
2451
|
+
if (existsSync2(path9.join(dir, "twin-runtime.js"))) return dir;
|
|
2452
|
+
}
|
|
2453
|
+
throw new Error(
|
|
2454
|
+
`\u672A\u627E\u5230\u9884\u89C8 runtime(twin-runtime.js)\u3002\u5DF2\u5C1D\u8BD5:${candidates.join(", ")}\u3002\u8BF7\u5148\u6784\u5EFA @easytwin/devkit\u3002`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
function mimeFor(file) {
|
|
2458
|
+
if (file.endsWith(".js")) return "text/javascript; charset=utf-8";
|
|
2459
|
+
return "application/octet-stream";
|
|
2460
|
+
}
|
|
2461
|
+
function sendJson(res, body, status = 200) {
|
|
2462
|
+
res.statusCode = status;
|
|
2463
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
2464
|
+
res.setHeader("cache-control", "no-store");
|
|
2465
|
+
res.end(JSON.stringify(body));
|
|
2466
|
+
}
|
|
2467
|
+
function sendText(res, body, status, contentType) {
|
|
2468
|
+
res.statusCode = status;
|
|
2469
|
+
res.setHeader("content-type", contentType);
|
|
2470
|
+
res.end(body);
|
|
2471
|
+
}
|
|
2472
|
+
async function readJsonBody(req) {
|
|
2473
|
+
const chunks = [];
|
|
2474
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
2475
|
+
if (chunks.length === 0) return {};
|
|
2476
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
2477
|
+
if (raw.length === 0) return {};
|
|
2478
|
+
return JSON.parse(raw);
|
|
2479
|
+
}
|
|
2480
|
+
function asRecord3(value) {
|
|
2481
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2482
|
+
}
|
|
2483
|
+
async function startPreviewServer(options) {
|
|
2484
|
+
const cwd = path9.resolve(options.cwd);
|
|
2485
|
+
const config = await loadConfig(cwd);
|
|
2486
|
+
const fileConfig = await readConfigFile(cwd);
|
|
2487
|
+
const runtimeDir = options.runtimeDir ?? resolvePreviewRuntimeDir();
|
|
2488
|
+
if (!existsSync2(path9.join(runtimeDir, "twin-runtime.js"))) {
|
|
2489
|
+
throw new Error(`\u9884\u89C8 runtime \u76EE\u5F55\u7F3A\u5C11 twin-runtime.js:${runtimeDir}`);
|
|
2490
|
+
}
|
|
2491
|
+
let sceneId;
|
|
2492
|
+
try {
|
|
2493
|
+
sceneId = resolvePreviewSceneId(fileConfig.scenes, options.sceneId);
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
if (config.mock) {
|
|
2496
|
+
sceneId = deriveExampleSceneId(await loadExampleScene());
|
|
2497
|
+
} else {
|
|
2498
|
+
throw err;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
const client = new EasyTwinClient(config);
|
|
2502
|
+
const scene = await pullScene(client, sceneId, { cwd });
|
|
2503
|
+
const tests = await listWorkspaceTests(cwd);
|
|
2504
|
+
const hostname = options.hostname ?? DEFAULT_PREVIEW_HOST;
|
|
2505
|
+
const log = options.log ?? ((line) => process.stderr.write(`${line}
|
|
2506
|
+
`));
|
|
2507
|
+
let lastBundledCode;
|
|
2508
|
+
const originRef = { url: "" };
|
|
2509
|
+
const html = () => buildPreviewHtml({
|
|
2510
|
+
baseUrl: config.baseUrl,
|
|
2511
|
+
ossUrl: config.ossUrl,
|
|
2512
|
+
sceneJson: JSON.stringify(scene.payload ?? scene, null, 2),
|
|
2513
|
+
mock: config.mock,
|
|
2514
|
+
runtimeUri: `${originRef.url}/runtime/twin-runtime.js`,
|
|
2515
|
+
appsUri: `${originRef.url}/runtime/twin-apps.js`,
|
|
2516
|
+
hostUri: `${originRef.url}/runtime/twin-app-host.js`,
|
|
2517
|
+
appId: config.appId,
|
|
2518
|
+
host: "http",
|
|
2519
|
+
tests
|
|
2520
|
+
});
|
|
2521
|
+
const server = http2.createServer((req, res) => {
|
|
2522
|
+
void handle(req, res);
|
|
2523
|
+
});
|
|
2524
|
+
async function handle(req, res) {
|
|
2525
|
+
try {
|
|
2526
|
+
const url = new URL(req.url ?? "/", originRef.url || `http://${hostname}`);
|
|
2527
|
+
const method = req.method ?? "GET";
|
|
2528
|
+
if (method === "GET" && url.pathname === "/") {
|
|
2529
|
+
sendText(res, html(), 200, "text/html; charset=utf-8");
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
if (method === "GET" && url.pathname.startsWith("/runtime/")) {
|
|
2533
|
+
const name = path9.posix.basename(url.pathname);
|
|
2534
|
+
if (!RUNTIME_FILES.has(name)) {
|
|
2535
|
+
sendText(res, "not found", 404, "text/plain; charset=utf-8");
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
const file = path9.join(runtimeDir, name);
|
|
2539
|
+
const buf = await fs8.readFile(file);
|
|
2540
|
+
res.statusCode = 200;
|
|
2541
|
+
res.setHeader("content-type", mimeFor(name));
|
|
2542
|
+
res.setHeader("cache-control", "no-store");
|
|
2543
|
+
res.end(buf);
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (method === "POST" && url.pathname === "/api/bundle") {
|
|
2547
|
+
try {
|
|
2548
|
+
const result = await bundleUserCode({ cwd });
|
|
2549
|
+
lastBundledCode = result.code;
|
|
2550
|
+
for (const warning of result.warnings) log(`[run] \u8B66\u544A ${warning}`);
|
|
2551
|
+
sendJson(res, { ok: true, code: result.code, warnings: result.warnings });
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2554
|
+
log(`[run] \u7F16\u8BD1\u5931\u8D25 ${error}`);
|
|
2555
|
+
sendJson(res, { ok: false, error });
|
|
2556
|
+
}
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
if (method === "GET" && url.pathname === "/api/tests") {
|
|
2560
|
+
sendJson(res, { tests: await listWorkspaceTests(cwd) });
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (method === "POST" && url.pathname === "/api/test-bundle") {
|
|
2564
|
+
const body = asRecord3(await readJsonBody(req));
|
|
2565
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
2566
|
+
const input = typeof body.input === "string" ? body.input : void 0;
|
|
2567
|
+
try {
|
|
2568
|
+
const listed = await listWorkspaceTests(cwd);
|
|
2569
|
+
const item = listed.find((t) => t.id === id);
|
|
2570
|
+
if (!item) throw new Error(`\u672A\u627E\u5230\u6D4B\u8BD5 ${id}`);
|
|
2571
|
+
log(`[test] bundle ${item.file} :: ${item.name}`);
|
|
2572
|
+
const result = await bundleWorkspaceTestFile({ cwd, file: item.file });
|
|
2573
|
+
lastBundledCode = result.code;
|
|
2574
|
+
for (const warning of result.warnings) log(`[test] \u8B66\u544A ${warning}`);
|
|
2575
|
+
sendJson(res, {
|
|
2576
|
+
ok: true,
|
|
2577
|
+
code: result.code,
|
|
2578
|
+
exportName: item.name,
|
|
2579
|
+
input,
|
|
2580
|
+
hasInput: item.hasInput,
|
|
2581
|
+
warnings: result.warnings
|
|
2582
|
+
});
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2585
|
+
log(`[test] \u7F16\u8BD1\u5931\u8D25 ${error}`);
|
|
2586
|
+
sendJson(res, { ok: false, error });
|
|
2587
|
+
}
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
if (method === "GET" && url.pathname === "/api/assets") {
|
|
2591
|
+
const rel = url.searchParams.get("path") ?? "";
|
|
2592
|
+
try {
|
|
2593
|
+
const file = resolveWorkspaceAssetPath(cwd, rel);
|
|
2594
|
+
sendJson(res, { text: await fs8.readFile(file, "utf8") });
|
|
2595
|
+
} catch (err) {
|
|
2596
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2597
|
+
log(`[assets] FAIL ${rel} ${error}`);
|
|
2598
|
+
sendJson(res, { error });
|
|
2599
|
+
}
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (method === "POST" && url.pathname === "/api/remap-error") {
|
|
2603
|
+
const body = asRecord3(await readJsonBody(req));
|
|
2604
|
+
const stack = typeof body.stack === "string" ? body.stack : "";
|
|
2605
|
+
sendJson(res, { stack: lastBundledCode ? remapErrorStack(stack, lastBundledCode) : stack });
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
sendText(res, "not found", 404, "text/plain; charset=utf-8");
|
|
2609
|
+
} catch (err) {
|
|
2610
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
2611
|
+
log(`[preview] ${error}`);
|
|
2612
|
+
if (!res.headersSent) sendJson(res, { error }, 500);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
const port = options.port ?? DEFAULT_PREVIEW_PORT;
|
|
2616
|
+
await new Promise((resolve, reject) => {
|
|
2617
|
+
server.once("error", reject);
|
|
2618
|
+
server.listen(port, hostname, () => resolve());
|
|
2619
|
+
});
|
|
2620
|
+
const address = server.address();
|
|
2621
|
+
const bound = typeof address === "object" && address !== null ? address.port : port;
|
|
2622
|
+
originRef.url = `http://${hostname}:${bound}`;
|
|
2623
|
+
return {
|
|
2624
|
+
url: originRef.url,
|
|
2625
|
+
port: bound,
|
|
2626
|
+
sceneId,
|
|
2627
|
+
mock: config.mock,
|
|
2628
|
+
close: () => new Promise((resolve, reject) => {
|
|
2629
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
2630
|
+
})
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
1339
2633
|
|
|
1340
2634
|
// src/bin.ts
|
|
1341
2635
|
async function readVersion() {
|
|
@@ -1372,16 +2666,17 @@ async function runInit(cwd, flags) {
|
|
|
1372
2666
|
if (baseUrlInput.trim().length > 0) input.baseUrl = baseUrlInput.trim();
|
|
1373
2667
|
const result = await initConfig(input, cwd);
|
|
1374
2668
|
console.log(`\u5DF2\u751F\u6210 ${result.configFile}`);
|
|
2669
|
+
const gitignoreHint = GITIGNORE_ENTRIES.join("\u3001");
|
|
1375
2670
|
console.log(
|
|
1376
|
-
result.gitignore.added ? `\u5DF2\u628A ${
|
|
2671
|
+
result.gitignore.added ? `\u5DF2\u628A ${gitignoreHint} \u8FFD\u52A0\u8FDB .gitignore(\u9632\u6B62\u5BC6\u94A5\u5165\u5E93)` : `.gitignore \u5DF2\u5305\u542B ${gitignoreHint}`
|
|
1377
2672
|
);
|
|
1378
2673
|
}
|
|
1379
2674
|
function buildProgram(cwd, version = "0.0.0") {
|
|
1380
2675
|
const program = new Command();
|
|
1381
2676
|
program.name("easytwin").description("EasyTwin DevKit \u547D\u4EE4\u884C\u5DE5\u5177").version(version, "-V, --version");
|
|
1382
|
-
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));
|
|
2677
|
+
program.command("init").description("\u4EA4\u4E92\u5F0F\u751F\u6210 .easytwin/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));
|
|
1383
2678
|
const scene = program.command("scene").description("\u573A\u666F\u7BA1\u7406");
|
|
1384
|
-
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 easytwin.config.json").action(async () => {
|
|
2679
|
+
scene.command("list").description("\u5217\u51FA\u51ED\u636E\u53EF\u89C1\u7684\u573A\u666F,\u5E76\u540C\u6B65\u6458\u8981\u5230 .easytwin/easytwin.config.json").action(async () => {
|
|
1385
2680
|
const config = await loadConfig(cwd);
|
|
1386
2681
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1387
2682
|
const client = new EasyTwinClient(config);
|
|
@@ -1391,30 +2686,35 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
1391
2686
|
} else {
|
|
1392
2687
|
for (const s of scenes) console.log(`${s.id} ${s.name}`);
|
|
1393
2688
|
}
|
|
1394
|
-
console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${CONFIG_FILE_NAME}`);
|
|
2689
|
+
console.log(`\u5DF2\u540C\u6B65 ${scenes.length} \u4E2A\u573A\u666F\u5230 ${EASYTWIN_DIR}/${CONFIG_FILE_NAME}`);
|
|
1395
2690
|
});
|
|
1396
|
-
scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u8F93\u51FA\
|
|
2691
|
+
scene.command("pull <id>").description("\u62C9\u53D6\u573A\u666F JSON \u5230\u672C\u5730").option("-o, --out <path>", "\u989D\u5916\u8F93\u51FA\u8DEF\u5F84(\u4ECD\u4F1A\u5199\u5165 .easytwin/scenes/<id>.scene.json)").action(async (id, opts) => {
|
|
1397
2692
|
const config = await loadConfig(cwd);
|
|
1398
2693
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1399
2694
|
const client = new EasyTwinClient(config);
|
|
1400
|
-
const scene2 = await pullScene(client, id);
|
|
1401
|
-
const out = opts.out
|
|
1402
|
-
const file = await saveSceneJson(scene2, out);
|
|
2695
|
+
const scene2 = await pullScene(client, id, { cwd });
|
|
2696
|
+
const out = opts.out;
|
|
2697
|
+
const file = out ? await saveSceneJson(scene2, out) : defaultSceneOutPath(cwd, scene2.id);
|
|
1403
2698
|
console.log(`\u5DF2\u4FDD\u5B58\u5230 ${file}`);
|
|
1404
2699
|
});
|
|
2700
|
+
scene.command("inspect <id>").description("\u6253\u5370\u672C\u5730\u573A\u666F\u5BF9\u8C61\u6811(id/name/type),\u4E0D\u8BFB\u5B8C\u6574 JSON").option("--name <substr>", "\u6309\u540D\u79F0\u5B50\u4E32\u8FC7\u6EE4(\u4E0D\u533A\u5206\u5927\u5C0F\u5199)").option("--type <type>", "\u6309\u7EC4\u4EF6 type \u7CBE\u786E\u8FC7\u6EE4").action(async (id, opts) => {
|
|
2701
|
+
const result = await inspectScene({ cwd, id, name: opts.name, type: opts.type });
|
|
2702
|
+
process.stdout.write(result.text);
|
|
2703
|
+
});
|
|
1405
2704
|
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) => {
|
|
1406
2705
|
const config = await loadConfig(cwd);
|
|
1407
2706
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1408
2707
|
const client = new EasyTwinClient(config);
|
|
1409
|
-
const target = dirArg ?
|
|
2708
|
+
const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
|
|
1410
2709
|
const result = await pullWorkspace(client, target, { force: opts.force, dryRun: opts.dryRun });
|
|
1411
2710
|
console.log(formatWorkspacePullResult(result));
|
|
1412
2711
|
});
|
|
1413
|
-
program.command("upload
|
|
2712
|
+
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
2713
|
const config = await loadConfig(cwd);
|
|
1415
2714
|
if (config.mock) console.log(MOCK_MODE_HINT);
|
|
1416
2715
|
const client = new EasyTwinClient(config);
|
|
1417
|
-
|
|
2716
|
+
const target = dirArg ? path10.resolve(cwd, dirArg) : cwd;
|
|
2717
|
+
const result = await uploadDirectory(client, target, {
|
|
1418
2718
|
onProgress: (p) => {
|
|
1419
2719
|
if (p.phase === "upload") {
|
|
1420
2720
|
process.stderr.write(`\r\u5DF2\u4E0A\u4F20 ${p.current}/${p.total} bytes${p.file ? ` (${p.file})` : ""}`);
|
|
@@ -1422,15 +2722,29 @@ function buildProgram(cwd, version = "0.0.0") {
|
|
|
1422
2722
|
}
|
|
1423
2723
|
});
|
|
1424
2724
|
process.stderr.write("\n");
|
|
2725
|
+
console.log(formatUploadResult(result));
|
|
1425
2726
|
});
|
|
1426
2727
|
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
2728
|
const hint = await typesMissingHint(cwd);
|
|
1428
2729
|
if (hint) console.warn(hint);
|
|
1429
|
-
const outFile =
|
|
2730
|
+
const outFile = path10.resolve(cwd, opts.out ?? DEFAULT_BUNDLE_OUT);
|
|
1430
2731
|
const result = await bundleUserCode({ cwd, outFile });
|
|
1431
2732
|
for (const warning of result.warnings) console.warn(warning);
|
|
1432
2733
|
console.log(`\u5DF2\u6253\u5305\u5230 ${outFile}`);
|
|
1433
2734
|
});
|
|
2735
|
+
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) => {
|
|
2736
|
+
const port = Number.parseInt(opts.port, 10);
|
|
2737
|
+
if (!Number.isFinite(port) || port < 0) throw new Error(`\u65E0\u6548\u7AEF\u53E3:${opts.port}`);
|
|
2738
|
+
const server = await startPreviewServer({ cwd, sceneId, port });
|
|
2739
|
+
console.log(formatPreviewReadyMessage(server));
|
|
2740
|
+
await new Promise((resolve, reject) => {
|
|
2741
|
+
const stop = () => {
|
|
2742
|
+
server.close().then(resolve, reject);
|
|
2743
|
+
};
|
|
2744
|
+
process.once("SIGINT", stop);
|
|
2745
|
+
process.once("SIGTERM", stop);
|
|
2746
|
+
});
|
|
2747
|
+
});
|
|
1434
2748
|
const skills = program.command("skills").description("skills \u540C\u6B65\u5230\u7528\u6237\u9879\u76EE");
|
|
1435
2749
|
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
2750
|
const { summaries, types } = await syncSkills({ cwd, targets: opts.target });
|
|
@@ -1464,4 +2778,3 @@ export {
|
|
|
1464
2778
|
main,
|
|
1465
2779
|
runInit
|
|
1466
2780
|
};
|
|
1467
|
-
//# sourceMappingURL=bin.js.map
|