Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

266 lines
13 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { execFile } from "node:child_process";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { NtExecutable, NtExecutableResource, Resource } from "resedit";
import { describe, expect, it } from "vitest";
import { materializeCachedUnpackedForInstaller } from "../src/win/builder.js";
import { createLauncherRuntimeSyncPowerShellScript } from "../src/win/custom-installer.js";
import type { WinPaths } from "../src/win/types.js";
import { readWinExecutableVersionSnapshot } from "../src/win/version-resource.js";
const execFileAsync = promisify(execFile);
function createPaths(root: string): WinPaths {
const namespaceRoot = join(root, "namespaces", "second");
return {
appBuilderConfigPath: join(namespaceRoot, "builder-config.json"),
appBuilderOutputRoot: join(namespaceRoot, "builder"),
assembledAppRoot: join(namespaceRoot, "assembled", "app"),
assembledMainEntryPath: join(namespaceRoot, "assembled", "app", "main.cjs"),
assembledPackageJsonPath: join(namespaceRoot, "assembled", "app", "package.json"),
assembledPrebundledRoot: join(namespaceRoot, "assembled", "app", "prebundled"),
blockmapPath: join(namespaceRoot, "builder", "Open Design-second-setup.exe.blockmap"),
builtManifestPath: join(namespaceRoot, "built-app.json"),
daemonCliPrebundleEntrypointPath: join(namespaceRoot, "prebundle-entrypoints", "daemon-cli.js"),
daemonCliPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "daemon", "daemon-cli.mjs"),
daemonPrebundleMetaPath: join(namespaceRoot, "prebundle-meta", "daemon.meta.json"),
daemonPrebundleRoot: join(namespaceRoot, "assembled", "app", "prebundled", "daemon"),
daemonSidecarPrebundleEntrypointPath: join(namespaceRoot, "prebundle-entrypoints", "daemon-sidecar.js"),
daemonSidecarPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "daemon", "daemon-sidecar.mjs"),
exePath: join(namespaceRoot, "builder", "Open Design-second.exe"),
installDir: join(namespaceRoot, "runtime", "install", "Open Design"),
installedExePath: join(namespaceRoot, "runtime", "install", "Open Design", "Open Design.exe"),
installerBasePayloadPath: join(namespaceRoot, "installer", "payload-base.7z"),
installerOverlayPayloadPath: join(namespaceRoot, "installer", "payload-overlay.7z"),
installerScriptPath: join(namespaceRoot, "installer", "installer.nsi"),
launcherPayloadPath: join(namespaceRoot, "payload", "Open Design-second-payload.7z"),
publicDesktopShortcutPath: join(namespaceRoot, "desktop", "public.lnk"),
latestYmlPath: join(namespaceRoot, "builder", "latest.yml"),
installMarkerPath: join(namespaceRoot, "logs", "install.marker.json"),
installTimingPath: join(namespaceRoot, "logs", "install.timing.json"),
nsisLogPath: join(namespaceRoot, "logs", "nsis.log"),
nsisIncludePath: join(namespaceRoot, "nsis", "installer.nsh"),
packagedConfigPath: join(namespaceRoot, "open-design-config.json"),
packagedMainPrebundleMetaPath: join(namespaceRoot, "prebundle-meta", "packaged-main.meta.json"),
packagedMainPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "packaged-main.mjs"),
resourceRoot: join(namespaceRoot, "resources", "open-design"),
setupPath: join(namespaceRoot, "builder", "Open Design-second-setup.exe"),
setupZipPath: join(namespaceRoot, "builder", "Open Design-second-portable.zip"),
startMenuShortcutPath: join(namespaceRoot, "start-menu.lnk"),
tarballsRoot: join(namespaceRoot, "tarballs"),
userDesktopShortcutPath: join(namespaceRoot, "desktop", "user.lnk"),
uninstallMarkerPath: join(namespaceRoot, "logs", "uninstall.marker.json"),
uninstallTimingPath: join(namespaceRoot, "logs", "uninstall.timing.json"),
uninstallerPath: join(namespaceRoot, "runtime", "install", "Open Design", "Uninstall.exe"),
webStandaloneHookAuditPath: join(namespaceRoot, "web-standalone-after-pack-audit.json"),
webStandaloneHookConfigPath: join(namespaceRoot, "web-standalone-after-pack-config.json"),
webSidecarPrebundleMetaPath: join(namespaceRoot, "prebundle-meta", "web-sidecar.meta.json"),
webSidecarPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "web-sidecar.mjs"),
winIconPath: join(namespaceRoot, "resources", "win", "icon.ico"),
unpackedExePath: join(namespaceRoot, "builder", "win-unpacked", "Open Design.exe"),
unpackedRoot: join(namespaceRoot, "builder", "win-unpacked"),
};
}
describe("materializeCachedUnpackedForInstaller", () => {
it("overwrites cached packaged config and app package version", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-win-builder-"));
const cachedUnpackedRoot = join(root, "cache", "builder", "win-unpacked");
const paths = createPaths(root);
try {
await mkdir(join(cachedUnpackedRoot, "resources"), { recursive: true });
await writeFile(join(cachedUnpackedRoot, "Open Design.exe"), await createVersionedExecutable("0.5.0-beta.1"));
await writeFile(
join(cachedUnpackedRoot, "resources", "open-design-config.json"),
`${JSON.stringify({ namespace: "first", version: 1 })}\n`,
"utf8",
);
await mkdir(join(cachedUnpackedRoot, "resources", "app"), { recursive: true });
await writeFile(
join(cachedUnpackedRoot, "resources", "app", "package.json"),
`${JSON.stringify({ name: "open-design-packaged-app", version: "0.5.0-beta.1" })}\n`,
"utf8",
);
await mkdir(join(paths.packagedConfigPath, ".."), { recursive: true });
await writeFile(
paths.packagedConfigPath,
`${JSON.stringify({ appVersion: "0.5.0-beta.2", namespace: "second", version: 1 })}\n`,
"utf8",
);
const manifest = await materializeCachedUnpackedForInstaller(cachedUnpackedRoot, paths, "0.5.0-beta.2");
expect(manifest.source).toBe("namespace");
expect(manifest.unpackedRoot).toBe(paths.unpackedRoot);
await expect(readFile(join(paths.unpackedRoot, "resources", "open-design-config.json"), "utf8")).resolves.toContain(
'"namespace":"second"',
);
await expect(readFile(join(paths.unpackedRoot, "resources", "app", "package.json"), "utf8")).resolves.toContain(
'"version": "0.5.0-beta.2"',
);
await expect(readFile(join(paths.unpackedRoot, "resources", "open-design-config.json"), "utf8")).resolves.toContain(
'"appVersion":"0.5.0-beta.2"',
);
await expect(readWinExecutableVersionSnapshot(join(paths.unpackedRoot, "Open Design.exe"))).resolves.toMatchObject({
fixedFileVersion: "0.5.0.0",
fixedProductVersion: "0.5.0.0",
stringTables: [
{
values: expect.objectContaining({
FileVersion: "0.5.0-beta.2",
ProductVersion: "0.5.0.0",
}),
},
],
});
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
describe("Windows pack artifact boundaries", () => {
it("does not build launcher payload artifacts for a pure dir target", async () => {
const source = await readFile(new URL("../src/win/build.ts", import.meta.url), "utf8");
expect(source).toContain("const hasLauncherPayloadTarget = hasNsisTarget || hasZipTarget");
expect(source).toContain("if (hasLauncherPayloadTarget)");
expect(source.indexOf("const hasLauncherPayloadTarget = hasNsisTarget || hasZipTarget")).toBeLessThan(
source.indexOf('runPhase("payload-artifact"'),
);
});
it("uses the electron-builder cache identity instead of hashing the unpacked tree when possible", async () => {
const source = await readFile(new URL("../src/win/builder.ts", import.meta.url), "utf8");
expect(source).toContain("builtApp.cacheEntryPath == null");
expect(source).toContain("hashWinNsisBasePayloadInputs(builtApp)");
expect(source).toContain("cacheEntryPath: builtApp.cacheEntryPath");
expect(source.indexOf("builtApp.cacheEntryPath == null")).toBeLessThan(
source.indexOf("cacheEntryPath: builtApp.cacheEntryPath"),
);
});
it("keeps NSIS payload archives on the fast LZMA2 path", async () => {
const source = await readFile(new URL("../src/win/custom-installer.ts", import.meta.url), "utf8");
expect(source).toContain('"nsis:payload-base-7z"');
expect(source).toContain('"-m0=LZMA2"');
expect(source).toContain('"-mf=off"');
expect(source).not.toContain('"-ms=off"');
});
it("invalidates Windows payload caches when the archive method changes", async () => {
const builderSource = await readFile(new URL("../src/win/builder.ts", import.meta.url), "utf8");
const payloadSource = await readFile(new URL("../src/win/payload.ts", import.meta.url), "utf8");
expect(builderSource).toContain("const WIN_NSIS_BASE_PAYLOAD_INPUT_HASH_CACHE_VERSION = 2");
expect(payloadSource).toContain("const WIN_LAUNCHER_PAYLOAD_BASE_CACHE_VERSION = 2");
expect(payloadSource).toContain("const WIN_LAUNCHER_PAYLOAD_ARCHIVE_CACHE_VERSION = 2");
});
it("invalidates the NSIS installer cache when installer helper code changes", async () => {
const source = await readFile(new URL("../src/win/builder.ts", import.meta.url), "utf8");
expect(source).toContain("hashWinNsisInstallerImplementation");
expect(source).toContain("nsisInstallerImplementation");
expect(source.indexOf("nsisInstallerImplementation")).toBeLessThan(source.indexOf('target: "nsis-installer"'));
});
});
describe("launcher runtime sync helper", () => {
it.runIf(process.platform === "win32")("writes cleanup.json for superseded launcher runtime pointers", async () => {
const root = await mkdtemp(join(tmpdir(), "open-design-launcher-sync-"));
const runtimePath = join(root, "runtime.json");
const attemptsPath = join(root, "state", "attempt.json");
const cleanupPath = join(root, "state", "cleanup.json");
const helperPath = join(root, "sync-launcher-runtime.ps1");
try {
await mkdir(join(root, "state"), { recursive: true });
await writeFile(helperPath, createLauncherRuntimeSyncPowerShellScript(), "utf8");
await writeFile(
runtimePath,
`${JSON.stringify({
active: { generation: 3, version: "0.10.2-beta.11" },
channel: "beta",
lastSuccessful: { generation: 3, version: "0.10.2-beta.11" },
namespace: "rr",
schemaVersion: 1,
updatedAt: "2026-01-01T00:00:00.000Z",
}, null, 2)}\n`,
"utf8",
);
await execFileAsync("powershell.exe", [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
helperPath,
"-RuntimePath",
runtimePath,
"-AttemptsPath",
attemptsPath,
"-CleanupPath",
cleanupPath,
"-Channel",
"beta",
"-Namespace",
"rr",
"-Version",
"0.10.2-beta.12",
]);
const runtime = JSON.parse(await readFile(runtimePath, "utf8")) as {
active: { version: string };
lastSuccessful: { version: string };
};
const cleanup = JSON.parse(await readFile(cleanupPath, "utf8")) as {
currentVersion: string;
versions: Array<{ reason: string; state: string; version: string }>;
};
expect(runtime.active.version).toBe("0.10.2-beta.12");
expect(runtime.lastSuccessful.version).toBe("0.10.2-beta.12");
expect(cleanup.currentVersion).toBe("0.10.2-beta.12");
expect(cleanup.versions).toEqual([
expect.objectContaining({
reason: "older-than-bound-package",
state: "deprecated",
version: "0.10.2-beta.11",
}),
expect.objectContaining({
reason: "current-bound-package",
state: "retained",
version: "0.10.2-beta.12",
}),
]);
} finally {
await rm(root, { force: true, recursive: true });
}
});
});
async function createVersionedExecutable(packagedVersion: string): Promise<Buffer> {
const executable = NtExecutable.createEmpty(false, false);
const resource = NtExecutableResource.from(executable);
const version = Resource.VersionInfo.createEmpty();
version.lang = 1033;
version.setFileVersion("0.5.0.0", 1033);
version.setProductVersion("0.5.0.0", 1033);
version.setStringValues(
{ codepage: 1200, lang: 1033 },
{
FileDescription: "Open Design",
FileVersion: packagedVersion,
ProductName: "Open Design",
ProductVersion: "0.5.0.0",
},
);
version.outputToResourceEntries(resource.entries);
resource.outputResource(executable);
return Buffer.from(executable.generate());
}