Archived
fix: harden cross-platform release builds
Skip direct POSIX entrypoint execution on Windows CI and prevent test-support modules from entering clean build or npm package output.
This commit is contained in:
+3
-2
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
"!dist/**/*.testSupport.*",
|
||||||
"install.sh",
|
"install.sh",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
"dev:server": "npm run dev:web",
|
"dev:server": "npm run dev:web",
|
||||||
"dev:client": "vite --host 0.0.0.0",
|
"dev:client": "vite --host 0.0.0.0",
|
||||||
"dev:plugins": "node scripts/build-plugins.mjs --watch",
|
"dev:plugins": "node scripts/build-plugins.mjs --watch",
|
||||||
"build": "tsc -p tsconfig.build.json && npm run build:plugin-api && npm run build:plugins && vite build",
|
"build": "npm run clean && tsc -p tsconfig.build.json && npm run build:plugin-api && npm run build:plugins && vite build",
|
||||||
"build:plugin-api": "tsc -p tsconfig.plugin-api.json",
|
"build:plugin-api": "tsc -p tsconfig.plugin-api.json",
|
||||||
"build:plugins": "tsc -p tsconfig.plugins.json && node scripts/build-plugins.mjs",
|
"build:plugins": "tsc -p tsconfig.plugins.json && node scripts/build-plugins.mjs",
|
||||||
"capture:screenshots": "node scripts/capture-screenshots.mjs",
|
"capture:screenshots": "node scripts/capture-screenshots.mjs",
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
"verify": "npm run typecheck && npm run lint && npm run knip && npm test",
|
"verify": "npm run typecheck && npm run lint && npm run knip && npm test",
|
||||||
"start": "tsx src/server/index.ts",
|
"start": "tsx src/server/index.ts",
|
||||||
"start:sessiond": "tsx src/server/sessiond.ts",
|
"start:sessiond": "tsx src/server/sessiond.ts",
|
||||||
"clean": "rm -rf dist",
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||||
"prepack": "npm run build",
|
"prepack": "npm run build",
|
||||||
"pack:dry": "npm pack --dry-run",
|
"pack:dry": "npm pack --dry-run",
|
||||||
"prepublishOnly": "npm run verify",
|
"prepublishOnly": "npm run verify",
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import ts from "typescript";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
|
||||||
|
describe("production build contents", () => {
|
||||||
|
it("keeps test-support modules out of the TypeScript build graph", () => {
|
||||||
|
const buildConfig = readBuildConfig();
|
||||||
|
const program = ts.createProgram({ rootNames: buildConfig.fileNames, options: buildConfig.options });
|
||||||
|
const projectSources = program.getSourceFiles()
|
||||||
|
.map((sourceFile) => normalizePath(relative(repoRoot, sourceFile.fileName)))
|
||||||
|
.filter((path) => path.startsWith("src/"));
|
||||||
|
|
||||||
|
expect(projectSources).toContain("src/server/app.ts");
|
||||||
|
expect(projectSources.filter(isTestSupportPath)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps test-support artifacts out of the npm tarball", async () => {
|
||||||
|
const fixtureRoot = await mkdtemp(join(tmpdir(), "pi-web-package-contents-"));
|
||||||
|
try {
|
||||||
|
const fixtureDist = join(fixtureRoot, "dist", "server");
|
||||||
|
await mkdir(fixtureDist, { recursive: true });
|
||||||
|
await Promise.all([
|
||||||
|
copyFile(join(repoRoot, "package.json"), join(fixtureRoot, "package.json")),
|
||||||
|
writeFile(join(fixtureDist, "app.js"), "export {};\n", "utf8"),
|
||||||
|
writeFile(join(fixtureDist, "app.testSupport.js"), "export {};\n", "utf8"),
|
||||||
|
writeFile(join(fixtureDist, "app.testSupport.js.map"), "{}\n", "utf8"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const npmExecPath = process.env["npm_execpath"];
|
||||||
|
if (npmExecPath === undefined || npmExecPath.length === 0) {
|
||||||
|
throw new Error("npm_execpath is required to verify npm package contents");
|
||||||
|
}
|
||||||
|
const stdout = await execUtf8(process.execPath, [npmExecPath, "pack", "--dry-run", "--json", "--ignore-scripts"], fixtureRoot);
|
||||||
|
const packagedFiles = packageFilePaths(stdout);
|
||||||
|
|
||||||
|
expect(packagedFiles).toContain("dist/server/app.js");
|
||||||
|
expect(packagedFiles.filter(isTestSupportPath)).toEqual([]);
|
||||||
|
} finally {
|
||||||
|
await rm(fixtureRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function readBuildConfig(): ts.ParsedCommandLine {
|
||||||
|
const configPath = join(repoRoot, "tsconfig.build.json");
|
||||||
|
const config = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
|
||||||
|
...ts.sys,
|
||||||
|
onUnRecoverableConfigFileDiagnostic(diagnostic) {
|
||||||
|
throw new Error(formatDiagnostics([diagnostic]));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (config === undefined) throw new Error(`Unable to parse ${configPath}`);
|
||||||
|
if (config.errors.length > 0) throw new Error(formatDiagnostics(config.errors));
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiagnostics(diagnostics: readonly ts.Diagnostic[]): string {
|
||||||
|
return ts.formatDiagnostics(diagnostics, {
|
||||||
|
getCanonicalFileName: (fileName) => fileName,
|
||||||
|
getCurrentDirectory: () => repoRoot,
|
||||||
|
getNewLine: () => "\n",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
return path.split(sep).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTestSupportPath(path: string): boolean {
|
||||||
|
return path.includes(".testSupport.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function execUtf8(file: string, args: string[], cwd: string): Promise<string> {
|
||||||
|
return new Promise((resolvePromise, reject) => {
|
||||||
|
execFile(file, args, { cwd, encoding: "utf8" }, (error, stdout) => {
|
||||||
|
if (error !== null) {
|
||||||
|
reject(error instanceof Error ? error : new Error("Command failed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolvePromise(stdout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageFilePaths(output: string): string[] {
|
||||||
|
const parsed: unknown = JSON.parse(output);
|
||||||
|
if (!Array.isArray(parsed) || parsed.length !== 1) throw new Error("npm pack returned an unexpected result");
|
||||||
|
|
||||||
|
const packResult: unknown = parsed[0];
|
||||||
|
if (!isRecord(packResult)) throw new Error("npm pack result was not an object");
|
||||||
|
const filesValue = packResult["files"];
|
||||||
|
if (!Array.isArray(filesValue)) throw new Error("npm pack result did not include files");
|
||||||
|
const files: unknown[] = filesValue;
|
||||||
|
|
||||||
|
return files.map((file) => {
|
||||||
|
if (!isRecord(file) || typeof file["path"] !== "string") {
|
||||||
|
throw new Error("npm pack returned an invalid file entry");
|
||||||
|
}
|
||||||
|
return file["path"];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -10,7 +10,10 @@ const execFile = promisify(execFileCallback);
|
|||||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||||
|
|
||||||
describe("pi-web-docker entrypoint", () => {
|
describe("pi-web-docker entrypoint", () => {
|
||||||
it("streams detached helper logs inline after scheduling runtime updates", async () => {
|
// The entrypoint intentionally supports POSIX hosts, so Windows CI cannot execute it directly.
|
||||||
|
const posixHostIt = it.skipIf(process.platform === "win32");
|
||||||
|
|
||||||
|
posixHostIt("streams detached helper logs inline after scheduling runtime updates", async () => {
|
||||||
const tempDir = await mkdtemp(join(tmpdir(), "pi-web-docker-entrypoint-"));
|
const tempDir = await mkdtemp(join(tmpdir(), "pi-web-docker-entrypoint-"));
|
||||||
try {
|
try {
|
||||||
const runtimeRoot = join(tempDir, "runtime");
|
const runtimeRoot = join(tempDir, "runtime");
|
||||||
|
|||||||
+1
-1
@@ -8,5 +8,5 @@
|
|||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["src/cli.ts", "src/server/**/*.ts"],
|
"include": ["src/cli.ts", "src/server/**/*.ts"],
|
||||||
"exclude": ["src/**/*.test.ts"]
|
"exclude": ["src/**/*.test.ts", "src/**/*.testSupport.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user