@netlify/edge-bundler 8.17.0 → 8.18.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/deno/config.ts CHANGED
@@ -1,6 +1,9 @@
1
- const [functionURL, collectorURL, rawExitCodes] = Deno.args
1
+ const [functionURL, collectorURL, bootstrapURL, rawExitCodes] = Deno.args
2
2
  const exitCodes = JSON.parse(rawExitCodes)
3
3
 
4
+ const { Netlify } = await import(bootstrapURL)
5
+ globalThis.Netlify = Netlify
6
+
4
7
  let func
5
8
 
6
9
  try {
@@ -14,8 +14,9 @@ interface BundleOptions {
14
14
  onBeforeDownload?: OnBeforeDownloadHook;
15
15
  systemLogger?: LogFunction;
16
16
  internalSrcFolder?: string;
17
+ bootstrapURL?: string;
17
18
  }
18
- declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, }?: BundleOptions) => Promise<{
19
+ declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, bootstrapURL, }?: BundleOptions) => Promise<{
19
20
  functions: import("./edge_function.js").EdgeFunction[];
20
21
  manifest: import("./manifest.js").Manifest;
21
22
  }>;
@@ -14,7 +14,7 @@ import { ImportMap } from './import_map.js';
14
14
  import { getLogger } from './logger.js';
15
15
  import { writeManifest } from './manifest.js';
16
16
  import { ensureLatestTypes } from './types.js';
17
- const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, } = {}) => {
17
+ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts', } = {}) => {
18
18
  const logger = getLogger(systemLogger, debug);
19
19
  const featureFlags = getFlags(inputFeatureFlags);
20
20
  const options = {
@@ -63,8 +63,8 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
63
63
  await createFinalBundles([functionBundle], distDirectory, buildID);
64
64
  // Retrieving a configuration object for each function.
65
65
  // Run `getFunctionConfig` in parallel as it is a non-trivial operation and spins up deno
66
- const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger)]);
67
- const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger)]);
66
+ const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger, bootstrapURL })]);
67
+ const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger, bootstrapURL })]);
68
68
  // Creating a hash of function names to configuration objects.
69
69
  const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
70
70
  const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
@@ -17,4 +17,10 @@ export interface FunctionConfig {
17
17
  name?: string;
18
18
  generator?: string;
19
19
  }
20
- export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger) => Promise<FunctionConfig>;
20
+ export declare const getFunctionConfig: ({ func, importMap, deno, bootstrapURL, log, }: {
21
+ func: EdgeFunction;
22
+ importMap: ImportMap;
23
+ deno: DenoBridge;
24
+ bootstrapURL: string;
25
+ log: Logger;
26
+ }) => Promise<FunctionConfig>;
@@ -26,7 +26,7 @@ const getConfigExtractor = () => {
26
26
  const configExtractorPath = join(packagePath, 'deno', 'config.ts');
27
27
  return configExtractorPath;
28
28
  };
