better dev script
This commit is contained in:
parent
df707bf8e1
commit
fb458cff2e
|
|
@ -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).
|
||||
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.)
|
||||
|
||||
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**
|
||||
|
||||
```bash
|
||||
# Run the frontend dev server
|
||||
./script/artcraft/unix_frontend_dev.sh
|
||||
# Start the frontend and Rust/Tauri together in one terminal
|
||||
./script/artcraft/unix_dev.sh
|
||||
|
||||
# Run the Tauri Rust application
|
||||
./script/artcraft/unix_rust_dev.sh
|
||||
# Optionally start the free-port search at another port
|
||||
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**
|
||||
|
||||
```powershell
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# Don't restart the dev server on Rust changes
|
||||
crates/
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
# Rust sources and Cargo manifests must remain watched for `cargo tauri dev`.
|
||||
# Vite owns frontend reloads; build artifacts must not trigger Rust rebuilds.
|
||||
target/
|
||||
node_modules/
|
||||
dist/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { defineConfig, type Plugin } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import path from "path";
|
||||
import { resolve } from "node:path";
|
||||
|
|
@ -58,6 +59,7 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
sparkWasmDataUrlFix(),
|
||||
tsconfigPaths({
|
||||
// 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));
|
||||
}
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue