- API: `pnpm build` bundles dist/ into apps/api/.output (esbuild), with Linux musl native packages for sharp and argon2 kept alongside - docker/api.Dockerfile copies that output; no pnpm install on the server - add dist/migrate.js (prisma migrate deploy equivalent, same _prisma_migrations table and lock) so the Prisma CLI is not needed at runtime - pnpm-workspace: supportedArchitectures for linux/x64/musl - admin: apps/admin/.env.production sets VITE_SITE_URL for the production build - drop NPM_REGISTRY / SITE_URL build args from compose and .env.example - gitignore: anchor node_modules rules so pre-built node_modules are committed - README: describe the pre-built deploy flow Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
113 lines
4.2 KiB
JavaScript
113 lines
4.2 KiB
JavaScript
// Bundles the compiled API (dist/, from `nest build`) into a self-contained .output/ that
|
|
// docker/api.Dockerfile copies as-is, so building the server image installs nothing:
|
|
// .output/dist/main.js, migrate.js, seed/seed.js all JS dependencies inlined
|
|
// .output/node_modules/ native packages, Linux (musl, x64) builds only
|
|
// The tsc output is bundled rather than src/ because esbuild does not emit the decorator
|
|
// metadata Nest's dependency injection relies on.
|
|
import { build } from "esbuild";
|
|
import { cpSync, existsSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const out = path.join(root, ".output");
|
|
|
|
// the docker/api.Dockerfile base image (node:*-alpine)
|
|
const TARGET = { os: "linux", cpu: "x64", libc: "musl" };
|
|
// loaded at runtime from .output/node_modules (platform-specific binaries)
|
|
const NATIVE = ["sharp", "@node-rs/argon2"];
|
|
// optional packages Nest and pg require inside try/catch; not installed, so left unresolved
|
|
const OPTIONAL = [
|
|
"@nestjs/microservices",
|
|
"@nestjs/microservices/*",
|
|
"@nestjs/websockets",
|
|
"@nestjs/websockets/*",
|
|
"class-transformer",
|
|
"class-transformer/*",
|
|
"class-validator",
|
|
"pg-native",
|
|
];
|
|
|
|
rmSync(out, { recursive: true, force: true });
|
|
|
|
await build({
|
|
absWorkingDir: root,
|
|
entryPoints: {
|
|
main: "dist/main.js",
|
|
migrate: "dist/migrate.js",
|
|
"seed/seed": "dist/seed/seed.js",
|
|
},
|
|
outdir: path.join(out, "dist"),
|
|
bundle: true,
|
|
splitting: true,
|
|
chunkNames: "chunks/[name]-[hash]",
|
|
platform: "node",
|
|
format: "esm",
|
|
target: "node24",
|
|
external: [...NATIVE, ...OPTIONAL],
|
|
// Nest uses class names (logs, throttler keys)
|
|
keepNames: true,
|
|
// CommonJS dependencies call require() for Node built-ins (aliased: bundled code imports createRequire too)
|
|
banner: {
|
|
js: 'import { createRequire as __bundleCreateRequire } from "node:module"; const require = __bundleCreateRequire(import.meta.url);',
|
|
},
|
|
legalComments: "none",
|
|
logLevel: "warning",
|
|
});
|
|
writeFileSync(
|
|
path.join(out, "package.json"),
|
|
JSON.stringify({ private: true, type: "module" }, null, 2) + "\n",
|
|
);
|
|
|
|
/** Node's lookup of `name` from `fromDir`; follows pnpm's symlinks to the real package folder */
|
|
function findPackage(name, fromDir) {
|
|
for (let dir = fromDir; ; dir = path.dirname(dir)) {
|
|
const candidate = path.join(dir, "node_modules", name);
|
|
if (existsSync(path.join(candidate, "package.json"))) return realpathSync(candidate);
|
|
if (path.dirname(dir) === dir) return null;
|
|
}
|
|
}
|
|
|
|
/** package.json os/cpu/libc fields, e.g. ["linux"] or ["!win32"] */
|
|
const allows = (list, value) =>
|
|
!list ||
|
|
((list.includes(value) || list.every((v) => v.startsWith("!"))) && !list.includes(`!${value}`));
|
|
|
|
const copied = new Map();
|
|
function copyPackage(name, fromDir) {
|
|
const dir = findPackage(name, fromDir);
|
|
if (!dir) return; // optional dependency for another platform, not installed
|
|
const pkg = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
if (!allows(pkg.os, TARGET.os) || !allows(pkg.cpu, TARGET.cpu) || !allows(pkg.libc, TARGET.libc))
|
|
return;
|
|
if (copied.has(name)) {
|
|
if (copied.get(name) !== pkg.version)
|
|
throw new Error(`${name}: needs both ${copied.get(name)} and ${pkg.version}`);
|
|
return;
|
|
}
|
|
copied.set(name, pkg.version);
|
|
cpSync(dir, path.join(out, "node_modules", name), { recursive: true, dereference: true });
|
|
for (const dep of Object.keys({ ...pkg.dependencies, ...pkg.optionalDependencies }))
|
|
copyPackage(dep, dir);
|
|
}
|
|
|
|
for (const name of NATIVE) copyPackage(name, root);
|
|
for (const name of NATIVE) {
|
|
if (!copied.has(name)) throw new Error(`${name} is not installed`);
|
|
}
|
|
// sharp and argon2 load their binary from a per-platform package; fail here, not in production
|
|
for (const name of [
|
|
"@img/sharp-linuxmusl-x64",
|
|
"@img/sharp-libvips-linuxmusl-x64",
|
|
"@node-rs/argon2-linux-x64-musl",
|
|
]) {
|
|
if (!copied.has(name)) {
|
|
throw new Error(
|
|
`${name} is missing; run \`pnpm install\` (pnpm-workspace.yaml supportedArchitectures adds it)`,
|
|
);
|
|
}
|
|
}
|
|
console.log(
|
|
`bundled into .output (native: ${[...copied].map(([n, v]) => `${n}@${v}`).join(", ")})`,
|
|
);
|