b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
124 lines
4.4 KiB
TypeScript
124 lines
4.4 KiB
TypeScript
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||
import { tmpdir } from "node:os";
|
||
import { dirname, join } from "node:path";
|
||
|
||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||
|
||
import { createTarball, MAX_FILE_SIZE } from "../src/bundle/utils.js";
|
||
import { extractManifestFromTarballForTest } from "../src/commands/publish.js";
|
||
|
||
/**
|
||
* Round-trip the tarball-extraction step from the publish CLI: build a real
|
||
* gzipped tarball on disk, read its bytes, and feed them through the same
|
||
* function the publish flow calls. This exercises the bundle-size-cap wiring
|
||
* end-to-end (decompression + per-file accounting + total accounting) which
|
||
* the pure `validateBundleSize` unit tests cannot.
|
||
*/
|
||
describe("extractManifestFromTarball (publish CLI)", () => {
|
||
let stagingDir: string;
|
||
let outDir: string;
|
||
|
||
beforeEach(async () => {
|
||
stagingDir = await mkdtemp(join(tmpdir(), "emdash-tar-staging-"));
|
||
outDir = await mkdtemp(join(tmpdir(), "emdash-tar-out-"));
|
||
});
|
||
|
||
afterEach(async () => {
|
||
await rm(stagingDir, { recursive: true, force: true });
|
||
await rm(outDir, { recursive: true, force: true });
|
||
});
|
||
|
||
async function buildTarball(files: Record<string, string | Uint8Array>): Promise<Uint8Array> {
|
||
for (const [name, body] of Object.entries(files)) {
|
||
const fullPath = join(stagingDir, name);
|
||
await mkdir(dirname(fullPath), { recursive: true });
|
||
await writeFile(fullPath, body);
|
||
}
|
||
const tarballPath = join(outDir, "test.tar.gz");
|
||
await createTarball(stagingDir, tarballPath);
|
||
return new Uint8Array(await readFile(tarballPath));
|
||
}
|
||
|
||
const minimalManifest = JSON.stringify({
|
||
id: "test-plugin",
|
||
version: "1.0.0",
|
||
capabilities: [],
|
||
allowedHosts: [],
|
||
storage: {},
|
||
hooks: [],
|
||
routes: [],
|
||
admin: {},
|
||
});
|
||
|
||
it("returns the manifest for a tarball within all caps", async () => {
|
||
const bytes = await buildTarball({
|
||
"manifest.json": minimalManifest,
|
||
"backend.js": "export default {};\n",
|
||
});
|
||
const manifest = await extractManifestFromTarballForTest(bytes);
|
||
expect(manifest.id).toBe("test-plugin");
|
||
expect(manifest.version).toBe("1.0.0");
|
||
});
|
||
|
||
it("rejects a tarball with a single file over the per-file cap", async () => {
|
||
const bytes = await buildTarball({
|
||
"manifest.json": minimalManifest,
|
||
"backend.js": "x".repeat(MAX_FILE_SIZE + 1),
|
||
});
|
||
await expect(extractManifestFromTarballForTest(bytes)).rejects.toThrow(
|
||
/violates bundle size caps[\s\S]*backend\.js/,
|
||
);
|
||
});
|
||
|
||
it("rejects a tarball whose total decompressed size exceeds the bundle cap", async () => {
|
||
// Three files at exactly MAX_FILE_SIZE — each within per-file cap, but
|
||
// total (3 × 128 KB = 384 KB) exceeds the 256 KB total cap.
|
||
const bytes = await buildTarball({
|
||
"manifest.json": minimalManifest,
|
||
"a.js": "a".repeat(MAX_FILE_SIZE),
|
||
"b.js": "b".repeat(MAX_FILE_SIZE),
|
||
"c.js": "c".repeat(MAX_FILE_SIZE),
|
||
});
|
||
await expect(extractManifestFromTarballForTest(bytes)).rejects.toThrow(
|
||
/violates bundle size caps[\s\S]*Bundle size/,
|
||
);
|
||
});
|
||
|
||
it("rejects a tarball with too many files even when each is tiny", async () => {
|
||
const files: Record<string, string> = { "manifest.json": minimalManifest };
|
||
for (let i = 0; i < 25; i++) files[`f-${String(i).padStart(2, "0")}.js`] = "x";
|
||
const bytes = await buildTarball(files);
|
||
await expect(extractManifestFromTarballForTest(bytes)).rejects.toThrow(
|
||
/violates bundle size caps[\s\S]*contains \d+ files/,
|
||
);
|
||
});
|
||
|
||
it("does not count non-file tar entries (symlinks etc.) toward the file cap", async () => {
|
||
// Hand-build a tarball with 25 symlink entries plus one real file.
|
||
// Symlinks have type "2" and size 0 in USTAR. The file cap is 20, so
|
||
// counting symlinks would reject this; the filter should only see one
|
||
// real file (manifest.json) and accept it.
|
||
const { packTar } = await import("modern-tar");
|
||
const { gzipSync } = await import("node:zlib");
|
||
const symlinkEntries = Array.from({ length: 25 }, (_, i) => ({
|
||
header: {
|
||
name: `link-${i}`,
|
||
size: 0,
|
||
type: "symlink" as const,
|
||
linkname: "manifest.json",
|
||
},
|
||
body: new Uint8Array(0),
|
||
}));
|
||
const tarBytes = await packTar([
|
||
{
|
||
header: { name: "manifest.json", size: minimalManifest.length, type: "file" as const },
|
||
body: new TextEncoder().encode(minimalManifest),
|
||
},
|
||
...symlinkEntries,
|
||
]);
|
||
const gzipped = gzipSync(tarBytes);
|
||
const manifest = await extractManifestFromTarballForTest(new Uint8Array(gzipped));
|
||
expect(manifest.id).toBe("test-plugin");
|
||
});
|
||
});
|