29
- export const getFunctionConfig = async (func, importMap, deno, log) => {
29
+ export const getFunctionConfig = async ({ func, importMap, deno, bootstrapURL, log, }) => {
30
30
  // The extractor is a Deno script that will import the function and run its
31
31
  // `config` export, if one exists.
32
32
  const extractorPath = getConfigExtractor();
@@ -50,6 +50,7 @@ export const getFunctionConfig = async (func, importMap, deno, log) => {
50
50
  extractorPath,
51
51
  pathToFileURL(func.path).href,
52
52
  pathToFileURL(collector.path).href,
53
+ bootstrapURL,
53
54
  JSON.stringify(ConfigExitCode),
54
55
  ], { rejectOnExitCode: false });
55
56
  if (exitCode !== ConfigExitCode.Success) {
@@ -9,6 +9,7 @@ import { DenoBridge } from './bridge.js';
9
9
  import { bundle } from './bundler.js';
10
10
  import { getFunctionConfig } from './config.js';
11
11
  import { ImportMap } from './import_map.js';
12
+ const bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts';
12
13
  const importMapFile = {
13
14
  baseURL: new URL('file:///some/path/import-map.json'),
14
15
  imports: {
@@ -114,9 +115,15 @@ describe('`getFunctionConfig` extracts configuration properties from function fi
114
115
  const path = join(tmpDir, `${func.name}.js`);
115
116
  await fs.writeFile(path, func.source);
116
117
  const funcCall = () => getFunctionConfig({
117
- name: func.name,
118
- path,
119
- }, new ImportMap([importMapFile]), deno, logger);
118
+ func: {
119
+ name: func.name,
120
+ path,
121
+ },
122
+ importMap: new ImportMap([importMapFile]),
123
+ deno,
124
+ log: logger,
125
+ bootstrapURL,
126
+ });
120
127
  if (func.error) {
121
128
  await expect(funcCall()).rejects.toThrowError(func.error);
122
129
  }
@@ -161,14 +168,49 @@ test('Loads function paths from the in-source `config` function', async () => {
161
168
  expect(bundles[0].format).toBe('eszip2');
162
169
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
163
170
  expect(routes.length).toBe(6);
164
- expect(routes[0]).toEqual({ function: 'framework-func2', pattern: '^/framework-func2/?$', excluded_patterns: [] });
165
- expect(routes[1]).toEqual({ function: 'user-func2', pattern: '^/user-func2/?$', excluded_patterns: [] });
166
- expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$', excluded_patterns: [] });
167
- expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$', excluded_patterns: [] });
168
- expect(routes[4]).toEqual({ function: 'user-func3', pattern: '^/user-func3/?$', excluded_patterns: [] });
169
- expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5(?:/(.*))/?$', excluded_patterns: [] });
171
+ expect(routes[0]).toEqual({
172
+ function: 'framework-func2',
173
+ pattern: '^/framework-func2/?$',
174
+ excluded_patterns: [],
175
+ path: '/framework-func2',
176
+ });
177
+ expect(routes[1]).toEqual({
178
+ function: 'user-func2',
179
+ pattern: '^/user-func2/?$',
180
+ excluded_patterns: [],
181
+ path: '/user-func2',
182
+ });
183
+ expect(routes[2]).toEqual({
184
+ function: 'framework-func1',
185
+ pattern: '^/framework-func1/?$',
186
+ excluded_patterns: [],
187
+ path: '/framework-func1',
188
+ });
189
+ expect(routes[3]).toEqual({
190
+ function: 'user-func1',
191
+ pattern: '^/user-func1/?$',
192
+ excluded_patterns: [],
193
+ path: '/user-func1',
194
+ });
195
+ expect(routes[4]).toEqual({
196
+ function: 'user-func3',
197
+ pattern: '^/user-func3/?$',
198
+ excluded_patterns: [],
199
+ path: '/user-func3',
200
+ });
201
+ expect(routes[5]).toEqual({
202
+ function: 'user-func5',
203
+ pattern: '^/user-func5(?:/(.*))/?$',
204
+ excluded_patterns: [],
205
+ path: '/user-func5/*',
206
+ });
170
207
  expect(postCacheRoutes.length).toBe(1);
171
- expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$', excluded_patterns: [] });
208
+ expect(postCacheRoutes[0]).toEqual({
209
+ function: 'user-func4',
210
+ pattern: '^/user-func4/?$',
211
+ excluded_patterns: [],
212
+ path: '/user-func4',
213
+ });
172
214
  expect(Object.keys(functionConfig)).toHaveLength(1);
173
215
  expect(functionConfig['user-func5']).toEqual({
174
216
  excluded_patterns: ['^/user-func5/excluded/?$'],
@@ -194,9 +236,15 @@ test('Passes validation if default export exists and is a function', async () =>
194
236
  const path = join(tmpDir, `${func.name}.ts`);
195
237
  await fs.writeFile(path, func.source);
196
238
  await expect(getFunctionConfig({
197
- name: func.name,
198
- path,
199
- }, new ImportMap([importMapFile]), deno, logger)).resolves.not.toThrow();
239
+ func: {
240
+ name: func.name,
241
+ path,
242
+ },
243
+ importMap: new ImportMap([importMapFile]),
244
+ deno,
245
+ log: logger,
246
+ bootstrapURL,
247
+ })).resolves.not.toThrow();
200
248
  await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
201
249
  });
202
250
  test('Fails validation if default export is not function', async () => {
@@ -218,9 +266,15 @@ test('Fails validation if default export is not function', async () => {
218
266
  const path = join(tmpDir, `${func.name}.ts`);
219
267
  await fs.writeFile(path, func.source);
220
268
  const config = getFunctionConfig({
221
- name: func.name,
222
- path,
223
- }, new ImportMap([importMapFile]), deno, logger);
269
+ func: {
270
+ name: func.name,
271
+ path,
272
+ },
273
+ importMap: new ImportMap([importMapFile]),
274
+ deno,
275
+ log: logger,
276
+ bootstrapURL,
277
+ });
224
278
  await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
225
279
  await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
226
280
  });
@@ -242,9 +296,15 @@ test('Fails validation if default export is not present', async () => {
242
296
  const path = join(tmpDir, `${func.name}.ts`);
243
297
  await fs.writeFile(path, func.source);
244
298
  const config = getFunctionConfig({
245
- name: func.name,
246
- path,
247
- }, new ImportMap([importMapFile]), deno, logger);
299
+ func: {
300
+ name: func.name,
301
+ path,
302
+ },
303
+ importMap: new ImportMap([importMapFile]),
304
+ deno,
305
+ log: logger,
306
+ bootstrapURL,
307
+ });
248
308
  await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
249
309
  await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
250
310
  });
@@ -66,7 +66,12 @@ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited
66
66
  // Validates and normalizes a pattern so that it's a valid regular expression
67
67
  // in Go, which is the engine used by our edge nodes.
68
68
  export const parsePattern = (pattern) => {
69
- const regexp = new RegExp(pattern);
69
+ let enclosedPattern = pattern;
70
+ if (!pattern.startsWith('^'))
71
+ enclosedPattern = `^${enclosedPattern}`;
72
+ if (!pattern.endsWith('$'))
73
+ enclosedPattern = `${enclosedPattern}$`;
74
+ const regexp = new RegExp(enclosedPattern);
70
75
  const newRegexp = regexpAST.transform(regexp, {
71
76
  Assertion(path) {
72
77
  // Lookaheads are not supported. If we find one, throw an error.
@@ -126,3 +126,9 @@ test('Escapes front slashes in a regex pattern', () => {
126
126
  const actual = parsePattern(regexPattern);
127
127
  expect(actual).toEqual(expected);
128
128
  });
129
+ test('Ensures pattern match on the whole path', () => {
130
+ const regexPattern = '/foo/.*/bar';
131
+ const expected = '^\\/foo\\/.*\\/bar$';
132
+ const actual = parsePattern(regexPattern);
133
+ expect(actual).toEqual(expected);
134
+ });
@@ -1,4 +1,5 @@
1
1
  import { join } from 'path';
2
+ import { pathToFileURL } from 'url';
2
3
  import { virtualRoot } from '../../shared/consts.js';
3
4
  import { BundleFormat } from '../bundle.js';
4
5
  import { wrapBundleError } from '../bundle_error.js';
@@ -9,13 +10,17 @@ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, exte
9
10
  const extension = '.eszip';
10
11
  const destPath = join(distDirectory, `${buildID}${extension}`);
11
12
  const { bundler, importMap: bundlerImportMap } = getESZIPPaths();
12
- const importMapData = JSON.stringify(importMap.getContents(basePath, virtualRoot));
13
+ // Transforming all paths under `basePath` to use the virtual root prefix.
14
+ const importMapPrefixes = {
15
+ [`${pathToFileURL(basePath)}/`]: virtualRoot,
16
+ };
17
+ const importMapData = importMap.getContents(importMapPrefixes);
13
18
  const payload = {
14
19
  basePath,
15
20
  destPath,
16
21
  externals,
17
22
  functions,
18
- importMapData,
23
+ importMapData: JSON.stringify(importMapData),
19
24
  };
20
25
  const flags = ['--allow-all', '--no-config', `--import-map=${bundlerImportMap}`];
21
26
  if (!debug) {
@@ -1,29 +1,32 @@
1
+ import { ParsedImportMap } from '@import-maps/resolve';
1
2
  import { Logger } from './logger.js';
2
3
  type Imports = Record<string, string>;
3
- interface ImportMapSource {
4
+ export interface ImportMapFile {
4
5
  baseURL: URL;
5
6
  imports: Imports;
6
7
  scopes?: Record<string, Imports>;
7
8
  }
8
- declare class ImportMap {
9
- sources: ImportMapSource[];
10
- constructor(sources?: ImportMapSource[]);
11
- static resolve(source: ImportMapSource, basePath?: string, prefix?: string): {
12
- imports: Record<string, string>;
13
- scopes: Record<string, Imports>;
14
- };
15
- static resolveImports(imports: Record<string, URL | null>, basePath?: string, prefix?: string): Record<string, string>;
16
- static resolvePath(url: URL, basePath?: string, prefix?: string): string;
17
- add(source: ImportMapSource): void;
9
+ export declare class ImportMap {
10
+ rootPath: string | null;
11
+ sources: ImportMapFile[];
12
+ constructor(sources?: ImportMapFile[], rootURL?: string | null);
13
+ add(source: ImportMapFile): void;
18
14
  addFile(path: string, logger: Logger): Promise<void>;
19
15
  addFiles(paths: (string | undefined)[], logger: Logger): Promise<void>;
20
- getContents(basePath?: string, prefix?: string): {
16
+ static applyPrefixesToImports(imports: Imports, prefixes: Record<string, string>): Imports;
17
+ static applyPrefixesToPath(path: string, prefixes: Record<string, string>): string;
18
+ filterImports(imports?: Record<string, URL | null>): Record<string, string>;
19
+ filterScopes(scopes?: ParsedImportMap['scopes']): Record<string, Imports>;
20
+ getContents(prefixes?: Record<string, string>): {
21
21
  imports: Imports;
22
+ scopes: {};
23
+ };
24
+ static readFile(path: string, logger: Logger): Promise<ImportMapFile>;
25
+ resolve(source: ImportMapFile): {
26
+ imports: Record<string, string>;
22
27
  scopes: Record<string, Imports>;
23
28
  };
24
29
  toDataURL(): string;
25
30
  writeToFile(path: string): Promise<void>;
26
31
  }
27
- declare const readFile: (path: string, logger: Logger) => Promise<ImportMapSource>;
28
- export { ImportMap, readFile };
29
- export type { ImportMapSource as ImportMapFile };
32
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { promises as fs } from 'fs';
3
- import { dirname, posix, relative, sep } from 'path';
3
+ import { dirname, relative } from 'path';
4
4
  import { fileURLToPath, pathToFileURL } from 'url';
5
5
  import { parse } from '@import-maps/resolve';
6
6
  import { isFileNotFoundError } from './utils/error.js';
@@ -9,74 +9,19 @@ const INTERNAL_IMPORTS = {
9
9
  };
10
10
  // ImportMap can take several import map files and merge them into a final
11
11
  // import map object, also adding the internal imports in the right order.
12
- class ImportMap {
13
- constructor(sources = []) {
12
+ export class ImportMap {
13
+ constructor(sources = [], rootURL = null) {
14
+ this.rootPath = rootURL ? fileURLToPath(rootURL) : null;
14
15
  this.sources = [];
15
16
  sources.forEach((file) => {
16
17
  this.add(file);
17
18
  });
18
19
  }
19
- // Transforms an import map by making any relative paths use a different path
20
- // as a base.
21
- static resolve(source, basePath, prefix = 'file://') {
22
- const { baseURL, ...importMap } = source;
23
- const parsedImportMap = parse(importMap, baseURL);
24
- const { imports = {}, scopes = {} } = parsedImportMap;
25
- const resolvedImports = ImportMap.resolveImports(imports, basePath, prefix);
26
- const resolvedScopes = {};
27
- Object.keys(scopes).forEach((path) => {
28
- const resolvedPath = ImportMap.resolvePath(new URL(path), basePath, prefix);
29
- resolvedScopes[resolvedPath] = ImportMap.resolveImports(scopes[path], basePath, prefix);
30
- });
31
- return { ...parsedImportMap, imports: resolvedImports, scopes: resolvedScopes };
32
- }
33
- // Takes an imports object and resolves relative specifiers with a given base
34
- // path and URL prefix.
35
- static resolveImports(imports, basePath, prefix) {
36
- const resolvedImports = {};
37
- Object.keys(imports).forEach((specifier) => {
38
- const url = imports[specifier];
39
- // If there's no URL, don't even add the specifier to the final imports.
40
- if (url === null) {
41
- return;
42
- }
43
- // If this is a file URL, we might want to transform it to use another
44
- // base path.
45
- if (url.protocol === 'file:') {
46
- resolvedImports[specifier] = ImportMap.resolvePath(url, basePath, prefix);
47
- return;
48
- }
49
- resolvedImports[specifier] = url.toString();
50
- });
51
- return resolvedImports;
52
- }
53
- // Takes a URL, turns it into a path relative to the given base, and prepends
54
- // a prefix (such as the virtual root prefix).
55
- static resolvePath(url, basePath, prefix) {
56
- if (basePath === undefined) {
57
- return url.toString();
58
- }
59
- const path = fileURLToPath(url);
60
- const relativePath = relative(basePath, path);
61
- if (relativePath.startsWith('..')) {
62
- throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${basePath}'`);
63
- }
64
- // We want to use POSIX paths for the import map regardless of the OS
65
- // we're building in.
66
- let normalizedPath = relativePath.split(sep).join(posix.sep);
67
- // If the original URL had a trailing slash, ensure the normalized path
68
- // has one too.
69
- if (normalizedPath !== '' && url.pathname.endsWith(posix.sep) && !normalizedPath.endsWith(posix.sep)) {
70
- normalizedPath += posix.sep;
71
- }
72
- const newURL = new URL(normalizedPath, prefix);
73
- return newURL.toString();
74
- }
75
20
  add(source) {
76
21
  this.sources.push(source);
77
22
  }
78
23
  async addFile(path, logger) {
79
- const source = await readFile(path, logger);
24
+ const source = await ImportMap.readFile(path, logger);
80
25
  if (Object.keys(source.imports).length === 0) {
81
26
  return;
82
27
  }
@@ -90,11 +35,73 @@ class ImportMap {
90
35
  await this.addFile(path, logger);
91
36
  }
92
37
  }
93
- getContents(basePath, prefix) {
38
+ // Applies a list of prefixes to an `imports` block, by transforming values
39
+ // with the `applyPrefixesToPath` method.
40
+ static applyPrefixesToImports(imports, prefixes) {
41
+ return Object.entries(imports).reduce((acc, [key, value]) => ({
42
+ ...acc,
43
+ [key]: ImportMap.applyPrefixesToPath(value, prefixes),
44
+ }), {});
45
+ }
46
+ // Applies a list of prefixes to a given path, returning the replaced path.
47
+ // For example, given a `path` of `file:///foo/bar/baz.js` and a `prefixes`
48
+ // object with `{"file:///foo/": "file:///hello/"}`, this method will return
49
+ // `file:///hello/bar/baz.js`. If no matching prefix is found, the original
50
+ // path is returned.
51
+ static applyPrefixesToPath(path, prefixes) {
52
+ for (const prefix in prefixes) {
53
+ if (path.startsWith(prefix)) {
54
+ return path.replace(prefix, prefixes[prefix]);
55
+ }
56
+ }
57
+ return path;
58
+ }
59
+ // Takes an `imports` object and filters out any entries without a URL. Also,
60
+ // it checks whether the import map is referencing a path outside `rootPath`,
61
+ // if one is set.
62
+ filterImports(imports = {}) {
63
+ const filteredImports = {};
64
+ Object.keys(imports).forEach((specifier) => {
65
+ const url = imports[specifier];
66
+ // If there's no URL, don't even add the specifier to the final imports.
67
+ if (url === null) {
68
+ return;
69
+ }
70
+ if (this.rootPath !== null) {
71
+ const path = fileURLToPath(url);
72
+ const relativePath = relative(this.rootPath, path);
73
+ if (relativePath.startsWith('..')) {
74
+ throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${this.rootPath}'`);
75
+ }
76
+ }
77
+ filteredImports[specifier] = url.toString();
78
+ });
79
+ return filteredImports;
80
+ }
81
+ // Takes a `scopes` object and runs all imports through `filterImports`,
82
+ // omitting any scopes for which there are no imports.
83
+ filterScopes(scopes) {
84
+ const filteredScopes = {};
85
+ if (scopes !== undefined) {
86
+ Object.keys(scopes).forEach((url) => {
87
+ const imports = this.filterImports(scopes[url]);
88
+ if (Object.keys(imports).length === 0) {
89
+ return;
90
+ }
91
+ filteredScopes[url] = imports;
92
+ });
93
+ }
94
+ return filteredScopes;
95
+ }
96
+ // Returns the import map as a plain object, with any relative paths resolved
97
+ // to full URLs. It takes an optional `prefixes` object that specifies a list
98
+ // of prefixes to replace path prefixes (see `applyPrefixesToPath`). Prefixes
99
+ // will be applied on both `imports` and `scopes`.
100
+ getContents(prefixes = {}) {
94
101
  let imports = {};
95
102
  let scopes = {};
96
103
  this.sources.forEach((file) => {
97
- const importMap = ImportMap.resolve(file, basePath, prefix);
104
+ const importMap = this.resolve(file);
98
105
  imports = { ...imports, ...importMap.imports };
99
106
  scopes = { ...scopes, ...importMap.scopes };
100
107
  });
@@ -104,11 +111,49 @@ class ImportMap {
104
111
  const [specifier, url] = internalImport;
105
112
  imports[specifier] = url;
106
113
  });
114
+ const transformedImports = ImportMap.applyPrefixesToImports(imports, prefixes);
115
+ const transformedScopes = Object.entries(scopes).reduce((acc, [key, value]) => ({
116
+ ...acc,
117
+ [ImportMap.applyPrefixesToPath(key, prefixes)]: ImportMap.applyPrefixesToImports(value, prefixes),
118
+ }), {});
119
+ return {
120
+ imports: transformedImports,
121
+ scopes: transformedScopes,
122
+ };
123
+ }
124
+ static async readFile(path, logger) {
125
+ const baseURL = pathToFileURL(path);
126
+ try {
127
+ const data = await fs.readFile(path, 'utf8');
128
+ const importMap = JSON.parse(data);
129
+ return {
130
+ ...importMap,
131
+ baseURL,
132
+ };
133
+ }
134
+ catch (error) {
135
+ if (isFileNotFoundError(error)) {
136
+ logger.system(`Did not find an import map file at '${path}'.`);
137
+ }
138
+ else {
139
+ logger.user(`Error while loading import map at '${path}':`, error);
140
+ }
141
+ }
107
142
  return {
108
- imports,
109
- scopes,
143
+ baseURL,
144
+ imports: {},
110
145
  };
111
146
  }
147
+ // Resolves an import map file by transforming all relative paths into full
148
+ // URLs. The `baseURL` property of each file is used to resolve all relative
149
+ // paths against.
150
+ resolve(source) {
151
+ const { baseURL, ...importMap } = source;
152
+ const parsedImportMap = parse(importMap, baseURL);
153
+ const imports = this.filterImports(parsedImportMap.imports);
154
+ const scopes = this.filterScopes(parsedImportMap.scopes);
155
+ return { ...parsedImportMap, imports, scopes };
156
+ }
112
157
  toDataURL() {
113
158
  const data = JSON.stringify(this.getContents());
114
159
  const encodedImportMap = Buffer.from(data).toString('base64');
@@ -121,27 +166,3 @@ class ImportMap {
121
166
  await fs.writeFile(path, JSON.stringify(contents));
122
167
  }
123
168
  }
124
- const readFile = async (path, logger) => {
125
- const baseURL = pathToFileURL(path);
126
- try {
127
- const data = await fs.readFile(path, 'utf8');
128
- const importMap = JSON.parse(data);
129
- return {
130
- ...importMap,
131
- baseURL,
132
- };
133
- }
134
- catch (error) {
135
- if (isFileNotFoundError(error)) {
136
- logger.system(`Did not find an import map file at '${path}'.`);
137
- }
138
- else {
139
- logger.user(`Error while loading import map at '${path}':`, error);
140
- }
141
- }
142
- return {
143
- baseURL,
144
- imports: {},
145
- };
146
- };
147
- export { ImportMap, readFile };
@@ -3,7 +3,7 @@ import { join } from 'path';
3
3
  import { cwd } from 'process';
4
4
  import { pathToFileURL } from 'url';
5
5
  import tmp from 'tmp-promise';
6
- import { test, expect } from 'vitest';
6
+ import { describe, test, expect } from 'vitest';
7
7
  import { ImportMap } from './import_map.js';
8
8
  test('Handles import maps with full URLs without specifying a base URL', () => {
9
9
  const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
@@ -39,35 +39,77 @@ test('Resolves relative paths to absolute paths if a base path is not provided',
39
39
  expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
40
40
  expect(imports['alias:pets']).toBe(`${pathToFileURL(expectedPath).toString()}/`);
41
41
  });
42
- test('Transforms relative paths so that they become relative to the base path', () => {
43
- const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
42
+ describe('Returns the fully resolved import map', () => {
44
43
  const inputFile1 = {
45
- baseURL: pathToFileURL(basePath),
44
+ baseURL: new URL('file:///some/full/path/import-map.json'),
46
45
  imports: {
47
- 'alias:pets': './heart/pets/',
46
+ specifier1: 'file:///some/full/path/file.js',
47
+ specifier2: './file2.js',
48
+ specifier3: 'file:///different/full/path/file3.js',
48
49
  },
49
50
  };
50
- // Without a prefix.
51
- const map1 = new ImportMap([inputFile1]);
52
- const { imports: imports1 } = map1.getContents(cwd());
53
- expect(imports1['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
54
- expect(imports1['alias:pets']).toBe('file:///my-cool-site/heart/pets/');
55
- // With a prefix.
56
- const map2 = new ImportMap([inputFile1]);
57
- const { imports: imports2 } = map2.getContents(cwd(), 'file:///root/');
58
- expect(imports2['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
59
- expect(imports2['alias:pets']).toBe('file:///root/my-cool-site/heart/pets/');
51
+ const inputFile2 = {
52
+ baseURL: new URL('file:///some/cool/path/import-map.json'),
53
+ imports: {
54
+ 'lib/*': './library/',
55
+ },
56
+ scopes: {
57
+ 'with/scopes/': {
58
+ 'lib/*': 'https://external.netlify/lib/',
59
+ foo: './foo-alias',
60
+ bar: 'file:///different/full/path/bar.js',
61
+ },
62
+ },
63
+ };
64
+ test('Without prefixes', () => {
65
+ const map = new ImportMap([inputFile1, inputFile2]);
66
+ const { imports, scopes } = map.getContents();
67
+ expect(imports).toStrictEqual({
68
+ 'lib/*': 'file:///some/cool/path/library/',
69
+ specifier3: 'file:///different/full/path/file3.js',
70
+ specifier2: 'file:///some/full/path/file2.js',
71
+ specifier1: 'file:///some/full/path/file.js',
72
+ 'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
73
+ });
74
+ expect(scopes).toStrictEqual({
75
+ 'file:///some/cool/path/with/scopes/': {
76
+ 'lib/*': 'https://external.netlify/lib/',
77
+ foo: 'file:///some/cool/path/foo-alias',
78
+ bar: 'file:///different/full/path/bar.js',
79
+ },
80
+ });
81
+ });
82
+ test('With prefixes', () => {
83
+ const map = new ImportMap([inputFile1, inputFile2]);
84
+ const { imports, scopes } = map.getContents({
85
+ 'file:///some/': 'file:///root/',
86
+ 'file:///different/': 'file:///vendor/',
87
+ });
88
+ expect(imports).toStrictEqual({
89
+ 'lib/*': 'file:///root/cool/path/library/',
90
+ specifier3: 'file:///vendor/full/path/file3.js',
91
+ specifier2: 'file:///root/full/path/file2.js',
92
+ specifier1: 'file:///root/full/path/file.js',
93
+ 'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
94
+ });
95
+ expect(scopes).toStrictEqual({
96
+ 'file:///root/cool/path/with/scopes/': {
97
+ 'lib/*': 'https://external.netlify/lib/',
98
+ foo: 'file:///root/cool/path/foo-alias',
99
+ bar: 'file:///vendor/full/path/bar.js',
100
+ },
101
+ });
102
+ });
60
103
  });
61
104
  test('Throws when an import map uses a relative path to reference a file outside of the base path', () => {
62
- const basePath = join(cwd(), 'my-cool-site');
63
105
  const inputFile1 = {
64
- baseURL: pathToFileURL(join(basePath, 'import_map.json')),
106
+ baseURL: pathToFileURL(join(cwd(), 'import-map.json')),
65
107
  imports: {
66
108
  'alias:file': '../file.js',
67
109
  },
68
110
  };
69
- const map = new ImportMap([inputFile1]);
70
- expect(() => map.getContents(basePath)).toThrowError(`Import map cannot reference '${join(cwd(), 'file.js')}' as it's outside of the base directory '${basePath}'`);
111
+ const map = new ImportMap([inputFile1], pathToFileURL(cwd()).toString());
112
+ expect(() => map.getContents()).toThrowError(`Import map cannot reference '${join(cwd(), '..', 'file.js')}' as it's outside of the base directory '${cwd()}'`);
71
113
  });
72
114
  test('Writes import map file to disk', async () => {
73
115
  const file = await tmp.file();
@@ -8,6 +8,7 @@ interface Route {
8
8
  function: string;
9
9
  pattern: string;
10
10
  excluded_patterns: string[];
11
+ path?: string;
11
12
  }
12
13
  interface EdgeFunctionConfig {
13
14
  excluded_patterns: string[];
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { join } from 'path';
3
3
  import globToRegExp from 'glob-to-regexp';
4
+ import { wrapBundleError } from './bundle_error.js';
4
5
  import { parsePattern } from './declaration.js';
5
6
  import { getPackageVersion } from './package_json.js';
6
7
  import { nonNullable } from './utils/non_nullable.js';
@@ -65,6 +66,9 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
65
66
  pattern: serializePattern(pattern),
66
67
  excluded_patterns: excludedPattern.map(serializePattern),
67
68
  };
69
+ if ('path' in declaration) {
70
+ route.path = declaration.path;
71
+ }
68
72
  if (declaration.cache === "manual" /* Cache.Manual */) {
69
73
  postCacheRoutes.push(route);
70
74
  }
@@ -89,15 +93,20 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
89
93
  };
90
94
  const pathToRegularExpression = (path, featureFlags) => {
91
95
  if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_path_urlpattern) {
92
- const pattern = new ExtendedURLPattern({ pathname: path });
93
- // Removing the `^` and `$` delimiters because we'll need to modify what's
94
- // between them.
95
- const source = pattern.regexp.pathname.source.slice(1, -1);
96
- // Wrapping the expression source with `^` and `$`. Also, adding an optional
97
- // trailing slash, so that a declaration of `path: "/foo"` matches requests
98
- // for both `/foo` and `/foo/`.
99
- const normalizedSource = `^${source}\\/?$`;
100
- return normalizedSource;
96
+ try {
97
+ const pattern = new ExtendedURLPattern({ pathname: path });
98
+ // Removing the `^` and `$` delimiters because we'll need to modify what's
99
+ // between them.
100
+ const source = pattern.regexp.pathname.source.slice(1, -1);
101
+ // Wrapping the expression source with `^` and `$`. Also, adding an optional
102
+ // trailing slash, so that a declaration of `path: "/foo"` matches requests
103
+ // for both `/foo` and `/foo/`.
104
+ const normalizedSource = `^${source}\\/?$`;
105
+ return normalizedSource;
106
+ }
107
+ catch (error) {
108
+ throw wrapBundleError(error);
109
+ }
101
110
  }
102
111
  // We use the global flag so that `globToRegExp` will not wrap the expression
103
112
  // with `^` and `$`. We'll do that ourselves.
@@ -2,6 +2,7 @@ import { env } from 'process';
2
2
  import { test, expect, vi } from 'vitest';
3
3
  import { getRouteMatcher } from '../test/util.js';
4
4
  import { BundleFormat } from './bundle.js';
5
+ import { BundleError } from './bundle_error.js';
5
6
  import { generateManifest } from './manifest.js';
6
7
  test('Generates a manifest with different bundles', () => {
7
8
  const bundle1 = {
@@ -21,7 +22,7 @@ test('Generates a manifest with different bundles', () => {
21
22
  { asset: bundle1.hash + bundle1.extension, format: bundle1.format },
22
23
  { asset: bundle2.hash + bundle2.extension, format: bundle2.format },
23
24
  ];
24
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
25
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
25
26
  expect(manifest.bundles).toEqual(expectedBundles);
26
27
  expect(manifest.routes).toEqual(expectedRoutes);
27
28
  expect(manifest.bundler_version).toBe(env.npm_package_version);
@@ -41,7 +42,7 @@ test('Generates a manifest with display names', () => {
41
42
  internalFunctionConfig,
42
43
  featureFlags: { edge_functions_path_urlpattern: true },
43
44
  });
44
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
45
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
45
46
  expect(manifest.function_config).toEqual({
46
47
  'func-1': { name: 'Display Name' },
47
48
  });
@@ -63,7 +64,7 @@ test('Generates a manifest with a generator field', () => {
63
64
  internalFunctionConfig,
64
65
  featureFlags: { edge_functions_path_urlpattern: true },
65
66
  });
66
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
67
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
67
68
  const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
68
69
  expect(manifest.routes).toEqual(expectedRoutes);
69
70
  expect(manifest.function_config).toEqual(expectedFunctionConfig);
@@ -87,12 +88,17 @@ test('Generates a manifest with excluded paths and patterns', () => {
87
88
  featureFlags: { edge_functions_path_urlpattern: true },
88
89
  });
89
90
  const expectedRoutes = [
90
- { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'] },
91
- { function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
91
+ { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'], path: '/f1/*' },
92
+ {
93
+ function: 'func-2',
94
+ pattern: '^/f2(?:/(.*))/?$',
95
+ excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'],
96
+ },
92
97
  {
93
98
  function: 'func-3',
94
99
  pattern: '^(?:/(.*))/?$',
95
100
  excluded_patterns: ['^(?:/((?:.*)(?:/(?:.*))*))?(?:/(.*))\\.html/?$'],
101
+ path: '/*',
96
102
  },
97
103
  ];
98
104
  expect(manifest.routes).toEqual(expectedRoutes);
@@ -115,7 +121,7 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
115
121
  userFunctionConfig,
116
122
  featureFlags: { edge_functions_path_urlpattern: true },
117
123
  });
118
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
124
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
119
125
  expect(manifest.routes).toEqual(expectedRoutes);
120
126
  expect(manifest.function_config).toEqual({
121
127
  'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
@@ -197,11 +203,13 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
197
203
  function: 'customisation',
198
204
  pattern: '^/showcases(?:/(.*))/?$',
199
205
  excluded_patterns: [],
206
+ path: '/showcases/*',
200
207
  },
201
208
  {
202
209
  function: 'customisation',
203
210
  pattern: '^/checkout(?:/(.*))/?$',
204
211
  excluded_patterns: ['^(?:/(.*))/terms-and-conditions/?$'],
212
+ path: '/checkout/*',
205
213
  },
206
214
  ]);
207
215
  expect(manifest.function_config).toEqual({
@@ -216,6 +224,44 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
216
224
  expect(matcher('/checkout/scrooge-mc-duck-animation.css')).toBeUndefined();
217
225
  expect(matcher('/showcases/boho-style/expensive-chair.jpg')).toBeUndefined();
218
226
  });
227
+ test('URLPattern named groups are supported', () => {
228
+ const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }];
229
+ const declarations = [{ function: 'customisation', path: '/products/:productId' }];
230
+ const userFunctionConfig = {};
231
+ const internalFunctionConfig = {};
232
+ const manifest = generateManifest({
233
+ bundles: [],
234
+ declarations,
235
+ functions,
236
+ userFunctionConfig,
237
+ internalFunctionConfig,
238
+ featureFlags: { edge_functions_path_urlpattern: true },
239
+ });
240
+ expect(manifest.routes).toEqual([
241
+ {
242
+ function: 'customisation',
243
+ pattern: '^/products(?:/([^/]+?))/?$',
244
+ excluded_patterns: [],
245
+ path: '/products/:productId',
246
+ },
247
+ ]);
248
+ const matcher = getRouteMatcher(manifest);
249
+ expect(matcher('/products/jigsaw-doweling-jig')).toBeDefined();
250
+ });
251
+ test('Invalid Path patterns throw bundling errors', () => {
252
+ const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }];
253
+ const declarations = [{ function: 'customisation', path: '/https://foo.netlify.app/' }];
254
+ const userFunctionConfig = {};
255
+ const internalFunctionConfig = {};
256
+ expect(() => generateManifest({
257
+ bundles: [],
258
+ declarations,
259
+ functions,
260
+ userFunctionConfig,
261
+ internalFunctionConfig,
262
+ featureFlags: { edge_functions_path_urlpattern: true },
263
+ })).toThrowError(BundleError);
264
+ });
219
265
  test('Includes failure modes in manifest', () => {
220
266
  const functions = [
221
267
  { name: 'func-1', path: '/path/to/func-1.ts' },
@@ -247,7 +293,7 @@ test('Excludes functions for which there are function files but no matching conf
247
293
  ];
248
294
  const declarations = [{ function: 'func-1', path: '/f1' }];
249
295
  const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
250
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
296
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
251
297
  expect(manifest.routes).toEqual(expectedRoutes);
252
298
  });
253
299
  test('Excludes functions for which there are config declarations but no matching function files', () => {
@@ -262,14 +308,14 @@ test('Excludes functions for which there are config declarations but no matching
262
308
  { function: 'func-2', path: '/f2' },
263
309
  ];
264
310
  const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
265
- const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [] }];
311
+ const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }];
266
312
  expect(manifest.routes).toEqual(expectedRoutes);
267
313
  });
268
314
  test('Generates a manifest without bundles', () => {
269
315
  const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
270
316
  const declarations = [{ function: 'func-1', path: '/f1' }];
271
317
  const manifest = generateManifest({ bundles: [], declarations, functions });
272
- const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
318
+ const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
273
319
  expect(manifest.bundles).toEqual([]);
274
320
  expect(manifest.routes).toEqual(expectedRoutes);
275
321
  expect(manifest.bundler_version).toBe(env.npm_package_version);
@@ -301,10 +347,12 @@ test('Generates a manifest with pre and post-cache routes', () => {
301
347
  { asset: bundle2.hash + bundle2.extension, format: bundle2.format },
302
348
  ];
303
349
  const expectedPreCacheRoutes = [
304
- { function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [] },
305
- { function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [] },
350
+ { function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' },
351
+ { function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' },
352
+ ];
353
+ const expectedPostCacheRoutes = [
354
+ { function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [], path: '/f3' },
306
355
  ];
307
- const expectedPostCacheRoutes = [{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [] }];
308
356
  expect(manifest.bundles).toEqual(expectedBundles);
309
357
  expect(manifest.routes).toEqual(expectedPreCacheRoutes);
310
358
  expect(manifest.post_cache_routes).toEqual(expectedPostCacheRoutes);
@@ -320,8 +368,8 @@ test('Generates a manifest with layers', () => {
320
368
  { function: 'func-2', path: '/f2/*' },
321
369
  ];
322
370
  const expectedRoutes = [
323
- { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] },
324
- { function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [] },
371
+ { function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' },
372
+ { function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [], path: '/f2/*' },
325
373
  ];
326
374
  const layers = [
327
375
  {
@@ -43,7 +43,7 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
43
43
  });
44
44
  let functionsConfig = [];
45
45
  if (options.getFunctionsConfig) {
46
- functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger)));
46
+ functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig({ func, importMap, deno, bootstrapURL, log: logger })));
47
47
  }
48
48
  const success = await waitForServer(port, processRef.ps);
49
49
  return {
@@ -27,6 +27,10 @@ test('Starts a server and serves requests for edge functions', async () => {
27
27
  name: 'greet',
28
28
  path: join(paths.internal, 'greet.ts'),
29
29
  },
30
+ {
31
+ name: 'global_netlify',
32
+ path: join(paths.user, 'global_netlify.ts'),
33
+ },
30
34
  ];
31
35
  const options = {
32
36
  getFunctionsConfig: true,
@@ -35,7 +39,7 @@ test('Starts a server and serves requests for edge functions', async () => {
35
39
  very_secret_secret: 'i love netlify',
36
40
  }, options);
37
41
  expect(success).toBe(true);
38
- expect(functionsConfig).toEqual([{ path: '/my-function' }, {}]);
42
+ expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]);
39
43
  for (const key in functions) {
40
44
  const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(
41
45
  // @ts-expect-error TODO: Module graph is currently not typed
@@ -60,4 +64,15 @@ test('Starts a server and serves requests for edge functions', async () => {
60
64
  });
61
65
  expect(response2.status).toBe(200);
62
66
  expect(await response2.text()).toBe('HELLO!');
67
+ const response3 = await fetch(`http://0.0.0.0:${port}/global-netlify`, {
68
+ headers: {
69
+ 'x-nf-edge-functions': 'global_netlify',
70
+ 'x-ef-passthrough': 'passthrough',
71
+ 'X-NF-Request-ID': uuidv4(),
72
+ },
73
+ });
74
+ expect(await response3.json()).toEqual({
75
+ global: 'i love netlify',
76
+ local: 'i love netlify',
77
+ });
63
78
  });
@@ -47,6 +47,9 @@ declare const edgeManifestSchema: {
47
47
  generator: {
48
48
  type: string;
49
49
  };
50
+ path: {
51
+ type: string;
52
+ };
50
53
  };
51
54
  additionalProperties: boolean;
52
55
  };
@@ -79,6 +82,9 @@ declare const edgeManifestSchema: {
79
82
  generator: {
80
83
  type: string;
81
84
  };
85
+ path: {
86
+ type: string;
87
+ };
82
88
  };
83
89
  additionalProperties: boolean;
84
90
  };
@@ -28,6 +28,7 @@ const routesSchema = {
28
28
  },
29
29
  excluded_patterns: excludedPatternsSchema,
30
30
  generator: { type: 'string' },
31
+ path: { type: 'string' },
31
32
  },
32
33
  additionalProperties: false,
33
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "8.17.0",
3
+ "version": "8.18.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -58,7 +58,7 @@
58
58
  "@types/node": "^14.18.32",
59
59
  "@types/semver": "^7.3.9",
60
60
  "@types/uuid": "^9.0.0",
61
- "@vitest/coverage-v8": "^0.33.0",
61
+ "@vitest/coverage-v8": "^0.34.0",
62
62
  "archiver": "^5.3.1",
63
63
  "chalk": "^4.1.2",
64
64
  "cpy": "^9.0.1",
@@ -67,7 +67,7 @@
67
67
  "nock": "^13.2.4",
68
68
  "tar": "^6.1.11",
69
69
  "typescript": "^5.0.0",
70
- "vitest": "^0.33.0"
70
+ "vitest": "^0.34.0"
71
71
  },
72
72
  "engines": {
73
73
  "node": "^14.16.0 || >=16.0.0"