mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
better dev script (#1961)
This commit is contained in:
+48
-7
@@ -9,21 +9,62 @@ To set up the ArtCraft development environment, install the following:
|
|||||||
|
|
||||||
1. [Install Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
1. [Install Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
||||||
2. [Install npm](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm). (Node version `v24.13.0` works at time of writing.)
|
2. [Install npm](https://nodejs.org/en/download) or [nvm](https://github.com/nvm-sh/nvm). (Node version `v24.13.0` works at time of writing.)
|
||||||
3. [Install nx](https://nx.dev/docs/getting-started/installation). (Nx version `v22.4.5` works at time of writing.)
|
3. The frontend's Nx and Vite dependencies are installed locally with npm; a global Nx installation is not required for the combined Unix launcher.
|
||||||
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
|
4. [Install Tauri CLI](https://v2.tauri.app/reference/cli/). (Version `tauri-cli 2.10.0` works at time of writing.)
|
||||||
|
|
||||||
An easy way to get started with running the app in development is to run the two commands (in separate terminals):
|
|
||||||
|
|
||||||
**Mac and Linux Development**
|
**Mac and Linux Development**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run the frontend dev server
|
# Start the frontend and Rust/Tauri together in one terminal
|
||||||
./script/artcraft/unix_frontend_dev.sh
|
./script/artcraft/unix_dev.sh
|
||||||
|
|
||||||
# Run the Tauri Rust application
|
# Optionally start the free-port search at another port
|
||||||
./script/artcraft/unix_rust_dev.sh
|
ARTCRAFT_DEV_PORT=6200 ./script/artcraft/unix_dev.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use the combined launcher instead of starting the two legacy dev scripts. It
|
||||||
|
works from any working directory when invoked by its path and installs frontend
|
||||||
|
dependencies if the local Vite installation is missing. Node.js 20+ is required.
|
||||||
|
|
||||||
|
The launcher binds Vite to `127.0.0.1`, starting at port **5193** and trying
|
||||||
|
successive ports until one is available. It leaves existing listeners alone.
|
||||||
|
Only after the real frontend socket is bound does it start Tauri with that exact
|
||||||
|
`devUrl`. HTTP and the HMR websocket share the selected port; Rust communicates
|
||||||
|
through native Tauri IPC and needs no separate HTTP port. Configuration overrides
|
||||||
|
are passed in memory, without changing the checked-in Tauri config.
|
||||||
|
|
||||||
|
JavaScript, TypeScript, React, and CSS changes use Vite hot reload; compatible
|
||||||
|
React component edits use Fast Refresh. Changes that cannot be hot-replaced
|
||||||
|
reload the page. Rust changes use `cargo tauri dev`'s automatic **rebuild and app
|
||||||
|
restart**, including dependent workspace crates. SQLx metadata and SQLite
|
||||||
|
migrations are also watched. Rust reload is a process restart, so it does not
|
||||||
|
preserve unsaved in-memory app state. See [Tauri's watch behavior](https://v2.tauri.app/develop/#reacting-to-source-code-changes).
|
||||||
|
|
||||||
|
After startup, a Vite configuration restart must reuse the chosen port or report
|
||||||
|
an error; it cannot silently switch to another port while Tauri uses an old URL.
|
||||||
|
Ctrl-C stops this session's frontend, Rust watcher/compiler, and desktop app.
|
||||||
|
If the Rust watcher exits, the launcher closes Vite and propagates the exit code.
|
||||||
|
It never finds or terminates another app by name or port.
|
||||||
|
|
||||||
|
The launcher retains the existing Unix defaults for `VITE_ENVIRONMENT_TYPE`,
|
||||||
|
`SQLX_OFFLINE`, `RUSTFLAGS`, and the Linux WebKit workarounds. Explicit environment
|
||||||
|
overrides are preserved. It uses the normal Artcraft app data and Cargo target
|
||||||
|
directory; port separation does not create an isolated account/data profile, and
|
||||||
|
concurrent Rust builds can wait on Cargo's build lock.
|
||||||
|
|
||||||
|
To verify the launcher without compiling Rust or opening a desktop window:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --test frontend/scripts/unix-dev.test.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
The tests use real Vite listeners and HMR, with a test-owned fake Cargo process
|
||||||
|
tree. They cover occupied ports, simultaneous launches, React edits, shutdown,
|
||||||
|
Rust-process failure, and watcher arguments. To verify native Rust reload, run
|
||||||
|
the launcher, wait for the desktop window, make a Rust source edit, and check for
|
||||||
|
a rebuild/restart in the same terminal. Frontend edits should update the webview
|
||||||
|
without restarting the Rust process.
|
||||||
|
|
||||||
**Windows Development**
|
**Windows Development**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Don't restart the dev server on Rust changes
|
# Rust sources and Cargo manifests must remain watched for `cargo tauri dev`.
|
||||||
crates/
|
# Vite owns frontend reloads; build artifacts must not trigger Rust rebuilds.
|
||||||
Cargo.toml
|
target/
|
||||||
Cargo.lock
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, type Plugin } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
@@ -58,6 +59,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
react(),
|
||||||
sparkWasmDataUrlFix(),
|
sparkWasmDataUrlFix(),
|
||||||
tsconfigPaths({
|
tsconfigPaths({
|
||||||
// Resolve workspace packages to source for every importer, including shared
|
// Resolve workspace packages to source for every importer, including shared
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// Invoked by script/artcraft/unix_dev.sh. Adapted from ArtcraftX's launcher.
|
||||||
|
// Living under frontend lets Node resolve the local Vite installation.
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
|
||||||
|
const rootDir = fileURLToPath(new URL("../../", import.meta.url));
|
||||||
|
const frontendDir = path.join(rootDir, "frontend");
|
||||||
|
const appDir = path.join(frontendDir, "apps/artcraft");
|
||||||
|
const rustDir = path.join(rootDir, "crates/desktop/artcraft");
|
||||||
|
const host = "127.0.0.1";
|
||||||
|
const startingPort = Number(process.env.ARTCRAFT_DEV_PORT ?? "5193");
|
||||||
|
|
||||||
|
let vite;
|
||||||
|
let rust;
|
||||||
|
let stopping = false;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!Number.isInteger(startingPort) || startingPort < 1024 || startingPort > 65535) {
|
||||||
|
throw new Error("ARTCRAFT_DEV_PORT must be an integer between 1024 and 65535.");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.VITE_ENVIRONMENT_TYPE ??= "production";
|
||||||
|
// Match the app build's cwd, including Tailwind config/content resolution.
|
||||||
|
process.chdir(appDir);
|
||||||
|
vite = await createServer({
|
||||||
|
configFile: path.join(appDir, "vite.config.ts"),
|
||||||
|
clearScreen: false,
|
||||||
|
server: {
|
||||||
|
host,
|
||||||
|
port: startingPort,
|
||||||
|
// Vite retries occupied ports while binding the real server. There is no
|
||||||
|
// port probe/release/rebind race, and no existing process is terminated.
|
||||||
|
strictPort: false,
|
||||||
|
open: false,
|
||||||
|
hmr: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (stopping) return;
|
||||||
|
await vite.listen();
|
||||||
|
const { port } = vite.httpServer.address();
|
||||||
|
const devUrl = `http://${host}:${port}`;
|
||||||
|
|
||||||
|
// Only initial startup may choose another port. On a Vite config restart,
|
||||||
|
// rebind this port or fail visibly so Tauri never follows a silent port hop.
|
||||||
|
for (const options of [vite.config.server, vite.config.inlineConfig.server]) {
|
||||||
|
options.port = port;
|
||||||
|
options.strictPort = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = path.join(rustDir, "tauri-dev-hot-reload.conf.json");
|
||||||
|
const config = JSON.parse(await readFile(configPath, "utf8"));
|
||||||
|
if (stopping) return;
|
||||||
|
const csp = config.app.security.csp;
|
||||||
|
const overrides = {
|
||||||
|
build: { devUrl, beforeDevCommand: "" },
|
||||||
|
app: {
|
||||||
|
security: {
|
||||||
|
devCsp: {
|
||||||
|
...csp,
|
||||||
|
"connect-src": [...csp["connect-src"], devUrl, `ws://${host}:${port}`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`\n[artcraft] Frontend + HMR: ${devUrl}`);
|
||||||
|
console.log("[artcraft] Rust/Tauri: native IPC; automatic rebuild and app restart enabled.");
|
||||||
|
console.log("[artcraft] Press Ctrl-C to stop both.\n");
|
||||||
|
|
||||||
|
rust = spawn("cargo", [
|
||||||
|
"tauri", "dev",
|
||||||
|
"--no-dev-server",
|
||||||
|
// Tauri watches the desktop crate and its workspace dependencies itself.
|
||||||
|
// SQLx's external metadata and migrations also affect the Rust build.
|
||||||
|
"--additional-watch-folders", path.join(rootDir, ".sqlx"),
|
||||||
|
"--additional-watch-folders", path.join(rootDir, "_database/sql/artcraft_migrations"),
|
||||||
|
"--config", configPath,
|
||||||
|
"--config", JSON.stringify(overrides),
|
||||||
|
], {
|
||||||
|
cwd: rootDir,
|
||||||
|
// All cargo/rustc/app descendants inherit this private Unix process group.
|
||||||
|
detached: true,
|
||||||
|
stdio: ["ignore", "inherit", "inherit"],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TAURI_FRONTEND_PATH: frontendDir,
|
||||||
|
TAURI_APP_PATH: rustDir,
|
||||||
|
SQLX_OFFLINE: process.env.SQLX_OFFLINE ?? "true",
|
||||||
|
RUSTFLAGS: process.env.RUSTFLAGS ?? "-Awarnings",
|
||||||
|
WEBKIT_DISABLE_DMABUF_RENDERER: process.env.WEBKIT_DISABLE_DMABUF_RENDERER ?? "1",
|
||||||
|
WEBKIT_DISABLE_COMPOSITING_MODE: process.env.WEBKIT_DISABLE_COMPOSITING_MODE ?? "1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
rust.on("error", fail);
|
||||||
|
rust.on("exit", (code, signal) => {
|
||||||
|
if (!stopping) {
|
||||||
|
console.log(`[artcraft] Rust/Tauri exited (${signal ?? code}).`);
|
||||||
|
void stop(code ?? 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop(exitCode) {
|
||||||
|
if (stopping) return;
|
||||||
|
stopping = true;
|
||||||
|
console.log("\n[artcraft] Stopping frontend and Rust/Tauri...");
|
||||||
|
|
||||||
|
const deadline = setTimeout(() => {
|
||||||
|
signalRustGroup("SIGKILL");
|
||||||
|
process.exit(exitCode);
|
||||||
|
}, 5000);
|
||||||
|
signalRustGroup("SIGTERM");
|
||||||
|
await Promise.allSettled([
|
||||||
|
vite?.close(),
|
||||||
|
(async () => {
|
||||||
|
while (signalRustGroup(0)) await delay(50);
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
|
clearTimeout(deadline);
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signalRustGroup(signal) {
|
||||||
|
if (!rust?.pid) return false;
|
||||||
|
try {
|
||||||
|
process.kill(-rust.pid, signal);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ESRCH") throw error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(error) {
|
||||||
|
console.error(`[artcraft] ${error.stack ?? error}`);
|
||||||
|
void stop(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("SIGINT", () => void stop(130));
|
||||||
|
process.on("SIGTERM", () => void stop(143));
|
||||||
|
process.on("SIGHUP", () => void stop(129));
|
||||||
|
process.on("uncaughtException", fail);
|
||||||
|
process.on("unhandledRejection", fail);
|
||||||
|
|
||||||
|
main().catch(fail);
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// Integration tests: real Vite sockets + a fake cargo process tree, no GUI/build.
|
||||||
|
// Run: node --test frontend/scripts/unix-dev.test.mjs
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { once } from "node:events";
|
||||||
|
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { createServer } from "node:net";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { setTimeout as delay } from "node:timers/promises";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const rootDir = fileURLToPath(new URL("../../", import.meta.url));
|
||||||
|
const launcher = path.join(rootDir, "script/artcraft/unix_dev.sh");
|
||||||
|
|
||||||
|
test("skips occupied ports, pairs Tauri with real Vite/HMR, and cleans up on Ctrl-C", { timeout: 40000 }, async (t) => {
|
||||||
|
const occupied = await occupyPort(t);
|
||||||
|
const startingPort = occupied.address().port;
|
||||||
|
const run = await fixture(t, { ARTCRAFT_DEV_PORT: String(startingPort) });
|
||||||
|
const record = await run.record();
|
||||||
|
const config = overrides(record);
|
||||||
|
const url = config.build.devUrl;
|
||||||
|
assert.ok(Number(new URL(url).port) > startingPort);
|
||||||
|
assert.equal(new URL(url).hostname, "127.0.0.1");
|
||||||
|
assert.equal(config.build.beforeDevCommand, "");
|
||||||
|
assert.ok(!record.args.includes("--no-watch"));
|
||||||
|
assert.ok(!record.args.includes("--no-dev-server-wait"));
|
||||||
|
assert.equal(record.env.TAURI_APP_PATH, path.join(rootDir, "crates/desktop/artcraft"));
|
||||||
|
assert.equal(record.env.TAURI_FRONTEND_PATH, path.join(rootDir, "frontend"));
|
||||||
|
assert.ok(config.app.security.devCsp["connect-src"].includes(url.replace("http:", "ws:")));
|
||||||
|
assert.match(await (await fetch(url)).text(), /\/src\/index\.tsx/);
|
||||||
|
assert.match(await (await fetch(`${url}/@vite/client`)).text(), /WebSocket/);
|
||||||
|
|
||||||
|
run.proc.kill("SIGINT");
|
||||||
|
assert.deepEqual(await run.exited, [130, null], run.output());
|
||||||
|
await until(() => !alive(record.pid) && !alive(record.childPid));
|
||||||
|
await assert.rejects(fetch(url));
|
||||||
|
assert.equal(occupied.listening, true, "must not terminate the existing listener");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Tauri failure propagates and stops Vite and leftover descendants", { timeout: 40000 }, async (t) => {
|
||||||
|
const run = await fixture(t, { TEST_EXIT_CODE: "17" });
|
||||||
|
const record = await run.record();
|
||||||
|
assert.deepEqual(await run.exited, [17, null], run.output());
|
||||||
|
await until(() => !alive(record.childPid));
|
||||||
|
await assert.rejects(fetch(overrides(record).build.devUrl));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SIGTERM shuts down the complete process tree", { timeout: 40000 }, async (t) => {
|
||||||
|
const run = await fixture(t);
|
||||||
|
const record = await run.record();
|
||||||
|
run.proc.kill("SIGTERM");
|
||||||
|
assert.deepEqual(await run.exited, [143, null], run.output());
|
||||||
|
await until(() => !alive(record.pid) && !alive(record.childPid));
|
||||||
|
await assert.rejects(fetch(overrides(record).build.devUrl));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid ports fail before starting Tauri", { timeout: 40000 }, async (t) => {
|
||||||
|
const run = await fixture(t, { ARTCRAFT_DEV_PORT: "65536" });
|
||||||
|
assert.deepEqual(await run.exited, [1, null]);
|
||||||
|
assert.match(run.output(), /ARTCRAFT_DEV_PORT must be an integer/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("concurrent launchers use different ports without replacing either frontend", { timeout: 40000 }, async (t) => {
|
||||||
|
const occupied = await occupyPort(t);
|
||||||
|
const env = { ARTCRAFT_DEV_PORT: String(occupied.address().port) };
|
||||||
|
const first = await fixture(t, env);
|
||||||
|
const second = await fixture(t, env);
|
||||||
|
const [a, b] = await Promise.all([first.record(), second.record()]);
|
||||||
|
const firstUrl = overrides(a).build.devUrl;
|
||||||
|
const secondUrl = overrides(b).build.devUrl;
|
||||||
|
assert.notEqual(firstUrl, secondUrl);
|
||||||
|
assert.equal((await fetch(firstUrl)).status, 200);
|
||||||
|
assert.equal((await fetch(secondUrl)).status, 200);
|
||||||
|
first.proc.kill("SIGINT");
|
||||||
|
await first.exited;
|
||||||
|
assert.equal((await fetch(secondUrl)).status, 200);
|
||||||
|
assert.ok(alive(b.pid));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a React source edit sends HMR on the selected port without restarting Rust", { timeout: 40000 }, async (t) => {
|
||||||
|
const run = await fixture(t);
|
||||||
|
const record = await run.record();
|
||||||
|
const url = overrides(record).build.devUrl;
|
||||||
|
const probeDir = await mkdtemp(path.join(rootDir, "frontend/apps/artcraft/app/src/dev-probe-"));
|
||||||
|
t.after(() => rm(probeDir, { recursive: true, force: true }));
|
||||||
|
const probe = path.join(probeDir, "Probe.tsx");
|
||||||
|
const moduleUrl = `/src/${path.basename(probeDir)}/Probe.tsx`;
|
||||||
|
await writeFile(probe, "export default function Probe() { return <div>before</div>; }\n");
|
||||||
|
const code = await (await fetch(`${url}${moduleUrl}`)).text();
|
||||||
|
assert.match(code, /\$RefreshReg\$/);
|
||||||
|
|
||||||
|
const client = await (await fetch(`${url}/@vite/client`)).text();
|
||||||
|
const token = client.match(/const wsToken = "([^"]+)"/)[1];
|
||||||
|
const ws = new WebSocket(`${url.replace("http:", "ws:")}/?token=${token}`, "vite-hmr");
|
||||||
|
t.after(() => ws.terminate());
|
||||||
|
const messages = [];
|
||||||
|
ws.on("message", (data) => messages.push(JSON.parse(data.toString())));
|
||||||
|
await once(ws, "open");
|
||||||
|
await writeFile(probe, "export default function Probe() { return <div>after</div>; }\n");
|
||||||
|
await until(() => messages.some((m) => m.type === "update" && m.updates.some((u) => u.path === moduleUrl)));
|
||||||
|
assert.match(await (await fetch(`${url}${moduleUrl}`)).text(), /after/);
|
||||||
|
assert.ok(alive(record.pid), "JS edits must leave the Rust watcher running");
|
||||||
|
assert.equal((await run.record()).pid, record.pid);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Rust watcher arguments include SQLx inputs and preserve caller overrides", { timeout: 40000 }, async (t) => {
|
||||||
|
const run = await fixture(t, { SQLX_OFFLINE: "false", RUSTFLAGS: "-C debuginfo=1" });
|
||||||
|
const record = await run.record();
|
||||||
|
assert.equal(record.env.SQLX_OFFLINE, "false");
|
||||||
|
assert.equal(record.env.RUSTFLAGS, "-C debuginfo=1");
|
||||||
|
assert.ok(record.args.includes(path.join(rootDir, ".sqlx")));
|
||||||
|
assert.ok(record.args.includes(path.join(rootDir, "_database/sql/artcraft_migrations")));
|
||||||
|
const ignores = await readFile(path.join(rootDir, "crates/desktop/artcraft/.taurignore"), "utf8");
|
||||||
|
assert.doesNotMatch(ignores, /^(crates\/|Cargo\.toml|Cargo\.lock)$/m);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function until(check) {
|
||||||
|
for (let attempt = 0; attempt < 200; attempt++) {
|
||||||
|
const result = await check();
|
||||||
|
if (result) return result;
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
throw new Error("Timed out waiting for dev launcher");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fixture(t, env = {}) {
|
||||||
|
const dir = await mkdtemp(path.join(os.tmpdir(), "artcraft-dev-test-"));
|
||||||
|
t.after(() => rm(dir, { recursive: true, force: true }));
|
||||||
|
await writeFile(path.join(dir, "cargo"), `#!/usr/bin/env node
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const { spawn } = require("node:child_process");
|
||||||
|
if (process.argv.includes("--version")) process.exit(0);
|
||||||
|
const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" });
|
||||||
|
fs.writeFileSync(process.env.TEST_RECORD, JSON.stringify({
|
||||||
|
args: process.argv.slice(2), cwd: process.cwd(),
|
||||||
|
env: Object.fromEntries(["TAURI_APP_PATH", "TAURI_FRONTEND_PATH", "SQLX_OFFLINE", "RUSTFLAGS"].map(key => [key, process.env[key]])),
|
||||||
|
pid: process.pid, childPid: child.pid,
|
||||||
|
}));
|
||||||
|
if (process.env.TEST_EXIT_CODE) setTimeout(() => process.exit(Number(process.env.TEST_EXIT_CODE)), 200);
|
||||||
|
setInterval(() => {}, 1000);
|
||||||
|
`, { mode: 0o755 });
|
||||||
|
|
||||||
|
const recordFile = path.join(dir, "record.json");
|
||||||
|
const proc = spawn(launcher, [], {
|
||||||
|
// Prove the launcher is independent of the invoking working directory.
|
||||||
|
cwd: dir,
|
||||||
|
env: { ...process.env, ...env, PATH: `${dir}:${process.env.PATH}`, TEST_RECORD: recordFile },
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
let output = "";
|
||||||
|
proc.stdout.on("data", (chunk) => { output += chunk; });
|
||||||
|
proc.stderr.on("data", (chunk) => { output += chunk; });
|
||||||
|
const exited = once(proc, "exit");
|
||||||
|
t.after(async () => {
|
||||||
|
if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGTERM");
|
||||||
|
await exited;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
proc, exited, output: () => output,
|
||||||
|
async record() {
|
||||||
|
return until(async () => {
|
||||||
|
if (proc.exitCode !== null) throw new Error(output);
|
||||||
|
try { return JSON.parse(await readFile(recordFile, "utf8")); }
|
||||||
|
catch (error) { if (error.code !== "ENOENT") throw error; }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function occupyPort(t) {
|
||||||
|
const server = createServer();
|
||||||
|
server.listen(0, "127.0.0.1");
|
||||||
|
await once(server, "listening");
|
||||||
|
t.after(() => new Promise((resolve) => server.close(resolve)));
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
function alive(pid) {
|
||||||
|
try { process.kill(pid, 0); return true; }
|
||||||
|
catch (error) { if (error.code === "ESRCH") return false; throw error; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function overrides(record) {
|
||||||
|
return JSON.parse(record.args.at(-1));
|
||||||
|
}
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Start the desktop frontend and Rust/Tauri watcher together on macOS or Linux.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
frontend_path="${root_dir}/frontend"
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||||
|
cat <<'HELP'
|
||||||
|
Usage: ./script/artcraft/unix_dev.sh
|
||||||
|
|
||||||
|
Starts Vite with React/CSS hot reload and Tauri with Rust rebuild/restart.
|
||||||
|
Finds and binds a free loopback port starting at ARTCRAFT_DEV_PORT (default: 5193),
|
||||||
|
then passes that exact URL to Tauri. HMR shares the frontend port.
|
||||||
|
Rust uses native Tauri IPC and does not need a second HTTP port.
|
||||||
|
Ctrl-C stops this launcher's frontend, Rust watcher, and desktop app.
|
||||||
|
|
||||||
|
Requires Node.js 20+, npm, Rust, cargo-tauri 2, and Tauri platform prerequisites.
|
||||||
|
Installs frontend dependencies only when the local Vite installation is missing.
|
||||||
|
Run this command instead of running the two legacy dev scripts together.
|
||||||
|
HELP
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -ne 0 ]]; then
|
||||||
|
echo "Unknown argument: $1 (see --help)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
source "${root_dir}/script/common/frontend_preflight.sh"
|
||||||
|
frontend_preflight "${frontend_path}"
|
||||||
|
|
||||||
|
if ! command -v cargo >/dev/null || ! cargo tauri --version >/dev/null 2>&1; then
|
||||||
|
echo 'ERROR: Install Rust and the Tauri CLI: cargo install tauri-cli --version "^2" --locked' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${frontend_path}"
|
||||||
|
if [[ ! -f node_modules/vite/package.json ]]; then
|
||||||
|
# Do not use the legacy install helper: its failure recovery deletes caches.
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec node "${frontend_path}/scripts/unix-dev.mjs"
|
||||||
Reference in New Issue
Block a user