@neon/config-runtime 0.12.5 → 1.0.0
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 +4 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lib/function-bundle.d.ts +22 -2
- package/dist/lib/function-bundle.d.ts.map +1 -1
- package/dist/lib/function-bundle.js +29 -6
- package/dist/lib/function-bundle.js.map +1 -1
- package/dist/lib/native-detect.d.ts +64 -0
- package/dist/lib/native-detect.d.ts.map +1 -0
- package/dist/lib/native-detect.js +202 -0
- package/dist/lib/native-detect.js.map +1 -0
- package/dist/lib/native-packages.d.ts +108 -0
- package/dist/lib/native-packages.d.ts.map +1 -0
- package/dist/lib/native-packages.js +424 -0
- package/dist/lib/native-packages.js.map +1 -0
- package/dist/lib/push-config.js +8 -4
- package/dist/lib/push-config.js.map +1 -1
- package/dist/lib/resolve-package.d.ts +28 -0
- package/dist/lib/resolve-package.d.ts.map +1 -0
- package/dist/lib/resolve-package.js +52 -0
- package/dist/lib/resolve-package.js.map +1 -0
- package/dist/v1.d.ts +3 -1
- package/dist/v1.js +3 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -17,7 +17,10 @@ npm install @neon/config-runtime
|
|
|
17
17
|
- `inspect` / `plan` / `apply` — read the current branch state, diff a policy against it, and apply the changes.
|
|
18
18
|
- `pullConfig` / `pushConfig` — lower-level pull/push primitives.
|
|
19
19
|
- `createBranch` — create a branch to target.
|
|
20
|
-
- `buildFunctionBundle` — bundle Neon Functions for deploy. Honours the function's `externalPackages
|
|
20
|
+
- `buildFunctionBundle` — bundle Neon Functions for deploy. Honours the function's `externalPackages`: every entry is passed to esbuild's `external`, and unless it sets `includeFiles: false` the package's real files are staged into the archive alongside the bundle. Reports a bundled-in native dependency that was never declared, as a warning.
|
|
21
|
+
- `traceNativePackages` — install the packages a function stages for the runtime target (`RUNTIME_TARGET`: linux-arm64, glibc) into a throwaway directory, trace the files they reach, and return them keyed by archive path with the `node_modules` layout preserved. Called by `buildFunctionBundle`; exported for a CLI that supplies its own bundler.
|
|
22
|
+
- `findUndeclaredNativePackages` / `describeNativeFinding` — advisory detection of a native package that was bundled in without being declared, and the report text for it. Evidence only: it cannot prove the code path is reached, so it must never fail a deploy.
|
|
23
|
+
- `assertZipWithinLimits` — check a finished archive against the build service's size limits (`DEFAULT_ARCHIVE_LIMITS`), with an error naming the largest files.
|
|
21
24
|
|
|
22
25
|
```ts
|
|
23
26
|
import { inspect, plan, apply } from "@neon/config-runtime/v1";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { ArchiveLimits, DEFAULT_ARCHIVE_LIMITS, NativeTraceDeps, NativeTraceResult, RUNTIME_TARGET, RUNTIME_TARGET_LABEL, assertZipWithinLimits, enforceLimits, traceNativePackages } from "./lib/native-packages.js";
|
|
1
2
|
import { FunctionBundler, buildFunctionBundle } from "./lib/function-bundle.js";
|
|
3
|
+
import { NativeEvidence, NativeFinding, describeNativeFinding, findUndeclaredNativePackages } from "./lib/native-detect.js";
|
|
2
4
|
import { PullConfigOptions, PulledBranchConfig, PulledPreview, pullConfig } from "./lib/pull-config.js";
|
|
3
5
|
import { PushConfigOptions, PushConfirmContext, pushConfig } from "./lib/push-config.js";
|
|
4
6
|
import { ApplyOptions, ConfigOperationOptions, CreateBranchOptions, CreateBranchResult, apply, createBranch, inspect, plan } from "./lib/operations.js";
|
|
5
7
|
import { AppliedChange, Config, ConfigLoadError, ConfigValidationError, ConflictReport, ErrorCode, LoadConfigOptions, MissingContextError, NeonApi, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, PushResult, createNeonApiFromOptions, createRealNeonApi, defineConfig, isPartialBranchCreateError, isPlatformError, loadConfigFromFile } from "./v1.js";
|
|
6
|
-
export { AppliedChange, ApplyOptions, Config, ConfigLoadError, ConfigOperationOptions, ConfigValidationError, ConflictReport, CreateBranchOptions, CreateBranchResult, ErrorCode, FunctionBundler, LoadConfigOptions, MissingContextError, NeonApi, PartialBranchCreateError, PlatformError, PullConfigOptions, PulledBranchConfig, PulledPreview, PushAbortedError, PushConfigOptions, PushConfirmContext, PushConflictError, PushResult, apply, buildFunctionBundle, createBranch, createNeonApiFromOptions, createRealNeonApi, defineConfig, inspect, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, plan, pullConfig, pushConfig };
|
|
8
|
+
export { AppliedChange, ApplyOptions, ArchiveLimits, Config, ConfigLoadError, ConfigOperationOptions, ConfigValidationError, ConflictReport, CreateBranchOptions, CreateBranchResult, DEFAULT_ARCHIVE_LIMITS, ErrorCode, FunctionBundler, LoadConfigOptions, MissingContextError, NativeEvidence, NativeFinding, NativeTraceDeps, NativeTraceResult, NeonApi, PartialBranchCreateError, PlatformError, PullConfigOptions, PulledBranchConfig, PulledPreview, PushAbortedError, PushConfigOptions, PushConfirmContext, PushConflictError, PushResult, RUNTIME_TARGET, RUNTIME_TARGET_LABEL, apply, assertZipWithinLimits, buildFunctionBundle, createBranch, createNeonApiFromOptions, createRealNeonApi, defineConfig, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, plan, pullConfig, pushConfig, traceNativePackages };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_ARCHIVE_LIMITS, RUNTIME_TARGET, RUNTIME_TARGET_LABEL, assertZipWithinLimits, enforceLimits, traceNativePackages } from "./lib/native-packages.js";
|
|
2
|
+
import { describeNativeFinding, findUndeclaredNativePackages } from "./lib/native-detect.js";
|
|
1
3
|
import { buildFunctionBundle } from "./lib/function-bundle.js";
|
|
2
4
|
import { pullConfig } from "./lib/pull-config.js";
|
|
3
5
|
import { pushConfig } from "./lib/push-config.js";
|
|
4
6
|
import { apply, createBranch, inspect, plan } from "./lib/operations.js";
|
|
5
7
|
import { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, createNeonApiFromOptions, createRealNeonApi, defineConfig, isPartialBranchCreateError, isPlatformError, loadConfigFromFile } from "./v1.js";
|
|
6
|
-
export { ConfigLoadError, ConfigValidationError, ErrorCode, MissingContextError, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, apply, buildFunctionBundle, createBranch, createNeonApiFromOptions, createRealNeonApi, defineConfig, inspect, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, plan, pullConfig, pushConfig };
|
|
8
|
+
export { ConfigLoadError, ConfigValidationError, DEFAULT_ARCHIVE_LIMITS, ErrorCode, MissingContextError, PartialBranchCreateError, PlatformError, PushAbortedError, PushConflictError, RUNTIME_TARGET, RUNTIME_TARGET_LABEL, apply, assertZipWithinLimits, buildFunctionBundle, createBranch, createNeonApiFromOptions, createRealNeonApi, defineConfig, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, isPlatformError, loadConfigFromFile, plan, pullConfig, pushConfig, traceNativePackages };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeTraceDeps } from "./native-packages.js";
|
|
1
2
|
import { ResolvedFunctionConfig } from "@neon/config";
|
|
2
3
|
|
|
3
4
|
//#region src/lib/function-bundle.d.ts
|
|
@@ -11,6 +12,14 @@ import { ResolvedFunctionConfig } from "@neon/config";
|
|
|
11
12
|
* into their bundle.
|
|
12
13
|
*/
|
|
13
14
|
type FunctionBundler = (fn: ResolvedFunctionConfig) => Promise<Uint8Array>;
|
|
15
|
+
/**
|
|
16
|
+
* Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM
|
|
17
|
+
* output (`format: "esm"`) has no `require` / `__filename` / `__dirname` in scope — so any
|
|
18
|
+
* bundled CJS code that calls `require(...)` would fail at load with
|
|
19
|
+
* `Dynamic require of "x" is not supported`. Re-create those globals via `createRequire`
|
|
20
|
+
* so CJS and ESM dependencies coexist in the single `index.mjs`.
|
|
21
|
+
*/
|
|
22
|
+
declare const ESM_CJS_INTEROP_BANNER = "import{createRequire as ___cr}from'module';import{fileURLToPath as ___f}from'url';import{dirname as ___d}from'path';const require=___cr(import.meta.url);const __filename=___f(import.meta.url);const __dirname=___d(__filename);";
|
|
14
23
|
/**
|
|
15
24
|
* Build the deployable bundle (a ZIP archive of the esbuild-bundled source) for a function.
|
|
16
25
|
*
|
|
@@ -34,7 +43,18 @@ type FunctionBundler = (fn: ResolvedFunctionConfig) => Promise<Uint8Array>;
|
|
|
34
43
|
* so an uploaded `index.mjs.map` is never consumed (a thrown error's stack still points into
|
|
35
44
|
* the minified bundle). Generating it only inflated the deployed archive, so it is omitted.
|
|
36
45
|
*/
|
|
37
|
-
declare function buildFunctionBundle(fn: ResolvedFunctionConfig
|
|
46
|
+
declare function buildFunctionBundle(fn: ResolvedFunctionConfig, options?: {
|
|
47
|
+
nativeDeps?: Partial<NativeTraceDeps>;
|
|
48
|
+
/**
|
|
49
|
+
* Where advisory findings go — the undeclared-native-dependency report, and a package
|
|
50
|
+
* whose version could not be pinned.
|
|
51
|
+
*
|
|
52
|
+
* Defaults to discarding them, because a library has no business writing to the
|
|
53
|
+
* console. Pass a handler: these are the only signal that a function will fail at
|
|
54
|
+
* invoke, and the CLI wires this to its logger for exactly that reason.
|
|
55
|
+
*/
|
|
56
|
+
onWarning?: (message: string) => void;
|
|
57
|
+
}): Promise<Uint8Array>;
|
|
38
58
|
//#endregion
|
|
39
|
-
export { FunctionBundler, buildFunctionBundle };
|
|
59
|
+
export { ESM_CJS_INTEROP_BANNER, FunctionBundler, buildFunctionBundle };
|
|
40
60
|
//# sourceMappingURL=function-bundle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"function-bundle.d.ts","names":[],"sources":["../../src/lib/function-bundle.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"function-bundle.d.ts","names":[],"sources":["../../src/lib/function-bundle.ts"],"mappings":";;;;;;;AA0BA;AAA2B;AACtB;AACQ;AAAR;AAAO;AASC,KAXD,eAAA,GAWuB,CAAA,EAAA,EAV9B,sBAU8B,EAAA,GAT9B,OAS8B,CATtB,UASsB,CAAA;AA0BnC;AAAyC;AACpC;AAEkB;AAAR;AAWJ;AAAR;AAAO,cAxCG,sBAAA,GAwCH,mOAAA;;;;;;;;;;;;;;;;;;;;;;;;iBAdY,mBAAA,KACjB;eAEU,QAAQ;;;;;;;;;;IAWpB,QAAQ"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { assertZipWithinLimits, enforceLimits, traceNativePackages } from "./native-packages.js";
|
|
2
|
+
import { describeNativeFinding, findUndeclaredNativePackages } from "./native-detect.js";
|
|
3
|
+
import { ErrorCode, PlatformError, packagesToStage } from "@neon/config";
|
|
4
|
+
import { basename, dirname } from "node:path";
|
|
3
5
|
//#region src/lib/function-bundle.ts
|
|
4
6
|
/**
|
|
5
7
|
* Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM
|
|
@@ -32,8 +34,11 @@ const ESM_CJS_INTEROP_BANNER = "import{createRequire as ___cr}from'module';impor
|
|
|
32
34
|
* so an uploaded `index.mjs.map` is never consumed (a thrown error's stack still points into
|
|
33
35
|
* the minified bundle). Generating it only inflated the deployed archive, so it is omitted.
|
|
34
36
|
*/
|
|
35
|
-
async function buildFunctionBundle(fn) {
|
|
37
|
+
async function buildFunctionBundle(fn, options = {}) {
|
|
36
38
|
const esbuild = await loadEsbuild();
|
|
39
|
+
const externalPackages = fn.externalPackages ?? [];
|
|
40
|
+
const staged = packagesToStage(externalPackages);
|
|
41
|
+
const onWarning = options.onWarning ?? (() => {});
|
|
37
42
|
let result;
|
|
38
43
|
try {
|
|
39
44
|
result = await esbuild.build({
|
|
@@ -45,7 +50,8 @@ async function buildFunctionBundle(fn) {
|
|
|
45
50
|
format: "esm",
|
|
46
51
|
platform: "node",
|
|
47
52
|
banner: { js: ESM_CJS_INTEROP_BANNER },
|
|
48
|
-
external:
|
|
53
|
+
external: externalPackages.map((pkg) => pkg.name),
|
|
54
|
+
metafile: true,
|
|
49
55
|
logLevel: "silent"
|
|
50
56
|
});
|
|
51
57
|
} catch (cause) {
|
|
@@ -53,7 +59,24 @@ async function buildFunctionBundle(fn) {
|
|
|
53
59
|
}
|
|
54
60
|
const entries = {};
|
|
55
61
|
for (const file of result.outputFiles ?? []) entries[basename(file.path)] = file.contents;
|
|
56
|
-
|
|
62
|
+
for (const finding of findUndeclaredNativePackages({
|
|
63
|
+
metafile: result.metafile,
|
|
64
|
+
declared: externalPackages.map((pkg) => pkg.name),
|
|
65
|
+
projectDir: dirname(fn.source)
|
|
66
|
+
})) onWarning(describeNativeFinding(fn.slug, finding));
|
|
67
|
+
if (staged.length === 0) return zipBundle(entries);
|
|
68
|
+
const traced = await traceNativePackages({
|
|
69
|
+
slug: fn.slug,
|
|
70
|
+
packages: staged,
|
|
71
|
+
projectDir: dirname(fn.source),
|
|
72
|
+
...options.nativeDeps ? { deps: options.nativeDeps } : {}
|
|
73
|
+
});
|
|
74
|
+
for (const warning of traced.warnings) onWarning(warning);
|
|
75
|
+
Object.assign(entries, traced.entries);
|
|
76
|
+
enforceLimits(fn.slug, entries);
|
|
77
|
+
const zip = await zipBundle(entries);
|
|
78
|
+
assertZipWithinLimits(fn.slug, zip, entries);
|
|
79
|
+
return zip;
|
|
57
80
|
}
|
|
58
81
|
async function zipBundle(entries) {
|
|
59
82
|
const { zipSync } = await loadFflate();
|
|
@@ -74,6 +97,6 @@ async function loadFflate() {
|
|
|
74
97
|
}
|
|
75
98
|
}
|
|
76
99
|
//#endregion
|
|
77
|
-
export { buildFunctionBundle };
|
|
100
|
+
export { ESM_CJS_INTEROP_BANNER, buildFunctionBundle };
|
|
78
101
|
|
|
79
102
|
//# sourceMappingURL=function-bundle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"function-bundle.js","names":[],"sources":["../../src/lib/function-bundle.ts"],"sourcesContent":["import { basename } from \"node:path\";\nimport {\n\tErrorCode,\n\tPlatformError,\n\ttype ResolvedFunctionConfig,\n} from \"@neon/config\";\n\n/**\n * Builds the deployable ZIP bundle for a single function. The default\n * implementation ({@link buildFunctionBundle}) shells out to esbuild, but\n * `pushConfig` / `apply` accept a custom bundler so a consumer that can't ship\n * esbuild's native binary (e.g. a single-file CLI) can supply its own — a WASM\n * build, an esbuild binary on PATH, etc. — without this package dragging esbuild\n * into their bundle.\n */\nexport type FunctionBundler = (\n\tfn: ResolvedFunctionConfig,\n) => Promise<Uint8Array>;\n\n/**\n * Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM\n * output (`format: \"esm\"`) has no `require` / `__filename` / `__dirname` in scope — so any\n * bundled CJS code that calls `require(...)` would fail at load with\n * `Dynamic require of \"x\" is not supported`. Re-create those globals via `createRequire`\n * so CJS and ESM dependencies coexist in the single `index.mjs`.\n */\nconst ESM_CJS_INTEROP_BANNER =\n\t\"import{createRequire as ___cr}from'module';import{fileURLToPath as ___f}from'url';import{dirname as ___d}from'path';const require=___cr(import.meta.url);const __filename=___f(import.meta.url);const __dirname=___d(__filename);\";\n\n/**\n * Build the deployable bundle (a ZIP archive of the esbuild-bundled source) for a function.\n *\n * This is the **imperative shell** step of function deploys, and the reason it lives in\n * `@neon/config-runtime` rather than `@neon/config`: it pulls in `esbuild`\n * (a native binary) and `fflate`. Keeping it out of `@neon/config` means a `neon.ts`\n * that only imports `defineConfig` never drags esbuild into the user's dependency tree or\n * bundle. Deploy-side consumers (the neonctl CLI, CI) import this package and get esbuild as\n * a normal, auto-installed dependency.\n *\n * esbuild and fflate are loaded with a dynamic `import()` (not a static top-level import) so\n * that nothing in this package's static graph names esbuild until a deploy actually runs —\n * a second layer of protection on top of the package split.\n *\n * Mirrors: `esbuild <source> --bundle --outfile=index.mjs --minify --format=esm\n * --platform=node --banner:js=<createRequire shim>`, then zips the emitted files into the\n * archive the Functions deploy endpoint expects. Dependencies are bundled into the entry\n * (Node built-ins stay external); see {@link ESM_CJS_INTEROP_BANNER} for why the banner.\n *\n * No source map is emitted: the Functions runtime does not run Node with source-map support,\n * so an uploaded `index.mjs.map` is never consumed (a thrown error's stack still points into\n * the minified bundle). Generating it only inflated the deployed archive, so it is omitted.\n */\nexport async function buildFunctionBundle(\n\tfn: ResolvedFunctionConfig,\n): Promise<Uint8Array> {\n\tconst esbuild = await loadEsbuild();\n\n\tlet result: Awaited<ReturnType<typeof esbuild.build>>;\n\ttry {\n\t\tresult = await esbuild.build({\n\t\t\tentryPoints: [fn.source],\n\t\t\tbundle: true,\n\t\t\twrite: false,\n\t\t\t// Emit `index.mjs`: the Functions runtime imports the archive's entry by the\n\t\t\t// conventional `index.{js,mjs}` name, and `.mjs` makes Node load the ESM output\n\t\t\t// directly. (With `write: false` and no outfile, esbuild would label the buffer\n\t\t\t// `<stdout>`.) No `sourcemap` — see the doc comment above for why.\n\t\t\toutfile: \"index.mjs\",\n\t\t\tminify: true,\n\t\t\tformat: \"esm\",\n\t\t\tplatform: \"node\",\n\t\t\t// Bundle dependencies into the entry so the deployed archive is self-contained\n\t\t\t// (the Functions runtime has no node_modules). Node built-ins stay external on\n\t\t\t// `platform: \"node\"`. The banner re-creates require/__filename/__dirname so\n\t\t\t// bundled CommonJS deps work inside the ESM output.\n\t\t\tbanner: { js: ESM_CJS_INTEROP_BANNER },\n\t\t\t// Packages the policy declared unbundleable (native addons, optional peers on\n\t\t\t// dead code paths). Their imports survive into the bundle and resolve against a\n\t\t\t// directory with no node_modules, so reaching one at runtime throws — which is\n\t\t\t// the documented contract of `FunctionDef.externalPackages`, not a bug here.\n\t\t\texternal: [...(fn.externalPackages ?? [])],\n\t\t\tlogLevel: \"silent\",\n\t\t});\n\t} catch (cause) {\n\t\tthrow new PlatformError(\n\t\t\tErrorCode.InvalidConfig,\n\t\t\t[\n\t\t\t\t`Failed to bundle function \"${fn.slug}\" from ${fn.source}.`,\n\t\t\t\t(cause as Error)?.message ?? String(cause),\n\t\t\t].join(\" \"),\n\t\t\t{ cause },\n\t\t);\n\t}\n\n\tconst entries: Record<string, Uint8Array> = {};\n\t// `write: false` guarantees `outputFiles`, but the type is optional — guard for safety.\n\tfor (const file of result.outputFiles ?? []) {\n\t\t// esbuild returns absolute output paths; archive them under their basename\n\t\t// (`index.mjs`) so the bundle layout is stable regardless of cwd.\n\t\tentries[basename(file.path)] = file.contents;\n\t}\n\n\treturn zipBundle(entries);\n}\n\nasync function zipBundle(\n\tentries: Record<string, Uint8Array>,\n): Promise<Uint8Array> {\n\tconst { zipSync } = await loadFflate();\n\treturn zipSync(entries, { level: 6 });\n}\n\nasync function loadEsbuild(): Promise<typeof import(\"esbuild\")> {\n\ttry {\n\t\treturn await import(\"esbuild\");\n\t} catch (cause) {\n\t\tthrow new PlatformError(\n\t\t\tErrorCode.InvalidConfig,\n\t\t\t[\n\t\t\t\t\"Deploying Neon Functions requires `esbuild`, which could not be loaded.\",\n\t\t\t\t\"It is a dependency of @neon/config-runtime — reinstall your dependencies (`pnpm install` / `npm install`).\",\n\t\t\t].join(\" \"),\n\t\t\t{ cause },\n\t\t);\n\t}\n}\n\nasync function loadFflate(): Promise<typeof import(\"fflate\")> {\n\ttry {\n\t\treturn await import(\"fflate\");\n\t} catch (cause) {\n\t\tthrow new PlatformError(\n\t\t\tErrorCode.InvalidConfig,\n\t\t\t[\n\t\t\t\t\"Deploying Neon Functions requires `fflate`, which could not be loaded.\",\n\t\t\t\t\"It is a dependency of @neon/config-runtime — reinstall your dependencies (`pnpm install` / `npm install`).\",\n\t\t\t].join(\" \"),\n\t\t\t{ cause },\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;AA0BA,MAAM,yBACL;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,eAAsB,oBACrB,IACsB;CACtB,MAAM,UAAU,MAAM,YAAY;CAElC,IAAI;CACJ,IAAI;EACH,SAAS,MAAM,QAAQ,MAAM;GAC5B,aAAa,CAAC,GAAG,MAAM;GACvB,QAAQ;GACR,OAAO;GAKP,SAAS;GACT,QAAQ;GACR,QAAQ;GACR,UAAU;GAKV,QAAQ,EAAE,IAAI,uBAAuB;GAKrC,UAAU,CAAC,GAAI,GAAG,oBAAoB,CAAC,CAAE;GACzC,UAAU;EACX,CAAC;CACF,SAAS,OAAO;EACf,MAAM,IAAI,cACT,UAAU,eACV,CACC,8BAA8B,GAAG,KAAK,SAAS,GAAG,OAAO,IACxD,OAAiB,WAAW,OAAO,KAAK,CAC1C,CAAC,CAAC,KAAK,GAAG,GACV,EAAE,MAAM,CACT;CACD;CAEA,MAAM,UAAsC,CAAC;CAE7C,KAAK,MAAM,QAAQ,OAAO,eAAe,CAAC,GAGzC,QAAQ,SAAS,KAAK,IAAI,KAAK,KAAK;CAGrC,OAAO,UAAU,OAAO;AACzB;AAEA,eAAe,UACd,SACsB;CACtB,MAAM,EAAE,YAAY,MAAM,WAAW;CACrC,OAAO,QAAQ,SAAS,EAAE,OAAO,EAAE,CAAC;AACrC;AAEA,eAAe,cAAiD;CAC/D,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,cACT,UAAU,eACV,CACC,2EACA,4GACD,CAAC,CAAC,KAAK,GAAG,GACV,EAAE,MAAM,CACT;CACD;AACD;AAEA,eAAe,aAA+C;CAC7D,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,cACT,UAAU,eACV,CACC,0EACA,4GACD,CAAC,CAAC,KAAK,GAAG,GACV,EAAE,MAAM,CACT;CACD;AACD"}
|
|
1
|
+
{"version":3,"file":"function-bundle.js","names":[],"sources":["../../src/lib/function-bundle.ts"],"sourcesContent":["import { basename, dirname } from \"node:path\";\nimport {\n\tErrorCode,\n\tPlatformError,\n\tpackagesToStage,\n\ttype ResolvedFunctionConfig,\n} from \"@neon/config\";\nimport {\n\tdescribeNativeFinding,\n\tfindUndeclaredNativePackages,\n} from \"./native-detect.js\";\nimport {\n\tassertZipWithinLimits,\n\tenforceLimits,\n\ttype NativeTraceDeps,\n\ttraceNativePackages,\n} from \"./native-packages.js\";\n\n/**\n * Builds the deployable ZIP bundle for a single function. The default\n * implementation ({@link buildFunctionBundle}) shells out to esbuild, but\n * `pushConfig` / `apply` accept a custom bundler so a consumer that can't ship\n * esbuild's native binary (e.g. a single-file CLI) can supply its own — a WASM\n * build, an esbuild binary on PATH, etc. — without this package dragging esbuild\n * into their bundle.\n */\nexport type FunctionBundler = (\n\tfn: ResolvedFunctionConfig,\n) => Promise<Uint8Array>;\n\n/**\n * Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM\n * output (`format: \"esm\"`) has no `require` / `__filename` / `__dirname` in scope — so any\n * bundled CJS code that calls `require(...)` would fail at load with\n * `Dynamic require of \"x\" is not supported`. Re-create those globals via `createRequire`\n * so CJS and ESM dependencies coexist in the single `index.mjs`.\n */\nexport const ESM_CJS_INTEROP_BANNER =\n\t\"import{createRequire as ___cr}from'module';import{fileURLToPath as ___f}from'url';import{dirname as ___d}from'path';const require=___cr(import.meta.url);const __filename=___f(import.meta.url);const __dirname=___d(__filename);\";\n\n/**\n * Build the deployable bundle (a ZIP archive of the esbuild-bundled source) for a function.\n *\n * This is the **imperative shell** step of function deploys, and the reason it lives in\n * `@neon/config-runtime` rather than `@neon/config`: it pulls in `esbuild`\n * (a native binary) and `fflate`. Keeping it out of `@neon/config` means a `neon.ts`\n * that only imports `defineConfig` never drags esbuild into the user's dependency tree or\n * bundle. Deploy-side consumers (the neonctl CLI, CI) import this package and get esbuild as\n * a normal, auto-installed dependency.\n *\n * esbuild and fflate are loaded with a dynamic `import()` (not a static top-level import) so\n * that nothing in this package's static graph names esbuild until a deploy actually runs —\n * a second layer of protection on top of the package split.\n *\n * Mirrors: `esbuild <source> --bundle --outfile=index.mjs --minify --format=esm\n * --platform=node --banner:js=<createRequire shim>`, then zips the emitted files into the\n * archive the Functions deploy endpoint expects. Dependencies are bundled into the entry\n * (Node built-ins stay external); see {@link ESM_CJS_INTEROP_BANNER} for why the banner.\n *\n * No source map is emitted: the Functions runtime does not run Node with source-map support,\n * so an uploaded `index.mjs.map` is never consumed (a thrown error's stack still points into\n * the minified bundle). Generating it only inflated the deployed archive, so it is omitted.\n */\nexport async function buildFunctionBundle(\n\tfn: ResolvedFunctionConfig,\n\toptions: {\n\t\tnativeDeps?: Partial<NativeTraceDeps>;\n\t\t/**\n\t\t * Where advisory findings go — the undeclared-native-dependency report, and a package\n\t\t * whose version could not be pinned.\n\t\t *\n\t\t * Defaults to discarding them, because a library has no business writing to the\n\t\t * console. Pass a handler: these are the only signal that a function will fail at\n\t\t * invoke, and the CLI wires this to its logger for exactly that reason.\n\t\t */\n\t\tonWarning?: (message: string) => void;\n\t} = {},\n): Promise<Uint8Array> {\n\tconst esbuild = await loadEsbuild();\n\tconst externalPackages = fn.externalPackages ?? [];\n\tconst staged = packagesToStage(externalPackages);\n\tconst onWarning = options.onWarning ?? (() => {});\n\n\tlet result: Awaited<ReturnType<typeof esbuild.build>>;\n\ttry {\n\t\tresult = await esbuild.build({\n\t\t\tentryPoints: [fn.source],\n\t\t\tbundle: true,\n\t\t\twrite: false,\n\t\t\t// Emit `index.mjs`: the Functions runtime imports the archive's entry by the\n\t\t\t// conventional `index.{js,mjs}` name, and `.mjs` makes Node load the ESM output\n\t\t\t// directly. (With `write: false` and no outfile, esbuild would label the buffer\n\t\t\t// `<stdout>`.) No `sourcemap` — see the doc comment above for why.\n\t\t\toutfile: \"index.mjs\",\n\t\t\tminify: true,\n\t\t\tformat: \"esm\",\n\t\t\tplatform: \"node\",\n\t\t\t// Bundle dependencies into the entry so the deployed archive is self-contained\n\t\t\t// (the Functions runtime has no node_modules). Node built-ins stay external on\n\t\t\t// `platform: \"node\"`. The banner re-creates require/__filename/__dirname so\n\t\t\t// bundled CommonJS deps work inside the ESM output.\n\t\t\tbanner: { js: ESM_CJS_INTEROP_BANNER },\n\t\t\t// Packages the policy declared unbundleable. Their imports survive into the\n\t\t\t// bundle instead of being followed; whether they then resolve at runtime depends\n\t\t\t// on `includeFiles`, which is acted on below. Both modes are external here.\n\t\t\texternal: externalPackages.map((pkg) => pkg.name),\n\t\t\t// Reveals which packages were bundled in, so an undeclared native dependency can\n\t\t\t// be reported. Costs nothing here — esbuild is already running.\n\t\t\tmetafile: true,\n\t\t\tlogLevel: \"silent\",\n\t\t});\n\t} catch (cause) {\n\t\tthrow new PlatformError(\n\t\t\tErrorCode.InvalidConfig,\n\t\t\t[\n\t\t\t\t`Failed to bundle function \"${fn.slug}\" from ${fn.source}.`,\n\t\t\t\t(cause as Error)?.message ?? String(cause),\n\t\t\t].join(\" \"),\n\t\t\t{ cause },\n\t\t);\n\t}\n\n\tconst entries: Record<string, Uint8Array> = {};\n\t// `write: false` guarantees `outputFiles`, but the type is optional — guard for safety.\n\tfor (const file of result.outputFiles ?? []) {\n\t\t// esbuild returns absolute output paths; archive them under their basename\n\t\t// (`index.mjs`) so the bundle layout is stable regardless of cwd.\n\t\tentries[basename(file.path)] = file.contents;\n\t}\n\n\t// Only reachable on a successful build, which is the point: `sharp` and its kind bundle\n\t// cleanly and fail at invoke, so this is exactly the case nothing else catches. A build\n\t// that throws has already failed loudly with esbuild's own diagnostic.\n\tfor (const finding of findUndeclaredNativePackages({\n\t\tmetafile: result.metafile,\n\t\tdeclared: externalPackages.map((pkg) => pkg.name),\n\t\tprojectDir: dirname(fn.source),\n\t})) {\n\t\tonWarning(describeNativeFinding(fn.slug, finding));\n\t}\n\n\t// The pre-existing path, byte for byte: no install, no trace, no size check, and an\n\t// archive of exactly the esbuild output. A policy that declares nothing to stage — or\n\t// sets `includeFiles: false` on everything — deploys the archive it always did.\n\tif (staged.length === 0) return zipBundle(entries);\n\n\tconst traced = await traceNativePackages({\n\t\tslug: fn.slug,\n\t\tpackages: staged,\n\t\t// The user's project, located from their entry — where their resolved versions are\n\t\t// read from. Only versions: the files themselves come from the staging install.\n\t\tprojectDir: dirname(fn.source),\n\t\t...(options.nativeDeps ? { deps: options.nativeDeps } : {}),\n\t});\n\tfor (const warning of traced.warnings) onWarning(warning);\n\t// Keys carry their `node_modules/...` path, so the archive reproduces the tree layout a\n\t// `.node` addon needs to find its sibling libraries. Do not flatten these.\n\tObject.assign(entries, traced.entries);\n\n\t// Re-checked against the final archive: the staged files were measured without the\n\t// bundle, so the entry count and uncompressed total are only complete now.\n\tenforceLimits(fn.slug, entries);\n\tconst zip = await zipBundle(entries);\n\tassertZipWithinLimits(fn.slug, zip, entries);\n\treturn zip;\n}\n\nasync function zipBundle(\n\tentries: Record<string, Uint8Array>,\n): Promise<Uint8Array> {\n\tconst { zipSync } = await loadFflate();\n\treturn zipSync(entries, { level: 6 });\n}\n\nasync function loadEsbuild(): Promise<typeof import(\"esbuild\")> {\n\ttry {\n\t\treturn await import(\"esbuild\");\n\t} catch (cause) {\n\t\tthrow new PlatformError(\n\t\t\tErrorCode.InvalidConfig,\n\t\t\t[\n\t\t\t\t\"Deploying Neon Functions requires `esbuild`, which could not be loaded.\",\n\t\t\t\t\"It is a dependency of @neon/config-runtime — reinstall your dependencies (`pnpm install` / `npm install`).\",\n\t\t\t].join(\" \"),\n\t\t\t{ cause },\n\t\t);\n\t}\n}\n\nasync function loadFflate(): Promise<typeof import(\"fflate\")> {\n\ttry {\n\t\treturn await import(\"fflate\");\n\t} catch (cause) {\n\t\tthrow new PlatformError(\n\t\t\tErrorCode.InvalidConfig,\n\t\t\t[\n\t\t\t\t\"Deploying Neon Functions requires `fflate`, which could not be loaded.\",\n\t\t\t\t\"It is a dependency of @neon/config-runtime — reinstall your dependencies (`pnpm install` / `npm install`).\",\n\t\t\t].join(\" \"),\n\t\t\t{ cause },\n\t\t);\n\t}\n}\n"],"mappings":";;;;;;;;;;;;AAqCA,MAAa,yBACZ;;;;;;;;;;;;;;;;;;;;;;;;AAyBD,eAAsB,oBACrB,IACA,UAWI,CAAC,GACiB;CACtB,MAAM,UAAU,MAAM,YAAY;CAClC,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;CACjD,MAAM,SAAS,gBAAgB,gBAAgB;CAC/C,MAAM,YAAY,QAAQ,oBAAoB,CAAC;CAE/C,IAAI;CACJ,IAAI;EACH,SAAS,MAAM,QAAQ,MAAM;GAC5B,aAAa,CAAC,GAAG,MAAM;GACvB,QAAQ;GACR,OAAO;GAKP,SAAS;GACT,QAAQ;GACR,QAAQ;GACR,UAAU;GAKV,QAAQ,EAAE,IAAI,uBAAuB;GAIrC,UAAU,iBAAiB,KAAK,QAAQ,IAAI,IAAI;GAGhD,UAAU;GACV,UAAU;EACX,CAAC;CACF,SAAS,OAAO;EACf,MAAM,IAAI,cACT,UAAU,eACV,CACC,8BAA8B,GAAG,KAAK,SAAS,GAAG,OAAO,IACxD,OAAiB,WAAW,OAAO,KAAK,CAC1C,CAAC,CAAC,KAAK,GAAG,GACV,EAAE,MAAM,CACT;CACD;CAEA,MAAM,UAAsC,CAAC;CAE7C,KAAK,MAAM,QAAQ,OAAO,eAAe,CAAC,GAGzC,QAAQ,SAAS,KAAK,IAAI,KAAK,KAAK;CAMrC,KAAK,MAAM,WAAW,6BAA6B;EAClD,UAAU,OAAO;EACjB,UAAU,iBAAiB,KAAK,QAAQ,IAAI,IAAI;EAChD,YAAY,QAAQ,GAAG,MAAM;CAC9B,CAAC,GACA,UAAU,sBAAsB,GAAG,MAAM,OAAO,CAAC;CAMlD,IAAI,OAAO,WAAW,GAAG,OAAO,UAAU,OAAO;CAEjD,MAAM,SAAS,MAAM,oBAAoB;EACxC,MAAM,GAAG;EACT,UAAU;EAGV,YAAY,QAAQ,GAAG,MAAM;EAC7B,GAAI,QAAQ,aAAa,EAAE,MAAM,QAAQ,WAAW,IAAI,CAAC;CAC1D,CAAC;CACD,KAAK,MAAM,WAAW,OAAO,UAAU,UAAU,OAAO;CAGxD,OAAO,OAAO,SAAS,OAAO,OAAO;CAIrC,cAAc,GAAG,MAAM,OAAO;CAC9B,MAAM,MAAM,MAAM,UAAU,OAAO;CACnC,sBAAsB,GAAG,MAAM,KAAK,OAAO;CAC3C,OAAO;AACR;AAEA,eAAe,UACd,SACsB;CACtB,MAAM,EAAE,YAAY,MAAM,WAAW;CACrC,OAAO,QAAQ,SAAS,EAAE,OAAO,EAAE,CAAC;AACrC;AAEA,eAAe,cAAiD;CAC/D,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,cACT,UAAU,eACV,CACC,2EACA,4GACD,CAAC,CAAC,KAAK,GAAG,GACV,EAAE,MAAM,CACT;CACD;AACD;AAEA,eAAe,aAA+C;CAC7D,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,cACT,UAAU,eACV,CACC,0EACA,4GACD,CAAC,CAAC,KAAK,GAAG,GACV,EAAE,MAAM,CACT;CACD;AACD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/lib/native-detect.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Why a package looks like it needs its files shipped. Carried through to the message so the
|
|
4
|
+
* report states the evidence rather than a prediction about what will happen at invoke.
|
|
5
|
+
*/
|
|
6
|
+
type NativeEvidence = {
|
|
7
|
+
kind: "binary";
|
|
8
|
+
file: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "platformDependency";
|
|
11
|
+
dependency: string;
|
|
12
|
+
file: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "buildsFromSource";
|
|
15
|
+
script: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "prebuiltManifest";
|
|
18
|
+
};
|
|
19
|
+
type NativeFinding = {
|
|
20
|
+
/** The package as it appears in the user's tree. */
|
|
21
|
+
name: string;
|
|
22
|
+
evidence: NativeEvidence;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The packages a bundle pulled in that carry native code and were not declared in
|
|
26
|
+
* `externalPackages`.
|
|
27
|
+
*
|
|
28
|
+
* **This is evidence, not a verdict.** It proves the package contains or depends on compiled
|
|
29
|
+
* code, never that this function's code path reaches it — a package with an optional native
|
|
30
|
+
* accelerator and a working JavaScript fallback (`ws` with `bufferutil`, a Prisma client with
|
|
31
|
+
* no Rust engine) looks identical from here and deploys fine today. So the caller reports it
|
|
32
|
+
* and continues; it must not fail a deploy.
|
|
33
|
+
*
|
|
34
|
+
* Only reachable after a successful build. A package esbuild could not resolve, or a `.node`
|
|
35
|
+
* it has no loader for, throws before any metafile exists — those already fail loudly with
|
|
36
|
+
* esbuild's own diagnostic.
|
|
37
|
+
*/
|
|
38
|
+
declare function findUndeclaredNativePackages(options: {
|
|
39
|
+
metafile: {
|
|
40
|
+
inputs?: Record<string, unknown>;
|
|
41
|
+
} | undefined;
|
|
42
|
+
declared: readonly string[];
|
|
43
|
+
projectDir: string;
|
|
44
|
+
/** What metafile input paths are relative to. Defaults to the process working directory. */
|
|
45
|
+
cwd?: string;
|
|
46
|
+
}): NativeFinding[];
|
|
47
|
+
/**
|
|
48
|
+
* The package an esbuild metafile input belongs to, or `undefined` for the user's own source.
|
|
49
|
+
*
|
|
50
|
+
* Keyed off the **last** `node_modules/` segment, which is what makes pnpm's
|
|
51
|
+
* `node_modules/.pnpm/sharp@0.35.3/node_modules/sharp/lib/x.js` resolve to `sharp` rather
|
|
52
|
+
* than `.pnpm`. Yarn PnP resolves out of zip archives with no `node_modules` segment at all
|
|
53
|
+
* and yields nothing here, which is the safe direction: a missed package is a missing
|
|
54
|
+
* warning, not a wrong one.
|
|
55
|
+
*/
|
|
56
|
+
declare function packageNameFromInput(input: string): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The report a user sees. States what was found and both ways to act on it, with the
|
|
59
|
+
* shipping form first because it is the one that produces a working function.
|
|
60
|
+
*/
|
|
61
|
+
declare function describeNativeFinding(slug: string, finding: NativeFinding): string;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { NativeEvidence, NativeFinding, describeNativeFinding, findUndeclaredNativePackages, packageNameFromInput };
|
|
64
|
+
//# sourceMappingURL=native-detect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-detect.d.ts","names":[],"sources":["../../src/lib/native-detect.ts"],"mappings":";;AAUA;AAMA;AAwCA;AAA4C,KA9ChC,cAAA,GA8CgC;EACtB,IAAA,EAAA,QAAA;EAKlB,IAAA,EAAA,MAAA;AAAa,CAAA,GAAA;EA2ED,IAAA,EAAA,oBAAoB;EAyIpB,UAAA,EAAA,MAAA;;;;;;;;KAlQJ,aAAA;;;YAGD;;;;;;;;;;;;;;;;iBAqCK,4BAAA;;aACM;;;;;;IAKlB;;;;;;;;;;iBA2EY,oBAAA;;;;;iBAyIA,qBAAA,wBAEN"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { findPackageDir, readPackageManifest } from "./resolve-package.js";
|
|
2
|
+
import { RUNTIME_TARGET, RUNTIME_TARGET_LABEL } from "./native-packages.js";
|
|
3
|
+
import { externalPackageRoot } from "@neon/config";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { readdirSync, realpathSync } from "node:fs";
|
|
6
|
+
//#region src/lib/native-detect.ts
|
|
7
|
+
/** Depth of the walk for a `.node`/`.so` inside one package. */
|
|
8
|
+
const SCAN_DEPTH = 4;
|
|
9
|
+
const BINARY_PATTERN = /\.(node|so)(\.\d+)*$/;
|
|
10
|
+
const GYP_PATTERN = /\b(node-gyp|node-pre-gyp|prebuild-install|cmake-js)\b/;
|
|
11
|
+
/**
|
|
12
|
+
* The packages a bundle pulled in that carry native code and were not declared in
|
|
13
|
+
* `externalPackages`.
|
|
14
|
+
*
|
|
15
|
+
* **This is evidence, not a verdict.** It proves the package contains or depends on compiled
|
|
16
|
+
* code, never that this function's code path reaches it — a package with an optional native
|
|
17
|
+
* accelerator and a working JavaScript fallback (`ws` with `bufferutil`, a Prisma client with
|
|
18
|
+
* no Rust engine) looks identical from here and deploys fine today. So the caller reports it
|
|
19
|
+
* and continues; it must not fail a deploy.
|
|
20
|
+
*
|
|
21
|
+
* Only reachable after a successful build. A package esbuild could not resolve, or a `.node`
|
|
22
|
+
* it has no loader for, throws before any metafile exists — those already fail loudly with
|
|
23
|
+
* esbuild's own diagnostic.
|
|
24
|
+
*/
|
|
25
|
+
function findUndeclaredNativePackages(options) {
|
|
26
|
+
const { metafile, declared, projectDir } = options;
|
|
27
|
+
const cwd = options.cwd ?? process.cwd();
|
|
28
|
+
const inputs = metafile?.inputs;
|
|
29
|
+
if (!inputs) return [];
|
|
30
|
+
const declaredRoots = new Set(declared.map(externalPackageRoot));
|
|
31
|
+
const findings = [];
|
|
32
|
+
const seen = /* @__PURE__ */ new Set();
|
|
33
|
+
for (const input of Object.keys(inputs)) {
|
|
34
|
+
const located = packageFromInput(input, cwd);
|
|
35
|
+
if (located === void 0) continue;
|
|
36
|
+
const { name } = located;
|
|
37
|
+
if (declaredRoots.has(name)) continue;
|
|
38
|
+
const dir = located.dir ?? findPackageDir(projectDir, name);
|
|
39
|
+
if (dir === void 0 || seen.has(dir)) continue;
|
|
40
|
+
seen.add(dir);
|
|
41
|
+
const evidence = nativeEvidence(dir, projectDir);
|
|
42
|
+
if (evidence !== void 0) findings.push({
|
|
43
|
+
name,
|
|
44
|
+
evidence
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The package an input belongs to, and where that exact copy lives on disk.
|
|
51
|
+
*
|
|
52
|
+
* Taking the directory straight from the input path rather than searching for the name is
|
|
53
|
+
* what makes this correct under pnpm, whose transitive dependencies are not linked at the
|
|
54
|
+
* project root at all and whose store holds several versions of the same package side by
|
|
55
|
+
* side. A search from the project root would miss the first and could inspect the wrong copy
|
|
56
|
+
* in the second.
|
|
57
|
+
*/
|
|
58
|
+
function packageFromInput(input, cwd) {
|
|
59
|
+
const name = packageNameFromInput(input);
|
|
60
|
+
if (name === void 0) return void 0;
|
|
61
|
+
const normalized = input.split("\\").join("/");
|
|
62
|
+
const prefixLength = normalized.lastIndexOf("node_modules/") + 13 + name.length;
|
|
63
|
+
const relativeDir = normalized.slice(0, prefixLength);
|
|
64
|
+
if (!relativeDir.endsWith(name)) return { name };
|
|
65
|
+
const absolute = isAbsolute(relativeDir) ? relativeDir : resolve(cwd, relativeDir);
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
name,
|
|
69
|
+
dir: realpathSync(absolute)
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return { name };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The package an esbuild metafile input belongs to, or `undefined` for the user's own source.
|
|
77
|
+
*
|
|
78
|
+
* Keyed off the **last** `node_modules/` segment, which is what makes pnpm's
|
|
79
|
+
* `node_modules/.pnpm/sharp@0.35.3/node_modules/sharp/lib/x.js` resolve to `sharp` rather
|
|
80
|
+
* than `.pnpm`. Yarn PnP resolves out of zip archives with no `node_modules` segment at all
|
|
81
|
+
* and yields nothing here, which is the safe direction: a missed package is a missing
|
|
82
|
+
* warning, not a wrong one.
|
|
83
|
+
*/
|
|
84
|
+
function packageNameFromInput(input) {
|
|
85
|
+
const normalized = input.split("\\").join("/");
|
|
86
|
+
const last = normalized.lastIndexOf("node_modules/");
|
|
87
|
+
if (last === -1) return void 0;
|
|
88
|
+
const segments = normalized.slice(last + 13).split("/").filter((segment) => segment.length > 0);
|
|
89
|
+
if (segments.length === 0) return void 0;
|
|
90
|
+
if (segments[0].startsWith("@")) return segments.length >= 2 ? `${segments[0]}/${segments[1]}` : void 0;
|
|
91
|
+
return segments[0];
|
|
92
|
+
}
|
|
93
|
+
const readManifest = (dir) => readPackageManifest(dir);
|
|
94
|
+
/**
|
|
95
|
+
* A manifest field npm accepts as either a string or an array of strings, narrowed to a list.
|
|
96
|
+
* Anything else is treated as absent: this is an advisory scan reading arbitrary third-party
|
|
97
|
+
* manifests, and a malformed field must not take a deploy down.
|
|
98
|
+
*/
|
|
99
|
+
function stringList(value) {
|
|
100
|
+
if (typeof value === "string") return [value];
|
|
101
|
+
if (!Array.isArray(value)) return void 0;
|
|
102
|
+
const entries = value.filter((entry) => typeof entry === "string");
|
|
103
|
+
return entries.length === value.length ? entries : void 0;
|
|
104
|
+
}
|
|
105
|
+
function optionalDependencies(manifest) {
|
|
106
|
+
const value = manifest.optionalDependencies;
|
|
107
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a manifest's own `os`/`cpu`/`libc` fields permit the Functions runtime. A package
|
|
111
|
+
* that excludes it — `fsevents` is darwin-only, a musl-only build cannot load on glibc —
|
|
112
|
+
* cannot run there whatever we do, so it is not a candidate for shipping and reporting it
|
|
113
|
+
* would be noise.
|
|
114
|
+
*/
|
|
115
|
+
function runsOnRuntime(manifest) {
|
|
116
|
+
const permits = (raw, value) => {
|
|
117
|
+
const field = stringList(raw);
|
|
118
|
+
if (field === void 0 || field.length === 0) return true;
|
|
119
|
+
if (field.some((entry) => entry === `!${value}`)) return false;
|
|
120
|
+
const positives = field.filter((entry) => !entry.startsWith("!"));
|
|
121
|
+
return positives.length === 0 || positives.includes(value);
|
|
122
|
+
};
|
|
123
|
+
return permits(manifest.os, RUNTIME_TARGET.os) && permits(manifest.cpu, RUNTIME_TARGET.cpu) && permits(manifest.libc, RUNTIME_TARGET.libc);
|
|
124
|
+
}
|
|
125
|
+
/** The first native signal a package shows, or `undefined` if it looks like plain JavaScript. */
|
|
126
|
+
function nativeEvidence(dir, projectDir) {
|
|
127
|
+
const manifest = readManifest(dir);
|
|
128
|
+
if (manifest === void 0) return void 0;
|
|
129
|
+
if (!runsOnRuntime(manifest)) return void 0;
|
|
130
|
+
const own = findBinary(dir, SCAN_DEPTH);
|
|
131
|
+
if (own !== void 0) return {
|
|
132
|
+
kind: "binary",
|
|
133
|
+
file: own
|
|
134
|
+
};
|
|
135
|
+
for (const dependency of Object.keys(optionalDependencies(manifest))) {
|
|
136
|
+
const dependencyDir = findPackageDir(dir, dependency) ?? findPackageDir(projectDir, dependency);
|
|
137
|
+
if (dependencyDir === void 0) continue;
|
|
138
|
+
const dependencyManifest = readManifest(dependencyDir);
|
|
139
|
+
if (dependencyManifest === void 0) continue;
|
|
140
|
+
if (!(stringList(dependencyManifest.os) !== void 0 || stringList(dependencyManifest.cpu) !== void 0 || stringList(dependencyManifest.libc) !== void 0)) continue;
|
|
141
|
+
const file = findBinary(dependencyDir, SCAN_DEPTH);
|
|
142
|
+
if (file !== void 0) return {
|
|
143
|
+
kind: "platformDependency",
|
|
144
|
+
dependency,
|
|
145
|
+
file
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const scripts = manifest.scripts;
|
|
149
|
+
const script = typeof scripts?.install === "string" ? scripts.install : typeof scripts?.postinstall === "string" ? scripts.postinstall : void 0;
|
|
150
|
+
if (script !== void 0 && GYP_PATTERN.test(script)) return {
|
|
151
|
+
kind: "buildsFromSource",
|
|
152
|
+
script
|
|
153
|
+
};
|
|
154
|
+
if (manifest.binary !== void 0) return { kind: "prebuiltManifest" };
|
|
155
|
+
}
|
|
156
|
+
/** First compiled artifact under `dir`, not descending into a nested `node_modules`. */
|
|
157
|
+
function findBinary(dir, depth) {
|
|
158
|
+
if (depth < 0) return void 0;
|
|
159
|
+
let listing;
|
|
160
|
+
try {
|
|
161
|
+
listing = readdirSync(dir, { withFileTypes: true });
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of listing) if (entry.isFile() && BINARY_PATTERN.test(entry.name)) return entry.name;
|
|
166
|
+
for (const entry of listing) {
|
|
167
|
+
if (!entry.isDirectory() || entry.name === "node_modules") continue;
|
|
168
|
+
const found = findBinary(join(dir, entry.name), depth - 1);
|
|
169
|
+
if (found !== void 0) return join(entry.name, found);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The report a user sees. States what was found and both ways to act on it, with the
|
|
174
|
+
* shipping form first because it is the one that produces a working function.
|
|
175
|
+
*/
|
|
176
|
+
function describeNativeFinding(slug, finding) {
|
|
177
|
+
const { name, evidence } = finding;
|
|
178
|
+
return [
|
|
179
|
+
`Function "${slug}" bundles "${name}", and ${(() => {
|
|
180
|
+
switch (evidence.kind) {
|
|
181
|
+
case "binary": return `it contains a compiled binary (${evidence.file} on this machine; the deploy needs the ${RUNTIME_TARGET_LABEL} build)`;
|
|
182
|
+
case "platformDependency": return `it loads a compiled binary from ${evidence.dependency} (${evidence.file} on this machine; the deploy needs the ${RUNTIME_TARGET_LABEL} build)`;
|
|
183
|
+
case "buildsFromSource": return `it compiles a binary at install time (${evidence.script})`;
|
|
184
|
+
case "prebuiltManifest": return "its manifest declares a prebuilt binary";
|
|
185
|
+
}
|
|
186
|
+
})()}.`,
|
|
187
|
+
`A compiled binary cannot be bundled, so if this function evaluates that import it`,
|
|
188
|
+
`fails at invoke rather than at deploy.`,
|
|
189
|
+
"",
|
|
190
|
+
` Ship its files with the deploy:`,
|
|
191
|
+
` externalPackages: ["${name}"]`,
|
|
192
|
+
"",
|
|
193
|
+
`If "${name}" is only reached behind a working JavaScript fallback, this deploy is`,
|
|
194
|
+
`already correct and needs no change — this is advisory and never fails a deploy.`,
|
|
195
|
+
`Do not add includeFiles: false to silence it: that externalizes "${name}" and ships`,
|
|
196
|
+
`nothing for it, so a top-level import of it would then fail on every invoke.`
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { describeNativeFinding, findUndeclaredNativePackages, packageNameFromInput };
|
|
201
|
+
|
|
202
|
+
//# sourceMappingURL=native-detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-detect.js","names":[],"sources":["../../src/lib/native-detect.ts"],"sourcesContent":["import { type Dirent, readdirSync, realpathSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { externalPackageRoot } from \"@neon/config\";\nimport { RUNTIME_TARGET, RUNTIME_TARGET_LABEL } from \"./native-packages.js\";\nimport { findPackageDir, readPackageManifest } from \"./resolve-package.js\";\n\n/**\n * Why a package looks like it needs its files shipped. Carried through to the message so the\n * report states the evidence rather than a prediction about what will happen at invoke.\n */\nexport type NativeEvidence =\n\t| { kind: \"binary\"; file: string }\n\t| { kind: \"platformDependency\"; dependency: string; file: string }\n\t| { kind: \"buildsFromSource\"; script: string }\n\t| { kind: \"prebuiltManifest\" };\n\nexport type NativeFinding = {\n\t/** The package as it appears in the user's tree. */\n\tname: string;\n\tevidence: NativeEvidence;\n};\n\n/** Depth of the walk for a `.node`/`.so` inside one package. */\nconst SCAN_DEPTH = 4;\n\nconst BINARY_PATTERN = /\\.(node|so)(\\.\\d+)*$/;\n\nconst GYP_PATTERN = /\\b(node-gyp|node-pre-gyp|prebuild-install|cmake-js)\\b/;\n\n/**\n * Third-party manifests are arbitrary JSON, so every field is `unknown` and narrowed at use.\n * npm accepts `os`/`cpu`/`libc` as a bare string as well as an array.\n */\ntype Manifest = {\n\tos?: unknown;\n\tcpu?: unknown;\n\tlibc?: unknown;\n\tbinary?: unknown;\n\toptionalDependencies?: unknown;\n\tscripts?: Record<string, unknown>;\n};\n\n/**\n * The packages a bundle pulled in that carry native code and were not declared in\n * `externalPackages`.\n *\n * **This is evidence, not a verdict.** It proves the package contains or depends on compiled\n * code, never that this function's code path reaches it — a package with an optional native\n * accelerator and a working JavaScript fallback (`ws` with `bufferutil`, a Prisma client with\n * no Rust engine) looks identical from here and deploys fine today. So the caller reports it\n * and continues; it must not fail a deploy.\n *\n * Only reachable after a successful build. A package esbuild could not resolve, or a `.node`\n * it has no loader for, throws before any metafile exists — those already fail loudly with\n * esbuild's own diagnostic.\n */\nexport function findUndeclaredNativePackages(options: {\n\tmetafile: { inputs?: Record<string, unknown> } | undefined;\n\tdeclared: readonly string[];\n\tprojectDir: string;\n\t/** What metafile input paths are relative to. Defaults to the process working directory. */\n\tcwd?: string;\n}): NativeFinding[] {\n\tconst { metafile, declared, projectDir } = options;\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst inputs = metafile?.inputs;\n\tif (!inputs) return [];\n\n\t// A declared subpath covers its whole package: esbuild externalizing `pkg` externalizes\n\t// `pkg/sub` with it, so the package is spoken for either way.\n\tconst declaredRoots = new Set(declared.map(externalPackageRoot));\n\n\tconst findings: NativeFinding[] = [];\n\tconst seen = new Set<string>();\n\tfor (const input of Object.keys(inputs)) {\n\t\tconst located = packageFromInput(input, cwd);\n\t\tif (located === undefined) continue;\n\t\tconst { name } = located;\n\t\tif (declaredRoots.has(name)) continue;\n\n\t\t// Keyed by directory, not name: pnpm and nested installs can put two versions of one\n\t\t// package in a graph, and they are genuinely different packages to inspect.\n\t\tconst dir = located.dir ?? findPackageDir(projectDir, name);\n\t\tif (dir === undefined || seen.has(dir)) continue;\n\t\tseen.add(dir);\n\n\t\tconst evidence = nativeEvidence(dir, projectDir);\n\t\tif (evidence !== undefined) findings.push({ name, evidence });\n\t}\n\treturn findings;\n}\n\n/**\n * The package an input belongs to, and where that exact copy lives on disk.\n *\n * Taking the directory straight from the input path rather than searching for the name is\n * what makes this correct under pnpm, whose transitive dependencies are not linked at the\n * project root at all and whose store holds several versions of the same package side by\n * side. A search from the project root would miss the first and could inspect the wrong copy\n * in the second.\n */\nfunction packageFromInput(\n\tinput: string,\n\tcwd: string,\n): { name: string; dir?: string } | undefined {\n\tconst name = packageNameFromInput(input);\n\tif (name === undefined) return undefined;\n\n\tconst normalized = input.split(\"\\\\\").join(\"/\");\n\tconst marker = \"node_modules/\";\n\tconst last = normalized.lastIndexOf(marker);\n\t// The path through the end of the package's own segments is its directory.\n\tconst prefixLength = last + marker.length + name.length;\n\tconst relativeDir = normalized.slice(0, prefixLength);\n\tif (!relativeDir.endsWith(name)) return { name };\n\n\tconst absolute = isAbsolute(relativeDir)\n\t\t? relativeDir\n\t\t: resolve(cwd, relativeDir);\n\ttry {\n\t\t// realpath so a pnpm symlink and its store target dedupe to one physical package.\n\t\treturn { name, dir: realpathSync(absolute) };\n\t} catch {\n\t\t// The path esbuild reported is not readable from here — fall back to a search.\n\t\treturn { name };\n\t}\n}\n\n/**\n * The package an esbuild metafile input belongs to, or `undefined` for the user's own source.\n *\n * Keyed off the **last** `node_modules/` segment, which is what makes pnpm's\n * `node_modules/.pnpm/sharp@0.35.3/node_modules/sharp/lib/x.js` resolve to `sharp` rather\n * than `.pnpm`. Yarn PnP resolves out of zip archives with no `node_modules` segment at all\n * and yields nothing here, which is the safe direction: a missed package is a missing\n * warning, not a wrong one.\n */\nexport function packageNameFromInput(input: string): string | undefined {\n\tconst normalized = input.split(\"\\\\\").join(\"/\");\n\tconst marker = \"node_modules/\";\n\tconst last = normalized.lastIndexOf(marker);\n\tif (last === -1) return undefined;\n\n\tconst rest = normalized.slice(last + marker.length);\n\tconst segments = rest.split(\"/\").filter((segment) => segment.length > 0);\n\tif (segments.length === 0) return undefined;\n\tif (segments[0].startsWith(\"@\")) {\n\t\treturn segments.length >= 2\n\t\t\t? `${segments[0]}/${segments[1]}`\n\t\t\t: undefined;\n\t}\n\treturn segments[0];\n}\n\nconst readManifest = (dir: string): Manifest | undefined =>\n\treadPackageManifest(dir) as Manifest | undefined;\n\n/**\n * A manifest field npm accepts as either a string or an array of strings, narrowed to a list.\n * Anything else is treated as absent: this is an advisory scan reading arbitrary third-party\n * manifests, and a malformed field must not take a deploy down.\n */\nfunction stringList(value: unknown): string[] | undefined {\n\tif (typeof value === \"string\") return [value];\n\tif (!Array.isArray(value)) return undefined;\n\tconst entries = value.filter(\n\t\t(entry): entry is string => typeof entry === \"string\",\n\t);\n\treturn entries.length === value.length ? entries : undefined;\n}\n\nfunction optionalDependencies(manifest: Manifest): Record<string, unknown> {\n\tconst value = manifest.optionalDependencies;\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: {};\n}\n\n/**\n * Whether a manifest's own `os`/`cpu`/`libc` fields permit the Functions runtime. A package\n * that excludes it — `fsevents` is darwin-only, a musl-only build cannot load on glibc —\n * cannot run there whatever we do, so it is not a candidate for shipping and reporting it\n * would be noise.\n */\nfunction runsOnRuntime(manifest: Manifest): boolean {\n\tconst permits = (raw: unknown, value: string): boolean => {\n\t\tconst field = stringList(raw);\n\t\tif (field === undefined || field.length === 0) return true;\n\t\tif (field.some((entry) => entry === `!${value}`)) return false;\n\t\tconst positives = field.filter((entry) => !entry.startsWith(\"!\"));\n\t\treturn positives.length === 0 || positives.includes(value);\n\t};\n\treturn (\n\t\tpermits(manifest.os, RUNTIME_TARGET.os) &&\n\t\tpermits(manifest.cpu, RUNTIME_TARGET.cpu) &&\n\t\tpermits(manifest.libc, RUNTIME_TARGET.libc)\n\t);\n}\n\n/** The first native signal a package shows, or `undefined` if it looks like plain JavaScript. */\nfunction nativeEvidence(\n\tdir: string,\n\tprojectDir: string,\n): NativeEvidence | undefined {\n\tconst manifest = readManifest(dir);\n\tif (manifest === undefined) return undefined;\n\tif (!runsOnRuntime(manifest)) return undefined;\n\n\tconst own = findBinary(dir, SCAN_DEPTH);\n\tif (own !== undefined) return { kind: \"binary\", file: own };\n\n\t// The modern prebuilt pattern: the parent is pure JavaScript and the binary lives in a\n\t// platform-gated optional dependency, which is why scanning only the parent misses it.\n\tfor (const dependency of Object.keys(optionalDependencies(manifest))) {\n\t\t// Search from the package's own directory first so a nested or pnpm-linked copy wins\n\t\t// over an unrelated one at the project root, then fall back to the project.\n\t\tconst dependencyDir =\n\t\t\tfindPackageDir(dir, dependency) ??\n\t\t\tfindPackageDir(projectDir, dependency);\n\t\tif (dependencyDir === undefined) continue;\n\t\tconst dependencyManifest = readManifest(dependencyDir);\n\t\tif (dependencyManifest === undefined) continue;\n\t\t// The sibling built for *this* host is the one installed locally; its os/cpu fields\n\t\t// are what mark the package as platform-gated at all.\n\t\tconst platformGated =\n\t\t\tstringList(dependencyManifest.os) !== undefined ||\n\t\t\tstringList(dependencyManifest.cpu) !== undefined ||\n\t\t\tstringList(dependencyManifest.libc) !== undefined;\n\t\tif (!platformGated) continue;\n\t\tconst file = findBinary(dependencyDir, SCAN_DEPTH);\n\t\tif (file !== undefined)\n\t\t\treturn { kind: \"platformDependency\", dependency, file };\n\t}\n\n\tconst scripts = manifest.scripts;\n\tconst script =\n\t\ttypeof scripts?.install === \"string\"\n\t\t\t? scripts.install\n\t\t\t: typeof scripts?.postinstall === \"string\"\n\t\t\t\t? scripts.postinstall\n\t\t\t\t: undefined;\n\tif (script !== undefined && GYP_PATTERN.test(script))\n\t\treturn { kind: \"buildsFromSource\", script };\n\n\tif (manifest.binary !== undefined) return { kind: \"prebuiltManifest\" };\n\n\treturn undefined;\n}\n\n/** First compiled artifact under `dir`, not descending into a nested `node_modules`. */\nfunction findBinary(dir: string, depth: number): string | undefined {\n\tif (depth < 0) return undefined;\n\tlet listing: Dirent[];\n\ttry {\n\t\tlisting = readdirSync(dir, { withFileTypes: true });\n\t} catch {\n\t\treturn undefined;\n\t}\n\tfor (const entry of listing) {\n\t\tif (entry.isFile() && BINARY_PATTERN.test(entry.name))\n\t\t\treturn entry.name;\n\t}\n\tfor (const entry of listing) {\n\t\tif (!entry.isDirectory() || entry.name === \"node_modules\") continue;\n\t\tconst found = findBinary(join(dir, entry.name), depth - 1);\n\t\tif (found !== undefined) return join(entry.name, found);\n\t}\n\treturn undefined;\n}\n\n/**\n * The report a user sees. States what was found and both ways to act on it, with the\n * shipping form first because it is the one that produces a working function.\n */\nexport function describeNativeFinding(\n\tslug: string,\n\tfinding: NativeFinding,\n): string {\n\tconst { name, evidence } = finding;\n\t// The evidence comes from the tree on *this* machine, so it names the host's build. Said\n\t// out loud, because otherwise a linux-arm64 deploy reporting a darwin binary reads as a\n\t// bug in the tool rather than as a description of the developer's own node_modules.\n\tconst because = (() => {\n\t\tswitch (evidence.kind) {\n\t\t\tcase \"binary\":\n\t\t\t\treturn `it contains a compiled binary (${evidence.file} on this machine; the deploy needs the ${RUNTIME_TARGET_LABEL} build)`;\n\t\t\tcase \"platformDependency\":\n\t\t\t\treturn `it loads a compiled binary from ${evidence.dependency} (${evidence.file} on this machine; the deploy needs the ${RUNTIME_TARGET_LABEL} build)`;\n\t\t\tcase \"buildsFromSource\":\n\t\t\t\treturn `it compiles a binary at install time (${evidence.script})`;\n\t\t\tcase \"prebuiltManifest\":\n\t\t\t\treturn \"its manifest declares a prebuilt binary\";\n\t\t}\n\t})();\n\n\treturn [\n\t\t`Function \"${slug}\" bundles \"${name}\", and ${because}.`,\n\t\t`A compiled binary cannot be bundled, so if this function evaluates that import it`,\n\t\t`fails at invoke rather than at deploy.`,\n\t\t\"\",\n\t\t` Ship its files with the deploy:`,\n\t\t` externalPackages: [\"${name}\"]`,\n\t\t\"\",\n\t\t`If \"${name}\" is only reached behind a working JavaScript fallback, this deploy is`,\n\t\t`already correct and needs no change — this is advisory and never fails a deploy.`,\n\t\t`Do not add includeFiles: false to silence it: that externalizes \"${name}\" and ships`,\n\t\t`nothing for it, so a top-level import of it would then fail on every invoke.`,\n\t].join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//#region src/lib/native-packages.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The architecture, OS and libc a Neon Function runs on. Native packages are installed for
|
|
4
|
+
* this triple rather than for the machine running the deploy, which is usually a macOS or
|
|
5
|
+
* linux-x64 laptop.
|
|
6
|
+
*/
|
|
7
|
+
declare const RUNTIME_TARGET: {
|
|
8
|
+
readonly cpu: "arm64";
|
|
9
|
+
readonly os: "linux";
|
|
10
|
+
readonly libc: "glibc";
|
|
11
|
+
};
|
|
12
|
+
/** Human-readable form of {@link RUNTIME_TARGET}, for error messages. */
|
|
13
|
+
declare const RUNTIME_TARGET_LABEL: string;
|
|
14
|
+
/**
|
|
15
|
+
* Size ceilings the deploy must respect. The build service rejects an archive over these,
|
|
16
|
+
* with an error that surfaces long after the CLI has exited, so they are checked here where
|
|
17
|
+
* the message can name the files responsible.
|
|
18
|
+
*
|
|
19
|
+
* These mirror the service's limits rather than defining them. Passing them in lets a caller
|
|
20
|
+
* track a change on the service side without editing this module.
|
|
21
|
+
*/
|
|
22
|
+
type ArchiveLimits = {
|
|
23
|
+
/** Compressed archive bytes — the ZIP that is uploaded. */
|
|
24
|
+
maxZipBytes: number;
|
|
25
|
+
/** Total uncompressed bytes across every archive entry. */
|
|
26
|
+
maxTotalBytes: number;
|
|
27
|
+
/** Number of archive entries. */
|
|
28
|
+
maxEntries: number;
|
|
29
|
+
};
|
|
30
|
+
declare const DEFAULT_ARCHIVE_LIMITS: ArchiveLimits;
|
|
31
|
+
/** Injectable side effects, so the tracing logic is testable without a registry or npm. */
|
|
32
|
+
type NativeTraceDeps = {
|
|
33
|
+
/** Install `specs` for the runtime target into `cwd`. Resolves on success. */
|
|
34
|
+
install: (cwd: string, specs: string[]) => Promise<void>;
|
|
35
|
+
/** Trace `entry` within `base`, returning archive-relative file paths. */
|
|
36
|
+
/**
|
|
37
|
+
* Trace `entry` within `base`.
|
|
38
|
+
*
|
|
39
|
+
* `files` are archive-relative paths. `unresolved` are the specifiers the tracer could
|
|
40
|
+
* not follow, which are classified rather than reported wholesale — see
|
|
41
|
+
* {@link concreteUnresolved}.
|
|
42
|
+
*/
|
|
43
|
+
trace: (base: string, entry: string) => Promise<{
|
|
44
|
+
files: string[];
|
|
45
|
+
unresolved?: string[];
|
|
46
|
+
}>;
|
|
47
|
+
/** Version of `pkg` as resolved in the user's project, or undefined if not installed. */
|
|
48
|
+
installedVersion: (projectDir: string, pkg: string) => string | undefined;
|
|
49
|
+
};
|
|
50
|
+
/** Files to place in the archive under `node_modules/`, keyed by archive-relative path. */
|
|
51
|
+
type NativeTraceResult = {
|
|
52
|
+
entries: Record<string, Uint8Array>;
|
|
53
|
+
/**
|
|
54
|
+
* Advisory findings from the staging run — currently a relative import the tracer could
|
|
55
|
+
* not resolve, meaning that file will not be in the archive.
|
|
56
|
+
*
|
|
57
|
+
* The caller must surface these. They are advisory rather than fatal because the tracer
|
|
58
|
+
* cannot tell a required import from one wrapped in a try/catch, but an unreported one is
|
|
59
|
+
* a function that deploys clean and fails at invoke.
|
|
60
|
+
*/
|
|
61
|
+
warnings: string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Collect the files the declared native packages need, ready to be merged into the archive.
|
|
65
|
+
*
|
|
66
|
+
* Three steps, in this order for reasons that are not interchangeable:
|
|
67
|
+
*
|
|
68
|
+
* 1. **Install for the runtime target into a throwaway directory.** The user's own
|
|
69
|
+
* `node_modules` is never read for these files. Its binaries are built for their machine,
|
|
70
|
+
* and a cross-platform install is not sticky — a later plain `npm install` re-resolves
|
|
71
|
+
* optional dependencies for the host and replaces the target's packages with the host's.
|
|
72
|
+
* Only the resolved *version* of the declared package is taken from the user's tree. That
|
|
73
|
+
* is not the same as honouring their lockfile: transitive pins, overrides, patches, and
|
|
74
|
+
* aliases are not carried across, so the staged graph can differ below the root.
|
|
75
|
+
* 2. **Trace the installed tree.** The file set cannot be discovered by asking Node:
|
|
76
|
+
* `@img/sharp-linux-arm64` exports only `./sharp.node` and `./package`, with no `.` and no
|
|
77
|
+
* wildcard, so its directory is unreachable through the resolver. A tracer that follows
|
|
78
|
+
* `createRequire` is required — which is also why esbuild cannot see these files, and why
|
|
79
|
+
* `sharp` bundles cleanly today and then fails at invoke.
|
|
80
|
+
* 3. **Copy the traced files, preserving the `node_modules` layout.** A `.node` addon locates
|
|
81
|
+
* its sibling shared libraries by a path relative to its own directory, so flattening the
|
|
82
|
+
* tree breaks loading even though every file is present.
|
|
83
|
+
*/
|
|
84
|
+
declare function traceNativePackages(options: {
|
|
85
|
+
slug: string;
|
|
86
|
+
packages: readonly string[];
|
|
87
|
+
projectDir: string;
|
|
88
|
+
limits?: ArchiveLimits;
|
|
89
|
+
deps?: Partial<NativeTraceDeps>;
|
|
90
|
+
}): Promise<NativeTraceResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Check the archive against the build service's limits, naming the largest contributors. The
|
|
93
|
+
* compressed size is not known until the archive is zipped, so only the counts and
|
|
94
|
+
* uncompressed total are checked here; {@link assertZipWithinLimits} covers the rest.
|
|
95
|
+
*
|
|
96
|
+
* Exported so the caller can re-check the **final** archive: these entries are the staged
|
|
97
|
+
* files alone, and the bundle is merged in afterwards.
|
|
98
|
+
*/
|
|
99
|
+
declare function enforceLimits(slug: string, entries: Record<string, Uint8Array>, limits?: ArchiveLimits): void;
|
|
100
|
+
/**
|
|
101
|
+
* Check the compressed archive, which is only measurable once it exists. Separate from
|
|
102
|
+
* {@link enforceLimits} for that reason, and exported so the bundler can call it on the
|
|
103
|
+
* finished ZIP.
|
|
104
|
+
*/
|
|
105
|
+
declare function assertZipWithinLimits(slug: string, zip: Uint8Array, entries: Record<string, Uint8Array>, limits?: ArchiveLimits): void;
|
|
106
|
+
//#endregion
|
|
107
|
+
export { ArchiveLimits, DEFAULT_ARCHIVE_LIMITS, NativeTraceDeps, NativeTraceResult, RUNTIME_TARGET, RUNTIME_TARGET_LABEL, assertZipWithinLimits, enforceLimits, traceNativePackages };
|
|
108
|
+
//# sourceMappingURL=native-packages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-packages.d.ts","names":[],"sources":["../../src/lib/native-packages.ts"],"mappings":";;AAqBA;AAOA;AAwLA;AASA;AAOY,cA/MC,cA+Mc,EAAA;EAAA,SAAA,GAAA,EAAA,OAAA;EAEiB,SAAA,EAAA,EAAA,OAAA;EAYtC,SAAA,IAAA,EAAA,OAAA;AAAO,CAAA;AAMb;AAA6B,cA5NhB,oBA4NgB,EAAA,MAAA;AACJ;AAAf;AAAM;AA0ChB;AAAyC;AAI/B;AACM;AAAR;AACI,KArFA,aAAA,GAqFA;EAAR;EAAO,WAAA,EAAA,MAAA;EAwRK;EAAa,aAAA,EAAA,MAAA;EAEJ;EAAf,UAAA,EAAA,MAAA;AACD,CAAA;AAAsC,cAvWlC,sBAuWkC,EAvWV,aAuWU;AAyB/C;AAAqC,KAzXzB,eAAA,GAyXyB;EAE/B;EACmB,OAAA,EAAA,CAAA,GAAA,EAAA,MAAA,EAAA,KAAA,EAAA,MAAA,EAAA,EAAA,GA1XmB,OA0XnB,CAAA,IAAA,CAAA;EAAf;EACD;AAAsC;;;;;;0CA/WzC;;;;;;;;KAMM,iBAAA;WACF,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CH,mBAAA;;;;WAIZ;SACF,QAAQ;IACZ,QAAQ;;;;;;;;;iBAwRI,aAAA,wBAEN,eAAe,sBAChB;;;;;;iBAyBO,qBAAA,oBAEV,qBACI,eAAe,sBAChB"}
|