alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
134 lines (124 loc) • 4.96 kB
text/typescript
import { $inject, AlephaError, z } from "alepha";
import { $command } from "alepha/command";
import { $logger } from "alepha/logger";
import { FileSystemProvider, ShellProvider } from "alepha/system";
/**
* Pack the workspace into a deployable `tar.gz`.
*
* The tar contains everything a remote runner (Alepha Rocket, or any
* `alepha platform <op> --prebuilt` consumer) needs to deploy the app:
*
* dist/ pre-built output (incl. manifest.json)
* migrations/ SQL files (if present)
*
* No source, no `alepha.config.ts`, no `package.json` — the deploy
* side reads everything from `dist/manifest.json` and never touches
* source. Excludes: `node_modules`, `.DS_Store`, macOS AppleDouble
* (`._*`), `.alepha` build cache, `e2e`, `playwright-report`,
* `coverage`.
*
* Output name: `<project-name>-<tag>.tar.gz` (default tag
* "latest"). Project name comes from `package.json.name`. Naming
* mirrors Docker tags — same artifact, different tag = different
* file.
*/
export class PackCommand {
protected readonly log = $logger();
protected readonly fs = $inject(FileSystemProvider);
protected readonly shell = $inject(ShellProvider);
public readonly pack = $command({
name: "pack",
description:
"Pack the workspace into a deployable tar.gz (for `alepha platform --prebuilt` consumers like Alepha Rocket).",
flags: z.object({
tag: z
.text({
aliases: ["t"],
description:
"Tag suffix for the artifact name (Docker-style). Defaults to `latest` → `<project>-latest.tar.gz`. Pass a real version like `0.0.2` for a pinned artifact.",
})
.optional(),
output: z
.text({
aliases: ["o"],
description:
"Output directory for the tar.gz (default: current dir).",
})
.optional(),
}),
handler: async ({ flags, root, run }) => {
const pkgPath = this.fs.join(root, "package.json");
let project: string;
try {
const pkg = await this.fs.readJsonFile<{ name?: string }>(pkgPath);
if (!pkg.name) {
throw new AlephaError(
'Missing "name" in package.json — `alepha pack` needs it for the artifact filename.',
);
}
project = pkg.name;
} catch (err) {
if (err instanceof AlephaError) throw err;
throw new AlephaError(
`Could not read package.json at ${pkgPath}. Run \`alepha pack\` from a workspace directory.`,
);
}
const tag = flags.tag ?? "latest";
const outputDir = flags.output ?? root;
const filename = `${project}-${tag}.tar.gz`;
const outputPath = this.fs.join(outputDir, filename);
// Include list: just `dist/` + `migrations/`. Everything else
// (src, alepha.config.ts, tsconfig.json, package.json) is
// dev-time scaffolding — the deploy side reads
// `dist/manifest.json` for project/env/resources and never
// touches source.
const candidates = ["dist", "migrations"];
const includes: string[] = [];
for (const candidate of candidates) {
if (await this.fs.exists(this.fs.join(root, candidate))) {
includes.push(candidate);
}
}
if (!includes.includes("dist")) {
throw new AlephaError(
"dist/ missing — run `alepha build --target=cloudflare` before `alepha pack`.",
);
}
const manifestPath = this.fs.join(root, "dist", "manifest.json");
if (!(await this.fs.exists(manifestPath))) {
throw new AlephaError(
`dist/manifest.json missing — required for prebuilt deploys. Rebuild with the current alepha version (\`alepha build --target=cloudflare\`).`,
);
}
// macOS sets COPYFILE_DISABLE=0 by default; tar will then include
// AppleDouble `._*` files. Force it off here so the tarball is
// portable. Also pass explicit excludes for `node_modules`,
// `.DS_Store`, etc. — they slip in via `dist/`.
const excludes = [
"node_modules",
".DS_Store",
"._*",
".alepha",
"e2e",
"playwright-report",
"test-results",
"coverage",
]
.map((p) => `--exclude='${p}'`)
.join(" ");
const tarCmd = `tar -czf '${outputPath}' ${excludes} ${includes.map((p) => `'${p}'`).join(" ")}`;
// Wrap in `sh -c` so the env-var assignment is interpreted by the
// shell instead of being parsed as the binary name. COPYFILE_DISABLE
// suppresses macOS AppleDouble (`._*`) entries that tar otherwise
// emits when running on HFS+/APFS.
const cmd = `sh -c "COPYFILE_DISABLE=1 ${tarCmd}"`;
await run({
name: `pack → ${filename}`,
handler: async () => {
await this.shell.run(cmd, { root });
},
});
this.log.info(`Packed ${filename} → ${outputPath}`);
},
});
}