@hyperframes/aws-lambda 0.6.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -0
- package/dist/cdk/HyperframesRenderStack.d.ts +65 -0
- package/dist/cdk/HyperframesRenderStack.d.ts.map +1 -0
- package/dist/cdk/index.d.ts +10 -0
- package/dist/cdk/index.d.ts.map +1 -0
- package/dist/cdk/index.js +263 -0
- package/dist/cdk/index.js.map +7 -0
- package/dist/chromium.d.ts +77 -0
- package/dist/chromium.d.ts.map +1 -0
- package/dist/events.d.ts +120 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/formatExtension.d.ts +10 -0
- package/dist/formatExtension.d.ts.map +1 -0
- package/dist/handler.d.ts +42 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +432 -0
- package/dist/handler.js.map +7 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +951 -0
- package/dist/index.js.map +7 -0
- package/dist/s3Transport.d.ts +55 -0
- package/dist/s3Transport.d.ts.map +1 -0
- package/dist/sdk/costAccounting.d.ts +51 -0
- package/dist/sdk/costAccounting.d.ts.map +1 -0
- package/dist/sdk/deploySite.d.ts +55 -0
- package/dist/sdk/deploySite.d.ts.map +1 -0
- package/dist/sdk/getRenderProgress.d.ts +72 -0
- package/dist/sdk/getRenderProgress.d.ts.map +1 -0
- package/dist/sdk/index.d.ts +16 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +578 -0
- package/dist/sdk/index.js.map +7 -0
- package/dist/sdk/renderToLambda.d.ts +66 -0
- package/dist/sdk/renderToLambda.d.ts.map +1 -0
- package/dist/sdk/validateConfig.d.ts +35 -0
- package/dist/sdk/validateConfig.d.ts.map +1 -0
- package/package.json +84 -0
- package/scripts/_formatBytes.ts +15 -0
- package/scripts/build-zip.ts +480 -0
- package/scripts/probe-beginframe.dockerfile +61 -0
- package/scripts/probe-beginframe.ts +157 -0
- package/scripts/verify-zip-size.ts +83 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* BeginFrame regression guard for `@sparticuz/chromium`.
|
|
4
|
+
*
|
|
5
|
+
* The load-bearing assumption of `@hyperframes/aws-lambda` is that the
|
|
6
|
+
* Chromium build shipped by `@sparticuz/chromium` honours CDP
|
|
7
|
+
* `HeadlessExperimental.beginFrame` with `screenshot: true`. This script
|
|
8
|
+
* boots that Chromium build (decompressing into `/tmp` per the library's
|
|
9
|
+
* runtime contract), navigates to a tiny static page, issues one
|
|
10
|
+
* `beginFrame` with a screenshot request, and asserts the response
|
|
11
|
+
* carries a PNG buffer.
|
|
12
|
+
*
|
|
13
|
+
* The script is the contract test, not a one-shot verification — every
|
|
14
|
+
* release should run it inside the Docker container at
|
|
15
|
+
* `scripts/probe-beginframe.dockerfile` to catch any future
|
|
16
|
+
* `@sparticuz/chromium` rebuild that drops `HeadlessExperimental` support.
|
|
17
|
+
*
|
|
18
|
+
* Exits 0 on pass, 1 on fail. Run via:
|
|
19
|
+
*
|
|
20
|
+
* bun run --cwd packages/aws-lambda probe:beginframe # host
|
|
21
|
+
* bun run --cwd packages/aws-lambda probe:beginframe:docker # Lambda-like
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { mkdtempSync, promises as fs } from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
|
|
28
|
+
interface ProbeResult {
|
|
29
|
+
passed: boolean;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
chromiumPath: string;
|
|
32
|
+
screenshotBytes: number;
|
|
33
|
+
hasDamage: boolean;
|
|
34
|
+
detail: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const PROBE_HTML = `<!doctype html>
|
|
38
|
+
<html><head><meta charset="utf-8"><title>hf-beginframe-probe</title>
|
|
39
|
+
<style>html,body{margin:0;background:#173;color:#fff;font:48px/1 sans-serif;display:flex;align-items:center;justify-content:center;height:100vh}</style>
|
|
40
|
+
</head><body><div id="x">hf-beginframe-probe</div></body></html>`;
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
const result = await probe();
|
|
45
|
+
result.durationMs = Date.now() - start;
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
47
|
+
if (!result.passed) {
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function probe(): Promise<ProbeResult> {
|
|
53
|
+
let chromiumPath = "";
|
|
54
|
+
try {
|
|
55
|
+
const { default: chromium } = await import("@sparticuz/chromium");
|
|
56
|
+
chromiumPath = await chromium.executablePath();
|
|
57
|
+
const args = chromium.args;
|
|
58
|
+
|
|
59
|
+
const puppeteer = await import("puppeteer-core");
|
|
60
|
+
|
|
61
|
+
// Write probe HTML to /tmp + serve via file:// — no HTTP server in the
|
|
62
|
+
// probe so we don't add a dependency surface that could mask a
|
|
63
|
+
// Chrome-side issue. `mkdtempSync` (vs `tmpdir() + Date.now()`) gives
|
|
64
|
+
// an unguessable directory name so two concurrent probes on the same
|
|
65
|
+
// host don't collide and CodeQL's insecure-tempfile rule clears.
|
|
66
|
+
const tmpHtmlDir = mkdtempSync(join(tmpdir(), "hf-beginframe-"));
|
|
67
|
+
const htmlPath = join(tmpHtmlDir, "probe.html");
|
|
68
|
+
await fs.writeFile(htmlPath, PROBE_HTML, "utf-8");
|
|
69
|
+
|
|
70
|
+
// BeginFrame requires the full compositor-driving flag set. These match
|
|
71
|
+
// the args the engine's `browserManager` passes when `captureMode !==
|
|
72
|
+
// "screenshot"`. Without the surface-synchronization + threaded-disable
|
|
73
|
+
// flags, Chrome's compositor returns `hasDamage: false` and skips the
|
|
74
|
+
// screenshot — the same observation pinned in the hyperframes memory
|
|
75
|
+
// ("Chrome's beginFrame with `screenshot` param always reports
|
|
76
|
+
// hasDamage=true").
|
|
77
|
+
const beginFrameFlags = [
|
|
78
|
+
"--deterministic-mode",
|
|
79
|
+
"--enable-begin-frame-control",
|
|
80
|
+
"--disable-new-content-rendering-timeout",
|
|
81
|
+
"--run-all-compositor-stages-before-draw",
|
|
82
|
+
"--disable-threaded-animation",
|
|
83
|
+
"--disable-threaded-scrolling",
|
|
84
|
+
"--disable-checker-imaging",
|
|
85
|
+
"--disable-image-animation-resync",
|
|
86
|
+
"--enable-surface-synchronization",
|
|
87
|
+
// Software GL — Lambda has no GPU; matches the in-process renderer's
|
|
88
|
+
// software-locked path.
|
|
89
|
+
"--use-gl=angle",
|
|
90
|
+
"--use-angle=swiftshader",
|
|
91
|
+
"--enable-unsafe-swiftshader",
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const browser = await puppeteer.launch({
|
|
95
|
+
executablePath: chromiumPath,
|
|
96
|
+
headless: "shell",
|
|
97
|
+
args: [...args, ...beginFrameFlags],
|
|
98
|
+
defaultViewport: { width: 800, height: 600 },
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
const page = await browser.newPage();
|
|
102
|
+
await page.goto(`file://${htmlPath}`, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
103
|
+
const session = await page.createCDPSession();
|
|
104
|
+
await session.send("HeadlessExperimental.enable");
|
|
105
|
+
// Warm-up beginFrame with noDisplayUpdates: true — drives the
|
|
106
|
+
// compositor without producing a screenshot, matching how the engine
|
|
107
|
+
// primes a capture loop.
|
|
108
|
+
await session.send("HeadlessExperimental.beginFrame", {
|
|
109
|
+
frameTimeTicks: 0,
|
|
110
|
+
interval: 33,
|
|
111
|
+
noDisplayUpdates: true,
|
|
112
|
+
});
|
|
113
|
+
const response = await session.send("HeadlessExperimental.beginFrame", {
|
|
114
|
+
frameTimeTicks: 1000,
|
|
115
|
+
interval: 33,
|
|
116
|
+
screenshot: { format: "png" },
|
|
117
|
+
});
|
|
118
|
+
await fs.rm(tmpHtmlDir, { recursive: true, force: true }).catch(() => {});
|
|
119
|
+
const screenshot = response.screenshotData ?? "";
|
|
120
|
+
const bytes = screenshot ? Buffer.from(screenshot, "base64") : Buffer.alloc(0);
|
|
121
|
+
const isPng =
|
|
122
|
+
bytes.length >= 8 &&
|
|
123
|
+
bytes[0] === 0x89 &&
|
|
124
|
+
bytes[1] === 0x50 &&
|
|
125
|
+
bytes[2] === 0x4e &&
|
|
126
|
+
bytes[3] === 0x47;
|
|
127
|
+
return {
|
|
128
|
+
passed: isPng && bytes.length > 0,
|
|
129
|
+
durationMs: 0,
|
|
130
|
+
chromiumPath,
|
|
131
|
+
screenshotBytes: bytes.length,
|
|
132
|
+
hasDamage: response.hasDamage,
|
|
133
|
+
detail: isPng
|
|
134
|
+
? "OK — BeginFrame returned a PNG buffer."
|
|
135
|
+
: `FAIL — BeginFrame returned ${bytes.length} bytes, PNG signature ${
|
|
136
|
+
bytes.length >= 4 ? bytes.subarray(0, 4).toString("hex") : "<empty>"
|
|
137
|
+
}`,
|
|
138
|
+
};
|
|
139
|
+
} finally {
|
|
140
|
+
await browser.close().catch(() => {});
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
passed: false,
|
|
145
|
+
durationMs: 0,
|
|
146
|
+
chromiumPath,
|
|
147
|
+
screenshotBytes: 0,
|
|
148
|
+
hasDamage: false,
|
|
149
|
+
detail: `FAIL — ${err instanceof Error ? err.message : String(err)}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
void main().catch((err) => {
|
|
155
|
+
console.error("[probe-beginframe] unexpected:", err);
|
|
156
|
+
process.exit(2);
|
|
157
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CI gate on the Lambda ZIP size.
|
|
4
|
+
*
|
|
5
|
+
* Reads `dist/handler.zip.manifest.json` (written by `build-zip.ts`) and
|
|
6
|
+
* exits non-zero if either the unzipped or zipped size exceeds the
|
|
7
|
+
* declared limits. Lambda's hard ceiling for ZIP-deployed functions is
|
|
8
|
+
* 250 MiB unzipped (262144000 bytes — AWS docs label it "250 MB" but use
|
|
9
|
+
* binary mebibytes); the in-house budget is 248 MiB to keep headroom for
|
|
10
|
+
* the Chrome tarball decompression that happens at cold start.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { formatBytes } from "./_formatBytes.js";
|
|
17
|
+
|
|
18
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const packageRoot = resolve(scriptDir, "..");
|
|
20
|
+
const distDir = join(packageRoot, "dist");
|
|
21
|
+
const zipPath = join(distDir, "handler.zip");
|
|
22
|
+
const manifestPath = join(distDir, "handler.zip.manifest.json");
|
|
23
|
+
|
|
24
|
+
interface Manifest {
|
|
25
|
+
unzippedBytes: number;
|
|
26
|
+
zippedBytes: number;
|
|
27
|
+
source: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const IN_HOUSE_UNZIPPED_LIMIT = 248 * 1024 * 1024;
|
|
31
|
+
const IN_HOUSE_ZIPPED_LIMIT = 150 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
function main(): void {
|
|
34
|
+
if (!existsSync(zipPath)) {
|
|
35
|
+
console.error(`[verify-zip-size] ${zipPath} not found. Run 'bun run build:zip' first.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
if (!existsSync(manifestPath)) {
|
|
39
|
+
console.error(
|
|
40
|
+
`[verify-zip-size] ${manifestPath} not found. The manifest is written by build-zip.ts; ` +
|
|
41
|
+
`re-run the build.`,
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as Manifest;
|
|
47
|
+
const actualZipped = statSync(zipPath).size;
|
|
48
|
+
if (actualZipped !== manifest.zippedBytes) {
|
|
49
|
+
console.warn(
|
|
50
|
+
`[verify-zip-size] note: zip file size on disk (${actualZipped}) differs from ` +
|
|
51
|
+
`manifest (${manifest.zippedBytes}). Using on-disk value.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let failed = false;
|
|
56
|
+
if (manifest.unzippedBytes > IN_HOUSE_UNZIPPED_LIMIT) {
|
|
57
|
+
console.error(
|
|
58
|
+
`[verify-zip-size] FAIL unzipped: ${formatBytes(manifest.unzippedBytes)} > ` +
|
|
59
|
+
`${formatBytes(IN_HOUSE_UNZIPPED_LIMIT)} (in-house limit; Lambda hard ceiling is 250 MiB).`,
|
|
60
|
+
);
|
|
61
|
+
failed = true;
|
|
62
|
+
}
|
|
63
|
+
if (actualZipped > IN_HOUSE_ZIPPED_LIMIT) {
|
|
64
|
+
console.error(
|
|
65
|
+
`[verify-zip-size] FAIL zipped: ${formatBytes(actualZipped)} > ` +
|
|
66
|
+
`${formatBytes(IN_HOUSE_ZIPPED_LIMIT)} (in-house limit; Lambda direct-upload ceiling is 50 MiB, ` +
|
|
67
|
+
`S3-deploy ceiling is 250 MiB).`,
|
|
68
|
+
);
|
|
69
|
+
failed = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (failed) {
|
|
73
|
+
console.error("[verify-zip-size] FAILED — bundle is too large for Lambda ZIP deploy.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(
|
|
77
|
+
`[verify-zip-size] OK source=${manifest.source} unzipped=${formatBytes(
|
|
78
|
+
manifest.unzippedBytes,
|
|
79
|
+
)} zipped=${formatBytes(actualZipped)}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
main();
|