@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.3 → 0.1.2-dev.31
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 +95 -16
- package/cli/README.md +4 -5
- package/cli/bin/create-muscat-plugin.mjs +1 -1
- package/cli/bin/muscat-pk.mjs +16 -10
- package/cli/docker/Dockerfile.bridge +17 -0
- package/cli/docker/Dockerfile.cpp +22 -0
- package/cli/docker/Dockerfile.dev +14 -0
- package/cli/docker/Dockerfile.python +20 -0
- package/cli/docker/bridge-package.json +8 -0
- package/cli/src/bridge.mjs +92 -0
- package/cli/src/build.mjs +406 -36
- package/cli/src/check-package.mjs +2 -1
- package/cli/src/cpp.mjs +65 -0
- package/cli/src/dev.mjs +16 -3
- package/cli/src/devBack.mjs +246 -49
- package/cli/src/devUi.mjs +118 -15
- package/cli/src/esbuild.mjs +51 -10
- package/cli/src/manifest.mjs +189 -0
- package/cli/src/new.mjs +152 -27
- package/cli/src/preview.mjs +91 -0
- package/cli/src/python.mjs +112 -0
- package/cli/src/ros2.mjs +66 -0
- package/cli/src/socketBridge.mjs +129 -0
- package/cli/src/type.mjs +114 -0
- package/cli/src/update.mjs +267 -30
- package/cli/src/util.mjs +65 -4
- package/cli/templates/common/README.md +382 -9
- package/cli/templates/common/manifest.json +13 -13
- package/cli/templates/common/package.json +5 -11
- package/cli/templates/common/py/main.py +88 -0
- package/cli/templates/common/requirements.txt +4 -0
- package/cli/templates/common/tsconfig.json +1 -1
- package/cli/templates/common/{src → ui}/content.tsx +3 -2
- package/cli/templates/common/vite.config.ts +22 -13
- package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
- package/cli/templates/cpp/cpp/main.cpp +25 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/__init__.py +0 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/resource/__PLUGIN_SLUG__ +0 -0
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +17 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +22 -0
- package/package.json +4 -4
- package/cli/src/pack.mjs +0 -58
- package/cli/templates/sdk/main.py +0 -21
- package/cli/templates/sdk/requirements.txt +0 -1
package/cli/src/devBack.mjs
CHANGED
|
@@ -1,65 +1,262 @@
|
|
|
1
|
-
// muscat-pk dev:back — run the plugin back end.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// muscat-pk dev:back — run the plugin back end during development.
|
|
2
|
+
// Every backend runs in a warm Docker container that matches the robot
|
|
3
|
+
// (Ubuntu 24.04), so dev behaves like production.
|
|
4
|
+
// python : Python 3.12 container, pip install, run main.py with watchfiles
|
|
5
|
+
// hot-reload. Shares the bridge container's netns (localhost:10000).
|
|
6
|
+
// cpp : Ubuntu + cmake container, build cpp/, run the binary, rebuild +
|
|
7
|
+
// restart on source changes.
|
|
8
|
+
// ros2 : Jazzy + Python 3.12 container, colcon build, run the node, restart
|
|
9
|
+
// on source changes (rclcpp rebuilds on change).
|
|
5
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, watch } from "node:fs";
|
|
7
12
|
import { resolve } from "node:path";
|
|
8
|
-
import { findPluginRoot, log } from "./util.mjs";
|
|
13
|
+
import { ensureDocker, findPluginRoot, log } from "./util.mjs";
|
|
14
|
+
import { readManifest as readManifestSchema, validateManifest } from "./manifest.mjs";
|
|
15
|
+
import {
|
|
16
|
+
PYTHON_DEV_IMAGE,
|
|
17
|
+
assertDockerReady as pyAssertDockerReady,
|
|
18
|
+
ensureImage as pyEnsureImage,
|
|
19
|
+
} from "./python.mjs";
|
|
20
|
+
import {
|
|
21
|
+
ROS2_DEV_IMAGE,
|
|
22
|
+
assertDockerReady,
|
|
23
|
+
ensureImage,
|
|
24
|
+
ros2PackageName,
|
|
25
|
+
} from "./ros2.mjs";
|
|
26
|
+
import {
|
|
27
|
+
CPP_DEV_IMAGE,
|
|
28
|
+
assertDockerReady as cppAssertDockerReady,
|
|
29
|
+
ensureImage as cppEnsureImage,
|
|
30
|
+
} from "./cpp.mjs";
|
|
9
31
|
|
|
10
|
-
function
|
|
32
|
+
function readManifest(root) {
|
|
11
33
|
try {
|
|
12
|
-
|
|
13
|
-
return m.runtime?.type ?? "none";
|
|
34
|
+
return JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf8"));
|
|
14
35
|
} catch {
|
|
15
|
-
return
|
|
36
|
+
return {};
|
|
16
37
|
}
|
|
17
38
|
}
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
function manifestRuntime(root) {
|
|
41
|
+
return readManifest(root).runtime?.type ?? "python";
|
|
42
|
+
}
|
|
22
43
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
44
|
+
// ── python ────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Runs in a warm Ubuntu 24.04 + Python 3.12 container (matches the robot).
|
|
46
|
+
// Under `muscat-pk dev`, it shares the bridge container's netns, so the SDK
|
|
47
|
+
// reaches the Socket.IO bridge at the default localhost:10000.
|
|
48
|
+
function runPython(root) {
|
|
49
|
+
pyAssertDockerReady();
|
|
50
|
+
pyEnsureImage();
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const manifest = readManifest(root);
|
|
53
|
+
const pluginId = manifest.id ?? "";
|
|
54
|
+
const container = `${PYTHON_DEV_IMAGE}-run`;
|
|
55
|
+
const envFlags = pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : [];
|
|
56
|
+
// Share the bridge container's network namespace so localhost:10000 is the
|
|
57
|
+
// bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
|
|
58
|
+
const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
|
|
59
|
+
const netFlags = bridgeContainer ? ["--network", `container:${bridgeContainer}`] : [];
|
|
60
|
+
const docker = (args, opts = {}) =>
|
|
61
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
62
|
+
|
|
63
|
+
// start a warm, long-lived container with the plugin mounted
|
|
64
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
65
|
+
log.step("starting Python dev container…");
|
|
66
|
+
const up = docker(
|
|
67
|
+
["run", "-d", "--name", container, ...netFlags,
|
|
68
|
+
"-v", `${root}:/plugin`, "-w", "/plugin", PYTHON_DEV_IMAGE, "sleep", "infinity"],
|
|
69
|
+
{ stdio: "inherit" },
|
|
70
|
+
);
|
|
71
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
35
72
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
log.step("pip install requirements + watchfiles (in container)…");
|
|
74
|
+
docker(["exec", container, "bash", "-lc",
|
|
75
|
+
"pip install --break-system-packages --ignore-installed -q watchfiles" +
|
|
76
|
+
" && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
|
|
77
|
+
|
|
78
|
+
if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
|
|
79
|
+
log.step("watchfiles → py/main.py (reload on any .py change in py/)");
|
|
80
|
+
const child = spawn(
|
|
81
|
+
"docker",
|
|
82
|
+
["exec", ...envFlags, container, "bash", "-lc",
|
|
83
|
+
"python3 -m watchfiles --filter python --target-type command 'python3 py/main.py' py"],
|
|
84
|
+
{ cwd: root, stdio: ["inherit", "inherit", "pipe"] },
|
|
85
|
+
);
|
|
86
|
+
// watchfiles prints a KeyboardInterrupt traceback on SIGTERM — suppress it
|
|
87
|
+
child.stderr.on("data", (buf) => {
|
|
88
|
+
const text = buf.toString();
|
|
89
|
+
if (!text.includes("KeyboardInterrupt") && !text.includes("raise_keyboard_interrupt")) {
|
|
90
|
+
process.stderr.write(buf);
|
|
49
91
|
}
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const cleanup = () => {
|
|
95
|
+
if (!child.killed) child.kill();
|
|
96
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
97
|
+
process.exit(0);
|
|
98
|
+
};
|
|
99
|
+
child.on("exit", (code) => {
|
|
100
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
101
|
+
process.exit(code ?? 0);
|
|
102
|
+
});
|
|
103
|
+
process.once("SIGINT", cleanup);
|
|
104
|
+
process.once("SIGTERM", cleanup);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── ros2 ───────────────────────────────────────────────────────────────────────
|
|
108
|
+
function runRos2(root) {
|
|
109
|
+
assertDockerReady();
|
|
110
|
+
const pkg = ros2PackageName(root);
|
|
111
|
+
if (!pkg) throw new Error("no ROS2 package found under ros2_ws/src.");
|
|
112
|
+
|
|
113
|
+
ensureImage();
|
|
114
|
+
|
|
115
|
+
const manifest = readManifest(root);
|
|
116
|
+
const pluginId = manifest.id ?? "";
|
|
117
|
+
const isRclcpp = manifest.runtime?.rosClient === "rclcpp";
|
|
118
|
+
const container = `${ROS2_DEV_IMAGE}-run`;
|
|
119
|
+
const envFlags = pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : [];
|
|
120
|
+
const docker = (args, opts = {}) =>
|
|
121
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
122
|
+
|
|
123
|
+
// start a warm, long-lived container with the workspace mounted
|
|
124
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
125
|
+
log.step("starting ROS2 dev container…");
|
|
126
|
+
const up = docker(
|
|
127
|
+
["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", ROS2_DEV_IMAGE, "sleep", "infinity"],
|
|
128
|
+
{ stdio: "inherit" },
|
|
129
|
+
);
|
|
130
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
131
|
+
|
|
132
|
+
// one-time deps + symlink build (edits to .py reflect without rebuilding)
|
|
133
|
+
log.step("pip install + colcon build --symlink-install (once)…");
|
|
134
|
+
docker(["exec", container, "bash", "-lc",
|
|
135
|
+
"pip install -r requirements.txt --break-system-packages --ignore-installed -q || true; cd ros2_ws && colcon build --symlink-install"]);
|
|
52
136
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
137
|
+
let child = null;
|
|
138
|
+
const startNode = () => {
|
|
139
|
+
if (child) child.kill();
|
|
140
|
+
log.step(`ros2 run ${pkg} ${pkg}_node`);
|
|
141
|
+
child = spawn(
|
|
142
|
+
"docker",
|
|
143
|
+
["exec", ...envFlags, container, "bash", "-lc",
|
|
144
|
+
`cd ros2_ws && source install/setup.bash && ros2 run ${pkg} ${pkg}_node`],
|
|
145
|
+
{ cwd: root, stdio: "inherit" },
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
startNode();
|
|
149
|
+
|
|
150
|
+
// restart node on source change. rclpy reflects .py edits live (symlink-install);
|
|
151
|
+
// rclcpp (C++) and package-metadata changes require a colcon rebuild.
|
|
152
|
+
let timer = null;
|
|
153
|
+
const onChange = (_event, filename) => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
timer = setTimeout(() => {
|
|
156
|
+
const structural = filename && /(setup\.py|package\.xml|CMakeLists\.txt)$/.test(String(filename));
|
|
157
|
+
if (structural || isRclcpp) {
|
|
158
|
+
log.info(structural ? "package metadata changed — colcon rebuild…" : "source changed — colcon rebuild…");
|
|
159
|
+
docker(["exec", container, "bash", "-lc", "cd ros2_ws && colcon build --symlink-install"]);
|
|
160
|
+
} else {
|
|
161
|
+
log.info("source changed — restarting node…");
|
|
162
|
+
}
|
|
163
|
+
startNode();
|
|
164
|
+
}, 300);
|
|
165
|
+
};
|
|
166
|
+
for (const target of ["ros2_ws/src", "main.py"]) {
|
|
167
|
+
const full = resolve(root, target);
|
|
168
|
+
if (existsSync(full)) watch(full, { recursive: true }, onChange);
|
|
60
169
|
}
|
|
61
170
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
171
|
+
const cleanup = () => {
|
|
172
|
+
if (child) child.kill();
|
|
173
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
174
|
+
process.exit(0);
|
|
175
|
+
};
|
|
176
|
+
process.on("SIGINT", cleanup);
|
|
177
|
+
process.on("SIGTERM", cleanup);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── cpp ──────────────────────────────────────────────────────────────────────
|
|
181
|
+
function runCpp(root) {
|
|
182
|
+
cppAssertDockerReady();
|
|
183
|
+
const manifest = readManifest(root);
|
|
184
|
+
const entry = manifest.runtime?.entry;
|
|
185
|
+
if (!entry) throw new Error("runtime.entry (the built executable name) is required for cpp.");
|
|
186
|
+
|
|
187
|
+
cppEnsureImage();
|
|
188
|
+
|
|
189
|
+
const pluginId = manifest.id ?? "";
|
|
190
|
+
const container = `${CPP_DEV_IMAGE}-run`;
|
|
191
|
+
const envFlags = pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : [];
|
|
192
|
+
const docker = (args, opts = {}) =>
|
|
193
|
+
spawnSync("docker", args, { cwd: root, stdio: "inherit", ...opts });
|
|
194
|
+
|
|
195
|
+
// start a warm, long-lived container with the plugin mounted
|
|
196
|
+
docker(["rm", "-f", container], { stdio: "ignore" });
|
|
197
|
+
log.step("starting C++ dev container…");
|
|
198
|
+
const up = docker(
|
|
199
|
+
["run", "-d", "--name", container, "-v", `${root}:/plugin`, "-w", "/plugin", CPP_DEV_IMAGE, "sleep", "infinity"],
|
|
200
|
+
{ stdio: "inherit" },
|
|
201
|
+
);
|
|
202
|
+
if (up.status !== 0) throw new Error("failed to start dev container.");
|
|
203
|
+
|
|
204
|
+
const build = () => {
|
|
205
|
+
log.step("cmake build…");
|
|
206
|
+
return docker(["exec", container, "bash", "-lc",
|
|
207
|
+
"cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Debug && cmake --build cpp/build -j"]);
|
|
208
|
+
};
|
|
209
|
+
build();
|
|
210
|
+
|
|
211
|
+
let child = null;
|
|
212
|
+
const startBin = () => {
|
|
213
|
+
if (child) child.kill();
|
|
214
|
+
log.step(`run cpp/build/${entry}`);
|
|
215
|
+
child = spawn(
|
|
216
|
+
"docker",
|
|
217
|
+
["exec", ...envFlags, container, "bash", "-lc", `./cpp/build/${entry}`],
|
|
218
|
+
{ cwd: root, stdio: "inherit" },
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
startBin();
|
|
222
|
+
|
|
223
|
+
// rebuild + restart on C++ source changes; ignore the build/ output dir.
|
|
224
|
+
let timer = null;
|
|
225
|
+
const onChange = (_event, filename) => {
|
|
226
|
+
if (filename && String(filename).startsWith("build")) return;
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
timer = setTimeout(() => {
|
|
229
|
+
log.info("source changed — rebuilding…");
|
|
230
|
+
const r = build();
|
|
231
|
+
if (r.status === 0) startBin();
|
|
232
|
+
else log.warn("build failed — keeping previous binary");
|
|
233
|
+
}, 300);
|
|
234
|
+
};
|
|
235
|
+
const cppDir = resolve(root, "cpp");
|
|
236
|
+
if (existsSync(cppDir)) watch(cppDir, { recursive: true }, onChange);
|
|
237
|
+
|
|
238
|
+
const cleanup = () => {
|
|
239
|
+
if (child) child.kill();
|
|
240
|
+
spawnSync("docker", ["rm", "-f", container], { stdio: "ignore" });
|
|
241
|
+
process.exit(0);
|
|
242
|
+
};
|
|
243
|
+
process.on("SIGINT", cleanup);
|
|
244
|
+
process.on("SIGTERM", cleanup);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function warnManifest(root) {
|
|
248
|
+
const manifest = readManifestSchema(root);
|
|
249
|
+
if (!manifest || Object.keys(manifest).length === 0) return;
|
|
250
|
+
const errors = validateManifest(manifest);
|
|
251
|
+
for (const e of errors) log.warn(`manifest.json: ${e}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function runDevBack() {
|
|
255
|
+
ensureDocker();
|
|
256
|
+
const root = findPluginRoot();
|
|
257
|
+
warnManifest(root);
|
|
258
|
+
const type = manifestRuntime(root);
|
|
259
|
+
if (type === "ros2") return runRos2(root);
|
|
260
|
+
if (type === "cpp") return runCpp(root);
|
|
261
|
+
return runPython(root);
|
|
65
262
|
}
|
package/cli/src/devUi.mjs
CHANGED
|
@@ -1,37 +1,133 @@
|
|
|
1
1
|
// muscat-pk dev:ui — preview the plugin inside a mini-Muscat (preview-host).
|
|
2
|
-
|
|
2
|
+
// UI bundling: vite build --watch (uses plugin project's own node_modules/vite).
|
|
3
|
+
// Falls back gracefully if Node.js or vite is not available.
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
5
|
import { createRequire } from "node:module";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
6
|
+
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
5
8
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import { checkKitUpdate, findPluginRoot, kitRoot, log } from "./util.mjs";
|
|
9
|
+
import { SANDBOX_HTML } from "./build.mjs";
|
|
10
|
+
import { checkKitUpdate, findPluginRoot, hasBin, kitRoot, log } from "./util.mjs";
|
|
11
|
+
|
|
12
|
+
const PREVIEW_PKG = "@rainbow-robotics/preview-host";
|
|
13
|
+
const CACHE_DIR = resolve(homedir(), ".muscat-pk");
|
|
14
|
+
const CACHE_ROOT = resolve(CACHE_DIR, "node_modules", "@rainbow-robotics", "preview-host");
|
|
15
|
+
|
|
16
|
+
// Dev builds go to a throwaway dir, not dist/ — dist/ is reserved for
|
|
17
|
+
// build/pack output (the shippable artifact).
|
|
18
|
+
const DEV_OUT = ".muscat-dev";
|
|
19
|
+
|
|
20
|
+
function findNodeBin() {
|
|
21
|
+
if (typeof Bun === "undefined") return process.execPath;
|
|
22
|
+
for (const c of ["node", "node20", "node18"]) {
|
|
23
|
+
if (hasBin(c)) return c;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
8
27
|
|
|
9
|
-
/** Resolve @rainbow-robotics/preview-host package root.
|
|
10
|
-
* Works in monorepo (yarn workspace hoisting) and when both packages are
|
|
11
|
-
* installed as deps of the plugin project. */
|
|
12
28
|
function findPreviewHostRoot() {
|
|
13
29
|
try {
|
|
14
30
|
const req = createRequire(import.meta.url);
|
|
15
|
-
return dirname(req.resolve(
|
|
31
|
+
return dirname(req.resolve(`${PREVIEW_PKG}/package.json`));
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
const sibling = resolve(kitRoot, "../preview-host");
|
|
35
|
+
if (existsSync(resolve(sibling, "package.json"))) return sibling;
|
|
36
|
+
|
|
37
|
+
if (existsSync(resolve(CACHE_ROOT, "package.json"))) return CACHE_ROOT;
|
|
38
|
+
|
|
39
|
+
if (!hasBin("npm")) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`preview-host를 찾을 수 없습니다.\n` +
|
|
42
|
+
`Node.js 20+ 를 설치하면 자동으로 준비됩니다: https://nodejs.org`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
log.step(`preview-host 설치 중 (~/.muscat-pk) — 첫 실행 시 한 번만 수행됩니다…`);
|
|
46
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
47
|
+
const r = spawnSync(
|
|
48
|
+
"npm",
|
|
49
|
+
["install", "--prefix", CACHE_DIR, "--save", PREVIEW_PKG],
|
|
50
|
+
{ stdio: "inherit" },
|
|
51
|
+
);
|
|
52
|
+
if (r.status !== 0) throw new Error("preview-host 설치 실패.");
|
|
53
|
+
if (!existsSync(resolve(CACHE_ROOT, "package.json"))) {
|
|
54
|
+
throw new Error("preview-host 설치 후에도 패키지를 찾을 수 없습니다.");
|
|
55
|
+
}
|
|
56
|
+
return CACHE_ROOT;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Write sandbox.html + copy manifest into the dev out dir (vite doesn't generate these).
|
|
60
|
+
function stageDevAssets(pluginRoot) {
|
|
61
|
+
const outDir = resolve(pluginRoot, DEV_OUT);
|
|
62
|
+
mkdirSync(outDir, { recursive: true });
|
|
63
|
+
writeFileSync(resolve(outDir, "sandbox.html"), SANDBOX_HTML, "utf8");
|
|
64
|
+
const manifestSrc = resolve(pluginRoot, "manifest.json");
|
|
65
|
+
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, resolve(outDir, "manifest.json"));
|
|
66
|
+
return outDir;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve vite bin path from the plugin root, hoisting-aware.
|
|
70
|
+
function findViteBin(pluginRoot) {
|
|
71
|
+
try {
|
|
72
|
+
const req = createRequire(resolve(pluginRoot, "package.json"));
|
|
73
|
+
const vitePkg = req.resolve("vite/package.json");
|
|
74
|
+
return resolve(dirname(vitePkg), "bin", "vite.js");
|
|
16
75
|
} catch {
|
|
17
|
-
|
|
18
|
-
return resolve(kitRoot, "../preview-host");
|
|
76
|
+
return null;
|
|
19
77
|
}
|
|
20
78
|
}
|
|
21
79
|
|
|
80
|
+
// Spawn vite build --watch as a background process.
|
|
81
|
+
// Returns the child process, or null if vite/node not available.
|
|
82
|
+
function spawnViteWatch(pluginRoot, node) {
|
|
83
|
+
const viteBin = findViteBin(pluginRoot);
|
|
84
|
+
if (!node || !viteBin || !existsSync(viteBin)) return null;
|
|
85
|
+
|
|
86
|
+
log.step(`vite build --watch (ui/content.tsx → ${DEV_OUT}/content.js)`);
|
|
87
|
+
return spawn(node, [viteBin, "build", "--watch", "--mode", "development", "--outDir", DEV_OUT], {
|
|
88
|
+
cwd: pluginRoot,
|
|
89
|
+
stdio: "inherit",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Wait until dist/content.js exists (vite initial build completes).
|
|
94
|
+
function waitForInitialBuild(distDir, timeoutMs = 30_000) {
|
|
95
|
+
return new Promise((res, rej) => {
|
|
96
|
+
const contentJs = resolve(distDir, "content.js");
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
const check = () => {
|
|
99
|
+
if (existsSync(contentJs)) return res();
|
|
100
|
+
if (Date.now() - start > timeoutMs) return rej(new Error("vite initial build timed out"));
|
|
101
|
+
setTimeout(check, 300);
|
|
102
|
+
};
|
|
103
|
+
check();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
22
107
|
export async function runDevUi(args = []) {
|
|
23
108
|
const portIdx = args.indexOf("--port");
|
|
24
109
|
const port = portIdx !== -1 ? args[portIdx + 1] : "4180";
|
|
25
110
|
|
|
26
111
|
await checkKitUpdate();
|
|
27
|
-
const
|
|
112
|
+
const pluginRoot = findPluginRoot();
|
|
113
|
+
const outDir = stageDevAssets(pluginRoot);
|
|
114
|
+
|
|
115
|
+
const node = findNodeBin();
|
|
116
|
+
const viteProc = spawnViteWatch(pluginRoot, node);
|
|
117
|
+
|
|
118
|
+
if (viteProc) {
|
|
119
|
+
log.step("waiting for initial vite build…");
|
|
120
|
+
await waitForInitialBuild(outDir);
|
|
121
|
+
} else {
|
|
122
|
+
log.warn("vite not found — UI will not be bundled. Run `npm install` in the plugin project.");
|
|
123
|
+
}
|
|
28
124
|
|
|
29
125
|
const previewRoot = findPreviewHostRoot();
|
|
30
126
|
const serveEntry = resolve(previewRoot, "serve.mjs");
|
|
31
127
|
if (!existsSync(resolve(previewRoot, "dist", "index.html"))) {
|
|
32
128
|
log.step("building preview-host bundle (first run)…");
|
|
33
129
|
await new Promise((res, rej) => {
|
|
34
|
-
const p = spawn(
|
|
130
|
+
const p = spawn(node ?? process.execPath, [resolve(previewRoot, "build.mjs")], {
|
|
35
131
|
stdio: "inherit",
|
|
36
132
|
});
|
|
37
133
|
p.on("exit", (c) => (c === 0 ? res() : rej(new Error("preview-host build failed"))));
|
|
@@ -40,11 +136,18 @@ export async function runDevUi(args = []) {
|
|
|
40
136
|
|
|
41
137
|
log.step(`starting preview-host on :${port}`);
|
|
42
138
|
const child = spawn(
|
|
43
|
-
|
|
44
|
-
[serveEntry, "--plugin",
|
|
139
|
+
node ?? process.execPath,
|
|
140
|
+
[serveEntry, "--plugin", outDir, "--port", String(port)],
|
|
45
141
|
{ stdio: "inherit" },
|
|
46
142
|
);
|
|
143
|
+
|
|
144
|
+
const cleanup = () => {
|
|
145
|
+
if (viteProc && !viteProc.killed) viteProc.kill();
|
|
146
|
+
if (!child.killed) child.kill();
|
|
147
|
+
};
|
|
148
|
+
process.once("SIGINT", cleanup);
|
|
149
|
+
process.once("SIGTERM", cleanup);
|
|
47
150
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
48
151
|
}
|
|
49
152
|
|
|
50
|
-
export { findPluginRoot };
|
|
153
|
+
export { findPluginRoot, findPreviewHostRoot };
|
package/cli/src/esbuild.mjs
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
|
+
// When compiled with `bun --compile`, esbuild's native subprocess binary is
|
|
2
|
+
// not bundled. Use Bun's built-in bundler instead.
|
|
3
|
+
async function buildWithBun(options) {
|
|
4
|
+
const { entryPoints, outfile, format, minify, loader, define, external } = options;
|
|
5
|
+
const { dirname } = await import("node:path");
|
|
6
|
+
const result = await Bun.build({
|
|
7
|
+
entrypoints: entryPoints,
|
|
8
|
+
outdir: dirname(outfile),
|
|
9
|
+
format: format ?? "esm",
|
|
10
|
+
minify: minify ?? false,
|
|
11
|
+
loader,
|
|
12
|
+
define,
|
|
13
|
+
external,
|
|
14
|
+
target: "node",
|
|
15
|
+
throw: false,
|
|
16
|
+
});
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
for (const log of result.logs) console.error(log.message);
|
|
19
|
+
throw new Error("Bun.build() failed");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function buildWithWasm(options) {
|
|
24
|
+
const wasm = await import("esbuild-wasm");
|
|
25
|
+
return wasm.build(options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isNativeMissing(err) {
|
|
29
|
+
const msg = String(err?.message || err?.code || err || "");
|
|
30
|
+
return (
|
|
31
|
+
msg.includes("another platform") ||
|
|
32
|
+
msg.includes("Cannot find module") ||
|
|
33
|
+
msg.includes("Cannot find package") ||
|
|
34
|
+
msg.includes("not found") ||
|
|
35
|
+
msg.includes("No such file") ||
|
|
36
|
+
msg.includes("ENOENT") ||
|
|
37
|
+
msg.includes("MODULE_NOT_FOUND")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
1
41
|
export async function buildWithFallback(options) {
|
|
2
|
-
|
|
42
|
+
if (typeof Bun !== "undefined") return buildWithBun(options);
|
|
43
|
+
|
|
44
|
+
// Try native esbuild (faster, no extra setup). Falls back to wasm when:
|
|
45
|
+
// - esbuild was not installed (removed from hard dependencies to avoid
|
|
46
|
+
// postinstall script blocking npm i -g on Windows)
|
|
47
|
+
// - binary was built for a different platform
|
|
3
48
|
try {
|
|
49
|
+
const native = await import("esbuild");
|
|
4
50
|
return await native.build(options);
|
|
5
|
-
} catch (
|
|
6
|
-
|
|
7
|
-
if (!message.includes("another platform")) {
|
|
8
|
-
throw error;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const wasm = await import("esbuild-wasm");
|
|
12
|
-
console.warn("[muscat-pk] native esbuild binary mismatch; falling back to esbuild-wasm");
|
|
13
|
-
return wasm.build(options);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (!isNativeMissing(err)) throw err;
|
|
14
53
|
}
|
|
54
|
+
|
|
55
|
+
return buildWithWasm(options);
|
|
15
56
|
}
|