Files
OmniRoute/scripts/build/prepublish.ts
diegosouzapw b7fdcdddf8 build(layer1): rename standalone output app/ -> dist/; delete both App-Router move hacks
- scripts/build/prepublish.ts: APP_DIR -> DIST_DIR; remove Step 1 and Step 2.5
  hack blocks; fix MCP esbuild outfile (app/ -> dist/); update all log messages
- scripts/build/build-next-isolated.mjs: remove legacy-app-snapshot entry from
  getTransientBuildPaths() (App-Router collision hack deleted)
- scripts/build/assembleStandalone.mjs: fix standalone package.json after copy —
  removes "type":"module" so Next.js standalone server.js (CJS) loads correctly;
  also adds .build/next/ to allowed staging prefixes so server bundles are kept
- scripts/build/pack-artifact-policy.ts: app/ -> dist/ in all PACK_ARTIFACT_* paths;
  add ".build/next/" to APP_STAGING_ALLOWED_PATH_PREFIXES (Layer 1 distDir change)
- scripts/build/validate-pack-artifact.ts: dist/ check instead of app/
- scripts/build/postinstall.mjs: all app/ paths -> dist/
- scripts/build/postinstallSupport.mjs: hasStandaloneAppBundle checks dist/server.js
- bin/cli/commands/serve.mjs: APP_DIR -> dist/
- package.json: files[] "app/" -> "dist/"
- .gitignore: remove both /app and /app/ entries (no longer needed)

Smoke: NO_APP_DIR_OK; DIST_OK; check:pack-artifact PASS; health 200 from dist/
2026-06-03 14:59:20 -03:00

