@netlify/edge-bundler 14.9.18 → 14.10.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.
@@ -13,7 +13,7 @@ import { bundle as bundleESZIP } from './formats/eszip.js';
13
13
  import { bundle as bundleTarball } from './formats/tarball.js';
14
14
  import { ImportMap } from './import_map.js';
15
15
  import { getLogger } from './logger.js';
16
- import { writeManifest } from './manifest.js';
16
+ import { generateManifestFunctionConfig, generateManifestRoutes, writeManifest } from './manifest.js';
17
17
  import { vendorNPMSpecifiers } from './npm_dependencies.js';
18
18
  import { ensureLatestTypes } from './types.js';
19
19
  import { nonNullable } from './utils/non_nullable.js';
@@ -66,8 +66,10 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
66
66
  }
67
67
  const bundles = [];
68
68
  let tarballBundleDurationMs;
69
+ let tarballDryRunError;
70
+ let finalizeTarballBundle;
69
71
  if (featureFlags.edge_bundler_generate_tarball || featureFlags.edge_bundler_dry_run_generate_tarball) {
70
- const tarballPromise = (async () => {
72
+ const tarballInitialPromise = (async () => {
71
73
  const start = Date.now();
72
74
  try {
73
75
  return await bundleTarball({
@@ -89,21 +91,15 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
89
91
  let tarballPromiseResolved = false;
90
92
  if (featureFlags.edge_bundler_dry_run_generate_tarball) {
91
93
  try {
92
- await tarballPromise;
93
- logger.system('Dry run: Tarball bundle generated successfully.');
94
+ await tarballInitialPromise;
94
95
  tarballPromiseResolved = true;
95
96
  }
96
97
  catch (error) {
97
- if (error instanceof Error) {
98
- logger.system(`Dry run: Tarball bundle generation failed: ${error.message}`);
99
- }
100
- else {
101
- logger.system(`Dry run: Tarball bundle generation failed: ${String(error)}`);
102
- }
98
+ tarballDryRunError = error ?? new Error('Unknown error during tarball bundle generation');
103
99
  }
104
100
  }
105
101
  if (featureFlags.edge_bundler_generate_tarball || tarballPromiseResolved) {
106
- bundles.push(await tarballPromise);
102
+ finalizeTarballBundle = await tarballInitialPromise;
107
103
  }
108
104
  }
109
105
  bundles.push(await bundleESZIP({
@@ -118,10 +114,6 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
118
114
  importMap,
119
115
  vendorDirectory: vendor?.directory,
120
116
  }));
121
- // The final file name of the bundles contains a SHA256 hash of the contents,
122
- // which we can only compute now that the files have been generated. So let's
123
- // rename the bundles to their permanent names.
124
- await createFinalBundles(bundles, distDirectory, buildID);
125
117
  const { internalFunctions: internalFunctionsWithConfig, userFunctions: userFunctionsWithConfig } = await getFunctionConfigs({
126
118
  deno,
127
119
  importMap,
@@ -136,14 +128,59 @@ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations
136
128
  internalFunctionsWithConfig,
137
129
  declarations,
138
130
  });
131
+ const manifestFunctionConfig = generateManifestFunctionConfig({
132
+ functions,
133
+ internalFunctionConfig,
134
+ userFunctionConfig: userFunctionsWithConfig,
135
+ });
136
+ const manifestRoutes = generateManifestRoutes({
137
+ functions,
138
+ declarations,
139
+ });
140
+ if (!tarballDryRunError && finalizeTarballBundle) {
141
+ try {
142
+ bundles.unshift(await finalizeTarballBundle({ manifestFunctionConfig, manifestRoutes }));
143
+ }
144
+ catch (error) {
145
+ if (featureFlags.edge_bundler_dry_run_generate_tarball) {
146
+ tarballDryRunError = error ?? new Error('Unknown error during tarball bundle finalization');
147
+ }
148
+ else {
149
+ throw error;
150
+ }
151
+ }
152
+ }
153
+ if (featureFlags.edge_bundler_dry_run_generate_tarball) {
154
+ let tarballLogMsg;
155
+ if (tarballDryRunError) {
156
+ if (tarballDryRunError instanceof Error) {
157
+ tarballLogMsg = `Dry run: Eszip successful, tarball bundle generation failed: ${tarballDryRunError.message}`;
158
+ }
159
+ else {
160
+ tarballLogMsg = `Dry run: Eszip successful, tarball bundle generation failed: ${String(tarballDryRunError)}`;
161
+ }
162
+ }
163
+ else {
164
+ tarballLogMsg = 'Dry run: Eszip and tarball bundle generated successfully.';
165
+ }
166
+ if (tarballLogMsg) {
167
+ // Log tarball generation status after eszip bundling succeeds (only set during dry runs).
168
+ // Reported errors might be multiple lines, so we replace newlines with the literal string '\n' to get a single log line,
169
+ // while still ensuring it could be expanded into the original multi-line message if needed.
170
+ logger.system(tarballLogMsg.replaceAll('\n', '\\n'));
171
+ }
172
+ }
173
+ // The final file name of the bundles contains a SHA256 hash of the contents,
174
+ // which we can only compute now that the files have been generated. So let's
175
+ // rename the bundles to their permanent names.
176
+ await createFinalBundles(bundles, distDirectory, buildID);
139
177
  const manifest = await writeManifest({
140
178
  bundles,
141
- declarations,
142
179
  distDirectory,
143
180
  featureFlags,
144
181
  functions,
145
- userFunctionConfig: userFunctionsWithConfig,
146
- internalFunctionConfig,
182
+ manifestFunctionConfig,
183
+ manifestRoutes,
147
184
  importMap: importMapSpecifier,
148
185
  layers: deployConfig.layers,
149
186
  bundlingTiming: tarballBundleDurationMs === undefined ? undefined : { tarball_ms: tarballBundleDurationMs },
@@ -3,6 +3,8 @@ import { Bundle } from '../bundle.js';
3
3
  import { EdgeFunction } from '../edge_function.js';
4
4
  import { FeatureFlags } from '../feature_flags.js';
5
5
  import { ImportMap } from '../import_map.js';
6
+ import { EdgeFunctionConfig } from '../index.js';
7
+ import { generateManifestRoutes } from '../manifest.js';
6
8
  interface BundleTarballOptions {
7
9
  basePath: string;
8
10
  buildID: string;
@@ -14,7 +16,11 @@ interface BundleTarballOptions {
14
16
  importMap: ImportMap;
15
17
  vendorDirectory?: string;
16
18
  }
17
- export declare const bundle: ({ buildID, deno, distDirectory, functions, importMap, vendorDirectory, }: BundleTarballOptions) => Promise<Bundle>;
19
+ interface FinalizeTarballBundleOptions {
20
+ manifestFunctionConfig: Record<string, EdgeFunctionConfig>;
21
+ manifestRoutes: ReturnType<typeof generateManifestRoutes>;
22
+ }
23
+ export declare const bundle: ({ buildID, deno, distDirectory, functions, importMap, vendorDirectory, }: BundleTarballOptions) => Promise<(arg: FinalizeTarballBundleOptions) => Promise<Bundle>>;
18
24
  /**
19
25
  * Rewrites import assert into import with in the bundle directory
20
26
  * Defaults to copying the file in its current form
@@ -13,9 +13,9 @@ const getUnixPath = (input) => input.split(path.sep).join('/');
13
13
  export const bundle = async ({ buildID, deno, distDirectory, functions, importMap, vendorDirectory, }) => {
14
14
  const bundleDir = await tmp.dir({ unsafeCleanup: true });
15
15
  const cleanup = [bundleDir.cleanup];
16
- const manifest = {
16
+ const initialManifest = {
17
17
  functions: {},
18
- version: 1,
18
+ version: 2,
19
19
  };
20
20
  const entryPoints = functions.map((func) => func.path);
21
21
  // Use deno info to get the module graph and identify which local files are actually needed.
@@ -24,6 +24,7 @@ export const bundle = async ({ buildID, deno, distDirectory, functions, importMa
24
24
  // Build prefix mappings to transform file:// URLs to relative paths
25
25
  const npmVendorDir = '.netlify-npm-vendor';
26
26
  const prefixes = {};
27
+ const additionalImportMapEntries = {};
27
28
  // Copy pre-bundled npm modules from vendorDirectory if present.
28
29
  // This supports the legacy approach where npm packages are pre-bundled and mapped
29
30
  // via import map. Modern code could use npm: specifiers instead, which Deno handles
@@ -51,10 +52,18 @@ export const bundle = async ({ buildID, deno, distDirectory, functions, importMa
51
52
  // Build the manifest mapping function names to their relative paths
52
53
  for (const func of functions) {
53
54
  const relativePath = path.relative(commonPath, func.path);
54
- manifest.functions[func.name] = getUnixPath(relativePath);
55
+ initialManifest.functions[func.name] = getUnixPath(relativePath);
55
56
  }
56
57
  for (const sourceFile of sourceFilesSet) {
57
- const relativePath = path.relative(commonPath, sourceFile);
58
+ let relativePath = path.relative(commonPath, sourceFile);
59
+ if (relativePath.startsWith('vendor' + path.sep)) {
60
+ // root vendor directory is reserved directory and can't be imported directly from with `vendor: true` or `--vendor` flag
61
+ // move from vendor/ to .root-vendor/
62
+ relativePath = relativePath.replace(/vendor[\\/]/, `.root-vendor/`);
63
+ // and import map rewrite so imports remain resolvable
64
+ additionalImportMapEntries['./vendor/'] = `./.root-vendor/`;
65
+ prefixes[pathToFileURL(path.join(commonPath, 'vendor') + path.sep).href] = './.root-vendor/';
66
+ }
58
67
  const destPath = path.join(bundleDir.path, relativePath);
59
68
  await fs.mkdir(path.dirname(destPath), { recursive: true });
60
69
  // Rewrite import assertions in user files
@@ -63,7 +72,7 @@ export const bundle = async ({ buildID, deno, distDirectory, functions, importMa
63
72
  // Map common path to relative paths
64
73
  prefixes[pathToFileURL(commonPath + path.sep).href] = './';
65
74
  // Get import map contents with file:// URLs transformed to relative paths
66
- const importMapContents = importMap.getContents(prefixes);
75
+ const importMapContents = importMap.getContents(prefixes, additionalImportMapEntries);
67
76
  // Create deno.json with import map contents for runtime resolution
68
77
  const denoConfigPath = path.join(bundleDir.path, 'deno.json');
69
78
  const denoConfigContents = JSON.stringify(importMapContents, null, 2);
@@ -78,7 +87,7 @@ export const bundle = async ({ buildID, deno, distDirectory, functions, importMa
78
87
  '--node-modules-dir=manual',
79
88
  '--vendor',
80
89
  '--entrypoint',
81
- ...Object.values(manifest.functions),
90
+ ...Object.values(initialManifest.functions),
82
91
  ], {
83
92
  cwd: bundleDir.path,
84
93
  });
@@ -90,37 +99,49 @@ export const bundle = async ({ buildID, deno, distDirectory, functions, importMa
90
99
  await rewriteImportAssertions(denoVendorFile, denoVendorFile);
91
100
  }
92
101
  }
93
- const manifestPath = path.join(bundleDir.path, '___netlify-edge-functions.json');
94
- const manifestContents = JSON.stringify(manifest);
95
- await fs.writeFile(manifestPath, manifestContents);
96
- const tarballPath = path.join(distDirectory, buildID + TARBALL_EXTENSION);
97
- await fs.mkdir(path.dirname(tarballPath), { recursive: true });
98
- // List files to include in the tarball as paths relative to the bundle dir.
99
- // Using absolute paths here leads to platform-specific quirks (notably on Windows),
100
- // where entries can include drive letters and break extraction/imports.
101
- // The './' prefix is required to prevent node-tar from interpreting entries
102
- // starting with '@' as GNU tar archive-include directives, which would cause
103
- // it to strip the '@' and stat a non-existent path (ENOENT).
104
- const files = (await listRecursively(bundleDir.path))
105
- .map((p) => path.relative(bundleDir.path, p))
106
- .map((p) => './' + getUnixPath(p))
107
- .sort();
108
- await tar.create({
109
- cwd: bundleDir.path,
110
- file: tarballPath,
111
- gzip: true,
112
- noDirRecurse: true,
113
- // Ensure forward slashes inside the tarball for cross-platform consistency.
114
- onWriteEntry(entry) {
115
- entry.path = getUnixPath(entry.path);
116
- },
117
- }, files);
118
- const hash = await getFileHash(tarballPath);
119
- await Promise.allSettled(cleanup);
120
- return {
121
- extension: TARBALL_EXTENSION,
122
- format: BundleFormat.TARBALL,
123
- hash,
102
+ // First stage of bundling is now done. To finalize bundling we require functionConfig, routes and postCacheRoutes
103
+ // so we could inject those into bundle manifest. Tarball bundling is done in 2-step process process to preserve ordering
104
+ // and potential errors messages that could be thrown to make sure we don't impact behaviors. Otherwise we would throw different
105
+ // kind of errors than we used to and introduce confusion for users.
106
+ return async function finalizeBundle({ manifestFunctionConfig, manifestRoutes }) {
107
+ const manifest = {
108
+ ...initialManifest,
109
+ function_config: manifestFunctionConfig,
110
+ routes: manifestRoutes.preCacheRoutes,
111
+ post_cache_routes: manifestRoutes.postCacheRoutes,
112
+ };
113
+ const manifestPath = path.join(bundleDir.path, '___netlify-edge-functions.json');
114
+ const manifestContents = JSON.stringify(manifest);
115
+ await fs.writeFile(manifestPath, manifestContents);
116
+ const tarballPath = path.join(distDirectory, buildID + TARBALL_EXTENSION);
117
+ await fs.mkdir(path.dirname(tarballPath), { recursive: true });
118
+ // List files to include in the tarball as paths relative to the bundle dir.
119
+ // Using absolute paths here leads to platform-specific quirks (notably on Windows),
120
+ // where entries can include drive letters and break extraction/imports.
121
+ // The './' prefix is required to prevent node-tar from interpreting entries
122
+ // starting with '@' as GNU tar archive-include directives, which would cause
123
+ // it to strip the '@' and stat a non-existent path (ENOENT).
124
+ const files = (await listRecursively(bundleDir.path))
125
+ .map((p) => path.relative(bundleDir.path, p))
126
+ .map((p) => './' + getUnixPath(p))
127
+ .sort();
128
+ await tar.create({
129
+ cwd: bundleDir.path,
130
+ file: tarballPath,
131
+ gzip: true,
132
+ noDirRecurse: true,
133
+ // Ensure forward slashes inside the tarball for cross-platform consistency.
134
+ onWriteEntry(entry) {
135
+ entry.path = getUnixPath(entry.path);
136
+ },
137
+ }, files);
138
+ const hash = await getFileHash(tarballPath);
139
+ await Promise.allSettled(cleanup);
140
+ return {
141
+ extension: TARBALL_EXTENSION,
142
+ format: BundleFormat.TARBALL,
143
+ hash,
144
+ };
124
145
  };
125
146
  };
126
147
  // Source file extensions that may contain import statements.
@@ -147,6 +168,11 @@ async function getRequiredSourceFiles(deno, entryPoints, importMap) {
147
168
  // Extract all local files from the module graph
148
169
  for (const module of graph.modules) {
149
170
  if (module.specifier.startsWith('file://')) {
171
+ if (module.error?.startsWith('Module not found')) {
172
+ // Module graph contains all found imported/required modules, even if they don't actually exist
173
+ // This can happen for optional dependencies (dynamic import or require in try/catch).
174
+ continue;
175
+ }
150
176
  const filePath = fileURLToPath(module.specifier);
151
177
  localFiles.add(filePath);
152
178
  }
@@ -20,7 +20,7 @@ export declare class ImportMap {
20
20
  static applyPrefixesToPath(path: string, prefixes: Record<string, string>): string;
21
21
  filterImports(imports?: Record<string, URL | null>): Record<string, string>;
22
22
  filterScopes(scopes?: ParsedImportMap['scopes']): Record<string, Imports>;
23
- getContents(prefixes?: Record<string, string>): {
23
+ getContents(prefixes?: Record<string, string>, imports?: Imports): {
24
24
  imports: Imports;
25
25
  scopes: {};
26
26
  };
@@ -116,8 +116,7 @@ export class ImportMap {
116
116
  // to full URLs. It takes an optional `prefixes` object that specifies a list
117
117
  // of prefixes to replace path prefixes (see `applyPrefixesToPath`). Prefixes
118
118
  // will be applied on both `imports` and `scopes`.
119
- getContents(prefixes = {}) {
120
- let imports = {};
119
+ getContents(prefixes = {}, imports = {}) {
121
120
  let scopes = {};
122
121
  this.sources.forEach((file) => {
123
122
  const importMap = this.resolve(file);
@@ -56,24 +56,42 @@ interface Manifest {
56
56
  post_cache_routes: Route[];
57
57
  function_config: Record<string, EdgeFunctionConfig>;
58
58
  }
59
- interface GenerateManifestOptions {
60
- bundles?: Bundle[];
59
+ interface GenerateManifestFunctionConfigOptions {
60
+ functions: EdgeFunction[];
61
+ internalFunctionConfig?: Record<string, FunctionConfig>;
62
+ userFunctionConfig?: Record<string, FunctionConfig>;
63
+ }
64
+ interface GenerateManifestRoutesOptions {
65
+ functions: EdgeFunction[];
61
66
  declarations?: Declaration[];
67
+ }
68
+ interface GenerateManifestOptionsBase {
69
+ bundles?: Bundle[];
62
70
  featureFlags?: FeatureFlags;
63
- functions: EdgeFunction[];
64
71
  importMap?: string;
65
- internalFunctionConfig?: Record<string, FunctionConfig>;
66
72
  layers?: Layer[];
67
- userFunctionConfig?: Record<string, FunctionConfig>;
68
73
  bundlingTiming?: BundlingTiming;
74
+ functions: EdgeFunction[];
69
75
  }
70
- declare const generateManifest: ({ bundles, declarations, functions, userFunctionConfig, internalFunctionConfig, importMap, layers, bundlingTiming, }: GenerateManifestOptions) => {
76
+ export declare const generateManifestFunctionConfig: ({ functions, userFunctionConfig, internalFunctionConfig, }: GenerateManifestFunctionConfigOptions) => Record<string, EdgeFunctionConfig>;
77
+ export declare const generateManifestRoutes: ({ functions, declarations }: GenerateManifestRoutesOptions) => {
78
+ preCacheRoutes: Route[];
79
+ postCacheRoutes: Route[];
80
+ unroutedFunctions: string[];
81
+ declarationsWithoutFunction: Set<string>;
82
+ };
83
+ type GenerateManifestOptions = GenerateManifestOptionsBase & (GenerateManifestFunctionConfigOptions | {
84
+ manifestFunctionConfig: ReturnType<typeof generateManifestFunctionConfig>;
85
+ }) & (GenerateManifestRoutesOptions | {
86
+ manifestRoutes: ReturnType<typeof generateManifestRoutes>;
87
+ });
88
+ declare const generateManifest: ({ bundles, importMap, layers, bundlingTiming, functions, ...rest }: GenerateManifestOptions) => {
71
89
  declarationsWithoutFunction: string[];
72
90
  manifest: Manifest;
73
91
  unroutedFunctions: string[];
74
92
  };
75
- interface WriteManifestOptions extends GenerateManifestOptions {
93
+ type WriteManifestOptions = GenerateManifestOptions & {
76
94
  distDirectory: string;
77
- }
95
+ };
78
96
  declare const writeManifest: ({ distDirectory, ...rest }: WriteManifestOptions) => Promise<Manifest>;
79
97
  export { generateManifest, Manifest, Route, writeManifest };
@@ -54,12 +54,8 @@ const normalizeMethods = (method, name) => {
54
54
  return method.toUpperCase();
55
55
  });
56
56
  };
57
- const generateManifest = ({ bundles = [], declarations = [], functions, userFunctionConfig = {}, internalFunctionConfig = {}, importMap, layers = [], bundlingTiming, }) => {
58
- const preCacheRoutes = [];
59
- const postCacheRoutes = [];
57
+ export const generateManifestFunctionConfig = ({ functions, userFunctionConfig = {}, internalFunctionConfig = {}, }) => {
60
58
  const manifestFunctionConfig = Object.fromEntries(functions.map(({ name }) => [name, { excluded_patterns: [] }]));
61
- const routedFunctions = new Set();
62
- const declarationsWithoutFunction = new Set();
63
59
  for (const [name, singleUserFunctionConfig] of Object.entries(userFunctionConfig)) {
64
60
  // If the config block is for a function that is not defined, discard it.
65
61
  if (manifestFunctionConfig[name] === undefined) {
@@ -97,6 +93,13 @@ const generateManifest = ({ bundles = [], declarations = [], functions, userFunc
97
93
  ...rest,
98
94
  };
99
95
  }
96
+ return sanitizeEdgeFunctionConfig(manifestFunctionConfig);
97
+ };
98
+ export const generateManifestRoutes = ({ functions, declarations = [] }) => {
99
+ const preCacheRoutes = [];
100
+ const postCacheRoutes = [];
101
+ const routedFunctions = new Set();
102
+ const declarationsWithoutFunction = new Set();
100
103
  declarations.forEach((declaration) => {
101
104
  const func = functions.find(({ name }) => declaration.function === name);
102
105
  if (func === undefined) {
@@ -132,14 +135,32 @@ const generateManifest = ({ bundles = [], declarations = [], functions, userFunc
132
135
  preCacheRoutes.push(route);
133
136
  }
134
137
  });
138
+ const unroutedFunctions = functions.filter(({ name }) => !routedFunctions.has(name)).map(({ name }) => name);
139
+ return {
140
+ preCacheRoutes: preCacheRoutes.filter(nonNullable),
141
+ postCacheRoutes: postCacheRoutes.filter(nonNullable),
142
+ unroutedFunctions,
143
+ declarationsWithoutFunction,
144
+ };
145
+ };
146
+ const generateManifest = ({ bundles = [], importMap, layers = [], bundlingTiming, functions, ...rest }) => {
147
+ const manifestFunctionConfig = 'manifestFunctionConfig' in rest
148
+ ? rest.manifestFunctionConfig
149
+ : generateManifestFunctionConfig({ functions, ...rest });
150
+ const { preCacheRoutes, postCacheRoutes, unroutedFunctions, declarationsWithoutFunction } = 'manifestRoutes' in rest
151
+ ? rest.manifestRoutes
152
+ : generateManifestRoutes({
153
+ functions,
154
+ ...rest,
155
+ });
135
156
  const manifestBundles = bundles.map(({ extension, format, hash }) => ({
136
157
  asset: hash + extension,
137
158
  format,
138
159
  }));
139
160
  const manifest = {
140
161
  bundles: manifestBundles,
141
- routes: preCacheRoutes.filter(nonNullable),
142
- post_cache_routes: postCacheRoutes.filter(nonNullable),
162
+ routes: preCacheRoutes,
163
+ post_cache_routes: postCacheRoutes,
143
164
  bundler_version: getPackageVersion(),
144
165
  layers,
145
166
  import_map: importMap,
@@ -148,7 +169,6 @@ const generateManifest = ({ bundles = [], declarations = [], functions, userFunc
148
169
  ? { bundling_timing: bundlingTiming }
149
170
  : {}),
150
171
  };
151
- const unroutedFunctions = functions.filter(({ name }) => !routedFunctions.has(name)).map(({ name }) => name);
152
172
  return { declarationsWithoutFunction: [...declarationsWithoutFunction], manifest, unroutedFunctions };
153
173
  };
154
174
  const getTrafficRulesConfig = (rl) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "14.9.18",
3
+ "version": "14.10.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -81,5 +81,5 @@
81
81
  "urlpattern-polyfill": "8.0.2",
82
82
  "uuid": "^11.0.0"
83
83
  },
84
- "gitHead": "d215694819e87e39ff42e863ed05124f5fd3da98"
84
+ "gitHead": "6a547b9ec049b48a1e028e180bc21082cb367643"
85
85
  }