@netlify/edge-bundler 15.1.0 → 16.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/dist/node/bundler.d.ts +3 -0
- package/dist/node/bundler.js +100 -28
- package/dist/node/config.d.ts +9 -0
- package/dist/node/config.js +30 -2
- package/dist/node/feature_flags.d.ts +2 -0
- package/dist/node/feature_flags.js +1 -0
- package/dist/node/formats/tarball.d.ts +6 -0
- package/dist/node/formats/tarball.js +27 -9
- package/dist/node/utils/transient_error.d.ts +3 -0
- package/dist/node/utils/transient_error.js +20 -0
- package/package.json +2 -2
package/dist/node/bundler.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export interface BundleOptions {
|
|
|
21
21
|
vendorDirectory?: string;
|
|
22
22
|
}
|
|
23
23
|
export declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, internalSrcFolder, onAfterDownload, onBeforeDownload, rootPath, userLogger, systemLogger, vendorDirectory, }?: BundleOptions) => Promise<{
|
|
24
|
+
functions: EdgeFunction[];
|
|
25
|
+
manifest: undefined;
|
|
26
|
+
} | {
|
|
24
27
|
functions: EdgeFunction[];
|
|
25
28
|
manifest: import("./manifest.js").Manifest;
|
|
26
29
|
}>;
|
package/dist/node/bundler.js
CHANGED
|
@@ -64,6 +64,79 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
|
|
|
64
64
|
if (vendor) {
|
|
65
65
|
importMap.add(vendor.importMap);
|
|
66
66
|
}
|
|
67
|
+
// Extracts the in-source configuration for each function and derives the
|
|
68
|
+
// manifest's function config and routes from it.
|
|
69
|
+
const computeManifestData = async (log) => {
|
|
70
|
+
const { internalFunctions: internalFunctionsWithConfig, userFunctions: userFunctionsWithConfig } = await getFunctionConfigs({
|
|
71
|
+
deno,
|
|
72
|
+
importMap,
|
|
73
|
+
internalFunctions,
|
|
74
|
+
log,
|
|
75
|
+
userFunctions,
|
|
76
|
+
});
|
|
77
|
+
// Creating a final declarations array by combining the TOML file with the
|
|
78
|
+
// deploy configuration API and the in-source configuration.
|
|
79
|
+
const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations, featureFlags);
|
|
80
|
+
const internalFunctionConfig = createFunctionConfig({
|
|
81
|
+
internalFunctionsWithConfig,
|
|
82
|
+
declarations,
|
|
83
|
+
});
|
|
84
|
+
const manifestFunctionConfig = generateManifestFunctionConfig({
|
|
85
|
+
functions,
|
|
86
|
+
internalFunctionConfig,
|
|
87
|
+
userFunctionConfig: userFunctionsWithConfig,
|
|
88
|
+
});
|
|
89
|
+
const manifestRoutes = generateManifestRoutes({
|
|
90
|
+
functions,
|
|
91
|
+
declarations,
|
|
92
|
+
});
|
|
93
|
+
return { manifestFunctionConfig, manifestRoutes };
|
|
94
|
+
};
|
|
95
|
+
const excludeUnroutedFunctions = Boolean(featureFlags.edge_bundler_exclude_unrouted_functions);
|
|
96
|
+
// Excluding unrouted functions is the only reason to extract the config
|
|
97
|
+
// before bundling - we need the routes to know what to leave out. When we're
|
|
98
|
+
// not, we keep extracting it after bundling, as we always have, so that the
|
|
99
|
+
// extra Deno subprocess never runs ahead of the bundle and can't change what
|
|
100
|
+
// a build failure looks like.
|
|
101
|
+
//
|
|
102
|
+
// Config extraction reports the underlying failure to the user before it
|
|
103
|
+
// throws. When it runs up front we may end up discarding that error in favour
|
|
104
|
+
// of a bundling error (see below), and printing the detail of an error we
|
|
105
|
+
// then discard is just noise - so on that path we hold those logs back and
|
|
106
|
+
// only flush them if we do surface the config error.
|
|
107
|
+
const heldUserLogs = [];
|
|
108
|
+
const configLogger = {
|
|
109
|
+
system: logger.system,
|
|
110
|
+
user: (...args) => {
|
|
111
|
+
heldUserLogs.push(args);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const manifestDataPromise = excludeUnroutedFunctions ? computeManifestData(configLogger) : undefined;
|
|
115
|
+
// A function with no route is never invoked, so bundling it only serves to
|
|
116
|
+
// eagerly load code that can never run - which can surface import-time
|
|
117
|
+
// failures. The manifest is still generated from the full set of functions,
|
|
118
|
+
// but its routes exclude unrouted functions by definition. If config
|
|
119
|
+
// extraction failed, we have no routes to go on, so we bundle everything and
|
|
120
|
+
// let bundling report the failure.
|
|
121
|
+
const routes = manifestDataPromise ? (await manifestDataPromise.catch(() => undefined))?.manifestRoutes : undefined;
|
|
122
|
+
const bundledFunctions = excludeUnroutedFunctions && routes
|
|
123
|
+
? functions.filter(({ name }) => !routes.unroutedFunctions.includes(name))
|
|
124
|
+
: functions;
|
|
125
|
+
// With nothing to bundle there is no code that can ever be invoked, so we
|
|
126
|
+
// produce neither a bundle nor a manifest. The deploy pipeline treats a
|
|
127
|
+
// manifest that exists but carries no ESZIP bundle as the deprecated JS format
|
|
128
|
+
// and can reject the deploy, so a missing manifest - the same path as a site
|
|
129
|
+
// with no edge functions at all - is safer than an empty one.
|
|
130
|
+
if (bundledFunctions.length === 0) {
|
|
131
|
+
// Config extraction may have produced function output that we were holding
|
|
132
|
+
// back; surface it before returning.
|
|
133
|
+
heldUserLogs.forEach((args) => {
|
|
134
|
+
logger.user(...args);
|
|
135
|
+
});
|
|
136
|
+
logger.system('Skipping bundling because no edge function has a route.');
|
|
137
|
+
await vendor?.cleanup();
|
|
138
|
+
return { functions, manifest: undefined };
|
|
139
|
+
}
|
|
67
140
|
const bundles = [];
|
|
68
141
|
let tarballBundleDurationMs;
|
|
69
142
|
let tarballDryRunError;
|
|
@@ -78,7 +151,7 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
|
|
|
78
151
|
debug,
|
|
79
152
|
deno,
|
|
80
153
|
distDirectory,
|
|
81
|
-
functions,
|
|
154
|
+
functions: bundledFunctions,
|
|
82
155
|
featureFlags,
|
|
83
156
|
importMap: importMap.clone(),
|
|
84
157
|
vendorDirectory: vendor?.directory,
|
|
@@ -109,34 +182,28 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
|
|
|
109
182
|
deno,
|
|
110
183
|
distDirectory,
|
|
111
184
|
externals,
|
|
112
|
-
functions,
|
|
185
|
+
functions: bundledFunctions,
|
|
113
186
|
featureFlags,
|
|
114
187
|
importMap,
|
|
115
188
|
vendorDirectory: vendor?.directory,
|
|
116
189
|
}));
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
userFunctionConfig: userFunctionsWithConfig,
|
|
135
|
-
});
|
|
136
|
-
const manifestRoutes = generateManifestRoutes({
|
|
137
|
-
functions,
|
|
138
|
-
declarations,
|
|
139
|
-
});
|
|
190
|
+
// When we extracted the config up front, bundling didn't fail, so no more
|
|
191
|
+
// precise error is coming - re-awaiting surfaces the config extraction error
|
|
192
|
+
// if there was one. When we didn't, we extract it now, after bundling, as we
|
|
193
|
+
// always have. Either way, we reach this point only once bundling has had its
|
|
194
|
+
// chance to throw, so any logs we held back are no longer masking a bundling
|
|
195
|
+
// error and should reach the user: on success they are the function's own
|
|
196
|
+
// output, on failure the detail of the error we are about to surface.
|
|
197
|
+
let manifestData;
|
|
198
|
+
try {
|
|
199
|
+
manifestData = await (manifestDataPromise ?? computeManifestData(logger));
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
heldUserLogs.forEach((args) => {
|
|
203
|
+
logger.user(...args);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const { manifestFunctionConfig, manifestRoutes } = manifestData;
|
|
140
207
|
if (!tarballDryRunError && finalizeTarballBundle) {
|
|
141
208
|
try {
|
|
142
209
|
bundles.unshift(await finalizeTarballBundle({ manifestFunctionConfig, manifestRoutes }));
|
|
@@ -255,11 +322,16 @@ const getBasePath = (sourceDirectories, inputBasePath) => {
|
|
|
255
322
|
// declaration level. We want these properties to live at the function level
|
|
256
323
|
// in their config object, so we translate that for backwards-compatibility.
|
|
257
324
|
const mergeWithDeclarationConfig = ({ functionName, config, declarations }) => {
|
|
258
|
-
|
|
325
|
+
// A function may have several declarations — for example, one synthesized
|
|
326
|
+
// from its in-source config and one coming from `netlify.toml` or the
|
|
327
|
+
// Frameworks API. Only the latter can carry `name` and `generator`, so we
|
|
328
|
+
// look for each field across all of the function's declarations rather than
|
|
329
|
+
// just the first one, which may well be the in-source one.
|
|
330
|
+
const functionDeclarations = declarations?.filter((decl) => decl.function === functionName) ?? [];
|
|
259
331
|
return {
|
|
260
332
|
...config,
|
|
261
|
-
name:
|
|
262
|
-
generator:
|
|
333
|
+
name: functionDeclarations.find((decl) => decl.name)?.name || config.name,
|
|
334
|
+
generator: functionDeclarations.find((decl) => decl.generator)?.generator || config.generator,
|
|
263
335
|
};
|
|
264
336
|
};
|
|
265
337
|
const addGeneratorFallback = (config) => ({
|
package/dist/node/config.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ import { DenoBridge } from './bridge.js';
|
|
|
2
2
|
import { ImportMap } from './import_map.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
import { RateLimit } from './rate_limit.js';
|
|
5
|
+
export declare enum ConfigExitCode {
|
|
6
|
+
Success = 0,
|
|
7
|
+
UnhandledError = 1,
|
|
8
|
+
ImportError = 2,
|
|
9
|
+
NoConfig = 3,
|
|
10
|
+
InvalidExport = 4,
|
|
11
|
+
SerializationError = 5,
|
|
12
|
+
InvalidDefaultExport = 6
|
|
13
|
+
}
|
|
5
14
|
export declare const enum Cache {
|
|
6
15
|
Off = "off",
|
|
7
16
|
Manual = "manual"
|
package/dist/node/config.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
+
import pRetry from 'p-retry';
|
|
4
5
|
import { SemVer } from 'semver';
|
|
5
6
|
import tmp from 'tmp-promise';
|
|
6
7
|
import { BundleError } from './bundle_error.js';
|
|
7
8
|
import { getPackagePath } from './package_json.js';
|
|
8
|
-
|
|
9
|
+
import { DENO_RETRIES, isTransientDenoError } from './utils/transient_error.js';
|
|
10
|
+
export var ConfigExitCode;
|
|
9
11
|
(function (ConfigExitCode) {
|
|
10
12
|
ConfigExitCode[ConfigExitCode["Success"] = 0] = "Success";
|
|
11
13
|
ConfigExitCode[ConfigExitCode["UnhandledError"] = 1] = "UnhandledError";
|
|
@@ -43,7 +45,7 @@ export const getFunctionConfig = async ({ deno, functionPath, importMap, log, })
|
|
|
43
45
|
// The extractor will use its exit code to signal different error scenarios,
|
|
44
46
|
// based on the list of exit codes we send as an argument. We then capture
|
|
45
47
|
// the exit code to know exactly what happened and guide people accordingly.
|
|
46
|
-
const
|
|
48
|
+
const runExtractor = () => deno.run([
|
|
47
49
|
'run',
|
|
48
50
|
'--allow-env',
|
|
49
51
|
version.major >= 2 ? '--allow-import' : '',
|
|
@@ -59,6 +61,32 @@ export const getFunctionConfig = async ({ deno, functionPath, importMap, log, })
|
|
|
59
61
|
pathToFileURL(collector.path).href,
|
|
60
62
|
JSON.stringify(ConfigExitCode),
|
|
61
63
|
].filter(Boolean), { rejectOnExitCode: false });
|
|
64
|
+
// The extractor imports the function, so a dropped connection while fetching a remote module
|
|
65
|
+
// looks exactly like an import error. Because we're not rejecting on the exit code, we have to
|
|
66
|
+
// read the transient failure out of `stderr` ourselves and throw so that `pRetry` has another go.
|
|
67
|
+
// If every attempt fails we fall through with the last result, leaving the error handling below
|
|
68
|
+
// to report it as it would have without any retries.
|
|
69
|
+
let lastResult;
|
|
70
|
+
const { exitCode, stderr, stdout } = await pRetry(async () => {
|
|
71
|
+
lastResult = await runExtractor();
|
|
72
|
+
if (lastResult.exitCode !== ConfigExitCode.Success && isTransientDenoError(lastResult.stderr)) {
|
|
73
|
+
throw new Error(lastResult.stderr);
|
|
74
|
+
}
|
|
75
|
+
return lastResult;
|
|
76
|
+
}, {
|
|
77
|
+
retries: DENO_RETRIES,
|
|
78
|
+
onFailedAttempt: (error) => {
|
|
79
|
+
log.system(`Config extraction for '${functionPath}' hit a transient error, retrying: ${error.message}`);
|
|
80
|
+
},
|
|
81
|
+
}).catch((error) => {
|
|
82
|
+
// If we never captured a result, the extractor rejected outright rather than resolving with a
|
|
83
|
+
// non-zero exit code (e.g. the binary failed to spawn). Surface that error instead of falling
|
|
84
|
+
// through to destructure `undefined`.
|
|
85
|
+
if (lastResult === undefined) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
return lastResult;
|
|
89
|
+
});
|
|
62
90
|
if (exitCode !== ConfigExitCode.Success) {
|
|
63
91
|
handleConfigError(functionPath, exitCode, stderr, log);
|
|
64
92
|
return {};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
declare const defaultFlags: {
|
|
2
2
|
edge_bundler_generate_tarball: boolean;
|
|
3
3
|
edge_bundler_dry_run_generate_tarball: boolean;
|
|
4
|
+
edge_bundler_exclude_unrouted_functions: boolean;
|
|
4
5
|
};
|
|
5
6
|
type FeatureFlag = keyof typeof defaultFlags;
|
|
6
7
|
type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
|
|
7
8
|
declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
8
9
|
edge_bundler_generate_tarball: boolean;
|
|
9
10
|
edge_bundler_dry_run_generate_tarball: boolean;
|
|
11
|
+
edge_bundler_exclude_unrouted_functions: boolean;
|
|
10
12
|
}) => FeatureFlags;
|
|
11
13
|
export { defaultFlags, getFlags };
|
|
12
14
|
export type { FeatureFlag, FeatureFlags };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const defaultFlags = {
|
|
2
2
|
edge_bundler_generate_tarball: false,
|
|
3
3
|
edge_bundler_dry_run_generate_tarball: false,
|
|
4
|
+
edge_bundler_exclude_unrouted_functions: false,
|
|
4
5
|
};
|
|
5
6
|
const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
|
|
6
7
|
...result,
|
|
@@ -21,6 +21,12 @@ interface FinalizeTarballBundleOptions {
|
|
|
21
21
|
manifestRoutes: ReturnType<typeof generateManifestRoutes>;
|
|
22
22
|
}
|
|
23
23
|
export declare const bundle: ({ buildID, deno, distDirectory, functions, importMap, vendorDirectory, }: BundleTarballOptions) => Promise<(arg: FinalizeTarballBundleOptions) => Promise<Bundle>>;
|
|
24
|
+
/**
|
|
25
|
+
* Uses deno info to get the module graph and extract only the local source files
|
|
26
|
+
* that are actually needed by the entry points. This avoids copying unnecessary
|
|
27
|
+
* files (like node_modules, .next, etc.) that may be under the common path.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getRequiredSourceFiles(deno: DenoBridge, entryPoints: string[], importMap: ImportMap): Promise<Set<string>>;
|
|
24
30
|
/**
|
|
25
31
|
* Rewrites import assert into import with in the bundle directory
|
|
26
32
|
* Defaults to copying the file in its current form
|
|
@@ -2,11 +2,13 @@ import { promises as fs, existsSync } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
4
|
import commonPathPrefix from 'common-path-prefix';
|
|
5
|
+
import pRetry, { AbortError } from 'p-retry';
|
|
5
6
|
import * as tar from 'tar';
|
|
6
7
|
import tmp from 'tmp-promise';
|
|
7
8
|
import { BundleFormat } from '../bundle.js';
|
|
8
9
|
import { listRecursively } from '../utils/fs.js';
|
|
9
10
|
import { getFileHash } from '../utils/sha256.js';
|
|
11
|
+
import { DENO_RETRIES, isTransientDenoErrorObject } from '../utils/transient_error.js';
|
|
10
12
|
import { rewriteSourceImportAssertions } from '../utils/import_attributes.js';
|
|
11
13
|
const TARBALL_EXTENSION = '.tar.gz';
|
|
12
14
|
const getUnixPath = (input) => input.split(path.sep).join('/');
|
|
@@ -154,19 +156,35 @@ const REWRITABLE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.m
|
|
|
154
156
|
* that are actually needed by the entry points. This avoids copying unnecessary
|
|
155
157
|
* files (like node_modules, .next, etc.) that may be under the common path.
|
|
156
158
|
*/
|
|
157
|
-
async function getRequiredSourceFiles(deno, entryPoints, importMap) {
|
|
159
|
+
export async function getRequiredSourceFiles(deno, entryPoints, importMap) {
|
|
158
160
|
const localFiles = new Set();
|
|
159
161
|
const importMapDataUrl = importMap.withNodeBuiltins().toDataURL();
|
|
160
162
|
// Run deno info for each entry point and combine the results
|
|
161
163
|
for (const entryPoint of entryPoints) {
|
|
162
|
-
const { stdout } = await
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
const { stdout } = await pRetry(async () => {
|
|
165
|
+
try {
|
|
166
|
+
return await deno.run([
|
|
167
|
+
'info',
|
|
168
|
+
'--json',
|
|
169
|
+
'--no-config',
|
|
170
|
+
'--import-map',
|
|
171
|
+
importMapDataUrl,
|
|
172
|
+
pathToFileURL(entryPoint).href,
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (isTransientDenoErrorObject(error)) {
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
// `AbortError` stops the retry loop and rethrows the error we pass it, unchanged.
|
|
180
|
+
throw new AbortError(error instanceof Error ? error : new Error(String(error)));
|
|
181
|
+
}
|
|
182
|
+
}, {
|
|
183
|
+
retries: DENO_RETRIES,
|
|
184
|
+
onFailedAttempt: (error) => {
|
|
185
|
+
deno.logger.system(`\`deno info\` failed with a transient error, retrying: ${error.message}`);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
170
188
|
const graph = JSON.parse(stdout);
|
|
171
189
|
// Collect the specifiers of every module reachable through a *code* (runtime)
|
|
172
190
|
// import edge. Deno's module graph classifies each dependency as either a `code`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Deno subprocesses fetch remote modules and npm registry metadata, so they can fail for reasons
|
|
2
|
+
// that have nothing to do with the user's code: a dropped connection, a DNS blip, a registry
|
|
3
|
+
// timeout. These are the messages Deno surfaces for those failures. Anything else (a syntax error,
|
|
4
|
+
// an unresolvable import) is deterministic, and retrying it just delays the real error.
|
|
5
|
+
const TRANSIENT_DENO_ERRORS = [
|
|
6
|
+
'error sending request',
|
|
7
|
+
'error reading a body from connection',
|
|
8
|
+
'connection closed before message completed',
|
|
9
|
+
'connection reset',
|
|
10
|
+
'operation timed out',
|
|
11
|
+
'tcp connect error',
|
|
12
|
+
'dns error',
|
|
13
|
+
];
|
|
14
|
+
export const isTransientDenoError = (output) => {
|
|
15
|
+
const haystack = output.toLowerCase();
|
|
16
|
+
return TRANSIENT_DENO_ERRORS.some((pattern) => haystack.includes(pattern));
|
|
17
|
+
};
|
|
18
|
+
// execa folds stderr into `message`, which is where Deno writes the `Caused by:` detail.
|
|
19
|
+
export const isTransientDenoErrorObject = (error) => error instanceof Error && isTransientDenoError(error.message);
|
|
20
|
+
export const DENO_RETRIES = 3;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "16.0.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"tmp-promise": "^3.0.3",
|
|
81
81
|
"urlpattern-polyfill": "8.0.2"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "af94ccf9fe17c7b2c653eaad23cc413283548d1a"
|
|
84
84
|
}
|