mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
test(lambda): behavioral guard for handler CJS banner shims (#1977)
Miguel's fix (f999b40d) added the require/__filename/__dirname shims to the
Lambda handler bundle after #1932 crashed every render at import with
"__dirname is not defined in ES module scope" (wawoff2's emscripten build
reads __dirname at module scope; it's inlined via producer -> fontCompression).
The accompanying test only grepped build-zip.ts for the banner literals, so it
passes even if the shim is renamed, reordered into a broken form, or if a new
inlined CJS dep needs a global the banner doesn't provide.
Replace it with a behavioral test: extract the banner to _handlerBanner.ts
(build-zip.ts self-executes on import, so it can't be imported directly),
bundle a fixture that touches __dirname/__filename/require with the real
banner, and import the output under real Node -- not the bun test runtime,
which defines __dirname in ESM and would mask a missing shim. The import
faithfully reproduces Lambda's Node ESM environment and fails with the exact
#1932 error when any shim is dropped.
Handler bundle output is unchanged (identical banner string).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
94996d8da9
commit
2b1109db4c
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* CJS-interop banner prepended to the ESM handler bundle.
|
||||
*
|
||||
* Lambda's Node 22 runtime treats `.mjs` as ESM, which has no `require`,
|
||||
* `__filename`, or `__dirname`. The handler bundle inlines CJS deps that
|
||||
* assume all three exist at module scope:
|
||||
* - postcss et al. call top-level `require(...)`
|
||||
* - wawoff2's emscripten build (pulled in unconditionally via
|
||||
* producer → fontCompression) reads `__dirname` at module scope
|
||||
* Without the shims the handler throws "Dynamic require of <X> is not
|
||||
* supported" / "__dirname is not defined in ES module scope" at import time,
|
||||
* before it can run — which is exactly how a freshly deployed stack crashed
|
||||
* on every render (#1932).
|
||||
*
|
||||
* This mirrors the producer's own CJS banner (packages/producer/build.mjs);
|
||||
* the handler bundle inlines producer source, so it needs the same shim.
|
||||
*
|
||||
* Kept in its own module (not inline in build-zip.ts, which self-executes on
|
||||
* import) so build-zip.test.ts can import the exact banner, bundle a fixture
|
||||
* with it, and assert the globals actually resolve.
|
||||
*/
|
||||
export const HANDLER_BANNER = [
|
||||
"// hyperframes-aws-lambda handler bundle",
|
||||
'import { createRequire as __hf_createRequire } from "module";',
|
||||
'import { fileURLToPath as __hf_fileURLToPath } from "url";',
|
||||
'import { dirname as __hf_dirname } from "path";',
|
||||
"const require = __hf_createRequire(import.meta.url);",
|
||||
"const __filename = __hf_fileURLToPath(import.meta.url);",
|
||||
"const __dirname = __hf_dirname(__filename);",
|
||||
].join("\n");
|
||||
@@ -1,14 +1,77 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import * as esbuild from "esbuild";
|
||||
import { HANDLER_BANNER } from "./_handlerBanner.js";
|
||||
|
||||
// The handler ships as ESM (.mjs) but inlines CJS deps that assume Node's CJS
|
||||
// globals exist at module scope: postcss et al. call top-level `require(...)`,
|
||||
// and wawoff2's emscripten build reads `__dirname`. A freshly deployed stack
|
||||
// crashed on every render (#1932) with "__dirname is not defined in ES module
|
||||
// scope"; the fix is the require/__filename/__dirname shim in HANDLER_BANNER.
|
||||
//
|
||||
// This bundles a fixture touching all three globals with the REAL banner and
|
||||
// imports the output, so it catches a dropped/renamed shim behaviourally
|
||||
// rather than by grepping for literals (which survives a broken refactor).
|
||||
//
|
||||
// The import MUST run under Node, not the `bun test` runtime: Bun defines
|
||||
// `__dirname`/`__filename` even in ESM, which would mask a missing shim and
|
||||
// green-light a broken bundle. Lambda runs Node, so we spawn `node` (guaranteed
|
||||
// present in CI alongside bun) to reproduce the deploy target faithfully.
|
||||
describe("build-zip handler banner", () => {
|
||||
it("defines CommonJS path globals for inlined CJS dependencies", () => {
|
||||
const source = readFileSync(fileURLToPath(new URL("./build-zip.ts", import.meta.url)), "utf8");
|
||||
it("shims require/__filename/__dirname so inlined CJS deps import under Node", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "hf-banner-test-"));
|
||||
try {
|
||||
const entry = join(dir, "fixture.ts");
|
||||
const outfile = join(dir, "out.mjs");
|
||||
// Reference each CJS global at module top level, the way inlined deps do.
|
||||
// If any shim is missing, importing `out.mjs` throws at eval time.
|
||||
writeFileSync(
|
||||
entry,
|
||||
[
|
||||
"const cjsDir = __dirname;",
|
||||
"const cjsFile = __filename;",
|
||||
"const path = require('node:path');",
|
||||
"if (typeof cjsDir !== 'string') throw new Error('__dirname missing');",
|
||||
"if (typeof cjsFile !== 'string') throw new Error('__filename missing');",
|
||||
"if (typeof path.join !== 'function') throw new Error('require missing');",
|
||||
"console.log('BANNER_OK');",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
expect(source).toContain('import { fileURLToPath as __hf_fileURLToPath } from "url";');
|
||||
expect(source).toContain('import { dirname as __hf_dirname } from "path";');
|
||||
expect(source).toContain("const __filename = __hf_fileURLToPath(import.meta.url);");
|
||||
expect(source).toContain("const __dirname = __hf_dirname(__filename);");
|
||||
esbuild.buildSync({
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
format: "esm",
|
||||
entryPoints: [entry],
|
||||
outfile,
|
||||
banner: { js: HANDLER_BANNER },
|
||||
});
|
||||
|
||||
// Import under real Node — Lambda's runtime — not the bun test runtime.
|
||||
const res = spawnSync(
|
||||
"node",
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
`await import(${JSON.stringify(pathToFileURL(outfile).href)});`,
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
|
||||
// A missing shim surfaces as a non-zero exit + ReferenceError on stderr.
|
||||
// Guard against a silent skip if `node` isn't on PATH (it is in CI).
|
||||
expect(res.error).toBeUndefined();
|
||||
expect(res.stderr).not.toContain("is not defined in ES module scope");
|
||||
expect(res.stderr).not.toContain("Dynamic require");
|
||||
expect(res.status).toBe(0);
|
||||
expect(res.stdout).toContain("BANNER_OK");
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as esbuild from "esbuild";
|
||||
import { formatBytes } from "./_formatBytes.js";
|
||||
import { HANDLER_BANNER } from "./_handlerBanner.js";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = resolve(scriptDir, "..");
|
||||
@@ -239,24 +240,10 @@ async function bundleHandler(stagingDir: string): Promise<void> {
|
||||
sourcemap: false,
|
||||
entryPoints: [entry],
|
||||
outfile,
|
||||
// Lambda's Node 22 runtime treats `.mjs` as ESM. Inject a real `require`
|
||||
// via `createRequire` so esbuild's `__require` shim resolves to it
|
||||
// instead of throwing "Dynamic require of <X> is not supported" on
|
||||
// CommonJS modules in the dependency graph (postcss, etc. that ship
|
||||
// top-level `require('path')` calls). The shim does
|
||||
// `typeof require !== "undefined" ? require : <throwing-proxy>`, so
|
||||
// making `require` a real value in module scope flips it onto the
|
||||
// happy path.
|
||||
// See HANDLER_BANNER (_handlerBanner.ts) for why the ESM bundle needs the
|
||||
// CJS require/__filename/__dirname shims.
|
||||
banner: {
|
||||
js: [
|
||||
"// hyperframes-aws-lambda handler bundle",
|
||||
'import { createRequire as __hf_createRequire } from "module";',
|
||||
"const require = __hf_createRequire(import.meta.url);",
|
||||
'import { fileURLToPath as __hf_fileURLToPath } from "url";',
|
||||
'import { dirname as __hf_dirname } from "path";',
|
||||
"const __filename = __hf_fileURLToPath(import.meta.url);",
|
||||
"const __dirname = __hf_dirname(__filename);",
|
||||
].join("\n"),
|
||||
js: HANDLER_BANNER,
|
||||
},
|
||||
});
|
||||
console.log(`[build-zip] bundled handler → ${outfile}`);
|
||||
|
||||
Reference in New Issue
Block a user