398 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* OmniRoute — Prepublish Build Script
*
* Consumes the .build/next/standalone artifact produced by `npm run build`
* (build-next-isolated.mjs) and assembles the npm staging `dist/` directory.
* Does NOT run a second `next build` — the caller must run `npm run build` first,
* or this script will invoke it exactly once if the artifact is absent.
*
* Run with: node scripts/build/prepublish.ts
*/
import { execFileSync } from "node:child_process";
import {
existsSync,
mkdirSync,
cpSync,
rmSync,
writeFileSync,
readFileSync,
readdirSync,
statSync,
chmodSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { assembleStandalone } from "./assembleStandalone.mjs";
import {
APP_STAGING_ALLOWED_EXACT_PATHS,
APP_STAGING_ALLOWED_PATH_PREFIXES,
APP_STAGING_REMOVAL_PATHS,
findUnexpectedArtifactPaths,
} from "./pack-artifact-policy.ts";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..", "..");
const NPX_BIN = process.platform === "win32" ? "npx.cmd" : "npx";
const DIST_DIR = join(ROOT, "dist");
function walkFiles(dir: string, rootDir: string = dir, files: string[] = []): string[] {
let entries: string[] = [];
try {
entries = readdirSync(dir);
} catch {
return files;
}
for (const entry of entries) {
const fullPath = join(dir, entry);
let stat;
try {
stat = statSync(fullPath);
} catch {
continue;
}
if (stat.isDirectory()) {
walkFiles(fullPath, rootDir, files);
continue;
}
files.push(
fullPath
.replace(rootDir, "")
.replace(/^[/\\]/, "")
.replace(/\\/g, "/")
);
}
return files;
}
function removeEmptyDirectories(dir: string): boolean {
let entries: string[] = [];
try {
entries = readdirSync(dir);
} catch {
return false;
}
let hasFiles = false;
for (const entry of entries) {
const fullPath = join(dir, entry);
let stat;
try {
stat = statSync(fullPath);
} catch {
continue;
}
if (stat.isDirectory()) {
const childHasFiles = removeEmptyDirectories(fullPath);
if (!childHasFiles) {
rmSync(fullPath, { recursive: true, force: true });
} else {
hasFiles = true;
}
continue;
}
hasFiles = true;
}
return hasFiles;
}
console.log("🔨 OmniRoute — Building for npm publish...\n");
// ── Step 1: Clean previous dist/ directory ─────────────────
if (existsSync(DIST_DIR)) {
console.log(" 🧹 Cleaning previous dist/ directory...");
rmSync(DIST_DIR, { recursive: true, force: true });
}
// ── Step 2: Assert / trigger the Next.js standalone build ──
// prepublish no longer runs its own `next build`. It consumes the
// .build/next/standalone artifact produced by `npm run build` (build-next-isolated.mjs).
// If the artifact is absent we invoke it exactly once.
const NEXT_DIST = process.env.NEXT_DIST_DIR || ".build/next";
const standaloneServerJs = join(ROOT, NEXT_DIST, "standalone", "server.js");
if (!existsSync(standaloneServerJs)) {
console.log(" 🏗️ .build/next/standalone not found — running `npm run build` once...");
execFileSync(process.execPath, ["scripts/build/build-next-isolated.mjs"], {
cwd: ROOT,
stdio: "inherit",
});
if (!existsSync(standaloneServerJs)) {
console.error("\n ❌ Standalone build not found after `npm run build` at:", standaloneServerJs);
console.error(" Make sure next.config.mjs has: output: 'standalone'");
process.exit(1);
}
}
console.log(" ✅ Standalone artifact present:", standaloneServerJs);
// ── Step 37: Assemble standalone into dist/ ───────────────
// All shared copy/sync/sanitize/chunk-patch operations are delegated to
// assembleStandalone. npm-UNIQUE steps (MITM, MCP, CLI, sidecars) follow.
console.log(" 📋 Assembling standalone bundle into dist/...");
assembleStandalone({
distDir: join(ROOT, NEXT_DIST),
outDir: DIST_DIR,
projectRoot: ROOT,
sanitizePaths: true,
patchTurbopackChunks: true,
copyNatives: true,
});
console.log(" ✅ Standalone bundle assembled to dist/");
// ── Step 8: Compile + copy MITM cert utilities ─────────────
const mitmSrc = join(ROOT, "src", "mitm");
const mitmDest = join(DIST_DIR, "src", "mitm");
if (existsSync(mitmSrc)) {
console.log(" 🔨 Compiling MITM utilities (TypeScript → JavaScript)...");
mkdirSync(mitmDest, { recursive: true });
// Write a temporary tsconfig.json targeting the mitm directory
const mitmTsconfig = {
compilerOptions: {
target: "ES2022",
module: "NodeNext",
moduleResolution: "NodeNext",
outDir: mitmDest,
rootDir: mitmSrc,
strict: false,
noImplicitAny: false,
strictNullChecks: false,
noEmitOnError: true,
allowImportingTsExtensions: true,
rewriteRelativeImportExtensions: true,
ignoreDeprecations: "6.0",
resolveJsonModule: true,
esModuleInterop: true,
skipLibCheck: true,
types: ["node"],
baseUrl: ".",
paths: {
"@/*": ["src/*"],
},
},
include: [mitmSrc + "/**/*"],
};
const tmpTsconfigPath = join(ROOT, "tsconfig.mitm.tmp.json");
writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2));
try {
execFileSync(NPX_BIN, ["tsc", "-p", "tsconfig.mitm.tmp.json"], {
cwd: ROOT,
stdio: "inherit",
});
const mitmServerSrc = join(mitmSrc, "server.cjs");
if (existsSync(mitmServerSrc)) {
cpSync(mitmServerSrc, join(mitmDest, "server.cjs"));
}
console.log(" ✅ MITM utilities compiled to dist/src/mitm/");
} catch (err: any) {
console.warn(" ⚠️ MITM compile warning (non-fatal):", err.message);
// Fallback: copy source files so at least they are present
cpSync(mitmSrc, mitmDest, { recursive: true });
} finally {
// Cleanup temp tsconfig
try {
rmSync(tmpTsconfigPath);
} catch {}
}
}
// ── Step 8.5: Bundle MCP server ────────────────────────────
const mcpSrcFile = join(ROOT, "open-sse", "mcp-server", "server.ts");
const mcpDestDir = join(DIST_DIR, "open-sse", "mcp-server");
const mcpDestFile = join(mcpDestDir, "server.js");
if (existsSync(mcpSrcFile)) {
console.log(" 🔨 Bundling MCP Server (TypeScript → JavaScript)...");
mkdirSync(mcpDestDir, { recursive: true });
try {
execFileSync(
NPX_BIN,
[
"esbuild",
"open-sse/mcp-server/server.ts",
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
"--outfile=dist/open-sse/mcp-server/server.js",
],
{ cwd: ROOT, stdio: "inherit" }
);
console.log(" ✅ MCP Server bundled to dist/open-sse/mcp-server/server.js");
} catch (err: any) {
console.warn(" ⚠️ MCP Server bundle error:", err.message);
}
}
// ── Step 8.7: Bundle CLI Entrypoint ──────────────────────────
const cliSrcFile = join(ROOT, "bin", "omniroute.ts");
const cliDestFile = join(ROOT, "bin", "omniroute.mjs");
if (existsSync(cliSrcFile)) {
console.log(" 🔨 Bundling CLI Entrypoint (TypeScript → JavaScript)...");
try {
execFileSync(
NPX_BIN,
[
"esbuild",
"bin/omniroute.ts",
"--bundle",
"--platform=node",
"--packages=external",
"--format=esm",
"--outfile=bin/omniroute.mjs",
],
{ cwd: ROOT, stdio: "inherit" }
);
chmodSync(cliDestFile, 0o755);
console.log(" ✅ CLI Entrypoint bundled to bin/omniroute.mjs");
} catch (err: any) {
console.warn(" ⚠️ CLI bundle error:", err.message);
}
}
// ── Step 9: Copy shared utilities needed at runtime ────────
const sharedApiKey = join(ROOT, "src", "shared", "utils", "apiKey.js");
const sharedApiKeyDest = join(DIST_DIR, "src", "shared", "utils");
if (existsSync(sharedApiKey)) {
console.log(" 📋 Copying shared utilities...");
mkdirSync(sharedApiKeyDest, { recursive: true });
cpSync(sharedApiKey, join(sharedApiKeyDest, "apiKey.js"));
}
// ── Step 9.5: Copy minimal runtime sidecars required outside .next ─────────
const envExampleSrc = join(ROOT, ".env.example");
if (existsSync(envExampleSrc)) {
cpSync(envExampleSrc, join(DIST_DIR, ".env.example"));
}
const openapiSpecSrc = join(ROOT, "docs", "openapi.yaml");
if (existsSync(openapiSpecSrc)) {
const docsDest = join(DIST_DIR, "docs");
mkdirSync(docsDest, { recursive: true });
cpSync(openapiSpecSrc, join(docsDest, "openapi.yaml"));
}
const docsMarkdownSrc = join(ROOT, "docs");
if (existsSync(docsMarkdownSrc)) {
const docsDest = join(DIST_DIR, "docs");
mkdirSync(docsDest, { recursive: true });
const mdFiles = readdirSync(docsMarkdownSrc).filter(
(f) => f.endsWith(".md") || f.endsWith(".mdx")
);
for (const mdFile of mdFiles) {
cpSync(join(docsMarkdownSrc, mdFile), join(docsDest, mdFile));
}
if (mdFiles.length > 0) {
console.log(`[prepublish] Copied ${mdFiles.length} docs markdown files to app/docs/`);
}
}
const syncEnvSrc = join(ROOT, "scripts", "sync-env.mjs");
if (existsSync(syncEnvSrc)) {
const scriptsDest = join(DIST_DIR, "scripts");
mkdirSync(scriptsDest, { recursive: true });
cpSync(syncEnvSrc, join(scriptsDest, "sync-env.mjs"));
}
const migrationsSrc = join(ROOT, "src", "lib", "db", "migrations");
if (existsSync(migrationsSrc)) {
const migrationsDest = join(DIST_DIR, "src", "lib", "db", "migrations");
mkdirSync(join(DIST_DIR, "src", "lib", "db"), { recursive: true });
cpSync(migrationsSrc, migrationsDest, { recursive: true, force: true });
}
const runtimeAssetDirs = [
{
source: join(ROOT, "open-sse", "services", "compression", "engines", "rtk", "filters"),
destination: join(DIST_DIR, "open-sse", "services", "compression", "engines", "rtk", "filters"),
},
{
source: join(ROOT, "open-sse", "services", "compression", "rules"),
destination: join(DIST_DIR, "open-sse", "services", "compression", "rules"),
},
];
for (const assetDir of runtimeAssetDirs) {
if (existsSync(assetDir.source)) {
mkdirSync(dirname(assetDir.destination), { recursive: true });
cpSync(assetDir.source, assetDir.destination, { recursive: true, force: true });
}
}
// ── Step 10: Ensure data/ directory exists ──────────────────
mkdirSync(join(DIST_DIR, "data"), { recursive: true });
// ── Step 10.5: Copy @swc/helpers into standalone ───────────
// Next.js standalone tracer sometimes omits @swc/helpers from dist/node_modules/,
// causing MODULE_NOT_FOUND at runtime. Always copy it explicitly.
const swcHelpersSrc = join(ROOT, "node_modules", "@swc", "helpers");
const swcHelpersDst = join(DIST_DIR, "node_modules", "@swc", "helpers");
if (existsSync(swcHelpersSrc) && !existsSync(swcHelpersDst)) {
console.log(" 📋 Copying @swc/helpers to standalone dist/node_modules...");
mkdirSync(join(DIST_DIR, "node_modules", "@swc"), { recursive: true });
cpSync(swcHelpersSrc, swcHelpersDst, { recursive: true });
console.log(" ✅ @swc/helpers included in standalone build.");
}
// ── Step 10.6: Remove development-only residue from staged dist/ ────────────
for (const relativePath of APP_STAGING_REMOVAL_PATHS) {
const targetPath = join(DIST_DIR, relativePath);
if (existsSync(targetPath)) {
console.log(` 🧹 Removing dist/${relativePath} (not needed in npm package)...`);
rmSync(targetPath, { recursive: true, force: true });
console.log(` ✅ dist/${relativePath} removed.`);
}
}
// ── Step 10.7: Prune any staged dist/ file outside the allowed runtime set ──
const stagedFiles = walkFiles(DIST_DIR);
const unexpectedStagedFiles = findUnexpectedArtifactPaths(stagedFiles, {
exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS,
prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES,
});
if (unexpectedStagedFiles.length > 0) {
console.log(" 🧹 Pruning unexpected files from staged dist/...");
unexpectedStagedFiles.forEach((unexpectedPath: string) => {
rmSync(join(DIST_DIR, unexpectedPath), { force: true });
console.log(` ✅ Removed dist/${unexpectedPath}`);
});
removeEmptyDirectories(DIST_DIR);
}
const remainingUnexpectedFiles = findUnexpectedArtifactPaths(walkFiles(DIST_DIR), {
exactPaths: APP_STAGING_ALLOWED_EXACT_PATHS,
prefixPaths: APP_STAGING_ALLOWED_PATH_PREFIXES,
});
if (remainingUnexpectedFiles.length > 0) {
console.error("\n ❌ Staged dist/ still contains unexpected publish artifacts:");
remainingUnexpectedFiles.forEach((violation: string) => console.error(` - dist/${violation}`));
process.exit(1);
}
// ── Done ───────────────────────────────────────────────────
const distPkg = join(DIST_DIR, "package.json");
if (existsSync(distPkg)) {
JSON.parse(readFileSync(distPkg, "utf8"));
console.log(`\n ✅ Build complete!`);
console.log(` Dist directory: dist/`);
console.log(` Server entry: dist/server.js`);
} else {
console.log(`\n ✅ Build complete! (dist/ ready for publish)`);
}
console.log("");