phibelle-kit 1.0.28 → 1.0.30
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.
|
@@ -2,9 +2,11 @@ import chalk from "chalk";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import { WebSocketServer } from "ws";
|
|
5
|
-
import { unpackageScene, parseSceneJson } from "../lib/scene/scene-packaging.js";
|
|
5
|
+
import { unpackageScene, parseSceneJson, packageSceneFromFilesystem } from "../lib/scene/scene-packaging.js";
|
|
6
6
|
import { createScriptWatcher } from "../lib/scene/script-watcher.js";
|
|
7
7
|
import { toErrorMessage } from "../lib/utils.js";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8
10
|
const WS_PORT = 31113;
|
|
9
11
|
const SCENE_SYNC_TYPE = "phibelle-kit-scene-sync";
|
|
10
12
|
const SCENE_FILE_NAME = "scene.phibelle";
|
|
@@ -22,7 +24,7 @@ export async function watchSceneCommand() {
|
|
|
22
24
|
console.log();
|
|
23
25
|
console.log(chalk.gray(" Press Ctrl+C to stop"));
|
|
24
26
|
console.log();
|
|
25
|
-
let latestSceneJson =
|
|
27
|
+
let latestSceneJson = readLocalSceneJson(sceneDir);
|
|
26
28
|
let scriptWatcher = null;
|
|
27
29
|
let currentClient = null;
|
|
28
30
|
function startWatcher() {
|
|
@@ -36,6 +38,9 @@ export async function watchSceneCommand() {
|
|
|
36
38
|
const wss = new WebSocketServer({ port: WS_PORT });
|
|
37
39
|
wss.on("connection", (ws) => {
|
|
38
40
|
currentClient = ws;
|
|
41
|
+
console.log(chalk.green(" ✓ Connected to web app"));
|
|
42
|
+
let initialSyncResolved = false;
|
|
43
|
+
let initialSyncPending = false;
|
|
39
44
|
ws.on("message", async (data) => {
|
|
40
45
|
const raw = Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
41
46
|
if (!raw || raw.length === 0)
|
|
@@ -49,6 +54,47 @@ export async function watchSceneCommand() {
|
|
|
49
54
|
catch {
|
|
50
55
|
return;
|
|
51
56
|
}
|
|
57
|
+
if (!initialSyncResolved) {
|
|
58
|
+
if (initialSyncPending)
|
|
59
|
+
return;
|
|
60
|
+
initialSyncPending = true;
|
|
61
|
+
try {
|
|
62
|
+
latestSceneJson = await packageAndPersistLocalScene(sceneDir, latestSceneJson);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
initialSyncPending = false;
|
|
66
|
+
console.log(chalk.red(" ✖ Failed to package local scene: " + toErrorMessage(error)));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (incoming.sceneData === latestSceneJson) {
|
|
70
|
+
initialSyncResolved = true;
|
|
71
|
+
initialSyncPending = false;
|
|
72
|
+
startWatcher();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const choice = await promptForInitialSyncChoice().catch((error) => {
|
|
76
|
+
console.log(chalk.red(" ✖ " + toErrorMessage(error)));
|
|
77
|
+
ws.close();
|
|
78
|
+
return null;
|
|
79
|
+
});
|
|
80
|
+
if (!choice || currentClient !== ws)
|
|
81
|
+
return;
|
|
82
|
+
if (choice === "push-local") {
|
|
83
|
+
initialSyncResolved = true;
|
|
84
|
+
initialSyncPending = false;
|
|
85
|
+
if (ws.readyState === ws.OPEN) {
|
|
86
|
+
ws.send(latestSceneJson);
|
|
87
|
+
startWatcher();
|
|
88
|
+
console.log(chalk.green(" ✓ Pushed local scene to app"));
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
initialSyncResolved = true;
|
|
93
|
+
initialSyncPending = false;
|
|
94
|
+
console.log(chalk.cyan(" Accepting scene from web app..."));
|
|
95
|
+
}
|
|
96
|
+
if (currentClient !== ws)
|
|
97
|
+
return;
|
|
52
98
|
latestSceneJson = incoming.sceneData;
|
|
53
99
|
const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
|
|
54
100
|
fs.writeFileSync(scenePath, incoming.sceneData, "utf8");
|
|
@@ -73,6 +119,7 @@ export async function watchSceneCommand() {
|
|
|
73
119
|
scriptWatcher?.close();
|
|
74
120
|
scriptWatcher = null;
|
|
75
121
|
wss.close();
|
|
122
|
+
rl.close();
|
|
76
123
|
console.log(chalk.gray(" Goodbye"));
|
|
77
124
|
console.log();
|
|
78
125
|
process.exit(0);
|
|
@@ -106,6 +153,39 @@ function printEditorLink(sceneId, lastPrintedLink) {
|
|
|
106
153
|
console.log(chalk.cyan(` Editor: ${editorUrl}`));
|
|
107
154
|
return editorUrl;
|
|
108
155
|
}
|
|
156
|
+
function readLocalSceneJson(sceneDir) {
|
|
157
|
+
const scenePath = path.join(sceneDir, SCENE_FILE_NAME);
|
|
158
|
+
if (!fs.existsSync(scenePath)) {
|
|
159
|
+
throw new Error(`Missing ${SCENE_FILE_NAME} in ${sceneDir}. Run "phibelle-kit clone" first.`);
|
|
160
|
+
}
|
|
161
|
+
const sceneJson = fs.readFileSync(scenePath, "utf8");
|
|
162
|
+
parseSceneJson(sceneJson);
|
|
163
|
+
return sceneJson;
|
|
164
|
+
}
|
|
165
|
+
async function packageAndPersistLocalScene(sceneDir, sceneJson) {
|
|
166
|
+
const packagedSceneJson = await packageSceneFromFilesystem(sceneJson, sceneDir);
|
|
167
|
+
parseSceneJson(packagedSceneJson);
|
|
168
|
+
fs.writeFileSync(path.join(sceneDir, SCENE_FILE_NAME), packagedSceneJson, "utf8");
|
|
169
|
+
return packagedSceneJson;
|
|
170
|
+
}
|
|
171
|
+
async function promptForInitialSyncChoice() {
|
|
172
|
+
console.log(chalk.yellow(" Choose initial sync direction:"));
|
|
173
|
+
console.log(chalk.cyan(" [p] Push local scene to the web app"));
|
|
174
|
+
console.log(chalk.cyan(" [a] Accept the current web app scene"));
|
|
175
|
+
while (true) {
|
|
176
|
+
const answer = (await askQuestion(" Initial sync [p/a]: ")).trim().toLowerCase();
|
|
177
|
+
if (answer === "p" || answer === "push" || answer === "local")
|
|
178
|
+
return "push-local";
|
|
179
|
+
if (answer === "a" || answer === "accept" || answer === "web" || answer === "app")
|
|
180
|
+
return "accept-web";
|
|
181
|
+
console.log(chalk.yellow(" Please enter 'p' to push local or 'a' to accept the web app scene."));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function askQuestion(prompt) {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
rl.question(prompt, resolve);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
109
189
|
function getRequiredManifestSceneId(sceneDir) {
|
|
110
190
|
const manifestPath = path.join(sceneDir, MANIFEST_FILE_NAME);
|
|
111
191
|
if (!fs.existsSync(manifestPath)) {
|
|
@@ -18,3 +18,4 @@ export declare function unpackageScene(sceneJson: string, sceneDir: string, scen
|
|
|
18
18
|
* Used by script watcher to produce scene snapshot to send to FE.
|
|
19
19
|
*/
|
|
20
20
|
export declare function patchSceneFromFile(sceneJson: string, sceneDir: string, relativePath: string, eventType: "change" | "add"): Promise<string>;
|
|
21
|
+
export declare function packageSceneFromFilesystem(sceneJson: string, sceneDir: string): Promise<string>;
|
|
@@ -133,6 +133,8 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
|
|
|
133
133
|
const fileContent = await fs.readFile(filePath, "utf8");
|
|
134
134
|
if (!parsed.sceneScriptStore)
|
|
135
135
|
parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
|
|
136
|
+
if (parsed.sceneScriptStore.sceneScript === fileContent)
|
|
137
|
+
return sceneJson;
|
|
136
138
|
parsed.sceneScriptStore.sceneScript = fileContent;
|
|
137
139
|
parsed.sceneScriptStore.lastModifiedSceneScript = Date.now();
|
|
138
140
|
return JSON.stringify(parsed);
|
|
@@ -142,7 +144,11 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
|
|
|
142
144
|
const sceneProperties = JSON.parse(fileContent);
|
|
143
145
|
if (!parsed.sceneScriptStore)
|
|
144
146
|
parsed.sceneScriptStore = { sceneScript: "", sceneProperties: [], lastModifiedSceneScript: 0 };
|
|
145
|
-
|
|
147
|
+
const nextSceneProperties = Array.isArray(sceneProperties) ? sceneProperties : [];
|
|
148
|
+
if (JSON.stringify(parsed.sceneScriptStore.sceneProperties ?? []) === JSON.stringify(nextSceneProperties)) {
|
|
149
|
+
return sceneJson;
|
|
150
|
+
}
|
|
151
|
+
parsed.sceneScriptStore.sceneProperties = nextSceneProperties;
|
|
146
152
|
return JSON.stringify(parsed);
|
|
147
153
|
}
|
|
148
154
|
const scriptMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_SCRIPT_FILE.replace(".", "\\.")}$`));
|
|
@@ -155,6 +161,8 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
|
|
|
155
161
|
const entity = entities[existingId];
|
|
156
162
|
if (!entity)
|
|
157
163
|
return sceneJson;
|
|
164
|
+
if (entity.script === fileContent)
|
|
165
|
+
return sceneJson;
|
|
158
166
|
entity.script = fileContent;
|
|
159
167
|
entity.scriptVersion = (entity.scriptVersion ?? 0) + 1;
|
|
160
168
|
return JSON.stringify(parsed);
|
|
@@ -175,7 +183,10 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
|
|
|
175
183
|
const entity = entities[entityId];
|
|
176
184
|
if (!entity)
|
|
177
185
|
return sceneJson;
|
|
178
|
-
|
|
186
|
+
const nextProperties = Array.isArray(properties) ? properties : [];
|
|
187
|
+
if (JSON.stringify(entity.properties ?? []) === JSON.stringify(nextProperties))
|
|
188
|
+
return sceneJson;
|
|
189
|
+
entity.properties = nextProperties;
|
|
179
190
|
return JSON.stringify(parsed);
|
|
180
191
|
}
|
|
181
192
|
const transformsMatch = relativePath.match(new RegExp(`^([^/]+)\\/${ENTITY_TRANSFORMS_FILE.replace(".", "\\.")}$`));
|
|
@@ -191,16 +202,63 @@ export async function patchSceneFromFile(sceneJson, sceneDir, relativePath, even
|
|
|
191
202
|
return sceneJson;
|
|
192
203
|
if (!entity.transform)
|
|
193
204
|
entity.transform = { position: DEFAULT_POSITION, rotation: DEFAULT_ROTATION, scale: DEFAULT_SCALE };
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
entity.transform.scale
|
|
205
|
+
const nextPosition = data.position !== undefined ? JSON.stringify(data.position) : entity.transform.position;
|
|
206
|
+
const nextRotation = data.rotation !== undefined ? JSON.stringify(data.rotation) : entity.transform.rotation;
|
|
207
|
+
const nextScale = data.scale !== undefined ? JSON.stringify(data.scale) : entity.transform.scale;
|
|
208
|
+
if (entity.transform.position === nextPosition &&
|
|
209
|
+
entity.transform.rotation === nextRotation &&
|
|
210
|
+
entity.transform.scale === nextScale) {
|
|
211
|
+
return sceneJson;
|
|
212
|
+
}
|
|
213
|
+
entity.transform.position = nextPosition;
|
|
214
|
+
entity.transform.rotation = nextRotation;
|
|
215
|
+
entity.transform.scale = nextScale;
|
|
200
216
|
return JSON.stringify(parsed);
|
|
201
217
|
}
|
|
202
218
|
return sceneJson;
|
|
203
219
|
}
|
|
220
|
+
export async function packageSceneFromFilesystem(sceneJson, sceneDir) {
|
|
221
|
+
const manifestRaw = await fs.readFile(path.join(sceneDir, "manifest.json"), "utf8");
|
|
222
|
+
const manifest = JSON.parse(manifestRaw);
|
|
223
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
224
|
+
let packagedSceneJson = sceneJson;
|
|
225
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, manifest.sceneScript ?? SCENE_SCRIPT_FILE);
|
|
226
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, manifest.sceneProperties ?? SCENE_PROPERTIES_FILE);
|
|
227
|
+
for (const folderName of Object.values(manifest.entities)) {
|
|
228
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(folderName, ENTITY_SCRIPT_FILE).replace(/\\/g, "/"));
|
|
229
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(folderName, ENTITY_PROPERTIES_FILE).replace(/\\/g, "/"));
|
|
230
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(folderName, ENTITY_TRANSFORMS_FILE).replace(/\\/g, "/"));
|
|
231
|
+
}
|
|
232
|
+
let appEntries = [];
|
|
233
|
+
try {
|
|
234
|
+
appEntries = await fs.readdir(appDir, { withFileTypes: true });
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return packagedSceneJson;
|
|
238
|
+
}
|
|
239
|
+
for (const entry of appEntries) {
|
|
240
|
+
if (!entry.isDirectory())
|
|
241
|
+
continue;
|
|
242
|
+
if (Object.values(manifest.entities).includes(entry.name))
|
|
243
|
+
continue;
|
|
244
|
+
const relativeScriptPath = path.join(entry.name, ENTITY_SCRIPT_FILE).replace(/\\/g, "/");
|
|
245
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, relativeScriptPath, "add");
|
|
246
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(entry.name, ENTITY_PROPERTIES_FILE).replace(/\\/g, "/"));
|
|
247
|
+
packagedSceneJson = await applyIfExists(packagedSceneJson, sceneDir, path.join(entry.name, ENTITY_TRANSFORMS_FILE).replace(/\\/g, "/"));
|
|
248
|
+
}
|
|
249
|
+
return packagedSceneJson;
|
|
250
|
+
}
|
|
251
|
+
async function applyIfExists(sceneJson, sceneDir, relativePath, eventType = "change") {
|
|
252
|
+
const appDir = getSceneAppDir(sceneDir);
|
|
253
|
+
const filePath = path.join(appDir, relativePath);
|
|
254
|
+
try {
|
|
255
|
+
await fs.access(filePath);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return sceneJson;
|
|
259
|
+
}
|
|
260
|
+
return patchSceneFromFile(sceneJson, sceneDir, relativePath, eventType);
|
|
261
|
+
}
|
|
204
262
|
async function addNewEntityFromFolder(parsed, manifest, sceneDir, folderName, appDir) {
|
|
205
263
|
if (Object.values(manifest.entities).includes(folderName))
|
|
206
264
|
return JSON.stringify(parsed);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TSCONFIG_FILE = "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModuleInterop\": true,\n \"allowUmdGlobalAccess\": true,\n \"baseUrl\": \".\",\n \"skipLibCheck\": true,\n \"lib\": [\"
|
|
1
|
+
export declare const TSCONFIG_FILE = "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\",\n \"esModuleInterop\": true,\n \"allowUmdGlobalAccess\": true,\n \"baseUrl\": \".\",\n \"skipLibCheck\": true,\n \"lib\": [\"ES2017\", \"DOM\"]\n },\n \"include\": [\n \"app/**/*.tsx\",\n \"global.d.ts\"\n ]\n}";
|