diff --git a/aphrody.config.json b/aphrody.config.json index 93fdb167..22ba6d70 100644 --- a/aphrody.config.json +++ b/aphrody.config.json @@ -3,13 +3,13 @@ "runtime": { "version": "1.4.4", "repository": "aphrody-labs/bun", - "revision": "c62d01d81506a8b78b1d39b44d17edc81b4a6355" + "revision": "cb64a09ae6dd618efc300c39b9b03ccfa861cac2" }, "runtimeTargets": { "windows-x64": { "version": "1.4.4", "repository": "aphrody-labs/bun", - "revision": "c62d01d81506a8b78b1d39b44d17edc81b4a6355" + "revision": "cb64a09ae6dd618efc300c39b9b03ccfa861cac2" }, "linux-x64": { "version": "1.4.4", diff --git a/apps/desktop/build.ts b/apps/desktop/build.ts index adb7ef38..a90f4268 100644 --- a/apps/desktop/build.ts +++ b/apps/desktop/build.ts @@ -1,9 +1,10 @@ -import { join, resolve } from "node:path"; +import { join } from "node:path"; import { loadConfig, assertRuntime, repositoryRoot } from "@aphrody/bun-workspace"; import { capabilityFor, type Catalog } from "./shared.ts"; import { nativeVideoCapabilities } from "@aphrody/bun-video-project/native"; export const desktopRoot = import.meta.dir; +export const desktopOutput = join(repositoryRoot, "build/apps/desktop"); export async function buildDesktop() { const config = await loadConfig(repositoryRoot); @@ -25,7 +26,7 @@ export async function buildDesktop() { }; const web = await Bun.build({ entrypoints: [join(desktopRoot, "src/app.ts")], - outdir: join(desktopRoot, "dist/web"), + outdir: join(desktopOutput, "web"), target: "browser", format: "esm", minify: true, @@ -33,19 +34,19 @@ export async function buildDesktop() { }); if (!web.success) throw new AggregateError(web.logs, "Bun frontend build failed"); await Promise.all([ - Bun.write(join(desktopRoot, "dist/web/index.html"), Bun.file(join(desktopRoot, "src/index.html"))), - Bun.write(join(desktopRoot, "dist/web/style.css"), Bun.file(join(desktopRoot, "src/style.css"))), + Bun.write(join(desktopOutput, "web/index.html"), Bun.file(join(desktopRoot, "src/index.html"))), + Bun.write(join(desktopOutput, "web/style.css"), Bun.file(join(desktopRoot, "src/style.css"))), ]); const server = await Bun.build({ entrypoints: [join(desktopRoot, "server.ts")], - outdir: join(desktopRoot, "dist"), + outdir: desktopOutput, target: "bun", format: "esm", minify: true, }); if (!server.success) throw new AggregateError(server.logs, "Bun backend build failed"); console.log("Aphrody: " + catalog.libraries.length + " libraries bundled."); - return resolve(desktopRoot, "dist"); + return desktopOutput; } if (import.meta.main) await buildDesktop(); diff --git a/apps/desktop/native.ts b/apps/desktop/native.ts index 2058e388..a02baaa2 100644 --- a/apps/desktop/native.ts +++ b/apps/desktop/native.ts @@ -50,7 +50,7 @@ if (command === "build") { target: "bun", minify: true, compile: { - outfile: join(desktopRoot, "src-tauri/binaries", filename), + outfile: join(output, "native", filename), autoloadDotenv: false, autoloadBunfig: false, autoloadPackageJson: false, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f2e03940..9b7a8669 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -14,7 +14,7 @@ "dependencies": { "@aphrody/bun-video-project": "workspace:*", "@aphrody/bun-workspace": "workspace:*", - "@aphrody/tauri": "file:../../toolchains/bun/packages/bun-tauri" + "@aphrody/tauri": "catalog:" }, "engines": { "bun": "1.4.4" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 37c57f0a..391fa093 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aphrody-bun-desktop" -version = "0.1.0" +version = "1.4.4" description = "Aphrody desktop library inspector" authors = ["aphrody-dev "] edition = "2024" diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index e792e4ec..cce23fce 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "productName": "Aphrody", - "version": "0.1.0", + "version": "1.4.4", "identifier": "com.aphrody.bun", - "build": { "frontendDist": "../dist/web" }, + "build": { "frontendDist": "../../../build/apps/desktop/web" }, "app": { "windows": [ { @@ -22,7 +22,7 @@ "bundle": { "active": true, "targets": "all", - "externalBin": ["binaries/aphrody-server"], + "externalBin": ["../../../build/apps/desktop/native/aphrody-server"], "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"] } } diff --git a/bun.lock b/bun.lock index 1e4203e7..0af3fe19 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@aphrody/bun-artifacts": "workspace:*", "@aphrody/bun-video-project": "workspace:*", "@aphrody/bun-workspace": "workspace:*", + "@aphrody/tauri": "catalog:", }, "devDependencies": { "@aphrody/bun-types": "1.4.4", @@ -20,12 +21,15 @@ "dependencies": { "@aphrody/bun-video-project": "workspace:*", "@aphrody/bun-workspace": "workspace:*", - "@aphrody/tauri": "file:../../toolchains/bun/packages/bun-tauri", + "@aphrody/tauri": "catalog:", }, }, "packages/bun-app-host": { "name": "@aphrody/bun-app-host", "version": "1.4.4", + "bin": { + "aphrody-server": "./src/cli.ts", + }, "dependencies": { "@aphrody/bun-artifacts": "workspace:*", "@aphrody/bun-video-project": "workspace:*", @@ -81,6 +85,9 @@ }, }, }, + "catalog": { + "@aphrody/tauri": "file:toolchains/bun/packages/bun-tauri", + }, "packages": { "@aphrody/bun-app-host": ["@aphrody/bun-app-host@workspace:packages/bun-app-host"], @@ -96,6 +103,8 @@ "@aphrody/bun-workspace": ["@aphrody/bun-workspace@workspace:packages/bun-workspace"], + "@aphrody/tauri": ["@aphrody/tauri@file:toolchains/bun/packages/bun-tauri", {}], + "@bun-os/package-converter": ["@bun-os/package-converter@workspace:packages/package-converter"], "@types/node": ["@types/node@26.6.5", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="], diff --git a/bunfig.toml b/bunfig.toml index 517dc63d..4a05ad48 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,5 +1,6 @@ [install] exact = true +linker = "hoisted" [run] shell = "bun" diff --git a/package.json b/package.json index 4c32b855..052ae900 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,15 @@ "workspace:manifests": "bun scripts/workspace-manifests.ts" }, "dependencies": { + "@aphrody/tauri": "catalog:", "@aphrody/bun-workspace": "workspace:*", "@aphrody/bun-artifacts": "workspace:*", "@aphrody/bun-video-project": "workspace:*", "@aphrody/bun-app-host": "workspace:*" }, + "catalog": { + "@aphrody/tauri": "file:toolchains/bun/packages/bun-tauri" + }, "devDependencies": { "@aphrody/bun-types": "1.4.4" }, diff --git a/scripts/node-modules.ts b/scripts/node-modules.ts new file mode 100644 index 00000000..2d0ebdc6 --- /dev/null +++ b/scripts/node-modules.ts @@ -0,0 +1,74 @@ +#!/usr/bin/env bun +/** Retire redundant workspace installs after proving the root package owner. */ +import { lstat, readdir, realpath, rm } from "node:fs/promises"; +import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { runProcess } from "../packages/bun-workspace/src/process.ts"; + +const root = resolve(import.meta.dir, ".."); +const product = await Bun.file(join(root, "package.json")).json(); +const members: string[] = product.workspaces; +for (const member of members) { + const directory = resolve(root, member, "node_modules"); + const within = relative(root, directory); + if ( + isAbsolute(within) || + within.startsWith(".." + sep) || + within === ".." || + directory === join(root, "node_modules") + ) + throw new Error("Workspace install target is outside the product root."); + const info = await lstat(directory).catch(error => { + if (error.code === "ENOENT") return undefined; + throw error; + }); + if (!info || info.isSymbolicLink()) continue; + const resolvedParent = relative(root, await realpath(dirname(directory))); + if (isAbsolute(resolvedParent) || resolvedParent.startsWith(".." + sep) || resolvedParent === "..") + throw new Error("Workspace install target resolves outside the product root."); + const packageDirectories: { name: string; path: string }[] = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.name === ".bin") continue; + if (entry.name.startsWith("@")) { + for (const scoped of await readdir(join(directory, entry.name))) + packageDirectories.push({ name: `${entry.name}/${scoped}`, path: join(directory, entry.name, scoped) }); + } else packageDirectories.push({ name: entry.name, path: join(directory, entry.name) }); + } + for (const installed of packageDirectories) { + const canonical = join(root, "node_modules", installed.name); + const [local, shared] = await Promise.all([ + Bun.file(join(installed.path, "package.json")).json(), + Bun.file(join(canonical, "package.json")).json(), + ]); + if (local.name !== shared.name || local.version !== shared.version) + throw new Error(`Root package does not replace ${installed.name}.`); + if (!(await lstat(installed.path)).isSymbolicLink()) { + for (const path of new Bun.Glob("**/*").scanSync({ cwd: installed.path, onlyFiles: true, dot: true })) { + const source = Bun.file(join(installed.path, path)); + const target = Bun.file(join(canonical, path)); + if ( + !(await target.exists()) || + (await source.arrayBuffer()).byteLength !== target.size || + Bun.CryptoHasher.hash("sha256", await source.arrayBuffer(), "hex") !== + Bun.CryptoHasher.hash("sha256", await target.arrayBuffer(), "hex") + ) + throw new Error(`Root package differs from workspace package ${installed.name}.`); + } + } + } + if (process.platform === "win32") { + const removed = await runProcess( + [ + "powershell.exe", + "-NoProfile", + "-NonInteractive", + "-Command", + "& { param([string]$target) Remove-Item -LiteralPath $target -Recurse -ErrorAction Stop }", + directory, + ], + root, + { timeoutMs: 30_000, maxOutputBytes: 1024 * 1024 }, + ); + if (removed.code !== 0) throw new Error("Workspace install cleanup failed."); + } else await rm(directory, { recursive: true }); + console.log(`Root node_modules now owns ${member}`); +} diff --git a/scripts/setup.ts b/scripts/setup.ts index f5d0b50d..5da8bc47 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -24,5 +24,6 @@ async function run(argv: string[]): Promise { await run(["git", "submodule", "update", "--init", "--depth", "1"]); await run([process.execPath, "install", "--frozen-lockfile"]); +await run([process.execPath, "scripts/node-modules.ts"]); const cargo = await inspectCargoWorkspace(await loadConfig(root), { root, timeoutMs: 60_000 }); console.log(`Cargo workspace: ${cargo.packageNames.length} packages in ${cargo.workspaceRoot}`); diff --git a/scripts/workspace-manifests.ts b/scripts/workspace-manifests.ts index 3d28c156..9ea9c1f2 100644 --- a/scripts/workspace-manifests.ts +++ b/scripts/workspace-manifests.ts @@ -16,6 +16,7 @@ for (const entry of [{ path: "package.json", manifest: product }, ...entries]) { for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) { for (const name of Object.keys(entry.manifest[field] ?? {})) { if (names.has(name)) entry.manifest[field][name] = "workspace:*"; + else if (Object.hasOwn(product.catalog ?? {}, name)) entry.manifest[field][name] = "catalog:"; } } }