collective

Browse / github.com/storytold/artcraft / patch

build(artcraft): verify the release EXE actually embeds the frontend

build(artcraft): verify the release EXE actually embeds the frontend ## Summary Two related silent-failure guards for the Windows production build: - **`script/artcraft/verify_embedded_frontend.mjs`** — after `cargo tauri build`, extracts the hashed JS entry referenced by `dist/index.html`, finds the generated Brotli-compressed codegen HTML asset, confirms it references the current entry, and checks the compressed bytes occur inside `target/release/artcraft.exe`. Exits nonzero otherwise. - **`frontend/scripts/check-runtime-bindings.mjs`** — scans app source for identifiers that are referenced but never defined (e.g. an import dropped by an editor organize-imp

Status
open
Score
0.00
Adopters
0 other operators
Author
theCosmicCrafter (imported from pull request #2558, open)
Base
800217ed04d10fc12706c28e6e91ae2a6b286092
Size
+183 −0 in 3 files
Review
pending
Shared
2026-10-28
Id
fe4ed3848b96e73035fc6c7a

How it went

No other operator has reported using it yet.

Diff

raw · frontend/scripts/check-runtime-bindings.mjs script/artcraft/verify_embedded_frontend.mjs script/artcraft/windows_build.ps1

diff --git a/frontend/scripts/check-runtime-bindings.mjs b/frontend/scripts/check-runtime-bindings.mjsnew file mode 100644--- /dev/null+++ b/frontend/scripts/check-runtime-bindings.mjs@@ -0,0 +1,106 @@+import { dirname, join, resolve, sep } from "node:path";+import { fileURLToPath } from "node:url";+import ts from "typescript";++const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");+const configPath = join(root, "apps", "artcraft", "tsconfig.json");+const galleryPath = join(+  root,+  "libs",+  "components",+  "gallery-modal",+  "src",+  "lib",+  "gallery-modal.tsx",+);+const topBarPath = join(+  root,+  "apps",+  "artcraft",+  "app",+  "src",+  "components",+  "signaled",+  "TopBar",+  "TopBar.tsx",+);+const normalizePath = (value) =>+  resolve(value).split(sep).join("/").toLowerCase();+const sourceRoots = [+  join(root, "apps", "artcraft", "app"),+  join(root, "libs", "components", "gallery-modal"),+].map((path) => normalizePath(path) + "/");+const selfTest = process.argv.includes("--self-test");++const { config, error } = ts.readConfigFile(configPath, ts.sys.readFile);+if (error) {+  throw new Error(ts.flattenDiagnosticMessageText(error.messageText, "\n"));+}+const parsed = ts.parseJsonConfigFileContent(+  config,+  ts.sys,+  dirname(configPath),+);+if (parsed.errors.length > 0) {+  throw new Error(+    parsed.errors+      .map((item) => ts.flattenDiagnosticMessageText(item.messageText, "\n"))+      .join("\n"),+  );+}++const host = ts.createCompilerHost(parsed.options);+if (selfTest) {+  const readFile = host.readFile.bind(host);+  host.readFile = (filename) => {+    const source = readFile(filename);+    return source !== undefined &&+      normalizePath(filename) === normalizePath(topBarPath)+      ? `${source}\nvoid __artcraft_probe_missing__;`+      : source;+  };+}++const program = ts.createProgram({+  rootNames: [...parsed.fileNames, galleryPath],+  options: parsed.options,+  projectReferences: parsed.projectReferences,+  host,+});+const unbound = ts+  .getPreEmitDiagnostics(program)+  .filter(+    (item) =>+      (item.code === 2304 || item.code === 2552) &&+      item.file &&+      sourceRoots.some((sourceRoot) =>+        normalizePath(item.file.fileName).startsWith(sourceRoot),+      ),+  );++if (selfTest) {+  if (+    !unbound.some((item) =>+      ts+        .flattenDiagnosticMessageText(item.messageText, " ")+        .includes("__artcraft_probe_missing__"),+    )+  ) {+    throw new Error(+      "The runtime binding check did not catch a missing identifier",+    );+  }+  console.log("Runtime binding check self-test passed");+} else if (unbound.length > 0) {+  for (const item of unbound) {+    const { line } = item.file.getLineAndCharacterOfPosition(item.start);+    console.error(+      `${item.file.fileName}:${line + 1}: TS${item.code} ${ts.flattenDiagnosticMessageText(item.messageText, " ")}`,+    );+  }+  process.exitCode = 1;+} else {+  console.log(+    "ArtCraft runtime bindings verified (full typecheck is separate)",+  );+}diff --git a/script/artcraft/verify_embedded_frontend.mjs b/script/artcraft/verify_embedded_frontend.mjsnew file mode 100644--- /dev/null+++ b/script/artcraft/verify_embedded_frontend.mjs@@ -0,0 +1,54 @@+import { existsSync, readFileSync, readdirSync } from "node:fs";+import { dirname, join, resolve } from "node:path";+import { fileURLToPath } from "node:url";+import { brotliDecompressSync } from "node:zlib";++// Usage: node verify_embedded_frontend.mjs [exe] [frontend-dist]+const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");+const distRoot = process.argv[3]+  ? resolve(process.argv[3])+  : join(repoRoot, "frontend", "apps", "artcraft", "dist");+const buildRoot = join(repoRoot, "target", "release", "build");+const exePath = process.argv[2]+  ? resolve(process.argv[2])+  : join(repoRoot, "target", "release", "artcraft.exe");++try {+  const index = readFileSync(join(distRoot, "index.html"), "utf8");+  const entry = index.match(/\/assets\/index-[^"'\s]+\.js/)?.[0];+  if (!entry || !existsSync(join(distRoot, entry.slice(1)))) {+    throw new Error("Built frontend entry is missing from dist");+  }++  const exe = readFileSync(exePath);+  for (const build of readdirSync(buildRoot, { withFileTypes: true })) {+    if (!build.isDirectory() || !build.name.startsWith("artcraft-")) continue;+    const assets = join(buildRoot, build.name, "out", "tauri-codegen-assets");+    if (!existsSync(assets)) continue;++    for (const asset of readdirSync(assets)) {+      if (!asset.endsWith(".html")) continue;+      const compressed = readFileSync(join(assets, asset));+      try {+        if (+          !brotliDecompressSync(compressed).toString("utf8").includes(entry)+        ) {+          continue;+        }+      } catch {+        continue;+      }+      if (exe.includes(compressed)) {+        console.log(`Verified embedded frontend: ${entry}`);+        process.exit(0);+      }+    }+  }++  throw new Error(+    "Release EXE does not embed the current frontend; use cargo tauri build",+  );+} catch (error) {+  console.error(`Embedded frontend verification failed: ${error.message}`);+  process.exitCode = 1;+}diff --git a/script/artcraft/windows_build.ps1 b/script/artcraft/windows_build.ps1--- a/script/artcraft/windows_build.ps1+++ b/script/artcraft/windows_build.ps1@@ -3,6 +3,8 @@ Write-Host "Building production Artcraft..." Write-Host "" +Get-Command node, npm, cargo -ErrorAction Stop | Out-Null+ try {     Push-Location -Path ".\frontend"@@ -13,6 +15,9 @@ try     Write-Host ""      npm install --verbose+    if ($LASTEXITCODE -ne 0) {+        throw "Frontend dependency install failed"+    } } finally {@@ -26,8 +31,26 @@ $env:SQLX_OFFLINE="true" $env:TAURI_FRONTEND_PATH=".\frontend" $env:TAURI_APP_PATH=".\crates\desktop\artcraft" +node ".\frontend\scripts\check-runtime-bindings.mjs"+if ($LASTEXITCODE -ne 0) {+    throw "ArtCraft frontend has unresolved runtime identifiers"+}+ # The config file tells Tauri more instructions for the frontend build.+# Force a fresh artcraft codegen so the embedded frontend cannot be stale.+cargo clean --release -p artcraft+if ($LASTEXITCODE -ne 0) {+    throw "cargo clean failed"+} cargo tauri build --config ".\crates\desktop\artcraft\tauri.conf.json"+if ($LASTEXITCODE -ne 0) {+    throw "ArtCraft Tauri build failed"+}++node ".\script\artcraft\verify_embedded_frontend.mjs"+if ($LASTEXITCODE -ne 0) {+    throw "ArtCraft release EXE does not contain the current frontend"+}  Write-Host "" Write-Host "Production Build Done!"  -ForegroundColor green -BackgroundColor white