@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/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
var EASYTWIN_DIR = ".easytwin";
|
|
4
5
|
var CONFIG_FILE_NAME = "easytwin.config.json";
|
|
6
|
+
var EASYTWIN_SCENES_DIRNAME = "scenes";
|
|
5
7
|
var DEFAULT_BASE_URL = "http://saas-twin.k8s.dtstack.cn/";
|
|
6
8
|
var TEST_BASE_URL = "http://172.16.125.3:10100/";
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
var
|
|
9
|
+
var TEST_OSS_URL = "https://dt-easyv-test.oss-cn-hangzhou.aliyuncs.com/";
|
|
10
|
+
var PROD_OSS_URL = "https://dt-easyv-prod.oss-cn-hangzhou.aliyuncs.com/";
|
|
11
|
+
var DEFAULT_OSS_URL = TEST_OSS_URL;
|
|
12
|
+
var RUNTIME_TYPES_RELPATH = "easytwin/system/libs/runtime/types/index.d.ts";
|
|
11
13
|
var MOCK_APP_ID = "test";
|
|
12
14
|
var MOCK_APP_SECRET = "test";
|
|
13
15
|
function isMockCredentials(appId, appSecret) {
|
|
@@ -20,8 +22,31 @@ var ConfigError = class extends Error {
|
|
|
20
22
|
}
|
|
21
23
|
};
|
|
22
24
|
function configFilePath(cwd) {
|
|
25
|
+
return path.join(cwd, EASYTWIN_DIR, CONFIG_FILE_NAME);
|
|
26
|
+
}
|
|
27
|
+
function joinOssPath(ossUrl, relPath) {
|
|
28
|
+
return `${ossUrl.replace(/\/+$/, "")}/${relPath.replace(/^\/+/, "")}`;
|
|
29
|
+
}
|
|
30
|
+
function resolveRuntimeTypesUrl(options = {}) {
|
|
31
|
+
const root = options.ossUrl ?? (options.env === "prod" ? PROD_OSS_URL : TEST_OSS_URL);
|
|
32
|
+
return joinOssPath(root, RUNTIME_TYPES_RELPATH);
|
|
33
|
+
}
|
|
34
|
+
async function resolveRuntimeTypesUrlForCwd(cwd, env = process.env) {
|
|
35
|
+
let file;
|
|
36
|
+
try {
|
|
37
|
+
file = await readConfigFile(cwd);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
const easyEnv = env.EASYTWIN_ENV === "test" || env.EASYTWIN_ENV === "prod" ? env.EASYTWIN_ENV : file?.env;
|
|
41
|
+
const ossUrl = env.EASYTWIN_OSS_URL ?? file?.ossUrl;
|
|
42
|
+
return resolveRuntimeTypesUrl({ env: easyEnv, ossUrl });
|
|
43
|
+
}
|
|
44
|
+
function legacyConfigFilePath(cwd) {
|
|
23
45
|
return path.join(cwd, CONFIG_FILE_NAME);
|
|
24
46
|
}
|
|
47
|
+
function defaultSceneOutPath(cwd, id) {
|
|
48
|
+
return path.join(cwd, EASYTWIN_DIR, EASYTWIN_SCENES_DIRNAME, `${path.basename(id)}.scene.json`);
|
|
49
|
+
}
|
|
25
50
|
function parseConfig(raw) {
|
|
26
51
|
let parsed;
|
|
27
52
|
try {
|
|
@@ -51,29 +76,14 @@ function validateConfigShape(value) {
|
|
|
51
76
|
if (v.env !== void 0 && v.env !== "prod" && v.env !== "test") {
|
|
52
77
|
throw new ConfigError("env \u5FC5\u987B\u662F prod \u6216 test");
|
|
53
78
|
}
|
|
54
|
-
const opAccountId = optionalGatewayId(v.opAccountId);
|
|
55
|
-
const opUserId = optionalGatewayId(v.opUserId);
|
|
56
|
-
const spaceId = optionalGatewayId(v.spaceId);
|
|
57
79
|
const config = { appId: v.appId, appSecret: v.appSecret };
|
|
58
80
|
if (v.env === "prod" || v.env === "test") config.env = v.env;
|
|
59
81
|
if (typeof v.baseUrl === "string") config.baseUrl = v.baseUrl;
|
|
60
82
|
if (typeof v.ossUrl === "string") config.ossUrl = v.ossUrl;
|
|
61
|
-
if (opAccountId) config.opAccountId = opAccountId;
|
|
62
|
-
if (opUserId) config.opUserId = opUserId;
|
|
63
|
-
if (spaceId) config.spaceId = spaceId;
|
|
64
83
|
const scenes = parseConfigScenes(v.scenes);
|
|
65
84
|
if (scenes) config.scenes = scenes;
|
|
66
85
|
return config;
|
|
67
86
|
}
|
|
68
|
-
function optionalGatewayId(value) {
|
|
69
|
-
if (typeof value === "string") {
|
|
70
|
-
const t = value.trim();
|
|
71
|
-
return t.length > 0 ? t : void 0;
|
|
72
|
-
}
|
|
73
|
-
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
74
|
-
if (value !== void 0) throw new ConfigError("opAccountId / opUserId / spaceId \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6216\u6570\u5B57");
|
|
75
|
-
return void 0;
|
|
76
|
-
}
|
|
77
87
|
function parseConfigScenes(value) {
|
|
78
88
|
if (value === void 0) return void 0;
|
|
79
89
|
if (!Array.isArray(value)) throw new ConfigError("scenes \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
@@ -104,15 +114,39 @@ function parseConfigScenes(value) {
|
|
|
104
114
|
return scene;
|
|
105
115
|
});
|
|
106
116
|
}
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
async function tryReadText(file) {
|
|
118
|
+
try {
|
|
119
|
+
return await fs.readFile(file, "utf8");
|
|
120
|
+
} catch {
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function removeLegacyConfigFile(cwd) {
|
|
110
125
|
try {
|
|
111
|
-
|
|
126
|
+
await fs.unlink(legacyConfigFilePath(cwd));
|
|
112
127
|
} catch {
|
|
113
|
-
throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
|
|
114
128
|
}
|
|
115
|
-
|
|
129
|
+
}
|
|
130
|
+
async function migrateLegacyConfigIfPresent(cwd) {
|
|
131
|
+
const raw = await tryReadText(legacyConfigFilePath(cwd));
|
|
132
|
+
if (raw === void 0) return void 0;
|
|
133
|
+
const config = parseConfig(raw);
|
|
134
|
+
await writeConfigFile(cwd, config);
|
|
135
|
+
await removeLegacyConfigFile(cwd);
|
|
136
|
+
await appendGitignore(cwd);
|
|
137
|
+
return config;
|
|
138
|
+
}
|
|
139
|
+
async function readConfigFile(cwd) {
|
|
140
|
+
const file = configFilePath(cwd);
|
|
141
|
+
const raw = await tryReadText(file);
|
|
142
|
+
if (raw !== void 0) {
|
|
143
|
+
const config = parseConfig(raw);
|
|
144
|
+
await removeLegacyConfigFile(cwd);
|
|
145
|
+
return config;
|
|
146
|
+
}
|
|
147
|
+
const migrated = await migrateLegacyConfigIfPresent(cwd);
|
|
148
|
+
if (migrated) return migrated;
|
|
149
|
+
throw new ConfigError(`\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin init\``);
|
|
116
150
|
}
|
|
117
151
|
function resolveConfig(file, env = process.env) {
|
|
118
152
|
const appId = env.EASYTWIN_APP_ID ?? file.appId;
|
|
@@ -123,30 +157,11 @@ function resolveConfig(file, env = process.env) {
|
|
|
123
157
|
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");
|
|
124
158
|
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");
|
|
125
159
|
const source = env.EASYTWIN_APP_ID !== void 0 || env.EASYTWIN_APP_SECRET !== void 0 ? "env" : "file";
|
|
126
|
-
|
|
127
|
-
const opAccountId = optionalGatewayId(env.EASYTWIN_OP_ACCOUNT_ID) ?? file.opAccountId ?? (useTestGateway ? TEST_OP_ACCOUNT_ID : void 0);
|
|
128
|
-
const opUserId = optionalGatewayId(env.EASYTWIN_OP_USER_ID) ?? file.opUserId ?? (useTestGateway ? TEST_OP_USER_ID : void 0);
|
|
129
|
-
const spaceId = optionalGatewayId(env.EASYTWIN_SPACE_ID) ?? file.spaceId ?? (useTestGateway ? TEST_SPACE_ID : void 0);
|
|
130
|
-
const resolved = { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
131
|
-
if (opAccountId) resolved.opAccountId = opAccountId;
|
|
132
|
-
if (opUserId) resolved.opUserId = opUserId;
|
|
133
|
-
if (spaceId) resolved.spaceId = spaceId;
|
|
134
|
-
return resolved;
|
|
160
|
+
return { appId, appSecret, baseUrl, ossUrl, mock: isMockCredentials(appId, appSecret), source };
|
|
135
161
|
}
|
|
136
162
|
async function loadConfig(cwd, env = process.env) {
|
|
137
163
|
const file = await readConfigFile(cwd);
|
|
138
|
-
|
|
139
|
-
if (file.env === "test" && env.EASYTWIN_OP_ACCOUNT_ID === void 0 && env.EASYTWIN_OP_USER_ID === void 0 && env.EASYTWIN_SPACE_ID === void 0) {
|
|
140
|
-
if (!file.opAccountId || !file.opUserId || !file.spaceId) {
|
|
141
|
-
await writeConfigFile(cwd, {
|
|
142
|
-
...file,
|
|
143
|
-
opAccountId: file.opAccountId ?? TEST_OP_ACCOUNT_ID,
|
|
144
|
-
opUserId: file.opUserId ?? TEST_OP_USER_ID,
|
|
145
|
-
spaceId: file.spaceId ?? TEST_SPACE_ID
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return resolved;
|
|
164
|
+
return resolveConfig(file, env);
|
|
150
165
|
}
|
|
151
166
|
async function writeConfigFile(cwd, config) {
|
|
152
167
|
const file = configFilePath(cwd);
|
|
@@ -154,19 +169,15 @@ async function writeConfigFile(cwd, config) {
|
|
|
154
169
|
if (config.env) body.env = config.env;
|
|
155
170
|
if (config.baseUrl) body.baseUrl = config.baseUrl;
|
|
156
171
|
if (config.ossUrl) body.ossUrl = config.ossUrl;
|
|
157
|
-
const opAccountId = config.opAccountId ?? (config.env === "test" ? TEST_OP_ACCOUNT_ID : void 0);
|
|
158
|
-
const opUserId = config.opUserId ?? (config.env === "test" ? TEST_OP_USER_ID : void 0);
|
|
159
|
-
const spaceId = config.spaceId ?? (config.env === "test" ? TEST_SPACE_ID : void 0);
|
|
160
|
-
if (opAccountId) body.opAccountId = opAccountId;
|
|
161
|
-
if (opUserId) body.opUserId = opUserId;
|
|
162
|
-
if (spaceId) body.spaceId = spaceId;
|
|
163
172
|
if (config.scenes !== void 0) body.scenes = config.scenes;
|
|
164
|
-
await fs.mkdir(
|
|
173
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
165
174
|
await fs.writeFile(file, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
175
|
+
await removeLegacyConfigFile(cwd);
|
|
166
176
|
return file;
|
|
167
177
|
}
|
|
168
178
|
var GITIGNORE_FILE_NAME = ".gitignore";
|
|
169
|
-
var GITIGNORE_ENTRY = "easytwin.config.json";
|
|
179
|
+
var GITIGNORE_ENTRY = ".easytwin/easytwin.config.json";
|
|
180
|
+
var GITIGNORE_ENTRIES = [GITIGNORE_ENTRY, ".easytwin/scenes/"];
|
|
170
181
|
async function appendGitignore(cwd) {
|
|
171
182
|
const file = path.join(cwd, GITIGNORE_FILE_NAME);
|
|
172
183
|
let content = "";
|
|
@@ -177,9 +188,10 @@ async function appendGitignore(cwd) {
|
|
|
177
188
|
created = true;
|
|
178
189
|
}
|
|
179
190
|
const lines = content.split(/\r?\n/);
|
|
180
|
-
|
|
191
|
+
const missing = GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
|
|
192
|
+
if (missing.length === 0) return { created, added: false };
|
|
181
193
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
182
|
-
await fs.writeFile(file, content + prefix +
|
|
194
|
+
await fs.writeFile(file, content + prefix + missing.join("\n") + "\n", "utf8");
|
|
183
195
|
return { created, added: true };
|
|
184
196
|
}
|
|
185
197
|
async function initConfig(input, cwd) {
|
|
@@ -197,22 +209,20 @@ async function writeConfigScenes(cwd, scenes) {
|
|
|
197
209
|
import http from "http";
|
|
198
210
|
import https from "https";
|
|
199
211
|
import { URL as URL2 } from "url";
|
|
212
|
+
var APP_ID_HEADER = "x-app-id";
|
|
200
213
|
var AUTH_HEADER = "x-app-secret";
|
|
201
|
-
var OP_ACCOUNT_ID_HEADER = "op-account-id";
|
|
202
|
-
var OP_USER_ID_HEADER = "op-user-id";
|
|
203
|
-
var SPACE_ID_HEADER = "space-id";
|
|
204
214
|
function enc(id) {
|
|
205
215
|
return encodeURIComponent(id);
|
|
206
216
|
}
|
|
207
217
|
var ENDPOINTS = {
|
|
208
|
-
/**
|
|
209
|
-
linkedScenes: (
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
|
|
218
|
+
/** POST 已关联场景列表(分享路径,空 body)。 */
|
|
219
|
+
linkedScenes: () => `/api/twin/v1/share/sdk-application-code/scenes`,
|
|
220
|
+
/** POST 拉取工作区全部代码文件(分享路径,空 body)。 */
|
|
221
|
+
workspaceCodePull: () => `/api/twin/v1/share/sdk-application-code/pull`,
|
|
222
|
+
/** POST 一次推送 create/update/delete(分享路径)。 */
|
|
223
|
+
workspaceCodePush: () => `/api/twin/v1/share/sdk-application-code/push`,
|
|
224
|
+
/** GET 工作区文件约束(仍带 applicationId,走 OP 网关;上传不调用,拉取失败则降级)。 */
|
|
225
|
+
workspaceConfig: (applicationId) => `/api/twin/v1/sdk-application-code/${enc(applicationId)}/workspace-config`
|
|
216
226
|
};
|
|
217
227
|
var EasyTwinApiError = class extends Error {
|
|
218
228
|
status;
|
|
@@ -255,35 +265,25 @@ var EasyTwinClient = class {
|
|
|
255
265
|
baseUrl;
|
|
256
266
|
/** twin runtime / 场景快照资产根(baseOSSUrl)。 */
|
|
257
267
|
ossUrl;
|
|
258
|
-
/** 接入凭证 App ID
|
|
268
|
+
/** 接入凭证 App ID;写入 `x-app-id`,workspace-config 路径仍可用。 */
|
|
259
269
|
appId;
|
|
260
270
|
/** 本地测试模式:true 时 scene/upload/pull 走本地 mock,不发网络请求(见 config.ts)。 */
|
|
261
271
|
mock;
|
|
262
272
|
appSecret;
|
|
263
|
-
opAccountId;
|
|
264
|
-
opUserId;
|
|
265
|
-
spaceId;
|
|
266
273
|
constructor(config) {
|
|
267
274
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
268
275
|
this.ossUrl = config.ossUrl.replace(/\/+$/, "");
|
|
269
276
|
this.mock = config.mock;
|
|
270
277
|
this.appId = config.appId;
|
|
271
278
|
this.appSecret = config.appSecret;
|
|
272
|
-
this.opAccountId = config.opAccountId;
|
|
273
|
-
this.opUserId = config.opUserId;
|
|
274
|
-
this.spaceId = config.spaceId;
|
|
275
279
|
}
|
|
276
280
|
/** 全仓唯一认证头注入点。 */
|
|
277
281
|
authHeaders() {
|
|
278
|
-
|
|
279
|
-
if (this.opAccountId) headers[OP_ACCOUNT_ID_HEADER] = this.opAccountId;
|
|
280
|
-
if (this.opUserId) headers[OP_USER_ID_HEADER] = this.opUserId;
|
|
281
|
-
if (this.spaceId) headers[SPACE_ID_HEADER] = this.spaceId;
|
|
282
|
-
return headers;
|
|
282
|
+
return { [APP_ID_HEADER]: this.appId, [AUTH_HEADER]: this.appSecret };
|
|
283
283
|
}
|
|
284
284
|
/** JSON 请求(原生 fetch)。 */
|
|
285
|
-
async request(
|
|
286
|
-
const url = `${this.baseUrl}${
|
|
285
|
+
async request(path9, options = {}) {
|
|
286
|
+
const url = `${this.baseUrl}${path9}`;
|
|
287
287
|
const headers = { ...this.authHeaders(), ...options.headers };
|
|
288
288
|
if (options.body !== void 0 && options.body !== null && !("Content-Type" in headers)) {
|
|
289
289
|
headers["Content-Type"] = "application/json";
|
|
@@ -305,8 +305,8 @@ var EasyTwinClient = class {
|
|
|
305
305
|
* multipart 流式上传(手写 http 请求以支持逐字节进度回调)。
|
|
306
306
|
* 这是 fetch 上传的替代路径:原生 fetch 无法上报上传进度。
|
|
307
307
|
*/
|
|
308
|
-
async upload(
|
|
309
|
-
const url = new URL2(`${this.baseUrl}${
|
|
308
|
+
async upload(path9, options) {
|
|
309
|
+
const url = new URL2(`${this.baseUrl}${path9}`);
|
|
310
310
|
const mod = url.protocol === "https:" ? https : http;
|
|
311
311
|
const headers = {
|
|
312
312
|
...this.authHeaders(),
|
|
@@ -463,31 +463,38 @@ function deriveExampleSceneId(payload) {
|
|
|
463
463
|
async function exampleSceneSummary(exampleFile) {
|
|
464
464
|
return { id: deriveExampleSceneId(await loadExampleScene(exampleFile)), name: MOCK_SCENE_NAME };
|
|
465
465
|
}
|
|
466
|
+
async function fetchLinkedScenes(client) {
|
|
467
|
+
return client.request(ENDPOINTS.linkedScenes(), { method: "POST" });
|
|
468
|
+
}
|
|
466
469
|
async function listScenes(client, options = {}) {
|
|
467
|
-
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await
|
|
470
|
+
const scenes = client.mock ? [await exampleSceneSummary(options.exampleFile)] : normalizeSceneList(await fetchLinkedScenes(client));
|
|
468
471
|
if (options.cwd) await writeConfigScenes(options.cwd, toConfigScenes(scenes));
|
|
469
472
|
return scenes;
|
|
470
473
|
}
|
|
471
474
|
async function pullScene(client, id, options = {}) {
|
|
475
|
+
let scene;
|
|
472
476
|
if (client.mock) {
|
|
473
|
-
const
|
|
474
|
-
const sceneId = deriveExampleSceneId(
|
|
477
|
+
const payload = await loadExampleScene(options.exampleFile);
|
|
478
|
+
const sceneId = deriveExampleSceneId(payload);
|
|
475
479
|
if (sceneId !== id) {
|
|
476
480
|
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)`);
|
|
477
481
|
}
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
482
|
+
scene = { id: sceneId, name: MOCK_SCENE_NAME, payload };
|
|
483
|
+
} else {
|
|
484
|
+
const data = await fetchLinkedScenes(client);
|
|
485
|
+
const scenes = normalizeLinkedScenes(data);
|
|
486
|
+
const hit = scenes.find((s) => s.sceneKey === id || s.id === id);
|
|
487
|
+
if (!hit) {
|
|
488
|
+
const available = scenes.map((s) => s.sceneKey).filter((k) => k.length > 0);
|
|
489
|
+
throw new Error(
|
|
490
|
+
available.length > 0 ? `\u672A\u627E\u5230\u573A\u666F ${id}(\u53EF\u7528 Scene Key:${available.join(", ")})` : `\u672A\u627E\u5230\u573A\u666F ${id}`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const payload = await fetchSnapshotJson(client.ossUrl, hit.snapshotUrl);
|
|
494
|
+
scene = { id: hit.sceneKey, name: hit.name, payload };
|
|
488
495
|
}
|
|
489
|
-
|
|
490
|
-
return
|
|
496
|
+
if (options.cwd) await saveSceneJson(scene, defaultSceneOutPath(options.cwd, scene.id));
|
|
497
|
+
return scene;
|
|
491
498
|
}
|
|
492
499
|
async function saveSceneJson(scene, out) {
|
|
493
500
|
await fs2.mkdir(path2.dirname(out), { recursive: true });
|
|
@@ -544,11 +551,127 @@ function parseSceneStructure(scene) {
|
|
|
544
551
|
};
|
|
545
552
|
return order.filter((id) => !isChild.has(id)).map((id) => build2(id, /* @__PURE__ */ new Set()));
|
|
546
553
|
}
|
|
554
|
+
var INSPECT_NODE_LIMIT = 200;
|
|
555
|
+
var INSPECT_INDENT = " ";
|
|
556
|
+
function nodeMatchesInspect(node, filter) {
|
|
557
|
+
if (filter.name !== void 0 && filter.name.length > 0) {
|
|
558
|
+
if (!node.name.toLowerCase().includes(filter.name.toLowerCase())) return false;
|
|
559
|
+
}
|
|
560
|
+
if (filter.type !== void 0 && filter.type.length > 0) {
|
|
561
|
+
if (node.type !== filter.type) return false;
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
function filterSceneTree(roots, filter = {}) {
|
|
566
|
+
const hasName = filter.name !== void 0 && filter.name.length > 0;
|
|
567
|
+
const hasType = filter.type !== void 0 && filter.type.length > 0;
|
|
568
|
+
if (!hasName && !hasType) return roots;
|
|
569
|
+
const walk = (node) => {
|
|
570
|
+
const children = [];
|
|
571
|
+
for (const child of node.children) {
|
|
572
|
+
const kept = walk(child);
|
|
573
|
+
if (kept) children.push(kept);
|
|
574
|
+
}
|
|
575
|
+
if (nodeMatchesInspect(node, filter) || children.length > 0) {
|
|
576
|
+
return { ...node, children };
|
|
577
|
+
}
|
|
578
|
+
return void 0;
|
|
579
|
+
};
|
|
580
|
+
return roots.flatMap((node) => {
|
|
581
|
+
const kept = walk(node);
|
|
582
|
+
return kept ? [kept] : [];
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
function countSceneTreeNodes(roots) {
|
|
586
|
+
let n = 0;
|
|
587
|
+
const walk = (nodes) => {
|
|
588
|
+
for (const node of nodes) {
|
|
589
|
+
n += 1;
|
|
590
|
+
walk(node.children);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
walk(roots);
|
|
594
|
+
return n;
|
|
595
|
+
}
|
|
596
|
+
function formatSceneTree(roots, limit = INSPECT_NODE_LIMIT) {
|
|
597
|
+
const total = countSceneTreeNodes(roots);
|
|
598
|
+
if (total === 0) {
|
|
599
|
+
return { text: "(\u65E0\u5BF9\u8C61\u7ED3\u6784)\n", printed: 0, total: 0, truncated: false };
|
|
600
|
+
}
|
|
601
|
+
const lines = [];
|
|
602
|
+
let printed = 0;
|
|
603
|
+
const walk = (nodes, depth) => {
|
|
604
|
+
for (const node of nodes) {
|
|
605
|
+
if (printed >= limit) return;
|
|
606
|
+
lines.push(`${INSPECT_INDENT.repeat(depth)}${node.id} ${node.name} ${node.type ?? ""}`);
|
|
607
|
+
printed += 1;
|
|
608
|
+
walk(node.children, depth + 1);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
walk(roots, 0);
|
|
612
|
+
const truncated = printed < total;
|
|
613
|
+
if (truncated) lines.push(`\u5176\u4F59 ${total - printed} \u4E2A,\u8BF7\u52A0 --name/--type`);
|
|
614
|
+
return { text: `${lines.join("\n")}
|
|
615
|
+
`, printed, total, truncated };
|
|
616
|
+
}
|
|
617
|
+
async function inspectScene(options) {
|
|
618
|
+
const file = defaultSceneOutPath(options.cwd, options.id);
|
|
619
|
+
let raw;
|
|
620
|
+
try {
|
|
621
|
+
raw = await fs2.readFile(file, "utf8");
|
|
622
|
+
} catch {
|
|
623
|
+
throw new Error(`\u672A\u627E\u5230\u672C\u5730\u573A\u666F\u6587\u4EF6 ${file},\u8BF7\u5148\u8FD0\u884C \`easytwin scene pull ${options.id}\``);
|
|
624
|
+
}
|
|
625
|
+
let payload;
|
|
626
|
+
try {
|
|
627
|
+
payload = JSON.parse(raw);
|
|
628
|
+
} catch {
|
|
629
|
+
throw new Error(`\u573A\u666F\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON:${file}`);
|
|
630
|
+
}
|
|
631
|
+
const roots = filterSceneTree(parseSceneStructure(payload), { name: options.name, type: options.type });
|
|
632
|
+
return { file, roots, ...formatSceneTree(roots, options.limit ?? INSPECT_NODE_LIMIT) };
|
|
633
|
+
}
|
|
547
634
|
|
|
548
635
|
// src/upload.ts
|
|
549
636
|
import { promises as fs3 } from "fs";
|
|
550
637
|
import path3 from "path";
|
|
551
|
-
var
|
|
638
|
+
var WORKSPACE_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
639
|
+
".git",
|
|
640
|
+
"node_modules",
|
|
641
|
+
"dist",
|
|
642
|
+
".easytwin",
|
|
643
|
+
".cursor",
|
|
644
|
+
".claude",
|
|
645
|
+
".qoder",
|
|
646
|
+
".vscode"
|
|
647
|
+
]);
|
|
648
|
+
var WORKSPACE_IGNORED_FILES = /* @__PURE__ */ new Set([CONFIG_FILE_NAME, GITIGNORE_FILE_NAME]);
|
|
649
|
+
function isTsconfigFile(base) {
|
|
650
|
+
return /^tsconfig(\..+)?\.json$/i.test(base);
|
|
651
|
+
}
|
|
652
|
+
var WORKSPACE_CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".json"];
|
|
653
|
+
function isWorkspaceSpecFile(relPath) {
|
|
654
|
+
const base = normalizePath(relPath).split("/").pop() ?? "";
|
|
655
|
+
return /\.spec\.ts$/i.test(base);
|
|
656
|
+
}
|
|
657
|
+
function isIgnoredWorkspacePath(relPath) {
|
|
658
|
+
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
659
|
+
if (parts.some((p) => WORKSPACE_IGNORED_DIRS.has(p))) return true;
|
|
660
|
+
const base = parts[parts.length - 1];
|
|
661
|
+
if (base === void 0) return false;
|
|
662
|
+
if (WORKSPACE_IGNORED_FILES.has(base)) return true;
|
|
663
|
+
if (isTsconfigFile(base)) return true;
|
|
664
|
+
return base.toLowerCase().endsWith(".scene.json");
|
|
665
|
+
}
|
|
666
|
+
function defaultWorkspaceIgnore(relPath) {
|
|
667
|
+
return isIgnoredWorkspacePath(relPath) || isWorkspaceSpecFile(relPath);
|
|
668
|
+
}
|
|
669
|
+
function combineUploadIgnore(extra) {
|
|
670
|
+
return (relPath) => defaultWorkspaceIgnore(relPath) || (extra?.(relPath) ?? false);
|
|
671
|
+
}
|
|
672
|
+
async function collectWorkspaceCodeFiles(dir, ignore) {
|
|
673
|
+
return (await collectFiles(dir, ignore)).filter((f) => isWorkspaceCodeFile(f.relPath));
|
|
674
|
+
}
|
|
552
675
|
async function collectFiles(dir, ignore = () => false) {
|
|
553
676
|
const files = [];
|
|
554
677
|
async function walk(current, rel) {
|
|
@@ -557,7 +680,7 @@ async function collectFiles(dir, ignore = () => false) {
|
|
|
557
680
|
const relPath = path3.posix.join(rel, entry.name);
|
|
558
681
|
if (ignore(relPath)) continue;
|
|
559
682
|
if (entry.isDirectory()) {
|
|
560
|
-
if (
|
|
683
|
+
if (entry.name === ".git") continue;
|
|
561
684
|
await walk(path3.join(current, entry.name), relPath);
|
|
562
685
|
} else if (entry.isFile()) {
|
|
563
686
|
const absPath = path3.join(current, entry.name);
|
|
@@ -639,6 +762,10 @@ function extensionOf(relPath) {
|
|
|
639
762
|
function allowedExtensionSet(list) {
|
|
640
763
|
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
641
764
|
}
|
|
765
|
+
var WORKSPACE_CODE_EXTENSION_SET = allowedExtensionSet(WORKSPACE_CODE_EXTENSIONS);
|
|
766
|
+
function isWorkspaceCodeFile(relPath) {
|
|
767
|
+
return WORKSPACE_CODE_EXTENSION_SET.has(extensionOf(relPath));
|
|
768
|
+
}
|
|
642
769
|
function directoryDepth(relPath) {
|
|
643
770
|
const parts = normalizePath(relPath).split("/").filter((p) => p.length > 0);
|
|
644
771
|
return Math.max(0, parts.length - 1);
|
|
@@ -665,65 +792,94 @@ function assertUploadable(files, config) {
|
|
|
665
792
|
}
|
|
666
793
|
if (errors.length > 0) throw new Error(`\u65E0\u6CD5\u4E0A\u4F20:${errors.join("; ")}`);
|
|
667
794
|
}
|
|
795
|
+
function countsFromPlan(plan) {
|
|
796
|
+
return {
|
|
797
|
+
created: plan.creates.length,
|
|
798
|
+
updated: plan.updates.length,
|
|
799
|
+
deleted: plan.deletes.length,
|
|
800
|
+
unchanged: plan.unchanged.length
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function planWorkspaceUpload(local, remote) {
|
|
804
|
+
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
805
|
+
const localPaths = new Set(local.map((item) => normalizePath(item.file.relPath)));
|
|
806
|
+
const deletes = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => ({ id: f.id, filePath: normalizePath(f.filePath) }));
|
|
807
|
+
const creates = [];
|
|
808
|
+
const updates = [];
|
|
809
|
+
const unchanged = [];
|
|
810
|
+
for (const item of local) {
|
|
811
|
+
const remoteFile = remoteByPath.get(normalizePath(item.file.relPath));
|
|
812
|
+
if (!remoteFile) creates.push(item);
|
|
813
|
+
else if (remoteFile.content !== item.content) {
|
|
814
|
+
updates.push({ file: item.file, id: remoteFile.id, content: item.content });
|
|
815
|
+
} else {
|
|
816
|
+
unchanged.push(item.file);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return { creates, updates, deletes, unchanged };
|
|
820
|
+
}
|
|
821
|
+
function formatUploadResult(result) {
|
|
822
|
+
const prefix = result.mock ? "[\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F] \u672A\u771F\u6B63\u4E0A\u4F20; " : "";
|
|
823
|
+
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}`;
|
|
824
|
+
}
|
|
668
825
|
async function mockUpload(dir, options) {
|
|
669
|
-
const ignore = options.ignore
|
|
670
|
-
const files = await
|
|
826
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
827
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
671
828
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
672
829
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
673
830
|
options.onProgress?.({ phase: "upload", current: total, total });
|
|
674
|
-
return { fileCount: files.length, byteCount: total, mock: true };
|
|
831
|
+
return { fileCount: files.length, byteCount: total, created: 0, updated: 0, deleted: 0, unchanged: files.length, mock: true };
|
|
832
|
+
}
|
|
833
|
+
function buildWorkspacePushBody(plan) {
|
|
834
|
+
const body = {};
|
|
835
|
+
if (plan.creates.length > 0) {
|
|
836
|
+
body.create = plan.creates.map((c) => ({
|
|
837
|
+
filePath: normalizePath(c.file.relPath),
|
|
838
|
+
content: c.content
|
|
839
|
+
}));
|
|
840
|
+
}
|
|
841
|
+
if (plan.updates.length > 0) {
|
|
842
|
+
body.update = plan.updates.map((p) => ({
|
|
843
|
+
id: p.id,
|
|
844
|
+
content: p.content,
|
|
845
|
+
filePath: normalizePath(p.file.relPath)
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
if (plan.deletes.length > 0) {
|
|
849
|
+
body.delete = plan.deletes.map((d) => d.id);
|
|
850
|
+
}
|
|
851
|
+
return Object.keys(body).length > 0 ? body : null;
|
|
675
852
|
}
|
|
676
853
|
async function uploadDirectory(client, dir, options = {}) {
|
|
677
854
|
if (client.mock) return mockUpload(dir, options);
|
|
678
|
-
const ignore = options.ignore
|
|
679
|
-
const files = await
|
|
855
|
+
const ignore = combineUploadIgnore(options.ignore);
|
|
856
|
+
const files = await collectWorkspaceCodeFiles(dir, ignore);
|
|
680
857
|
const total = files.reduce((sum, f) => sum + f.size, 0);
|
|
681
858
|
options.onProgress?.({ phase: "collect", current: total, total });
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
assertUploadable(files, wsConfig);
|
|
685
|
-
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCode(appId), { method: "GET" }));
|
|
686
|
-
const remoteByPath = new Map(remote.map((f) => [normalizePath(f.filePath), f]));
|
|
687
|
-
const localPaths = new Set(files.map((f) => normalizePath(f.relPath)));
|
|
688
|
-
const toDelete = remote.filter((f) => !localPaths.has(normalizePath(f.filePath))).map((f) => f.id);
|
|
689
|
-
if (toDelete.length > 0) {
|
|
690
|
-
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
691
|
-
method: "DELETE",
|
|
692
|
-
body: JSON.stringify({ ids: toDelete })
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
const creates = [];
|
|
696
|
-
const patches = [];
|
|
697
|
-
const unchanged = [];
|
|
859
|
+
const remote = normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
860
|
+
const local = [];
|
|
698
861
|
for (const file of files) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
862
|
+
local.push({ file, content: await fs3.readFile(file.absPath, "utf8") });
|
|
863
|
+
}
|
|
864
|
+
const plan = planWorkspaceUpload(local, remote);
|
|
865
|
+
const counts = countsFromPlan(plan);
|
|
866
|
+
const pushBody = buildWorkspacePushBody(plan);
|
|
867
|
+
if (pushBody) {
|
|
868
|
+
await client.request(ENDPOINTS.workspaceCodePush(), {
|
|
869
|
+
method: "POST",
|
|
870
|
+
body: JSON.stringify(pushBody)
|
|
871
|
+
});
|
|
704
872
|
}
|
|
705
873
|
let sent = 0;
|
|
706
874
|
const bump = (file) => {
|
|
707
875
|
sent += file.size;
|
|
708
876
|
options.onProgress?.({ phase: "upload", current: sent, total, file: file.relPath });
|
|
709
877
|
};
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
body: JSON.stringify({ files: patches.map((p) => ({ id: p.id, content: p.content })) })
|
|
714
|
-
});
|
|
715
|
-
for (const p of patches) bump(p.file);
|
|
716
|
-
}
|
|
717
|
-
for (const c of creates) {
|
|
718
|
-
await client.request(ENDPOINTS.workspaceCodeFiles(appId), {
|
|
719
|
-
method: "POST",
|
|
720
|
-
body: JSON.stringify({ filePath: normalizePath(c.file.relPath), content: c.content })
|
|
721
|
-
});
|
|
722
|
-
bump(c.file);
|
|
723
|
-
}
|
|
724
|
-
for (const u of unchanged) bump(u);
|
|
878
|
+
for (const c of plan.creates) bump(c.file);
|
|
879
|
+
for (const p of plan.updates) bump(p.file);
|
|
880
|
+
for (const u of plan.unchanged) bump(u);
|
|
725
881
|
if (files.length === 0) options.onProgress?.({ phase: "upload", current: 0, total: 0 });
|
|
726
|
-
return { fileCount: files.length, byteCount: total };
|
|
882
|
+
return { fileCount: files.length, byteCount: total, ...counts };
|
|
727
883
|
}
|
|
728
884
|
|
|
729
885
|
// src/workspace.ts
|
|
@@ -771,42 +927,22 @@ export default class App extends TwinApp {
|
|
|
771
927
|
}
|
|
772
928
|
}
|
|
773
929
|
`;
|
|
774
|
-
var PULL_IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
775
|
-
".git",
|
|
776
|
-
"node_modules",
|
|
777
|
-
"dist",
|
|
778
|
-
".easytwin",
|
|
779
|
-
".cursor",
|
|
780
|
-
".claude",
|
|
781
|
-
".qoder",
|
|
782
|
-
".vscode"
|
|
783
|
-
]);
|
|
784
|
-
var PULL_IGNORED_FILES = /* @__PURE__ */ new Set(["easytwin.config.json"]);
|
|
785
|
-
var DEFAULT_PULL_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css", ".html", ".md"];
|
|
786
930
|
function defaultPullIgnore(relPath) {
|
|
787
|
-
|
|
788
|
-
if (parts.some((p) => PULL_IGNORED_DIRS.has(p))) return true;
|
|
789
|
-
const base = parts[parts.length - 1];
|
|
790
|
-
return base !== void 0 && PULL_IGNORED_FILES.has(base);
|
|
931
|
+
return defaultWorkspaceIgnore(relPath);
|
|
791
932
|
}
|
|
792
933
|
function isSafeRelPath(relPath) {
|
|
793
934
|
const n = normalizePath(relPath);
|
|
794
935
|
if (!n) return false;
|
|
795
|
-
|
|
936
|
+
const lower = n.toLowerCase();
|
|
937
|
+
if (lower === "easytwin.config.json" || lower.endsWith("/easytwin.config.json")) return false;
|
|
938
|
+
if (lower.split("/")[0] === ".easytwin") return false;
|
|
939
|
+
const base = n.split("/").pop() ?? "";
|
|
940
|
+
if (/^tsconfig(\..+)?\.json$/i.test(base)) return false;
|
|
796
941
|
if (path5.isAbsolute(n) || path5.win32.isAbsolute(n.replace(/\//g, "\\"))) return false;
|
|
797
942
|
const parts = n.split("/");
|
|
798
943
|
if (parts.some((p) => p === ".." || p === "." || p === "")) return false;
|
|
799
944
|
return true;
|
|
800
945
|
}
|
|
801
|
-
function extensionOf2(relPath) {
|
|
802
|
-
const base = relPath.split("/").pop() ?? "";
|
|
803
|
-
const i = base.lastIndexOf(".");
|
|
804
|
-
if (i <= 0) return "";
|
|
805
|
-
return base.slice(i).toLowerCase();
|
|
806
|
-
}
|
|
807
|
-
function allowedExtensionSet2(list) {
|
|
808
|
-
return new Set(list.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`));
|
|
809
|
-
}
|
|
810
946
|
function normalizeEol(text) {
|
|
811
947
|
return text.replace(/\r\n/g, "\n");
|
|
812
948
|
}
|
|
@@ -822,30 +958,18 @@ async function readLocalText(absPath) {
|
|
|
822
958
|
throw err;
|
|
823
959
|
}
|
|
824
960
|
}
|
|
825
|
-
async function loadAllowedExtensions(client) {
|
|
826
|
-
if (client.mock) return DEFAULT_PULL_EXTENSIONS;
|
|
827
|
-
try {
|
|
828
|
-
const cfg = normalizeWorkspaceConfig(
|
|
829
|
-
await client.request(ENDPOINTS.workspaceConfig(client.appId), { method: "GET" })
|
|
830
|
-
);
|
|
831
|
-
return cfg.allowedFileExtensions.length > 0 ? cfg.allowedFileExtensions : DEFAULT_PULL_EXTENSIONS;
|
|
832
|
-
} catch {
|
|
833
|
-
return DEFAULT_PULL_EXTENSIONS;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
961
|
async function fetchRemoteFiles(client) {
|
|
837
962
|
if (client.mock) return [];
|
|
838
|
-
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.
|
|
963
|
+
return normalizeWorkspaceFiles(await client.request(ENDPOINTS.workspaceCodePull(), { method: "POST" }));
|
|
839
964
|
}
|
|
840
965
|
async function planWorkspacePull(client, dir, options = {}) {
|
|
841
966
|
const ignore = combineIgnore(options.ignore);
|
|
842
|
-
const allowed = allowedExtensionSet2(await loadAllowedExtensions(client));
|
|
843
967
|
const remoteRaw = await fetchRemoteFiles(client);
|
|
844
968
|
const skippedRemotePaths = [];
|
|
845
969
|
const remoteByPath = /* @__PURE__ */ new Map();
|
|
846
970
|
for (const file of remoteRaw) {
|
|
847
971
|
const rel = normalizePath(file.filePath);
|
|
848
|
-
if (!isSafeRelPath(rel) || ignore(rel)) {
|
|
972
|
+
if (!isSafeRelPath(rel) || ignore(rel) || !isWorkspaceCodeFile(rel)) {
|
|
849
973
|
skippedRemotePaths.push(rel || file.filePath);
|
|
850
974
|
continue;
|
|
851
975
|
}
|
|
@@ -861,7 +985,7 @@ async function planWorkspacePull(client, dir, options = {}) {
|
|
|
861
985
|
const localByPath = /* @__PURE__ */ new Map();
|
|
862
986
|
for (const file of localFiles) {
|
|
863
987
|
const rel = normalizePath(file.relPath);
|
|
864
|
-
if (!
|
|
988
|
+
if (!isWorkspaceCodeFile(rel)) continue;
|
|
865
989
|
const content = await readLocalText(file.absPath);
|
|
866
990
|
if (content !== void 0) localByPath.set(rel, content);
|
|
867
991
|
}
|
|
@@ -982,382 +1106,46 @@ function workspacePullPendingWrites(plan, force = false) {
|
|
|
982
1106
|
return plan.changes.filter((c) => c.kind === "seed" || c.kind === "remote-only" || force && c.kind === "modified").map((c) => c.path);
|
|
983
1107
|
}
|
|
984
1108
|
|
|
985
|
-
// src/
|
|
986
|
-
import {
|
|
1109
|
+
// src/workspaceTests.ts
|
|
1110
|
+
import { promises as fs6 } from "fs";
|
|
1111
|
+
import path7 from "path";
|
|
1112
|
+
|
|
1113
|
+
// src/bundle.ts
|
|
1114
|
+
import * as esbuild from "esbuild-wasm";
|
|
1115
|
+
import { promises as fs5 } from "fs";
|
|
987
1116
|
import path6 from "path";
|
|
988
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
989
|
-
var SKILL_NAMES = [
|
|
990
|
-
"easytwin-render",
|
|
991
|
-
"easytwin-core",
|
|
992
|
-
"easytwin-develop",
|
|
993
|
-
"easytwin-bootstrap",
|
|
994
|
-
"easytwin-scene",
|
|
995
|
-
"easytwin-upload"
|
|
996
|
-
];
|
|
997
|
-
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
998
|
-
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
999
|
-
var META_FILE_NAME = ".easytwin-meta.json";
|
|
1000
|
-
var EASYTWIN_TYPES_DIR = ".easytwin/types";
|
|
1001
|
-
var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
1002
|
-
{
|
|
1003
|
-
compilerOptions: {
|
|
1004
|
-
target: "ES2022",
|
|
1005
|
-
module: "ESNext",
|
|
1006
|
-
moduleResolution: "bundler",
|
|
1007
|
-
strict: true,
|
|
1008
|
-
skipLibCheck: true,
|
|
1009
|
-
noEmit: true,
|
|
1010
|
-
paths: {
|
|
1011
|
-
"@easytwin/runtime": [".easytwin/types"],
|
|
1012
|
-
"@easytwin/apps": [".easytwin/types/apps"]
|
|
1013
|
-
}
|
|
1014
|
-
},
|
|
1015
|
-
include: ["src/**/*.ts"]
|
|
1016
|
-
},
|
|
1017
|
-
null,
|
|
1018
|
-
2
|
|
1019
|
-
)}
|
|
1020
|
-
`;
|
|
1021
|
-
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
1022
|
-
"skipLibCheck": true,
|
|
1023
|
-
"paths": {
|
|
1024
|
-
"@easytwin/runtime": [".easytwin/types"],
|
|
1025
|
-
"@easytwin/apps": [".easytwin/types/apps"]
|
|
1026
|
-
}`;
|
|
1027
|
-
var APPS_TYPES_FILE = "apps.d.ts";
|
|
1028
|
-
var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
|
|
1029
1117
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
camera: RuntimeScene["camera"]["main"] | null;
|
|
1040
|
-
sceneManager: {
|
|
1041
|
-
currentSceneId: string;
|
|
1042
|
-
runtime: SceneManager | null;
|
|
1043
|
-
loadScene(sceneId: string): Promise<void>;
|
|
1044
|
-
};
|
|
1045
|
-
logger: {
|
|
1046
|
-
log(...args: unknown[]): void;
|
|
1047
|
-
info(...args: unknown[]): void;
|
|
1048
|
-
warn(...args: unknown[]): void;
|
|
1049
|
-
error(...args: unknown[]): void;
|
|
1050
|
-
};
|
|
1051
|
-
assets: {
|
|
1052
|
-
text(path: string): Promise<string>;
|
|
1053
|
-
json<T = unknown>(path: string): Promise<T>;
|
|
1054
|
-
};
|
|
1055
|
-
cleanup(fn: () => void | Promise<void>): void;
|
|
1056
|
-
sceneCleanup(fn: () => void | Promise<void>): void;
|
|
1118
|
+
// src/apps.ts
|
|
1119
|
+
var PORTABLE_RUNTIME_EXPORTS = [
|
|
1120
|
+
"THREE",
|
|
1121
|
+
"RuntimeEngine",
|
|
1122
|
+
"SceneManager",
|
|
1123
|
+
"LoadSceneMode",
|
|
1124
|
+
"convertObjToComponentJson"
|
|
1125
|
+
];
|
|
1126
|
+
var TwinApp = class {
|
|
1057
1127
|
};
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
init?(ctx: TwinAppContext): void | Promise<void>;
|
|
1061
|
-
onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
|
|
1062
|
-
onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
|
|
1063
|
-
onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
|
|
1064
|
-
onDispose?(ctx: TwinAppContext): void | Promise<void>;
|
|
1065
|
-
onError?(ctx: TwinAppContext, error: unknown): boolean | void;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
export declare function defineApp<T>(app: T): T;
|
|
1069
|
-
`;
|
|
1070
|
-
function buildRuntimeTypesContent(sourceDts) {
|
|
1071
|
-
return `${sourceDts.replace(/\s+$/, "")}
|
|
1072
|
-
`;
|
|
1128
|
+
function defineApp(app) {
|
|
1129
|
+
return app;
|
|
1073
1130
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1131
|
+
var LIFECYCLE_NAMES = [
|
|
1132
|
+
"init",
|
|
1133
|
+
"onUpdate",
|
|
1134
|
+
"onBeforeSceneUnload",
|
|
1135
|
+
"onSceneLoaded",
|
|
1136
|
+
"onDispose",
|
|
1137
|
+
"onError"
|
|
1138
|
+
];
|
|
1139
|
+
function isLifecycleApp(value) {
|
|
1140
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1141
|
+
return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
|
|
1078
1142
|
}
|
|
1079
|
-
function
|
|
1080
|
-
if (typeof
|
|
1081
|
-
|
|
1143
|
+
function createAppInstance(appExport) {
|
|
1144
|
+
if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
|
|
1145
|
+
return new appExport();
|
|
1082
1146
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
function resolveRuntimeTypesSourceFile() {
|
|
1087
|
-
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
1088
|
-
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 typesSourceFile");
|
|
1089
|
-
}
|
|
1090
|
-
const here = path6.dirname(fileURLToPath2(import.meta.url));
|
|
1091
|
-
const fromDist = path6.join(here, "runtime-types", "index.d.ts");
|
|
1092
|
-
const fromSrc = path6.resolve(here, "lib", "index.d.ts");
|
|
1093
|
-
if (existsSync(fromDist)) return fromDist;
|
|
1094
|
-
if (existsSync(fromSrc)) return fromSrc;
|
|
1095
|
-
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromDist}, ${fromSrc}`);
|
|
1096
|
-
}
|
|
1097
|
-
async function readJson(file) {
|
|
1098
|
-
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
1099
|
-
}
|
|
1100
|
-
async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
|
|
1101
|
-
const marker = path6.join(skillsDir, ".easytwin-source.json");
|
|
1102
|
-
try {
|
|
1103
|
-
const meta = await readJson(marker);
|
|
1104
|
-
if (typeof meta.version === "string") return meta.version;
|
|
1105
|
-
} catch {
|
|
1106
|
-
}
|
|
1107
|
-
try {
|
|
1108
|
-
const pkg = await readJson(path6.resolve(skillsDir, "..", "package.json"));
|
|
1109
|
-
return pkg.version;
|
|
1110
|
-
} catch {
|
|
1111
|
-
return "0.0.0";
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
async function copyDir(src, dest) {
|
|
1115
|
-
await fs5.mkdir(dest, { recursive: true });
|
|
1116
|
-
const entries = await fs5.readdir(src, { withFileTypes: true });
|
|
1117
|
-
for (const entry of entries) {
|
|
1118
|
-
const s = path6.join(src, entry.name);
|
|
1119
|
-
const d = path6.join(dest, entry.name);
|
|
1120
|
-
if (entry.isDirectory()) await copyDir(s, d);
|
|
1121
|
-
else if (entry.isFile()) await fs5.copyFile(s, d);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
async function dirMatches(src, dest) {
|
|
1125
|
-
let sourceEntries;
|
|
1126
|
-
try {
|
|
1127
|
-
sourceEntries = await fs5.readdir(src);
|
|
1128
|
-
} catch {
|
|
1129
|
-
return false;
|
|
1130
|
-
}
|
|
1131
|
-
for (const name of sourceEntries) {
|
|
1132
|
-
const s = path6.join(src, name);
|
|
1133
|
-
const d = path6.join(dest, name);
|
|
1134
|
-
const sStat = await fs5.stat(s);
|
|
1135
|
-
if (sStat.isDirectory()) {
|
|
1136
|
-
if (!await dirMatches(s, d)) return false;
|
|
1137
|
-
} else {
|
|
1138
|
-
let dStat;
|
|
1139
|
-
try {
|
|
1140
|
-
dStat = await fs5.stat(d);
|
|
1141
|
-
} catch {
|
|
1142
|
-
return false;
|
|
1143
|
-
}
|
|
1144
|
-
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
1145
|
-
if (!(await fs5.readFile(s)).equals(await fs5.readFile(d))) return false;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return true;
|
|
1149
|
-
}
|
|
1150
|
-
function actionFor(exists, matches) {
|
|
1151
|
-
if (!exists) return "created";
|
|
1152
|
-
return matches ? "unchanged" : "updated";
|
|
1153
|
-
}
|
|
1154
|
-
async function writeMeta(destDir, version) {
|
|
1155
|
-
const meta = { name: "@easytwin/devkit", version };
|
|
1156
|
-
await fs5.writeFile(path6.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
1157
|
-
}
|
|
1158
|
-
async function syncToDir(target, sourceRoot, targetRoot, version) {
|
|
1159
|
-
const entries = [];
|
|
1160
|
-
for (const name of SKILL_NAMES) {
|
|
1161
|
-
const src = path6.join(sourceRoot, name);
|
|
1162
|
-
const dest = path6.join(targetRoot, name);
|
|
1163
|
-
const exists = await fs5.stat(dest).then(() => true).catch(() => false);
|
|
1164
|
-
const matches = await dirMatches(src, dest);
|
|
1165
|
-
const action = actionFor(exists, matches);
|
|
1166
|
-
if (action !== "unchanged") {
|
|
1167
|
-
await fs5.rm(dest, { recursive: true, force: true });
|
|
1168
|
-
await copyDir(src, dest);
|
|
1169
|
-
}
|
|
1170
|
-
entries.push({ name, action });
|
|
1171
|
-
}
|
|
1172
|
-
await writeMeta(targetRoot, version);
|
|
1173
|
-
return { target, entries };
|
|
1174
|
-
}
|
|
1175
|
-
function buildCodexSegment(version) {
|
|
1176
|
-
return [
|
|
1177
|
-
CODEX_MARKER_BEGIN,
|
|
1178
|
-
"<!-- \u672C\u6BB5\u7531 `easytwin skills sync` \u7BA1\u7406,\u53EF\u6574\u6BB5\u66FF\u6362;\u624B\u52A8\u4FEE\u6539\u4F1A\u88AB\u4E0B\u6B21\u540C\u6B65\u8986\u76D6\u3002 -->",
|
|
1179
|
-
`EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
|
|
1180
|
-
"",
|
|
1181
|
-
"- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
|
|
1182
|
-
"- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(easytwin.config.json)\u3002",
|
|
1183
|
-
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3002",
|
|
1184
|
-
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
1185
|
-
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
1186
|
-
"- `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",
|
|
1187
|
-
"",
|
|
1188
|
-
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
1189
|
-
CODEX_MARKER_END
|
|
1190
|
-
].join("\n");
|
|
1191
|
-
}
|
|
1192
|
-
async function syncToCodex(sourceRoot, cwd, version) {
|
|
1193
|
-
const agentsFile = path6.join(cwd, "AGENTS.md");
|
|
1194
|
-
const segment = buildCodexSegment(version);
|
|
1195
|
-
let content = "";
|
|
1196
|
-
let exists = true;
|
|
1197
|
-
try {
|
|
1198
|
-
content = await fs5.readFile(agentsFile, "utf8");
|
|
1199
|
-
} catch {
|
|
1200
|
-
exists = false;
|
|
1201
|
-
}
|
|
1202
|
-
const start = content.indexOf(CODEX_MARKER_BEGIN);
|
|
1203
|
-
const end = content.indexOf(CODEX_MARKER_END);
|
|
1204
|
-
let action;
|
|
1205
|
-
let next;
|
|
1206
|
-
if (start === -1 || end === -1 || end < start) {
|
|
1207
|
-
action = exists ? "updated" : "created";
|
|
1208
|
-
next = content.length > 0 && !content.endsWith("\n") ? content + "\n\n" : content + (content.length > 0 ? "\n" : "");
|
|
1209
|
-
next += segment + "\n";
|
|
1210
|
-
} else {
|
|
1211
|
-
const current = content.slice(start, end + CODEX_MARKER_END.length);
|
|
1212
|
-
action = current === segment ? "unchanged" : "updated";
|
|
1213
|
-
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
1214
|
-
}
|
|
1215
|
-
if (action !== "unchanged") {
|
|
1216
|
-
await fs5.mkdir(cwd, { recursive: true });
|
|
1217
|
-
await fs5.writeFile(agentsFile, next, "utf8");
|
|
1218
|
-
}
|
|
1219
|
-
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
1220
|
-
}
|
|
1221
|
-
async function syncRuntimeTypes(cwd, typesSourceFile) {
|
|
1222
|
-
const destDir = path6.join(cwd, ".easytwin", "types");
|
|
1223
|
-
const destFile = path6.join(destDir, "index.d.ts");
|
|
1224
|
-
const appsFile = path6.join(destDir, APPS_TYPES_FILE);
|
|
1225
|
-
const content = buildRuntimeTypesContent(await fs5.readFile(typesSourceFile, "utf8"));
|
|
1226
|
-
let runtimeCurrent = "";
|
|
1227
|
-
let appsCurrent = "";
|
|
1228
|
-
let runtimeExists = true;
|
|
1229
|
-
let appsExists = true;
|
|
1230
|
-
try {
|
|
1231
|
-
runtimeCurrent = await fs5.readFile(destFile, "utf8");
|
|
1232
|
-
} catch {
|
|
1233
|
-
runtimeExists = false;
|
|
1234
|
-
}
|
|
1235
|
-
try {
|
|
1236
|
-
appsCurrent = await fs5.readFile(appsFile, "utf8");
|
|
1237
|
-
} catch {
|
|
1238
|
-
appsExists = false;
|
|
1239
|
-
}
|
|
1240
|
-
const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
|
|
1241
|
-
const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
|
|
1242
|
-
const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
|
|
1243
|
-
if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
|
|
1244
|
-
await fs5.mkdir(destDir, { recursive: true });
|
|
1245
|
-
if (runtimeAction !== "unchanged") await fs5.writeFile(destFile, content, "utf8");
|
|
1246
|
-
if (appsAction !== "unchanged") await fs5.writeFile(appsFile, APPS_DTS, "utf8");
|
|
1247
|
-
}
|
|
1248
|
-
const tsconfigPath = path6.join(cwd, "tsconfig.json");
|
|
1249
|
-
let tsconfig;
|
|
1250
|
-
let pathsHint;
|
|
1251
|
-
try {
|
|
1252
|
-
const existing = await fs5.readFile(tsconfigPath, "utf8");
|
|
1253
|
-
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
1254
|
-
else {
|
|
1255
|
-
tsconfig = "manual-paths";
|
|
1256
|
-
pathsHint = TSCONFIG_PATHS_HINT;
|
|
1257
|
-
}
|
|
1258
|
-
} catch {
|
|
1259
|
-
await fs5.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
1260
|
-
tsconfig = "created";
|
|
1261
|
-
}
|
|
1262
|
-
return { action, tsconfig, pathsHint };
|
|
1263
|
-
}
|
|
1264
|
-
async function syncSkills(options) {
|
|
1265
|
-
const targets = normalizeTargets(options.targets ?? "all");
|
|
1266
|
-
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
1267
|
-
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
1268
|
-
const summaries = [];
|
|
1269
|
-
for (const target of targets) {
|
|
1270
|
-
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path6.join(options.cwd, ".cursor", "skills"), version));
|
|
1271
|
-
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path6.join(options.cwd, ".claude", "skills"), version));
|
|
1272
|
-
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path6.join(options.cwd, ".qoder", "skills"), version));
|
|
1273
|
-
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
1274
|
-
}
|
|
1275
|
-
const types = await syncRuntimeTypes(options.cwd, options.typesSourceFile ?? resolveRuntimeTypesSourceFile());
|
|
1276
|
-
return { summaries, types };
|
|
1277
|
-
}
|
|
1278
|
-
async function detectSkillsStatus(cwd, sourceDir) {
|
|
1279
|
-
const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
|
|
1280
|
-
const version = await readDevkitVersion(sourceRoot);
|
|
1281
|
-
const targets = [];
|
|
1282
|
-
const dirTargets = ["cursor", "claude", "qoder"];
|
|
1283
|
-
const DIR_ROOTS = {
|
|
1284
|
-
cursor: ".cursor/skills",
|
|
1285
|
-
claude: ".claude/skills",
|
|
1286
|
-
qoder: ".qoder/skills"
|
|
1287
|
-
};
|
|
1288
|
-
for (const target of dirTargets) {
|
|
1289
|
-
const root = path6.join(cwd, DIR_ROOTS[target]);
|
|
1290
|
-
const missing = [];
|
|
1291
|
-
for (const name of SKILL_NAMES) {
|
|
1292
|
-
if (!await fs5.stat(path6.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
|
|
1293
|
-
}
|
|
1294
|
-
let stale = false;
|
|
1295
|
-
try {
|
|
1296
|
-
const meta = await readJson(path6.join(root, META_FILE_NAME));
|
|
1297
|
-
stale = meta.version !== version;
|
|
1298
|
-
} catch {
|
|
1299
|
-
stale = missing.length === 0;
|
|
1300
|
-
}
|
|
1301
|
-
targets.push({
|
|
1302
|
-
target,
|
|
1303
|
-
synced: missing.length === 0 && !stale,
|
|
1304
|
-
reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
let agentsContent = "";
|
|
1308
|
-
try {
|
|
1309
|
-
agentsContent = await fs5.readFile(path6.join(cwd, "AGENTS.md"), "utf8");
|
|
1310
|
-
} catch {
|
|
1311
|
-
}
|
|
1312
|
-
const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
|
|
1313
|
-
targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
|
|
1314
|
-
let typesSynced = false;
|
|
1315
|
-
try {
|
|
1316
|
-
const dts = await fs5.readFile(path6.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
|
|
1317
|
-
const appsDts = await fs5.readFile(path6.join(cwd, ".easytwin", "types", APPS_TYPES_FILE), "utf8");
|
|
1318
|
-
typesSynced = dts.includes("registerEngineTickListener") && appsDts.includes("export declare abstract class TwinApp");
|
|
1319
|
-
} catch {
|
|
1320
|
-
typesSynced = false;
|
|
1321
|
-
}
|
|
1322
|
-
return { cwd, targets, typesSynced };
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
// src/bundle.ts
|
|
1326
|
-
import * as esbuild from "esbuild-wasm";
|
|
1327
|
-
import { promises as fs6 } from "fs";
|
|
1328
|
-
import path7 from "path";
|
|
1329
|
-
|
|
1330
|
-
// src/apps.ts
|
|
1331
|
-
var PORTABLE_RUNTIME_EXPORTS = [
|
|
1332
|
-
"THREE",
|
|
1333
|
-
"RuntimeEngine",
|
|
1334
|
-
"SceneManager",
|
|
1335
|
-
"LoadSceneMode",
|
|
1336
|
-
"convertObjToComponentJson"
|
|
1337
|
-
];
|
|
1338
|
-
var TwinApp = class {
|
|
1339
|
-
};
|
|
1340
|
-
function defineApp(app) {
|
|
1341
|
-
return app;
|
|
1342
|
-
}
|
|
1343
|
-
var LIFECYCLE_NAMES = [
|
|
1344
|
-
"init",
|
|
1345
|
-
"onUpdate",
|
|
1346
|
-
"onBeforeSceneUnload",
|
|
1347
|
-
"onSceneLoaded",
|
|
1348
|
-
"onDispose",
|
|
1349
|
-
"onError"
|
|
1350
|
-
];
|
|
1351
|
-
function isLifecycleApp(value) {
|
|
1352
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1353
|
-
return LIFECYCLE_NAMES.some((name) => typeof Reflect.get(value, name) === "function");
|
|
1354
|
-
}
|
|
1355
|
-
function createAppInstance(appExport) {
|
|
1356
|
-
if (typeof appExport === "function" && (appExport.prototype instanceof TwinApp || isLifecycleApp(appExport.prototype))) {
|
|
1357
|
-
return new appExport();
|
|
1358
|
-
}
|
|
1359
|
-
if (isLifecycleApp(appExport)) return appExport;
|
|
1360
|
-
throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
|
|
1147
|
+
if (isLifecycleApp(appExport)) return appExport;
|
|
1148
|
+
throw new Error("\u5165\u53E3\u9ED8\u8BA4\u5BFC\u51FA\u5FC5\u987B\u662F TwinApp \u5B50\u7C7B\u6216 defineApp({...}) \u751F\u547D\u5468\u671F\u5BF9\u8C61");
|
|
1361
1149
|
}
|
|
1362
1150
|
function portableRuntimeWarning(names) {
|
|
1363
1151
|
const extra = [...new Set(names)].filter(
|
|
@@ -1382,7 +1170,7 @@ var BundleError = class extends Error {
|
|
|
1382
1170
|
}
|
|
1383
1171
|
};
|
|
1384
1172
|
function isRelativeOrAbsolute(spec) {
|
|
1385
|
-
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") ||
|
|
1173
|
+
return spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || path6.isAbsolute(spec);
|
|
1386
1174
|
}
|
|
1387
1175
|
function whitelistPlugin() {
|
|
1388
1176
|
return {
|
|
@@ -1424,14 +1212,14 @@ function collectBundledRuntimeExports(code) {
|
|
|
1424
1212
|
}
|
|
1425
1213
|
return names;
|
|
1426
1214
|
}
|
|
1427
|
-
async function
|
|
1428
|
-
const cwd =
|
|
1429
|
-
const entry =
|
|
1215
|
+
async function bundleWorkspaceModule(options) {
|
|
1216
|
+
const cwd = path6.resolve(options.cwd);
|
|
1217
|
+
const entry = path6.isAbsolute(options.entry) ? options.entry : path6.join(cwd, options.entry);
|
|
1430
1218
|
try {
|
|
1431
|
-
await
|
|
1219
|
+
await fs5.access(entry);
|
|
1432
1220
|
} catch {
|
|
1433
1221
|
throw new BundleError(
|
|
1434
|
-
`\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002
|
|
1222
|
+
options.missingEntryMessage ?? `\u672A\u627E\u5230\u5165\u53E3\u6587\u4EF6 ${entry}\u3002`
|
|
1435
1223
|
);
|
|
1436
1224
|
}
|
|
1437
1225
|
let result;
|
|
@@ -1465,107 +1253,663 @@ async function bundleUserCode(options) {
|
|
|
1465
1253
|
const portable = portableRuntimeWarning(collectBundledRuntimeExports(code));
|
|
1466
1254
|
if (portable) warnings.push(portable);
|
|
1467
1255
|
if (options.outFile) {
|
|
1468
|
-
const outFile =
|
|
1469
|
-
await
|
|
1470
|
-
await
|
|
1256
|
+
const outFile = path6.isAbsolute(options.outFile) ? options.outFile : path6.join(cwd, options.outFile);
|
|
1257
|
+
await fs5.mkdir(path6.dirname(outFile), { recursive: true });
|
|
1258
|
+
await fs5.writeFile(outFile, code, "utf8");
|
|
1259
|
+
}
|
|
1260
|
+
return { code, warnings };
|
|
1261
|
+
}
|
|
1262
|
+
async function bundleUserCode(options) {
|
|
1263
|
+
const cwd = path6.resolve(options.cwd);
|
|
1264
|
+
const entry = path6.join(cwd, USER_ENTRY);
|
|
1265
|
+
return bundleWorkspaceModule({
|
|
1266
|
+
cwd: options.cwd,
|
|
1267
|
+
entry: USER_ENTRY,
|
|
1268
|
+
outFile: options.outFile,
|
|
1269
|
+
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`
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
async function typesMissingHint(cwd) {
|
|
1273
|
+
try {
|
|
1274
|
+
await fs5.access(path6.join(cwd, ".easytwin", "types", "index.d.ts"));
|
|
1275
|
+
return void 0;
|
|
1276
|
+
} catch {
|
|
1277
|
+
return "\u672A\u627E\u5230 .easytwin/types,\u8FD0\u884C `easytwin skills sync` \u4EE5\u5206\u53D1 @easytwin/runtime \u7C7B\u578B\u58F0\u660E\u3002";
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
var SOURCEMAP_RE = /sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/]+=*)/;
|
|
1281
|
+
var VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
1282
|
+
function decodeVLQValues(str) {
|
|
1283
|
+
const values = [];
|
|
1284
|
+
let i = 0;
|
|
1285
|
+
while (i < str.length) {
|
|
1286
|
+
let result = 0;
|
|
1287
|
+
let shift = 0;
|
|
1288
|
+
let continuation = true;
|
|
1289
|
+
while (continuation) {
|
|
1290
|
+
if (i >= str.length) return values;
|
|
1291
|
+
const digit = VLQ_CHARS.indexOf(str[i++] ?? "");
|
|
1292
|
+
if (digit < 0) return values;
|
|
1293
|
+
continuation = (digit & 32) !== 0;
|
|
1294
|
+
result += (digit & 31) << shift;
|
|
1295
|
+
shift += 5;
|
|
1296
|
+
}
|
|
1297
|
+
values.push(result & 1 ? -(result >> 1) : result >> 1);
|
|
1298
|
+
}
|
|
1299
|
+
return values;
|
|
1300
|
+
}
|
|
1301
|
+
function decodeMappings(map) {
|
|
1302
|
+
const sources = map.sources ?? [];
|
|
1303
|
+
const lines = (map.mappings ?? "").split(";");
|
|
1304
|
+
let sourceIndex = 0;
|
|
1305
|
+
let originalLine = 0;
|
|
1306
|
+
let originalColumn = 0;
|
|
1307
|
+
const decoded = [];
|
|
1308
|
+
for (const line of lines) {
|
|
1309
|
+
let generatedColumn = 0;
|
|
1310
|
+
const segs = [];
|
|
1311
|
+
if (line) {
|
|
1312
|
+
for (const raw of line.split(",")) {
|
|
1313
|
+
if (!raw) continue;
|
|
1314
|
+
const nums = decodeVLQValues(raw);
|
|
1315
|
+
if (nums[0] === void 0) continue;
|
|
1316
|
+
generatedColumn += nums[0];
|
|
1317
|
+
if (nums.length >= 4) {
|
|
1318
|
+
sourceIndex += nums[1] ?? 0;
|
|
1319
|
+
originalLine += nums[2] ?? 0;
|
|
1320
|
+
originalColumn += nums[3] ?? 0;
|
|
1321
|
+
segs.push({
|
|
1322
|
+
generatedColumn,
|
|
1323
|
+
source: sources[sourceIndex] ?? USER_ENTRY,
|
|
1324
|
+
originalLine,
|
|
1325
|
+
originalColumn
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
decoded.push(segs);
|
|
1331
|
+
}
|
|
1332
|
+
return decoded;
|
|
1333
|
+
}
|
|
1334
|
+
function originalPositionFor(map, line, column) {
|
|
1335
|
+
const decoded = decodeMappings(map);
|
|
1336
|
+
for (let i = line - 1; i >= 0; i--) {
|
|
1337
|
+
const segs = decoded[i];
|
|
1338
|
+
if (!segs || segs.length === 0) continue;
|
|
1339
|
+
const col = i === line - 1 ? column - 1 : Number.POSITIVE_INFINITY;
|
|
1340
|
+
let best = segs[0];
|
|
1341
|
+
for (const seg of segs) {
|
|
1342
|
+
if (seg.generatedColumn <= col) best = seg;
|
|
1343
|
+
else break;
|
|
1344
|
+
}
|
|
1345
|
+
if (!best) continue;
|
|
1346
|
+
return { source: best.source, line: best.originalLine + 1, column: best.originalColumn };
|
|
1347
|
+
}
|
|
1348
|
+
return void 0;
|
|
1349
|
+
}
|
|
1350
|
+
function extractInlineSourceMap(code) {
|
|
1351
|
+
const m = code.match(SOURCEMAP_RE);
|
|
1352
|
+
if (!m?.[1]) return void 0;
|
|
1353
|
+
try {
|
|
1354
|
+
return JSON.parse(Buffer.from(m[1], "base64").toString("utf8"));
|
|
1355
|
+
} catch {
|
|
1356
|
+
return void 0;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function remapErrorStack(stack, bundledCode) {
|
|
1360
|
+
const map = extractInlineSourceMap(bundledCode);
|
|
1361
|
+
if (!map?.mappings) return stack;
|
|
1362
|
+
return stack.replace(/(blob:[^\s)]+?):(\d+):(\d+)/g, (full, _url, line, col) => {
|
|
1363
|
+
const orig = originalPositionFor(map, Number(line), Number(col));
|
|
1364
|
+
if (!orig) return full;
|
|
1365
|
+
return `${orig.source}:${orig.line}:${orig.column}`;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/workspaceTests.ts
|
|
1370
|
+
var IDENT = "[A-Za-z_$][\\w$]*";
|
|
1371
|
+
var FUNCTION_EXPORT = new RegExp(
|
|
1372
|
+
`export\\s+(?:async\\s+)?function\\s+(${IDENT})\\s*(?:<[^>]*>)?\\s*\\(`,
|
|
1373
|
+
"g"
|
|
1374
|
+
);
|
|
1375
|
+
var CONST_EXPORT = new RegExp(
|
|
1376
|
+
`export\\s+const\\s+(${IDENT})\\s*=\\s*(?:async\\s+)?(?:function\\s*)?(?:<[^>]*>)?\\s*\\(`,
|
|
1377
|
+
"g"
|
|
1378
|
+
);
|
|
1379
|
+
function stripTsCommentsAndStringsKeepCode(source) {
|
|
1380
|
+
let out = "";
|
|
1381
|
+
let i = 0;
|
|
1382
|
+
const n = source.length;
|
|
1383
|
+
while (i < n) {
|
|
1384
|
+
const c = source[i];
|
|
1385
|
+
const next = source[i + 1];
|
|
1386
|
+
if (c === "/" && next === "/") {
|
|
1387
|
+
i += 2;
|
|
1388
|
+
while (i < n && source[i] !== "\n") i++;
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (c === "/" && next === "*") {
|
|
1392
|
+
i += 2;
|
|
1393
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
1394
|
+
i = Math.min(n, i + 2);
|
|
1395
|
+
out += " ";
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
1399
|
+
const quote = c;
|
|
1400
|
+
out += " ";
|
|
1401
|
+
i++;
|
|
1402
|
+
while (i < n) {
|
|
1403
|
+
const ch = source[i];
|
|
1404
|
+
if (ch === "\\") {
|
|
1405
|
+
i += 2;
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
if (ch === quote) {
|
|
1409
|
+
i++;
|
|
1410
|
+
break;
|
|
1411
|
+
}
|
|
1412
|
+
i++;
|
|
1413
|
+
}
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
out += c;
|
|
1417
|
+
i++;
|
|
1418
|
+
}
|
|
1419
|
+
return out;
|
|
1420
|
+
}
|
|
1421
|
+
function matchingClose(src, openIndex, open, close) {
|
|
1422
|
+
let depth = 0;
|
|
1423
|
+
let angle = 0;
|
|
1424
|
+
let brace = 0;
|
|
1425
|
+
for (let i = openIndex; i < src.length; i++) {
|
|
1426
|
+
const c = src[i];
|
|
1427
|
+
if (c === open) depth++;
|
|
1428
|
+
else if (c === close) {
|
|
1429
|
+
depth--;
|
|
1430
|
+
if (depth === 0 && angle <= 0 && brace <= 0) return i;
|
|
1431
|
+
} else if (c === "<") angle++;
|
|
1432
|
+
else if (c === ">" && angle > 0) angle--;
|
|
1433
|
+
else if (c === "{") brace++;
|
|
1434
|
+
else if (c === "}" && brace > 0) brace--;
|
|
1435
|
+
}
|
|
1436
|
+
return -1;
|
|
1437
|
+
}
|
|
1438
|
+
function splitTopLevelParams(list) {
|
|
1439
|
+
const params = [];
|
|
1440
|
+
let current = "";
|
|
1441
|
+
let paren = 0;
|
|
1442
|
+
let angle = 0;
|
|
1443
|
+
let brace = 0;
|
|
1444
|
+
let bracket = 0;
|
|
1445
|
+
for (const c of list) {
|
|
1446
|
+
if (c === "(") paren++;
|
|
1447
|
+
else if (c === ")") paren--;
|
|
1448
|
+
else if (c === "<") angle++;
|
|
1449
|
+
else if (c === ">" && angle > 0) angle--;
|
|
1450
|
+
else if (c === "{") brace++;
|
|
1451
|
+
else if (c === "}" && brace > 0) brace--;
|
|
1452
|
+
else if (c === "[") bracket++;
|
|
1453
|
+
else if (c === "]" && bracket > 0) bracket--;
|
|
1454
|
+
if (c === "," && paren === 0 && angle === 0 && brace === 0 && bracket === 0) {
|
|
1455
|
+
if (current.trim()) params.push(current.trim());
|
|
1456
|
+
current = "";
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
current += c;
|
|
1460
|
+
}
|
|
1461
|
+
if (current.trim()) params.push(current.trim());
|
|
1462
|
+
return params;
|
|
1463
|
+
}
|
|
1464
|
+
function paramName(raw) {
|
|
1465
|
+
let s = raw.trim();
|
|
1466
|
+
if (!s || s === "this") return void 0;
|
|
1467
|
+
if (s.startsWith("{") || s.startsWith("[")) return "input";
|
|
1468
|
+
s = s.replace(/^\.\.\./, "");
|
|
1469
|
+
const token = s.split(/[?:]/)[0]?.trim().split(/\s+/)[0];
|
|
1470
|
+
if (!token || !new RegExp(`^${IDENT}$`).test(token)) return "input";
|
|
1471
|
+
return token;
|
|
1472
|
+
}
|
|
1473
|
+
function collectFromPattern(source, pattern) {
|
|
1474
|
+
const found = [];
|
|
1475
|
+
pattern.lastIndex = 0;
|
|
1476
|
+
let match;
|
|
1477
|
+
while (match = pattern.exec(source)) {
|
|
1478
|
+
const name = match[1];
|
|
1479
|
+
if (!name) continue;
|
|
1480
|
+
const openIndex = match.index + match[0].length - 1;
|
|
1481
|
+
const closeIndex = matchingClose(source, openIndex, "(", ")");
|
|
1482
|
+
if (closeIndex < 0) continue;
|
|
1483
|
+
const params = splitTopLevelParams(source.slice(openIndex + 1, closeIndex));
|
|
1484
|
+
const hasInput = params.length >= 2;
|
|
1485
|
+
const second = hasInput ? paramName(params[1] ?? "") : void 0;
|
|
1486
|
+
found.push({
|
|
1487
|
+
id: name,
|
|
1488
|
+
file: "",
|
|
1489
|
+
name,
|
|
1490
|
+
hasInput,
|
|
1491
|
+
inputName: hasInput ? second : void 0
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
return found;
|
|
1495
|
+
}
|
|
1496
|
+
function parseExportedTestFunctions(source) {
|
|
1497
|
+
const text = stripTsCommentsAndStringsKeepCode(source);
|
|
1498
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1499
|
+
const out = [];
|
|
1500
|
+
for (const item of [...collectFromPattern(text, FUNCTION_EXPORT), ...collectFromPattern(text, CONST_EXPORT)]) {
|
|
1501
|
+
if (seen.has(item.name)) continue;
|
|
1502
|
+
seen.add(item.name);
|
|
1503
|
+
out.push({ name: item.name, hasInput: item.hasInput, inputName: item.inputName });
|
|
1504
|
+
}
|
|
1505
|
+
return out;
|
|
1506
|
+
}
|
|
1507
|
+
function workspaceTestId(file, name) {
|
|
1508
|
+
return `${normalizePath(file)}:${name}`;
|
|
1509
|
+
}
|
|
1510
|
+
async function listWorkspaceTests(cwd) {
|
|
1511
|
+
const root = path7.resolve(cwd);
|
|
1512
|
+
let files;
|
|
1513
|
+
try {
|
|
1514
|
+
files = await collectFiles(root, isIgnoredWorkspacePath);
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
const code = err.code;
|
|
1517
|
+
if (code === "ENOENT") return [];
|
|
1518
|
+
throw err;
|
|
1519
|
+
}
|
|
1520
|
+
const tests = [];
|
|
1521
|
+
for (const file of files) {
|
|
1522
|
+
if (!isWorkspaceSpecFile(file.relPath)) continue;
|
|
1523
|
+
const posix = normalizePath(file.relPath);
|
|
1524
|
+
const source = await fs6.readFile(file.absPath, "utf8");
|
|
1525
|
+
for (const fn of parseExportedTestFunctions(source)) {
|
|
1526
|
+
tests.push({
|
|
1527
|
+
id: workspaceTestId(posix, fn.name),
|
|
1528
|
+
file: posix,
|
|
1529
|
+
name: fn.name,
|
|
1530
|
+
hasInput: fn.hasInput,
|
|
1531
|
+
inputName: fn.inputName
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
tests.sort((a, b) => a.id.localeCompare(b.id));
|
|
1536
|
+
return tests;
|
|
1537
|
+
}
|
|
1538
|
+
async function bundleWorkspaceTestFile(options) {
|
|
1539
|
+
const file = normalizePath(options.file);
|
|
1540
|
+
return bundleWorkspaceModule({
|
|
1541
|
+
cwd: options.cwd,
|
|
1542
|
+
entry: file,
|
|
1543
|
+
missingEntryMessage: `\u672A\u627E\u5230\u6D4B\u8BD5\u6587\u4EF6 ${file}\u3002`
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/skills.ts
|
|
1548
|
+
import { existsSync, promises as fs7 } from "fs";
|
|
1549
|
+
import path8 from "path";
|
|
1550
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1551
|
+
var SKILL_NAMES = [
|
|
1552
|
+
"easytwin-render",
|
|
1553
|
+
"easytwin-core",
|
|
1554
|
+
"easytwin-develop",
|
|
1555
|
+
"easytwin-bootstrap",
|
|
1556
|
+
"easytwin-scene",
|
|
1557
|
+
"easytwin-upload",
|
|
1558
|
+
"easytwin-test"
|
|
1559
|
+
];
|
|
1560
|
+
var CODEX_MARKER_BEGIN = "<!-- EASYTWIN-SKILLS:BEGIN -->";
|
|
1561
|
+
var CODEX_MARKER_END = "<!-- EASYTWIN-SKILLS:END -->";
|
|
1562
|
+
var META_FILE_NAME = ".easytwin-meta.json";
|
|
1563
|
+
var EASYTWIN_TYPES_DIR = ".easytwin/types";
|
|
1564
|
+
var MINIMAL_TSCONFIG = `${JSON.stringify(
|
|
1565
|
+
{
|
|
1566
|
+
compilerOptions: {
|
|
1567
|
+
target: "ES2022",
|
|
1568
|
+
module: "ESNext",
|
|
1569
|
+
moduleResolution: "bundler",
|
|
1570
|
+
strict: true,
|
|
1571
|
+
skipLibCheck: true,
|
|
1572
|
+
noEmit: true,
|
|
1573
|
+
paths: {
|
|
1574
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
1575
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
include: ["src/**/*.ts"]
|
|
1579
|
+
},
|
|
1580
|
+
null,
|
|
1581
|
+
2
|
|
1582
|
+
)}
|
|
1583
|
+
`;
|
|
1584
|
+
var TSCONFIG_PATHS_HINT = `\u5DF2\u6709 tsconfig.json,\u672A\u6539\u52A8\u3002\u8BF7\u5728 compilerOptions \u4E2D\u52A0\u5165:
|
|
1585
|
+
"skipLibCheck": true,
|
|
1586
|
+
"paths": {
|
|
1587
|
+
"@easytwin/runtime": [".easytwin/types"],
|
|
1588
|
+
"@easytwin/apps": [".easytwin/types/apps"]
|
|
1589
|
+
}`;
|
|
1590
|
+
var APPS_TYPES_FILE = "apps.d.ts";
|
|
1591
|
+
var APPS_DTS = `import type { RuntimeEngine, RuntimeScene, SceneManager } from "@easytwin/runtime";
|
|
1592
|
+
|
|
1593
|
+
export type TwinAppContext = {
|
|
1594
|
+
app: {
|
|
1595
|
+
id: string;
|
|
1596
|
+
mode: "preview" | "publish";
|
|
1597
|
+
};
|
|
1598
|
+
engine: RuntimeEngine;
|
|
1599
|
+
container: HTMLElement;
|
|
1600
|
+
runtimeScene: RuntimeScene | null;
|
|
1601
|
+
scene: RuntimeScene["sceneObject"] | null;
|
|
1602
|
+
camera: RuntimeScene["camera"]["main"] | null;
|
|
1603
|
+
sceneManager: {
|
|
1604
|
+
currentSceneId: string;
|
|
1605
|
+
runtime: SceneManager | null;
|
|
1606
|
+
loadScene(sceneId: string): Promise<void>;
|
|
1607
|
+
};
|
|
1608
|
+
logger: {
|
|
1609
|
+
log(...args: unknown[]): void;
|
|
1610
|
+
info(...args: unknown[]): void;
|
|
1611
|
+
warn(...args: unknown[]): void;
|
|
1612
|
+
error(...args: unknown[]): void;
|
|
1613
|
+
};
|
|
1614
|
+
assets: {
|
|
1615
|
+
text(path: string): Promise<string>;
|
|
1616
|
+
json<T = unknown>(path: string): Promise<T>;
|
|
1617
|
+
};
|
|
1618
|
+
cleanup(fn: () => void | Promise<void>): void;
|
|
1619
|
+
sceneCleanup(fn: () => void | Promise<void>): void;
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
export declare abstract class TwinApp {
|
|
1623
|
+
init?(ctx: TwinAppContext): void | Promise<void>;
|
|
1624
|
+
onUpdate?(ctx: TwinAppContext, delta: number, elapsed: number): void;
|
|
1625
|
+
onBeforeSceneUnload?(ctx: TwinAppContext): void | Promise<void>;
|
|
1626
|
+
onSceneLoaded?(ctx: TwinAppContext): void | Promise<void>;
|
|
1627
|
+
onDispose?(ctx: TwinAppContext): void | Promise<void>;
|
|
1628
|
+
onError?(ctx: TwinAppContext, error: unknown): boolean | void;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
export declare function defineApp<T>(app: T): T;
|
|
1632
|
+
`;
|
|
1633
|
+
function buildRuntimeTypesContent(sourceDts) {
|
|
1634
|
+
return `${sourceDts.replace(/\s+$/, "")}
|
|
1635
|
+
`;
|
|
1636
|
+
}
|
|
1637
|
+
var SkillsError = class extends Error {
|
|
1638
|
+
constructor(message) {
|
|
1639
|
+
super(message);
|
|
1640
|
+
this.name = "SkillsError";
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
function normalizeTargets(target = "all") {
|
|
1644
|
+
if (target === "all") return ["cursor", "claude", "codex", "qoder"];
|
|
1645
|
+
if (Array.isArray(target)) return [...new Set(target)];
|
|
1646
|
+
return [target];
|
|
1647
|
+
}
|
|
1648
|
+
function resolveSkillsSourceDir() {
|
|
1649
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
1650
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D skills \u6E90\u76EE\u5F55:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 sourceDir");
|
|
1651
|
+
}
|
|
1652
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1653
|
+
return path8.resolve(here, "..", "skills");
|
|
1654
|
+
}
|
|
1655
|
+
function resolveRuntimeTypesSourceFile() {
|
|
1656
|
+
if (typeof import.meta.url !== "string" || import.meta.url.length === 0) {
|
|
1657
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B:\u63D2\u4EF6(bundle/CJS)\u573A\u666F\u8BF7\u8D70 OSS \u62C9\u53D6,\u4E0D\u8981\u89E3\u6790\u5305\u5185 d.ts");
|
|
1658
|
+
}
|
|
1659
|
+
const here = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1660
|
+
const fromSrc = path8.resolve(here, "lib", "index.d.ts");
|
|
1661
|
+
if (existsSync(fromSrc)) return fromSrc;
|
|
1662
|
+
throw new Error(`\u65E0\u6CD5\u5B9A\u4F4D runtime \u7C7B\u578B\u58F0\u660E,\u5DF2\u5C1D\u8BD5:${fromSrc}`);
|
|
1663
|
+
}
|
|
1664
|
+
async function fetchRuntimeTypesDts(url, fetchImpl) {
|
|
1665
|
+
let res;
|
|
1666
|
+
try {
|
|
1667
|
+
res = await fetchImpl(url);
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
throw new SkillsError(
|
|
1670
|
+
`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:${url} (${err instanceof Error ? err.message : String(err)})`
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
if (!res.ok) {
|
|
1674
|
+
throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:HTTP ${res.status} ${url}`);
|
|
1675
|
+
}
|
|
1676
|
+
const text = await res.text();
|
|
1677
|
+
if (text.trim().length === 0) {
|
|
1678
|
+
throw new SkillsError(`\u62C9\u53D6 @easytwin/runtime \u7C7B\u578B\u5931\u8D25:\u7A7A\u54CD\u5E94 ${url}`);
|
|
1679
|
+
}
|
|
1680
|
+
return text;
|
|
1681
|
+
}
|
|
1682
|
+
async function readJson(file) {
|
|
1683
|
+
return JSON.parse(await fs7.readFile(file, "utf8"));
|
|
1684
|
+
}
|
|
1685
|
+
async function readDevkitVersion(skillsDir = resolveSkillsSourceDir()) {
|
|
1686
|
+
const marker = path8.join(skillsDir, ".easytwin-source.json");
|
|
1687
|
+
try {
|
|
1688
|
+
const meta = await readJson(marker);
|
|
1689
|
+
if (typeof meta.version === "string") return meta.version;
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
const pkg = await readJson(path8.resolve(skillsDir, "..", "package.json"));
|
|
1694
|
+
return pkg.version;
|
|
1695
|
+
} catch {
|
|
1696
|
+
return "0.0.0";
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
async function copyDir(src, dest) {
|
|
1700
|
+
await fs7.mkdir(dest, { recursive: true });
|
|
1701
|
+
const entries = await fs7.readdir(src, { withFileTypes: true });
|
|
1702
|
+
for (const entry of entries) {
|
|
1703
|
+
const s = path8.join(src, entry.name);
|
|
1704
|
+
const d = path8.join(dest, entry.name);
|
|
1705
|
+
if (entry.isDirectory()) await copyDir(s, d);
|
|
1706
|
+
else if (entry.isFile()) await fs7.copyFile(s, d);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
async function dirMatches(src, dest) {
|
|
1710
|
+
let sourceEntries;
|
|
1711
|
+
try {
|
|
1712
|
+
sourceEntries = await fs7.readdir(src);
|
|
1713
|
+
} catch {
|
|
1714
|
+
return false;
|
|
1715
|
+
}
|
|
1716
|
+
for (const name of sourceEntries) {
|
|
1717
|
+
const s = path8.join(src, name);
|
|
1718
|
+
const d = path8.join(dest, name);
|
|
1719
|
+
const sStat = await fs7.stat(s);
|
|
1720
|
+
if (sStat.isDirectory()) {
|
|
1721
|
+
if (!await dirMatches(s, d)) return false;
|
|
1722
|
+
} else {
|
|
1723
|
+
let dStat;
|
|
1724
|
+
try {
|
|
1725
|
+
dStat = await fs7.stat(d);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
if (!sStat.isFile() || !dStat.isFile() || sStat.size !== dStat.size) return false;
|
|
1730
|
+
if (!(await fs7.readFile(s)).equals(await fs7.readFile(d))) return false;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return true;
|
|
1734
|
+
}
|
|
1735
|
+
function actionFor(exists, matches) {
|
|
1736
|
+
if (!exists) return "created";
|
|
1737
|
+
return matches ? "unchanged" : "updated";
|
|
1738
|
+
}
|
|
1739
|
+
async function writeMeta(destDir, version) {
|
|
1740
|
+
const meta = { name: "@easytwin/devkit", version };
|
|
1741
|
+
await fs7.writeFile(path8.join(destDir, META_FILE_NAME), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
1742
|
+
}
|
|
1743
|
+
async function syncToDir(target, sourceRoot, targetRoot, version) {
|
|
1744
|
+
const entries = [];
|
|
1745
|
+
for (const name of SKILL_NAMES) {
|
|
1746
|
+
const src = path8.join(sourceRoot, name);
|
|
1747
|
+
const dest = path8.join(targetRoot, name);
|
|
1748
|
+
const exists = await fs7.stat(dest).then(() => true).catch(() => false);
|
|
1749
|
+
const matches = await dirMatches(src, dest);
|
|
1750
|
+
const action = actionFor(exists, matches);
|
|
1751
|
+
if (action !== "unchanged") {
|
|
1752
|
+
await fs7.rm(dest, { recursive: true, force: true });
|
|
1753
|
+
await copyDir(src, dest);
|
|
1754
|
+
}
|
|
1755
|
+
entries.push({ name, action });
|
|
1756
|
+
}
|
|
1757
|
+
await writeMeta(targetRoot, version);
|
|
1758
|
+
return { target, entries };
|
|
1759
|
+
}
|
|
1760
|
+
function buildCodexSegment(version) {
|
|
1761
|
+
return [
|
|
1762
|
+
CODEX_MARKER_BEGIN,
|
|
1763
|
+
"<!-- \u672C\u6BB5\u7531 `easytwin skills sync` \u7BA1\u7406,\u53EF\u6574\u6BB5\u66FF\u6362;\u624B\u52A8\u4FEE\u6539\u4F1A\u88AB\u4E0B\u6B21\u540C\u6B65\u8986\u76D6\u3002 -->",
|
|
1764
|
+
`EasyTwin \u5F00\u53D1\u6280\u80FD(\u7531 @easytwin/devkit v${version} \u540C\u6B65):`,
|
|
1765
|
+
"",
|
|
1766
|
+
"- `easytwin-develop`:\u5DE5\u4F5C\u6D41\u603B\u7EB2,\u5F00\u59CB EasyTwin \u5F00\u53D1\u524D\u5FC5\u8BFB\u3002",
|
|
1767
|
+
"- `easytwin-bootstrap`:\u521D\u59CB\u5316\u5E94\u7528\u7EA7\u51ED\u8BC1(`.easytwin/easytwin.config.json`)\u3002",
|
|
1768
|
+
"- `easytwin-scene`:\u5217\u51FA\u573A\u666F\u3001\u62C9\u53D6\u573A\u666F JSON\u3001inspect \u672C\u5730\u5BF9\u8C61\u6811\u3002",
|
|
1769
|
+
"- `easytwin-render`:\u7528 twin runtime \u5F00\u53D1\u6E32\u67D3\u529F\u80FD(\u4E3B\u529B)\u3002",
|
|
1770
|
+
"- `easytwin-core`:\u5F15\u64CE\u5185\u6838 API(RuntimeEngine/\u57FA\u7C7B/Time/\u76F8\u673A/\u7269\u7406)\u3002",
|
|
1771
|
+
"- `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",
|
|
1772
|
+
"- `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",
|
|
1773
|
+
"",
|
|
1774
|
+
"\u8BE6\u7EC6\u7528\u6CD5\u89C1\u5404\u6280\u80FD;\u91CD\u65B0\u540C\u6B65\u8FD0\u884C `easytwin skills sync --target codex`\u3002",
|
|
1775
|
+
CODEX_MARKER_END
|
|
1776
|
+
].join("\n");
|
|
1777
|
+
}
|
|
1778
|
+
async function syncToCodex(sourceRoot, cwd, version) {
|
|
1779
|
+
const agentsFile = path8.join(cwd, "AGENTS.md");
|
|
1780
|
+
const segment = buildCodexSegment(version);
|
|
1781
|
+
let content = "";
|
|
1782
|
+
let exists = true;
|
|
1783
|
+
try {
|
|
1784
|
+
content = await fs7.readFile(agentsFile, "utf8");
|
|
1785
|
+
} catch {
|
|
1786
|
+
exists = false;
|
|
1787
|
+
}
|
|
1788
|
+
const start = content.indexOf(CODEX_MARKER_BEGIN);
|
|
1789
|
+
const end = content.indexOf(CODEX_MARKER_END);
|
|
1790
|
+
let action;
|
|
1791
|
+
let next;
|
|
1792
|
+
if (start === -1 || end === -1 || end < start) {
|
|
1793
|
+
action = exists ? "updated" : "created";
|
|
1794
|
+
next = content.length > 0 && !content.endsWith("\n") ? content + "\n\n" : content + (content.length > 0 ? "\n" : "");
|
|
1795
|
+
next += segment + "\n";
|
|
1796
|
+
} else {
|
|
1797
|
+
const current = content.slice(start, end + CODEX_MARKER_END.length);
|
|
1798
|
+
action = current === segment ? "unchanged" : "updated";
|
|
1799
|
+
next = content.slice(0, start) + segment + content.slice(end + CODEX_MARKER_END.length);
|
|
1800
|
+
}
|
|
1801
|
+
if (action !== "unchanged") {
|
|
1802
|
+
await fs7.mkdir(cwd, { recursive: true });
|
|
1803
|
+
await fs7.writeFile(agentsFile, next, "utf8");
|
|
1471
1804
|
}
|
|
1472
|
-
return {
|
|
1805
|
+
return { target: "codex", entries: [], codex: { name: "AGENTS.md", action } };
|
|
1473
1806
|
}
|
|
1474
|
-
async function
|
|
1807
|
+
async function syncRuntimeTypes(cwd, sourceDts) {
|
|
1808
|
+
const destDir = path8.join(cwd, ".easytwin", "types");
|
|
1809
|
+
const destFile = path8.join(destDir, "index.d.ts");
|
|
1810
|
+
const appsFile = path8.join(destDir, APPS_TYPES_FILE);
|
|
1811
|
+
const content = buildRuntimeTypesContent(sourceDts);
|
|
1812
|
+
let runtimeCurrent = "";
|
|
1813
|
+
let appsCurrent = "";
|
|
1814
|
+
let runtimeExists = true;
|
|
1815
|
+
let appsExists = true;
|
|
1475
1816
|
try {
|
|
1476
|
-
await
|
|
1477
|
-
return void 0;
|
|
1817
|
+
runtimeCurrent = await fs7.readFile(destFile, "utf8");
|
|
1478
1818
|
} catch {
|
|
1479
|
-
|
|
1819
|
+
runtimeExists = false;
|
|
1480
1820
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1821
|
+
try {
|
|
1822
|
+
appsCurrent = await fs7.readFile(appsFile, "utf8");
|
|
1823
|
+
} catch {
|
|
1824
|
+
appsExists = false;
|
|
1825
|
+
}
|
|
1826
|
+
const runtimeAction = actionFor(runtimeExists, runtimeCurrent === content);
|
|
1827
|
+
const appsAction = actionFor(appsExists, appsCurrent === APPS_DTS);
|
|
1828
|
+
const action = runtimeAction === "unchanged" && appsAction === "unchanged" ? "unchanged" : !runtimeExists && !appsExists ? "created" : "updated";
|
|
1829
|
+
if (runtimeAction !== "unchanged" || appsAction !== "unchanged") {
|
|
1830
|
+
await fs7.mkdir(destDir, { recursive: true });
|
|
1831
|
+
if (runtimeAction !== "unchanged") await fs7.writeFile(destFile, content, "utf8");
|
|
1832
|
+
if (appsAction !== "unchanged") await fs7.writeFile(appsFile, APPS_DTS, "utf8");
|
|
1833
|
+
}
|
|
1834
|
+
const tsconfigPath = path8.join(cwd, "tsconfig.json");
|
|
1835
|
+
let tsconfig;
|
|
1836
|
+
let pathsHint;
|
|
1837
|
+
try {
|
|
1838
|
+
const existing = await fs7.readFile(tsconfigPath, "utf8");
|
|
1839
|
+
if (existing === MINIMAL_TSCONFIG) tsconfig = "unchanged";
|
|
1840
|
+
else {
|
|
1841
|
+
tsconfig = "manual-paths";
|
|
1842
|
+
pathsHint = TSCONFIG_PATHS_HINT;
|
|
1498
1843
|
}
|
|
1499
|
-
|
|
1844
|
+
} catch {
|
|
1845
|
+
await fs7.writeFile(tsconfigPath, MINIMAL_TSCONFIG, "utf8");
|
|
1846
|
+
tsconfig = "created";
|
|
1500
1847
|
}
|
|
1501
|
-
return
|
|
1848
|
+
return { action, tsconfig, pathsHint };
|
|
1502
1849
|
}
|
|
1503
|
-
function
|
|
1504
|
-
const
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
if (line) {
|
|
1514
|
-
for (const raw of line.split(",")) {
|
|
1515
|
-
if (!raw) continue;
|
|
1516
|
-
const nums = decodeVLQValues(raw);
|
|
1517
|
-
if (nums[0] === void 0) continue;
|
|
1518
|
-
generatedColumn += nums[0];
|
|
1519
|
-
if (nums.length >= 4) {
|
|
1520
|
-
sourceIndex += nums[1] ?? 0;
|
|
1521
|
-
originalLine += nums[2] ?? 0;
|
|
1522
|
-
originalColumn += nums[3] ?? 0;
|
|
1523
|
-
segs.push({
|
|
1524
|
-
generatedColumn,
|
|
1525
|
-
source: sources[sourceIndex] ?? USER_ENTRY,
|
|
1526
|
-
originalLine,
|
|
1527
|
-
originalColumn
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
decoded.push(segs);
|
|
1850
|
+
async function syncSkills(options) {
|
|
1851
|
+
const targets = normalizeTargets(options.targets ?? "all");
|
|
1852
|
+
const sourceRoot = options.sourceDir ?? resolveSkillsSourceDir();
|
|
1853
|
+
const version = options.version ?? await readDevkitVersion(sourceRoot);
|
|
1854
|
+
const summaries = [];
|
|
1855
|
+
for (const target of targets) {
|
|
1856
|
+
if (target === "cursor") summaries.push(await syncToDir("cursor", sourceRoot, path8.join(options.cwd, ".cursor", "skills"), version));
|
|
1857
|
+
else if (target === "claude") summaries.push(await syncToDir("claude", sourceRoot, path8.join(options.cwd, ".claude", "skills"), version));
|
|
1858
|
+
else if (target === "qoder") summaries.push(await syncToDir("qoder", sourceRoot, path8.join(options.cwd, ".qoder", "skills"), version));
|
|
1859
|
+
else summaries.push(await syncToCodex(sourceRoot, options.cwd, version));
|
|
1533
1860
|
}
|
|
1534
|
-
|
|
1861
|
+
const typesSource = options.typesSourceFile ? await fs7.readFile(options.typesSourceFile, "utf8") : await fetchRuntimeTypesDts(
|
|
1862
|
+
await resolveRuntimeTypesUrlForCwd(options.cwd, options.env ?? process.env),
|
|
1863
|
+
options.fetch ?? fetch
|
|
1864
|
+
);
|
|
1865
|
+
const types = await syncRuntimeTypes(options.cwd, typesSource);
|
|
1866
|
+
return { summaries, types };
|
|
1535
1867
|
}
|
|
1536
|
-
function
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1868
|
+
async function detectSkillsStatus(cwd, sourceDir) {
|
|
1869
|
+
const sourceRoot = sourceDir ?? resolveSkillsSourceDir();
|
|
1870
|
+
const version = await readDevkitVersion(sourceRoot);
|
|
1871
|
+
const targets = [];
|
|
1872
|
+
const dirTargets = ["cursor", "claude", "qoder"];
|
|
1873
|
+
const DIR_ROOTS = {
|
|
1874
|
+
cursor: ".cursor/skills",
|
|
1875
|
+
claude: ".claude/skills",
|
|
1876
|
+
qoder: ".qoder/skills"
|
|
1877
|
+
};
|
|
1878
|
+
for (const target of dirTargets) {
|
|
1879
|
+
const root = path8.join(cwd, DIR_ROOTS[target]);
|
|
1880
|
+
const missing = [];
|
|
1881
|
+
for (const name of SKILL_NAMES) {
|
|
1882
|
+
if (!await fs7.stat(path8.join(root, name)).then(() => true).catch(() => false)) missing.push(name);
|
|
1546
1883
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1884
|
+
let stale = false;
|
|
1885
|
+
try {
|
|
1886
|
+
const meta = await readJson(path8.join(root, META_FILE_NAME));
|
|
1887
|
+
stale = meta.version !== version;
|
|
1888
|
+
} catch {
|
|
1889
|
+
stale = missing.length === 0;
|
|
1890
|
+
}
|
|
1891
|
+
targets.push({
|
|
1892
|
+
target,
|
|
1893
|
+
synced: missing.length === 0 && !stale,
|
|
1894
|
+
reason: missing.length > 0 ? `\u7F3A\u5C11 ${missing.join(", ")}` : stale ? "\u7248\u672C\u8FC7\u671F" : void 0
|
|
1895
|
+
});
|
|
1549
1896
|
}
|
|
1550
|
-
|
|
1551
|
-
}
|
|
1552
|
-
function extractInlineSourceMap(code) {
|
|
1553
|
-
const m = code.match(SOURCEMAP_RE);
|
|
1554
|
-
if (!m?.[1]) return void 0;
|
|
1897
|
+
let agentsContent = "";
|
|
1555
1898
|
try {
|
|
1556
|
-
|
|
1899
|
+
agentsContent = await fs7.readFile(path8.join(cwd, "AGENTS.md"), "utf8");
|
|
1557
1900
|
} catch {
|
|
1558
|
-
return void 0;
|
|
1559
1901
|
}
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1902
|
+
const hasCodex = agentsContent.includes(CODEX_MARKER_BEGIN) && agentsContent.includes(CODEX_MARKER_END) && agentsContent.includes(`v${version}`);
|
|
1903
|
+
targets.push({ target: "codex", synced: hasCodex, reason: hasCodex ? void 0 : "AGENTS.md \u7F3A\u5C11\u540C\u6B65\u6807\u8BB0\u6BB5" });
|
|
1904
|
+
let typesSynced = false;
|
|
1905
|
+
try {
|
|
1906
|
+
const dts = await fs7.readFile(path8.join(cwd, ".easytwin", "types", "index.d.ts"), "utf8");
|
|
1907
|
+
const appsDts = await fs7.readFile(path8.join(cwd, ".easytwin", "types", APPS_TYPES_FILE), "utf8");
|
|
1908
|
+
typesSynced = dts.includes("registerEngineTickListener") && appsDts.includes("export declare abstract class TwinApp");
|
|
1909
|
+
} catch {
|
|
1910
|
+
typesSynced = false;
|
|
1911
|
+
}
|
|
1912
|
+
return { cwd, targets, typesSynced };
|
|
1569
1913
|
}
|
|
1570
1914
|
|
|
1571
1915
|
// src/twinAppHost.ts
|
|
@@ -1598,6 +1942,20 @@ var TwinAppPreviewHost = class {
|
|
|
1598
1942
|
getEngine() {
|
|
1599
1943
|
return this.engine;
|
|
1600
1944
|
}
|
|
1945
|
+
/** 给 spec 调用:场景字段取当前引擎主场景(不要求已 Run TwinApp)。 */
|
|
1946
|
+
getTestContext() {
|
|
1947
|
+
if (!this.engine) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
|
|
1948
|
+
const ctx = this.getContext();
|
|
1949
|
+
const runtimeScene = this.engine.mainScene ?? null;
|
|
1950
|
+
ctx.runtimeScene = runtimeScene;
|
|
1951
|
+
ctx.scene = runtimeScene?.sceneObject ?? null;
|
|
1952
|
+
ctx.camera = runtimeScene?.camera?.main ?? null;
|
|
1953
|
+
return ctx;
|
|
1954
|
+
}
|
|
1955
|
+
async invokeTest(fn, input) {
|
|
1956
|
+
if (this.disposed) throw new Error("\u9884\u89C8\u5BBF\u4E3B\u5DF2\u9500\u6BC1");
|
|
1957
|
+
return fn(this.getTestContext(), input);
|
|
1958
|
+
}
|
|
1601
1959
|
async boot() {
|
|
1602
1960
|
const { runtime, containerId, ossUrl, customComponentDeps } = this.options;
|
|
1603
1961
|
this.engine = await runtime.RuntimeEngine.create({
|
|
@@ -1814,10 +2172,669 @@ var TwinAppPreviewHost = class {
|
|
|
1814
2172
|
return this.ctx;
|
|
1815
2173
|
}
|
|
1816
2174
|
};
|
|
2175
|
+
|
|
2176
|
+
// src/previewHtml.ts
|
|
2177
|
+
function escapeHtml(text) {
|
|
2178
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2179
|
+
}
|
|
2180
|
+
function escapeJsonForScript(json) {
|
|
2181
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2182
|
+
}
|
|
2183
|
+
function buildStatusHtml(message) {
|
|
2184
|
+
return `<!DOCTYPE html>
|
|
2185
|
+
<html lang="zh-CN">
|
|
2186
|
+
<head>
|
|
2187
|
+
<meta charset="UTF-8">
|
|
2188
|
+
</head>
|
|
2189
|
+
<body>
|
|
2190
|
+
<p>${escapeHtml(message)}</p>
|
|
2191
|
+
</body>
|
|
2192
|
+
</html>`;
|
|
2193
|
+
}
|
|
2194
|
+
var PREVIEW_STYLE = `
|
|
2195
|
+
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #111; }
|
|
2196
|
+
body { display: flex; flex-direction: column; font: 13px/1.5 -apple-system, "Segoe UI", sans-serif; color: #f3f3f3; }
|
|
2197
|
+
#stage { position: relative; flex: 1; min-height: 0; }
|
|
2198
|
+
#twin-root { position: relative; width: 100%; height: 100%; overflow: hidden; background: #111; }
|
|
2199
|
+
#twin-root canvas { display: block; }
|
|
2200
|
+
#status {
|
|
2201
|
+
position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
|
|
2202
|
+
padding: 24px; text-align: center; pointer-events: none; white-space: pre-wrap; word-break: break-word;
|
|
2203
|
+
}
|
|
2204
|
+
#status.error { pointer-events: auto; color: #ffb4b4; background: rgba(17,17,17,.85); }
|
|
2205
|
+
/* \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 */
|
|
2206
|
+
#status[hidden] { display: none; }
|
|
2207
|
+
#mock-banner {
|
|
2208
|
+
position: absolute; top: 0; left: 0; right: 0; z-index: 2;
|
|
2209
|
+
padding: 6px 12px; background: rgba(255,248,225,.92); color: #1f1f1f;
|
|
2210
|
+
border-bottom: 1px solid #f0c36d; pointer-events: none;
|
|
2211
|
+
}
|
|
2212
|
+
#debug-log {
|
|
2213
|
+
display: none; position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 4;
|
|
2214
|
+
max-height: 42%; overflow: auto; padding: 8px 10px; border-radius: 6px;
|
|
2215
|
+
background: rgba(0,0,0,.88); color: #c8e1c8; font: 11px/1.45 ui-monospace, Consolas, monospace;
|
|
2216
|
+
white-space: pre-wrap; word-break: break-all; pointer-events: auto;
|
|
2217
|
+
}
|
|
2218
|
+
#debug-log.open { display: block; }
|
|
2219
|
+
#run-bar {
|
|
2220
|
+
flex: 0 0 auto; display: flex; align-items: center; gap: 8px;
|
|
2221
|
+
padding: 6px 10px; background: rgba(20,20,20,.94); border-top: 1px solid #333; z-index: 5;
|
|
2222
|
+
}
|
|
2223
|
+
#run-bar button {
|
|
2224
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
2225
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
2226
|
+
}
|
|
2227
|
+
#run-bar button:disabled { opacity: .55; cursor: default; }
|
|
2228
|
+
#btn-run { background: #0e639c; border-color: #1177bb; font-weight: 600; }
|
|
2229
|
+
#run-status { color: #bbb; min-width: 4em; }
|
|
2230
|
+
#run-bar .spacer { flex: 1; }
|
|
2231
|
+
#test-panel {
|
|
2232
|
+
flex: 0 0 auto; display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
|
|
2233
|
+
padding: 6px 10px; background: rgba(18,18,18,.96); border-top: 1px solid #333; z-index: 5;
|
|
2234
|
+
max-height: 30%; overflow: auto;
|
|
2235
|
+
}
|
|
2236
|
+
#test-panel .test-label { color: #888; padding-top: 4px; flex: 0 0 auto; }
|
|
2237
|
+
#test-list { display: flex; flex-wrap: wrap; gap: 6px 8px; align-items: center; flex: 1; min-width: 0; }
|
|
2238
|
+
#test-panel .test-file { width: 100%; color: #8a8a8a; font-size: 11px; }
|
|
2239
|
+
#test-panel .test-empty { color: #777; }
|
|
2240
|
+
#test-panel .test-item { display: inline-flex; align-items: center; gap: 4px; }
|
|
2241
|
+
#test-panel input.test-input {
|
|
2242
|
+
width: 9em; background: #1a1a1a; border: 1px solid #555; color: #eee;
|
|
2243
|
+
padding: 3px 6px; border-radius: 4px; font: 12px inherit;
|
|
2244
|
+
}
|
|
2245
|
+
#test-panel button {
|
|
2246
|
+
cursor: pointer; border: 1px solid #555; background: #222; color: #eee;
|
|
2247
|
+
padding: 4px 10px; border-radius: 4px; font: 12px/1.3 inherit;
|
|
2248
|
+
}
|
|
2249
|
+
#test-panel button:disabled { opacity: .55; cursor: default; }
|
|
2250
|
+
`;
|
|
2251
|
+
var RENDER_SCRIPT = `
|
|
2252
|
+
var statusEl = document.getElementById("status");
|
|
2253
|
+
var logEl = document.getElementById("debug-log");
|
|
2254
|
+
var engine = null;
|
|
2255
|
+
var runtime = null;
|
|
2256
|
+
var sceneJson = null;
|
|
2257
|
+
var host = null;
|
|
2258
|
+
var running = false;
|
|
2259
|
+
var runningTest = false;
|
|
2260
|
+
var testsReady = false;
|
|
2261
|
+
var hostKind = __HOST_KIND__;
|
|
2262
|
+
var vscodeApi = null;
|
|
2263
|
+
if (hostKind === "vscode") {
|
|
2264
|
+
try { vscodeApi = acquireVsCodeApi(); } catch (e) { /* \u975E vscode \u5BBF\u4E3B */ }
|
|
2265
|
+
}
|
|
2266
|
+
var pending = {};
|
|
2267
|
+
var seq = 0;
|
|
2268
|
+
var origFetch = window.fetch.bind(window);
|
|
2269
|
+
function now() { return new Date().toISOString().slice(11, 23); }
|
|
2270
|
+
function log(line) {
|
|
2271
|
+
var text = "[" + now() + "] " + line;
|
|
2272
|
+
if (logEl) {
|
|
2273
|
+
logEl.textContent = (logEl.textContent ? logEl.textContent + "\\n" : "") + text;
|
|
2274
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
2275
|
+
}
|
|
2276
|
+
if (vscodeApi) vscodeApi.postMessage({ type: "log", line: text });
|
|
2277
|
+
console.log("[EasyTwin]", line);
|
|
2278
|
+
}
|
|
2279
|
+
function nextId() { return String(++seq); }
|
|
2280
|
+
function handleHostMessage(msg) {
|
|
2281
|
+
if (!msg) return;
|
|
2282
|
+
if (msg.type === "proxy-fetch-result") {
|
|
2283
|
+
var p = pending[msg.id];
|
|
2284
|
+
if (!p) return;
|
|
2285
|
+
delete pending[msg.id];
|
|
2286
|
+
if (msg.error) p.reject(new Error(msg.error));
|
|
2287
|
+
else p.resolve(msg);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
if (msg.type === "bundle-result") {
|
|
2291
|
+
if (!msg.ok) {
|
|
2292
|
+
log("\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
2293
|
+
setRunStatus("\u7F16\u8BD1\u5931\u8D25");
|
|
2294
|
+
setRunBusy(false);
|
|
2295
|
+
if (logEl) logEl.classList.add("open");
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
for (var i = 0; i < (msg.warnings || []).length; i++) log("\u7F16\u8BD1\u8B66\u544A " + msg.warnings[i]);
|
|
2299
|
+
runUserCode(msg.code);
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (msg.type === "read-asset-result") {
|
|
2303
|
+
var ap = pending[msg.id];
|
|
2304
|
+
if (!ap) return;
|
|
2305
|
+
delete pending[msg.id];
|
|
2306
|
+
if (msg.error) ap.reject(new Error(msg.error));
|
|
2307
|
+
else ap.resolve(msg.text);
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
if (msg.type === "run-error-mapped") {
|
|
2311
|
+
log("\u6E90\u7801\u6620\u5C04\\n" + (msg.stack || ""));
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
if (msg.type === "tests") {
|
|
2315
|
+
renderTests(msg.tests || []);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
if (msg.type === "test-bundle") {
|
|
2319
|
+
if (!msg.ok) {
|
|
2320
|
+
log("\u6D4B\u8BD5\u7F16\u8BD1\u5931\u8D25 " + (msg.error || ""));
|
|
2321
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
2322
|
+
runningTest = false;
|
|
2323
|
+
setRunBusy(running);
|
|
2324
|
+
if (logEl) logEl.classList.add("open");
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
for (var ti = 0; ti < (msg.warnings || []).length; ti++) log("\u6D4B\u8BD5\u8B66\u544A " + msg.warnings[ti]);
|
|
2328
|
+
runExportedTest(msg.code, msg.exportName, msg.input, msg.hasInput).then(function () {
|
|
2329
|
+
log("\u6D4B\u8BD5\u5B8C\u6210 " + msg.exportName);
|
|
2330
|
+
setRunStatus("\u6D4B\u8BD5\u5B8C\u6210");
|
|
2331
|
+
}).catch(function (err) {
|
|
2332
|
+
log("\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
2333
|
+
setRunStatus("\u6D4B\u8BD5\u5931\u8D25");
|
|
2334
|
+
if (logEl) logEl.classList.add("open");
|
|
2335
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
2336
|
+
}).then(function () {
|
|
2337
|
+
runningTest = false;
|
|
2338
|
+
setRunBusy(running);
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function httpJson(url, init) {
|
|
2343
|
+
return origFetch(url, init).then(function (res) {
|
|
2344
|
+
return res.json().then(function (body) {
|
|
2345
|
+
if (!res.ok) throw new Error((body && body.error) || ("HTTP " + res.status));
|
|
2346
|
+
return body;
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
function postToHost(msg) {
|
|
2351
|
+
if (vscodeApi) {
|
|
2352
|
+
vscodeApi.postMessage(msg);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (msg.type === "log" || msg.type === "open-devtools" || msg.type === "show-output") return;
|
|
2356
|
+
if (msg.type === "run") {
|
|
2357
|
+
httpJson("/api/bundle", { method: "POST" }).then(function (body) {
|
|
2358
|
+
handleHostMessage({ type: "bundle-result", ok: body.ok !== false, code: body.code, warnings: body.warnings, error: body.error });
|
|
2359
|
+
}).catch(function (err) {
|
|
2360
|
+
handleHostMessage({ type: "bundle-result", ok: false, error: formatError(err) });
|
|
2361
|
+
});
|
|
2362
|
+
return;
|
|
2363
|
+
}
|
|
2364
|
+
if (msg.type === "run-error") {
|
|
2365
|
+
httpJson("/api/remap-error", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ stack: msg.stack }) }).then(function (body) {
|
|
2366
|
+
handleHostMessage({ type: "run-error-mapped", stack: body.stack || msg.stack });
|
|
2367
|
+
}).catch(function (err) {
|
|
2368
|
+
log("\u6E90\u7801\u6620\u5C04\u5931\u8D25 " + formatError(err));
|
|
2369
|
+
});
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (msg.type === "read-asset") {
|
|
2373
|
+
httpJson("/api/assets?path=" + encodeURIComponent(msg.path)).then(function (body) {
|
|
2374
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, text: body.text, error: body.error });
|
|
2375
|
+
}).catch(function (err) {
|
|
2376
|
+
handleHostMessage({ type: "read-asset-result", id: msg.id, error: formatError(err) });
|
|
2377
|
+
});
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
if (msg.type === "list-tests") {
|
|
2381
|
+
httpJson("/api/tests").then(function (body) {
|
|
2382
|
+
handleHostMessage({ type: "tests", tests: body.tests || [] });
|
|
2383
|
+
}).catch(function (err) {
|
|
2384
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5931\u8D25 " + formatError(err));
|
|
2385
|
+
});
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (msg.type === "run-test") {
|
|
2389
|
+
httpJson("/api/test-bundle", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: msg.id, input: msg.input }) }).then(function (body) {
|
|
2390
|
+
handleHostMessage(Object.assign({ type: "test-bundle" }, body));
|
|
2391
|
+
}).catch(function (err) {
|
|
2392
|
+
handleHostMessage({ type: "test-bundle", ok: false, error: formatError(err) });
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function showStatus(text, isError) {
|
|
2397
|
+
if (!statusEl) return;
|
|
2398
|
+
statusEl.textContent = text;
|
|
2399
|
+
statusEl.className = isError ? "error" : "";
|
|
2400
|
+
statusEl.hidden = false;
|
|
2401
|
+
if (isError && logEl) logEl.classList.add("open");
|
|
2402
|
+
}
|
|
2403
|
+
function hideStatus() { if (statusEl) statusEl.hidden = true; }
|
|
2404
|
+
function formatError(err) {
|
|
2405
|
+
var msg = err && err.message ? err.message : String(err);
|
|
2406
|
+
var stack = err && err.stack ? "\\n" + err.stack : "";
|
|
2407
|
+
return msg + stack;
|
|
2408
|
+
}
|
|
2409
|
+
window.addEventListener("message", function (ev) {
|
|
2410
|
+
handleHostMessage(ev.data);
|
|
2411
|
+
});
|
|
2412
|
+
function b64ToBuf(b64) {
|
|
2413
|
+
var bin = atob(b64);
|
|
2414
|
+
var bytes = new Uint8Array(bin.length);
|
|
2415
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
2416
|
+
return bytes.buffer;
|
|
2417
|
+
}
|
|
2418
|
+
function proxyFetch(url, method) {
|
|
2419
|
+
return new Promise(function (resolve, reject) {
|
|
2420
|
+
if (!vscodeApi) {
|
|
2421
|
+
reject(new Error("\u65E0 vscode API,\u65E0\u6CD5\u4EE3\u7406 " + url));
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
var id = nextId();
|
|
2425
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
2426
|
+
vscodeApi.postMessage({ type: "proxy-fetch", id: id, url: url, method: method || "GET" });
|
|
2427
|
+
}).then(function (msg) {
|
|
2428
|
+
var buf = msg.bodyBase64 ? b64ToBuf(msg.bodyBase64) : new ArrayBuffer(0);
|
|
2429
|
+
return new Response(buf, {
|
|
2430
|
+
status: msg.status || 0,
|
|
2431
|
+
statusText: msg.statusText || "",
|
|
2432
|
+
headers: msg.headers || {}
|
|
2433
|
+
});
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
function isHttpUrl(url) {
|
|
2437
|
+
return typeof url === "string" && (url.indexOf("http://") === 0 || url.indexOf("https://") === 0);
|
|
2438
|
+
}
|
|
2439
|
+
function isPlainHttp(url) {
|
|
2440
|
+
return typeof url === "string" && url.indexOf("http://") === 0;
|
|
2441
|
+
}
|
|
2442
|
+
if (hostKind === "vscode") {
|
|
2443
|
+
window.fetch = function (input, init) {
|
|
2444
|
+
var url = typeof input === "string" ? input : (input && input.url);
|
|
2445
|
+
log("fetch " + url);
|
|
2446
|
+
if (isPlainHttp(url)) {
|
|
2447
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
2448
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
2449
|
+
return res;
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
return origFetch(input, init).catch(function (err) {
|
|
2453
|
+
log("direct fetch \u5931\u8D25,\u6539\u8D70\u4EE3\u7406: " + url + " :: " + formatError(err));
|
|
2454
|
+
if (!isHttpUrl(url)) throw err;
|
|
2455
|
+
return proxyFetch(url, init && init.method).then(function (res) {
|
|
2456
|
+
log("proxy " + res.status + " " + url + " type=" + (res.headers.get("content-type") || ""));
|
|
2457
|
+
return res;
|
|
2458
|
+
});
|
|
2459
|
+
});
|
|
2460
|
+
};
|
|
2461
|
+
var origOpen = XMLHttpRequest.prototype.open;
|
|
2462
|
+
var origSend = XMLHttpRequest.prototype.send;
|
|
2463
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
2464
|
+
this.__etMethod = method;
|
|
2465
|
+
this.__etUrl = String(url);
|
|
2466
|
+
return origOpen.apply(this, arguments);
|
|
2467
|
+
};
|
|
2468
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
2469
|
+
var xhr = this;
|
|
2470
|
+
var url = xhr.__etUrl;
|
|
2471
|
+
if (!isPlainHttp(url)) return origSend.call(this, body);
|
|
2472
|
+
log("xhr proxy " + xhr.__etMethod + " " + url);
|
|
2473
|
+
proxyFetch(url, xhr.__etMethod).then(function (res) {
|
|
2474
|
+
return res.arrayBuffer().then(function (buf) {
|
|
2475
|
+
var text = "";
|
|
2476
|
+
try { text = new TextDecoder().decode(buf); } catch (e) { /* binary */ }
|
|
2477
|
+
Object.defineProperty(xhr, "status", { configurable: true, value: res.status });
|
|
2478
|
+
Object.defineProperty(xhr, "statusText", { configurable: true, value: res.statusText });
|
|
2479
|
+
Object.defineProperty(xhr, "responseURL", { configurable: true, value: url });
|
|
2480
|
+
Object.defineProperty(xhr, "readyState", { configurable: true, value: 4 });
|
|
2481
|
+
var rt = xhr.responseType;
|
|
2482
|
+
var response = buf;
|
|
2483
|
+
if (rt === "" || rt === "text") response = text;
|
|
2484
|
+
else if (rt === "json") { try { response = JSON.parse(text); } catch (e) { response = null; } }
|
|
2485
|
+
Object.defineProperty(xhr, "response", { configurable: true, value: response });
|
|
2486
|
+
Object.defineProperty(xhr, "responseText", { configurable: true, value: text });
|
|
2487
|
+
if (typeof xhr.onload === "function") xhr.onload(new ProgressEvent("load"));
|
|
2488
|
+
xhr.dispatchEvent(new Event("load"));
|
|
2489
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
2490
|
+
});
|
|
2491
|
+
}).catch(function (err) {
|
|
2492
|
+
log("xhr \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
2493
|
+
if (typeof xhr.onerror === "function") xhr.onerror(new ProgressEvent("error"));
|
|
2494
|
+
xhr.dispatchEvent(new Event("error"));
|
|
2495
|
+
xhr.dispatchEvent(new Event("loadend"));
|
|
2496
|
+
});
|
|
2497
|
+
};
|
|
2498
|
+
function patchHttpSrc(proto, prop) {
|
|
2499
|
+
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
2500
|
+
if (!desc || typeof desc.set !== "function") return;
|
|
2501
|
+
Object.defineProperty(proto, prop, {
|
|
2502
|
+
configurable: true,
|
|
2503
|
+
enumerable: desc.enumerable,
|
|
2504
|
+
get: function () { return desc.get.call(this); },
|
|
2505
|
+
set: function (value) {
|
|
2506
|
+
var el = this;
|
|
2507
|
+
var url = String(value);
|
|
2508
|
+
if (!isPlainHttp(url)) { desc.set.call(el, value); return; }
|
|
2509
|
+
log("media proxy " + url);
|
|
2510
|
+
proxyFetch(url).then(function (res) { return res.blob(); }).then(function (blob) {
|
|
2511
|
+
desc.set.call(el, URL.createObjectURL(blob));
|
|
2512
|
+
}).catch(function (err) {
|
|
2513
|
+
log("media proxy \u5931\u8D25 " + url + " :: " + formatError(err));
|
|
2514
|
+
try { el.dispatchEvent(new Event("error")); } catch (e) { /* \u65E0\u76D1\u542C\u65F6\u5FFD\u7565 */ }
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
patchHttpSrc(HTMLImageElement.prototype, "src");
|
|
2520
|
+
patchHttpSrc(HTMLMediaElement.prototype, "src");
|
|
2521
|
+
}
|
|
2522
|
+
document.getElementById("btn-debug").addEventListener("click", function () {
|
|
2523
|
+
if (logEl) logEl.classList.toggle("open");
|
|
2524
|
+
});
|
|
2525
|
+
var btnDevtools = document.getElementById("btn-devtools");
|
|
2526
|
+
var btnOutput = document.getElementById("btn-output");
|
|
2527
|
+
if (hostKind === "http") {
|
|
2528
|
+
if (btnDevtools) btnDevtools.hidden = true;
|
|
2529
|
+
if (btnOutput) btnOutput.hidden = true;
|
|
2530
|
+
}
|
|
2531
|
+
if (btnDevtools) btnDevtools.addEventListener("click", function () {
|
|
2532
|
+
postToHost({ type: "open-devtools" });
|
|
2533
|
+
});
|
|
2534
|
+
if (btnOutput) btnOutput.addEventListener("click", function () {
|
|
2535
|
+
postToHost({ type: "show-output" });
|
|
2536
|
+
});
|
|
2537
|
+
function setRunStatus(text) {
|
|
2538
|
+
var el = document.getElementById("run-status");
|
|
2539
|
+
if (el) el.textContent = text;
|
|
2540
|
+
}
|
|
2541
|
+
function setRunBusy(busy) {
|
|
2542
|
+
running = busy;
|
|
2543
|
+
var btn = document.getElementById("btn-run");
|
|
2544
|
+
if (btn) btn.disabled = !!busy || runningTest;
|
|
2545
|
+
syncTestControls();
|
|
2546
|
+
}
|
|
2547
|
+
function syncTestControls() {
|
|
2548
|
+
var panel = document.getElementById("test-panel");
|
|
2549
|
+
if (!panel) return;
|
|
2550
|
+
var disabled = !testsReady || running || runningTest;
|
|
2551
|
+
var nodes = panel.querySelectorAll("button.test-run, input.test-input");
|
|
2552
|
+
for (var i = 0; i < nodes.length; i++) nodes[i].disabled = disabled;
|
|
2553
|
+
}
|
|
2554
|
+
function renderTests(tests) {
|
|
2555
|
+
var list = document.getElementById("test-list");
|
|
2556
|
+
if (!list) return;
|
|
2557
|
+
list.textContent = "";
|
|
2558
|
+
if (!tests || !tests.length) {
|
|
2559
|
+
var empty = document.createElement("span");
|
|
2560
|
+
empty.className = "test-empty";
|
|
2561
|
+
empty.textContent = "\u6CA1\u6709 *.spec.ts \u5BFC\u51FA";
|
|
2562
|
+
list.appendChild(empty);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
var groups = {};
|
|
2566
|
+
var order = [];
|
|
2567
|
+
for (var i = 0; i < tests.length; i++) {
|
|
2568
|
+
var t = tests[i];
|
|
2569
|
+
if (!groups[t.file]) { groups[t.file] = []; order.push(t.file); }
|
|
2570
|
+
groups[t.file].push(t);
|
|
2571
|
+
}
|
|
2572
|
+
for (var g = 0; g < order.length; g++) {
|
|
2573
|
+
var file = order[g];
|
|
2574
|
+
var heading = document.createElement("span");
|
|
2575
|
+
heading.className = "test-file";
|
|
2576
|
+
heading.textContent = file;
|
|
2577
|
+
list.appendChild(heading);
|
|
2578
|
+
var items = groups[file];
|
|
2579
|
+
for (var j = 0; j < items.length; j++) list.appendChild(makeTestControl(items[j]));
|
|
2580
|
+
}
|
|
2581
|
+
syncTestControls();
|
|
2582
|
+
}
|
|
2583
|
+
function makeTestControl(t) {
|
|
2584
|
+
var wrap = document.createElement("span");
|
|
2585
|
+
wrap.className = "test-item";
|
|
2586
|
+
if (t.hasInput) {
|
|
2587
|
+
var input = document.createElement("input");
|
|
2588
|
+
input.type = "text";
|
|
2589
|
+
input.className = "test-input";
|
|
2590
|
+
input.placeholder = t.inputName || "input";
|
|
2591
|
+
var btn = document.createElement("button");
|
|
2592
|
+
btn.type = "button";
|
|
2593
|
+
btn.className = "test-run";
|
|
2594
|
+
btn.textContent = t.name;
|
|
2595
|
+
btn.addEventListener("click", function () { requestRunTest(t.id, input.value); });
|
|
2596
|
+
input.addEventListener("keydown", function (ev) {
|
|
2597
|
+
if (ev.key === "Enter") requestRunTest(t.id, input.value);
|
|
2598
|
+
});
|
|
2599
|
+
wrap.appendChild(input);
|
|
2600
|
+
wrap.appendChild(btn);
|
|
2601
|
+
} else {
|
|
2602
|
+
var only = document.createElement("button");
|
|
2603
|
+
only.type = "button";
|
|
2604
|
+
only.className = "test-run";
|
|
2605
|
+
only.textContent = t.name;
|
|
2606
|
+
only.addEventListener("click", function () { requestRunTest(t.id); });
|
|
2607
|
+
wrap.appendChild(only);
|
|
2608
|
+
}
|
|
2609
|
+
return wrap;
|
|
2610
|
+
}
|
|
2611
|
+
function requestRunTest(id, input) {
|
|
2612
|
+
if (!testsReady || running || runningTest) return;
|
|
2613
|
+
if (!host) { log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5"); return; }
|
|
2614
|
+
runningTest = true;
|
|
2615
|
+
setRunBusy(running);
|
|
2616
|
+
setRunStatus("\u6D4B\u8BD5\u7F16\u8BD1\u4E2D\u2026");
|
|
2617
|
+
log("\u6D4B\u8BD5:\u7F16\u8BD1 " + id);
|
|
2618
|
+
postToHost({ type: "run-test", id: id, input: input });
|
|
2619
|
+
}
|
|
2620
|
+
async function runExportedTest(code, exportName, input, hasInput) {
|
|
2621
|
+
var blob = new Blob([code], { type: "text/javascript" });
|
|
2622
|
+
var url = URL.createObjectURL(blob);
|
|
2623
|
+
try {
|
|
2624
|
+
var mod = await import(url);
|
|
2625
|
+
var fn = mod[exportName];
|
|
2626
|
+
if (typeof fn !== "function") throw new Error("\u5BFC\u51FA " + exportName + " \u4E0D\u662F\u51FD\u6570");
|
|
2627
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5");
|
|
2628
|
+
var ctx = host.getTestContext();
|
|
2629
|
+
var result = hasInput ? fn(ctx, input == null ? "" : input) : fn(ctx);
|
|
2630
|
+
await Promise.resolve(result);
|
|
2631
|
+
} finally {
|
|
2632
|
+
URL.revokeObjectURL(url);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function applyRenderPatch() {
|
|
2636
|
+
// 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,
|
|
2637
|
+
// \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
|
|
2638
|
+
var scene = engine && engine.mainScene;
|
|
2639
|
+
if (
|
|
2640
|
+
scene && scene.rootComponent && scene.rootComponent.version &&
|
|
2641
|
+
!scene.postprocessingComponent &&
|
|
2642
|
+
runtime.compareVersion && runtime.compareVersion(scene.rootComponent.version, "0.0.31") > 0
|
|
2643
|
+
) {
|
|
2644
|
+
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");
|
|
2645
|
+
scene.registerRenderCallback(function () {
|
|
2646
|
+
scene.renderer.render(scene.sceneObject, scene.camera.main);
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
async function loadHostScene(nextEngine, nextJson) {
|
|
2651
|
+
engine = nextEngine;
|
|
2652
|
+
var sceneManager = engine.getManager(runtime.SceneManager);
|
|
2653
|
+
await sceneManager.loadScene(nextJson, runtime.RuntimeSceneMode.Publish, runtime.LoadSceneMode.Single);
|
|
2654
|
+
applyRenderPatch();
|
|
2655
|
+
}
|
|
2656
|
+
function readAsset(relPath) {
|
|
2657
|
+
return new Promise(function (resolve, reject) {
|
|
2658
|
+
var id = nextId();
|
|
2659
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
2660
|
+
postToHost({ type: "read-asset", id: id, path: relPath });
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
async function runUserCode(code) {
|
|
2664
|
+
setRunStatus("\u8FD0\u884C\u4E2D\u2026");
|
|
2665
|
+
try {
|
|
2666
|
+
if (!host) throw new Error("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
2667
|
+
await host.run(code);
|
|
2668
|
+
log("Run \u5B8C\u6210");
|
|
2669
|
+
setRunStatus("\u8FD0\u884C\u4E2D");
|
|
2670
|
+
} catch (err) {
|
|
2671
|
+
log("Run \u5931\u8D25 " + formatError(err));
|
|
2672
|
+
setRunStatus("\u5931\u8D25");
|
|
2673
|
+
if (logEl) logEl.classList.add("open");
|
|
2674
|
+
postToHost({ type: "run-error", stack: err && err.stack ? err.stack : String(err) });
|
|
2675
|
+
} finally {
|
|
2676
|
+
setRunBusy(false);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
document.getElementById("btn-run").addEventListener("click", function () {
|
|
2680
|
+
if (running || runningTest) return;
|
|
2681
|
+
if (!engine) {
|
|
2682
|
+
log("\u573A\u666F\u5C1A\u672A\u52A0\u8F7D\u5B8C\u6210,\u65E0\u6CD5 Run");
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
setRunBusy(true);
|
|
2686
|
+
setRunStatus("\u7F16\u8BD1\u4E2D\u2026");
|
|
2687
|
+
log("Run:\u8BF7\u6C42\u7F16\u8BD1 src/main.ts");
|
|
2688
|
+
postToHost({ type: "run" });
|
|
2689
|
+
});
|
|
2690
|
+
document.getElementById("btn-refresh-tests").addEventListener("click", function () {
|
|
2691
|
+
log("\u5237\u65B0\u6D4B\u8BD5\u5217\u8868");
|
|
2692
|
+
postToHost({ type: "list-tests" });
|
|
2693
|
+
});
|
|
2694
|
+
function readEmbeddedTests() {
|
|
2695
|
+
var el = document.getElementById("workspace-tests");
|
|
2696
|
+
if (!el || !el.textContent) return [];
|
|
2697
|
+
try { return JSON.parse(el.textContent); } catch (e) { return []; }
|
|
2698
|
+
}
|
|
2699
|
+
renderTests(readEmbeddedTests());
|
|
2700
|
+
function normalizeHierarchyConfig(vo) {
|
|
2701
|
+
var objs = vo && vo.objs ? vo.objs : [];
|
|
2702
|
+
for (var i = 0; i < objs.length; i++) {
|
|
2703
|
+
var hc = objs[i].hierarchyConfig || {};
|
|
2704
|
+
if (typeof hc.active !== "boolean") {
|
|
2705
|
+
hc.active = hc.inActive === true ? false : hc.visible !== false;
|
|
2706
|
+
}
|
|
2707
|
+
if (typeof hc.lock !== "boolean") hc.lock = false;
|
|
2708
|
+
if (typeof hc.collapsed !== "boolean") hc.collapsed = !!hc.isCollapsed;
|
|
2709
|
+
objs[i].hierarchyConfig = hc;
|
|
2710
|
+
}
|
|
2711
|
+
return vo;
|
|
2712
|
+
}
|
|
2713
|
+
function toSceneJson(runtime, raw) {
|
|
2714
|
+
var vo = raw && raw.payload && (raw.payload.objs || raw.payload.sceneComponent) ? raw.payload : raw;
|
|
2715
|
+
if (vo && vo.sceneComponent) {
|
|
2716
|
+
return {
|
|
2717
|
+
id: vo.id || "preview",
|
|
2718
|
+
name: vo.name || "\u573A\u666F\u9884\u89C8",
|
|
2719
|
+
sceneComponent: vo.sceneComponent
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
vo = normalizeHierarchyConfig(vo);
|
|
2723
|
+
var sceneEntity = (vo.objs || []).find(function (o) { return o && o.type === "Scene"; });
|
|
2724
|
+
var rootObj = sceneEntity || (vo.objs && vo.objs[0] ? vo.objs[0] : null);
|
|
2725
|
+
return {
|
|
2726
|
+
id: rootObj && rootObj.sceneId ? rootObj.sceneId : "preview",
|
|
2727
|
+
name: rootObj && rootObj.name ? rootObj.name : "\u573A\u666F\u9884\u89C8",
|
|
2728
|
+
sceneComponent: runtime.convertObjToComponentJson(vo)
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
try {
|
|
2732
|
+
showStatus("\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026");
|
|
2733
|
+
log("runtimeUri=" + __RUNTIME_URI__);
|
|
2734
|
+
log("ossUrl(\u8D44\u4EA7\u6839)=" + __BASE_OSS_URL__ + " (\u7F3A\u7701\u5B98\u65B9 OSS,HTTP \u8D70\u6269\u5C55\u5BBF\u4E3B\u4EE3\u7406)");
|
|
2735
|
+
log("1/4 import twin-runtime / TwinApp host");
|
|
2736
|
+
runtime = await import("@easytwin/runtime");
|
|
2737
|
+
var hostMod = await import(__HOST_URI__);
|
|
2738
|
+
log("2/4 parse scene JSON");
|
|
2739
|
+
var sceneVo = JSON.parse(document.getElementById("scene-data").textContent);
|
|
2740
|
+
sceneJson = toSceneJson(runtime, sceneVo);
|
|
2741
|
+
log("scene id=" + sceneJson.id + " name=" + sceneJson.name);
|
|
2742
|
+
log("3/4 RuntimeEngine.create (webp/draco/basis/component script \u6309 ossUrl/easytwin/system/libs/ \u4E0E components/custom/ \u52A0\u8F7D)");
|
|
2743
|
+
host = new hostMod.TwinAppPreviewHost({
|
|
2744
|
+
runtime: runtime,
|
|
2745
|
+
containerId: "twin-root",
|
|
2746
|
+
ossUrl: __BASE_OSS_URL__,
|
|
2747
|
+
appId: __APP_ID__,
|
|
2748
|
+
sceneId: sceneJson.id,
|
|
2749
|
+
sceneJson: sceneJson,
|
|
2750
|
+
customComponentDeps: {
|
|
2751
|
+
"@easytwin/runtime": runtime,
|
|
2752
|
+
"@easytwin/runtime-frontend": { EasyVIcon: {}, MobxReactLite: { observer: function (c) { return c; } } },
|
|
2753
|
+
react: { createElement: function () { return null; }, Fragment: "div" }
|
|
2754
|
+
},
|
|
2755
|
+
loadScene: loadHostScene,
|
|
2756
|
+
readAsset: readAsset,
|
|
2757
|
+
log: log
|
|
2758
|
+
});
|
|
2759
|
+
await host.boot();
|
|
2760
|
+
engine = host.getEngine();
|
|
2761
|
+
log("4/4 loadScene");
|
|
2762
|
+
log("\u5B8C\u6210");
|
|
2763
|
+
hideStatus();
|
|
2764
|
+
testsReady = true;
|
|
2765
|
+
setRunBusy(false);
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
log("\u5931\u8D25 " + formatError(err));
|
|
2768
|
+
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);
|
|
2769
|
+
}
|
|
2770
|
+
window.addEventListener("pagehide", function () {
|
|
2771
|
+
if (host) { try { host.dispose(); } catch (e) { /* webview \u9500\u6BC1\u9636\u6BB5\u5BB9\u9519 */ } }
|
|
2772
|
+
});
|
|
2773
|
+
`;
|
|
2774
|
+
function originOf(url) {
|
|
2775
|
+
return new URL(url).origin;
|
|
2776
|
+
}
|
|
2777
|
+
function buildPreviewHtml(options) {
|
|
2778
|
+
const { baseUrl, ossUrl, sceneJson, mock, runtimeUri, cspSource } = options;
|
|
2779
|
+
const appsUri = options.appsUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-apps.js");
|
|
2780
|
+
const hostUri = options.hostUri ?? runtimeUri.replace(/twin-runtime\.js$/, "twin-app-host.js");
|
|
2781
|
+
const appId = options.appId ?? "preview";
|
|
2782
|
+
const hostKind = options.host ?? "vscode";
|
|
2783
|
+
const apiOrigin = originOf(baseUrl);
|
|
2784
|
+
const ossOrigin = originOf(ossUrl);
|
|
2785
|
+
const runtimeOrigin = originOf(runtimeUri);
|
|
2786
|
+
const resourceSrc = cspSource && cspSource.length > 0 ? cspSource : runtimeOrigin;
|
|
2787
|
+
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));
|
|
2788
|
+
const importMap = JSON.stringify({
|
|
2789
|
+
imports: {
|
|
2790
|
+
"@easytwin/runtime": runtimeUri,
|
|
2791
|
+
"@easytwin/apps": appsUri
|
|
2792
|
+
}
|
|
2793
|
+
});
|
|
2794
|
+
const testsJson = JSON.stringify(options.tests ?? []);
|
|
2795
|
+
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>` : "";
|
|
2796
|
+
return `<!DOCTYPE html>
|
|
2797
|
+
<html lang="zh-CN">
|
|
2798
|
+
<head>
|
|
2799
|
+
<meta charset="UTF-8">
|
|
2800
|
+
<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:;">
|
|
2801
|
+
<title>EasyTwin \u573A\u666F\u9884\u89C8${mock ? "(\u672C\u5730\u6D4B\u8BD5\u6A21\u5F0F)" : ""}</title>
|
|
2802
|
+
<style>${PREVIEW_STYLE}</style>
|
|
2803
|
+
<script type="importmap">${importMap}</script>
|
|
2804
|
+
</head>
|
|
2805
|
+
<body>
|
|
2806
|
+
<div id="stage">
|
|
2807
|
+
<div id="twin-root"></div>
|
|
2808
|
+
${mockBanner}
|
|
2809
|
+
<div id="status">\u6B63\u5728\u52A0\u8F7D\u4E09\u7EF4\u573A\u666F\u2026</div>
|
|
2810
|
+
<pre id="debug-log"></pre>
|
|
2811
|
+
</div>
|
|
2812
|
+
<div id="test-panel">
|
|
2813
|
+
<span class="test-label">\u6D4B\u8BD5</span>
|
|
2814
|
+
<div id="test-list"></div>
|
|
2815
|
+
<button type="button" id="btn-refresh-tests">\u5237\u65B0\u6D4B\u8BD5</button>
|
|
2816
|
+
</div>
|
|
2817
|
+
<div id="run-bar">
|
|
2818
|
+
<button type="button" id="btn-run">Run</button>
|
|
2819
|
+
<span id="run-status">\u5C31\u7EEA</span>
|
|
2820
|
+
<span class="spacer"></span>
|
|
2821
|
+
<button type="button" id="btn-debug">\u8C03\u8BD5\u65E5\u5FD7</button>
|
|
2822
|
+
<button type="button" id="btn-devtools">\u5F00\u53D1\u8005\u5DE5\u5177</button>
|
|
2823
|
+
<button type="button" id="btn-output">\u8F93\u51FA\u901A\u9053</button>
|
|
2824
|
+
</div>
|
|
2825
|
+
<script id="scene-data" type="application/json">${escapeJsonForScript(sceneJson)}</script>
|
|
2826
|
+
<script id="workspace-tests" type="application/json">${escapeJsonForScript(testsJson)}</script>
|
|
2827
|
+
<script type="module">
|
|
2828
|
+
${renderScript}
|
|
2829
|
+
</script>
|
|
2830
|
+
</body>
|
|
2831
|
+
</html>`;
|
|
2832
|
+
}
|
|
1817
2833
|
export {
|
|
1818
2834
|
APPS_DTS,
|
|
1819
2835
|
APPS_MODULE,
|
|
1820
2836
|
APPS_TYPES_FILE,
|
|
2837
|
+
APP_ID_HEADER,
|
|
1821
2838
|
AUTH_HEADER,
|
|
1822
2839
|
BundleError,
|
|
1823
2840
|
CODEX_MARKER_BEGIN,
|
|
@@ -1829,13 +2846,17 @@ export {
|
|
|
1829
2846
|
DEFAULT_ENTRY_PATH,
|
|
1830
2847
|
DEFAULT_MAIN_TS,
|
|
1831
2848
|
DEFAULT_OSS_URL,
|
|
2849
|
+
EASYTWIN_DIR,
|
|
2850
|
+
EASYTWIN_SCENES_DIRNAME,
|
|
1832
2851
|
EASYTWIN_TYPES_DIR,
|
|
1833
2852
|
ENDPOINTS,
|
|
1834
2853
|
EXAMPLE_SCENE_FILE,
|
|
1835
2854
|
EasyTwinApiError,
|
|
1836
2855
|
EasyTwinClient,
|
|
2856
|
+
GITIGNORE_ENTRIES,
|
|
1837
2857
|
GITIGNORE_ENTRY,
|
|
1838
2858
|
GITIGNORE_FILE_NAME,
|
|
2859
|
+
INSPECT_NODE_LIMIT,
|
|
1839
2860
|
LOCAL_LOAD_SCENE_ERROR,
|
|
1840
2861
|
META_FILE_NAME,
|
|
1841
2862
|
MINIMAL_TSCONFIG,
|
|
@@ -1843,45 +2864,67 @@ export {
|
|
|
1843
2864
|
MOCK_APP_ID,
|
|
1844
2865
|
MOCK_APP_SECRET,
|
|
1845
2866
|
MOCK_SCENE_NAME,
|
|
1846
|
-
OP_ACCOUNT_ID_HEADER,
|
|
1847
|
-
OP_USER_ID_HEADER,
|
|
1848
2867
|
PORTABLE_RUNTIME_EXPORTS,
|
|
2868
|
+
PROD_OSS_URL,
|
|
1849
2869
|
RUNTIME_MODULE,
|
|
2870
|
+
RUNTIME_TYPES_RELPATH,
|
|
1850
2871
|
SKILL_NAMES,
|
|
1851
|
-
|
|
2872
|
+
SkillsError,
|
|
1852
2873
|
TEST_BASE_URL,
|
|
1853
|
-
|
|
1854
|
-
TEST_OP_USER_ID,
|
|
1855
|
-
TEST_SPACE_ID,
|
|
2874
|
+
TEST_OSS_URL,
|
|
1856
2875
|
TSCONFIG_PATHS_HINT,
|
|
1857
2876
|
TwinApp,
|
|
1858
2877
|
TwinAppPreviewHost,
|
|
1859
2878
|
USER_ENTRY,
|
|
2879
|
+
WORKSPACE_CODE_EXTENSIONS,
|
|
2880
|
+
WORKSPACE_IGNORED_DIRS,
|
|
2881
|
+
WORKSPACE_IGNORED_FILES,
|
|
1860
2882
|
appendGitignore,
|
|
1861
2883
|
applyWorkspacePull,
|
|
1862
2884
|
assertUploadable,
|
|
1863
2885
|
buildMultipartBody,
|
|
2886
|
+
buildPreviewHtml,
|
|
1864
2887
|
buildRuntimeTypesContent,
|
|
2888
|
+
buildStatusHtml,
|
|
2889
|
+
buildWorkspacePushBody,
|
|
1865
2890
|
bundleUserCode,
|
|
2891
|
+
bundleWorkspaceModule,
|
|
2892
|
+
bundleWorkspaceTestFile,
|
|
1866
2893
|
collectFiles,
|
|
1867
2894
|
configFilePath,
|
|
2895
|
+
countSceneTreeNodes,
|
|
1868
2896
|
createAppInstance,
|
|
1869
2897
|
defaultPullIgnore,
|
|
2898
|
+
defaultSceneOutPath,
|
|
2899
|
+
defaultWorkspaceIgnore,
|
|
1870
2900
|
defineApp,
|
|
1871
2901
|
deriveExampleSceneId,
|
|
1872
2902
|
detectSkillsStatus,
|
|
1873
2903
|
directoryDepth,
|
|
2904
|
+
escapeHtml,
|
|
2905
|
+
escapeJsonForScript,
|
|
1874
2906
|
exampleSceneSummary,
|
|
1875
2907
|
extractInlineSourceMap,
|
|
1876
2908
|
extractSceneArray,
|
|
2909
|
+
filterSceneTree,
|
|
2910
|
+
formatSceneTree,
|
|
2911
|
+
formatUploadResult,
|
|
1877
2912
|
formatWorkspacePullPlan,
|
|
1878
2913
|
formatWorkspacePullResult,
|
|
1879
2914
|
initConfig,
|
|
2915
|
+
inspectScene,
|
|
2916
|
+
isIgnoredWorkspacePath,
|
|
1880
2917
|
isMockCredentials,
|
|
1881
2918
|
isSafeRelPath,
|
|
2919
|
+
isWorkspaceCodeFile,
|
|
2920
|
+
isWorkspaceSpecFile,
|
|
2921
|
+
joinOssPath,
|
|
2922
|
+
legacyConfigFilePath,
|
|
1882
2923
|
listScenes,
|
|
2924
|
+
listWorkspaceTests,
|
|
1883
2925
|
loadConfig,
|
|
1884
2926
|
loadExampleScene,
|
|
2927
|
+
nodeMatchesInspect,
|
|
1885
2928
|
normalizeLinkedScenes,
|
|
1886
2929
|
normalizePath,
|
|
1887
2930
|
normalizeSceneList,
|
|
@@ -1889,8 +2932,10 @@ export {
|
|
|
1889
2932
|
normalizeWorkspaceConfig,
|
|
1890
2933
|
normalizeWorkspaceFiles,
|
|
1891
2934
|
parseConfig,
|
|
2935
|
+
parseExportedTestFunctions,
|
|
1892
2936
|
parseSceneStructure,
|
|
1893
2937
|
planWorkspacePull,
|
|
2938
|
+
planWorkspaceUpload,
|
|
1894
2939
|
portableRuntimeWarning,
|
|
1895
2940
|
pullScene,
|
|
1896
2941
|
pullWorkspace,
|
|
@@ -1900,6 +2945,8 @@ export {
|
|
|
1900
2945
|
resolveConfig,
|
|
1901
2946
|
resolveExampleScenePath,
|
|
1902
2947
|
resolveRuntimeTypesSourceFile,
|
|
2948
|
+
resolveRuntimeTypesUrl,
|
|
2949
|
+
resolveRuntimeTypesUrlForCwd,
|
|
1903
2950
|
resolveSkillsSourceDir,
|
|
1904
2951
|
resolveSnapshotUrl,
|
|
1905
2952
|
resolveWorkspaceAssetPath,
|
|
@@ -1911,7 +2958,7 @@ export {
|
|
|
1911
2958
|
validateConfigShape,
|
|
1912
2959
|
workspacePullHasConflicts,
|
|
1913
2960
|
workspacePullPendingWrites,
|
|
2961
|
+
workspaceTestId,
|
|
1914
2962
|
writeConfigFile,
|
|
1915
2963
|
writeConfigScenes
|
|
1916
2964
|
};
|
|
1917
|
-
//# sourceMappingURL=index.js.map
|