@netlify/edge-bundler 8.19.1 → 9.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/deno/bundle.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { writeStage2 } from './lib/stage2.ts'
2
2
 
3
3
  const [payload] = Deno.args
4
- const { basePath, destPath, externals, functions, importMapData } = JSON.parse(payload)
4
+ const { basePath, destPath, externals, functions, importMapData, vendorDirectory } = JSON.parse(payload)
5
5
 
6
6
  try {
7
- await writeStage2({ basePath, destPath, externals, functions, importMapData })
7
+ await writeStage2({ basePath, destPath, externals, functions, importMapData, vendorDirectory })
8
8
  } catch (error) {
9
9
  if (error instanceof Error && error.message.includes("The module's source code could not be parsed")) {
10
10
  delete error.stack
@@ -3,7 +3,7 @@ import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.40.0/mod.ts'
3
3
  import * as path from 'https://deno.land/std@0.177.0/path/mod.ts'
4
4
 
5
5
  import type { InputFunction, WriteStage2Options } from '../../shared/stage2.ts'
6
- import { importMapSpecifier, virtualRoot } from '../../shared/consts.ts'
6
+ import { importMapSpecifier, virtualRoot, virtualVendorRoot } from '../../shared/consts.ts'
7
7
  import { LEGACY_PUBLIC_SPECIFIER, PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts'
8
8
  import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'
9
9
 
@@ -63,7 +63,13 @@ const getVirtualPath = (basePath: string, filePath: string) => {
63
63
  return url
64
64
  }
65
65
 
66
- const stage2Loader = (basePath: string, functions: InputFunction[], externals: Set<string>, importMapData?: string) => {
66
+ const stage2Loader = (
67
+ basePath: string,
68
+ functions: InputFunction[],
69
+ externals: Set<string>,
70
+ importMapData: string | undefined,
71
+ vendorDirectory?: string,
72
+ ) => {
67
73
  return async (specifier: string): Promise<LoadResponse | undefined> => {
68
74
  if (specifier === STAGE2_SPECIFIER) {
69
75
  const stage2Entry = getStage2Entry(basePath, functions)
@@ -91,13 +97,24 @@ const stage2Loader = (basePath: string, functions: InputFunction[], externals: S
91
97
  return loadFromVirtualRoot(specifier, virtualRoot, basePath)
92
98
  }
93
99
 
100
+ if (vendorDirectory !== undefined && specifier.startsWith(virtualVendorRoot)) {
101
+ return loadFromVirtualRoot(specifier, virtualVendorRoot, vendorDirectory)
102
+ }
103
+
94
104
  return await loadWithRetry(specifier)
95
105
  }
96
106
  }
97
107
 
98
- const writeStage2 = async ({ basePath, destPath, externals, functions, importMapData }: WriteStage2Options) => {
108
+ const writeStage2 = async ({
109
+ basePath,
110
+ destPath,
111
+ externals,
112
+ functions,
113
+ importMapData,
114
+ vendorDirectory,
115
+ }: WriteStage2Options) => {
99
116
  const importMapURL = importMapData ? importMapSpecifier : undefined
100
- const loader = stage2Loader(basePath, functions, new Set(externals), importMapData)
117
+ const loader = stage2Loader(basePath, functions, new Set(externals), importMapData, vendorDirectory)
101
118
  const bytes = await build([STAGE2_SPECIFIER], loader, importMapURL)
102
119
  const directory = path.dirname(destPath)
103
120
 
@@ -1,24 +1,24 @@
1
1
  import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
2
2
  import { Declaration } from './declaration.js';
3
+ import { EdgeFunction } from './edge_function.js';
3
4
  import { FeatureFlags } from './feature_flags.js';
4
5
  import { LogFunction } from './logger.js';
5
- interface BundleOptions {
6
+ export interface BundleOptions {
6
7
  basePath?: string;
8
+ bootstrapURL?: string;
7
9
  cacheDirectory?: string;
8
10
  configPath?: string;
9
11
  debug?: boolean;
10
12
  distImportMapPath?: string;
11
13
  featureFlags?: FeatureFlags;
12
14
  importMapPaths?: (string | undefined)[];
15
+ internalSrcFolder?: string;
13
16
  onAfterDownload?: OnAfterDownloadHook;
14
17
  onBeforeDownload?: OnBeforeDownloadHook;
15
18
  systemLogger?: LogFunction;
16
- internalSrcFolder?: string;
17
- bootstrapURL?: string;
19
+ vendorDirectory?: string;
18
20
  }
19
- declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, }?: BundleOptions) => Promise<{
20
- functions: import("./edge_function.js").EdgeFunction[];
21
+ export declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, internalSrcFolder, onAfterDownload, onBeforeDownload, systemLogger, vendorDirectory, }?: BundleOptions) => Promise<{
22
+ functions: EdgeFunction[];
21
23
  manifest: import("./manifest.js").Manifest;
22
24
  }>;
23
- export { bundle };
24
- export type { BundleOptions };
@@ -13,8 +13,9 @@ import { bundle as bundleESZIP } from './formats/eszip.js';
13
13
  import { ImportMap } from './import_map.js';
14
14
  import { getLogger } from './logger.js';
15
15
  import { writeManifest } from './manifest.js';
16
+ import { vendorNPMSpecifiers } from './npm_dependencies.js';
16
17
  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, } = {}) => {
18
+ export const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], internalSrcFolder, onAfterDownload, onBeforeDownload, systemLogger, vendorDirectory, } = {}) => {
18
19
  const logger = getLogger(systemLogger, debug);
19
20
  const featureFlags = getFlags(inputFeatureFlags);
20
21
  const options = {
@@ -46,6 +47,17 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
46
47
  const userFunctions = userSourceDirectories.length === 0 ? [] : await findFunctions(userSourceDirectories);
47
48
  const internalFunctions = internalSrcFolder ? await findFunctions([internalSrcFolder]) : [];
48
49
  const functions = [...internalFunctions, ...userFunctions];
50
+ const vendor = await safelyVendorNPMSpecifiers({
51
+ basePath,
52
+ featureFlags,
53
+ functions,
54
+ importMap,
55
+ logger,
56
+ vendorDirectory,
57
+ });
58
+ if (vendor) {
59
+ importMap.add(vendor.importMap);
60
+ }
49
61
  const functionBundle = await bundleESZIP({
50
62
  basePath,
51
63
  buildID,
@@ -56,6 +68,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
56
68
  functions,
57
69
  featureFlags,
58
70
  importMap,
71
+ vendorDirectory: vendor === null || vendor === void 0 ? void 0 : vendor.directory,
59
72
  });
60
73
  // The final file name of the bundles contains a SHA256 hash of the contents,
61
74
  // which we can only compute now that the files have been generated. So let's
@@ -86,6 +99,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
86
99
  importMap: importMapSpecifier,
87
100
  layers: deployConfig.layers,
88
101
  });
102
+ await (vendor === null || vendor === void 0 ? void 0 : vendor.cleanup());
89
103
  if (distImportMapPath) {
90
104
  await importMap.writeToFile(distImportMapPath);
91
105
  }
@@ -133,4 +147,20 @@ const createFunctionConfig = ({ internalFunctionsWithConfig, declarations }) =>
133
147
  [functionName]: addGeneratorFallback(mergedConfigFields),
134
148
  };
135
149
  }, {});
136
- export { bundle };
150
+ const safelyVendorNPMSpecifiers = async ({ basePath, featureFlags, functions, importMap, logger, vendorDirectory, }) => {
151
+ if (!featureFlags.edge_functions_npm_modules) {
152
+ return;
153
+ }
154
+ try {
155
+ return await vendorNPMSpecifiers({
156
+ basePath,
157
+ directory: vendorDirectory,
158
+ functions: functions.map(({ path }) => path),
159
+ importMap,
160
+ logger,
161
+ });
162
+ }
163
+ catch (error) {
164
+ logger.system(error);
165
+ }
166
+ };
@@ -105,7 +105,7 @@ test('Adds a custom error property to user errors during bundling', async () =>
105
105
  await cleanup();
106
106
  }
107
107
  });
108
- test('Prints a nice error message when user tries importing NPM module', async () => {
108
+ test('Prints a nice error message when user tries importing an npm module and npm support is disabled', async () => {
109
109
  expect.assertions(2);
110
110
  const { basePath, cleanup, distPath } = await useFixture('imports_npm_module');
111
111
  const sourceDirectory = join(basePath, 'functions');
@@ -120,7 +120,31 @@ test('Prints a nice error message when user tries importing NPM module', async (
120
120
  }
121
121
  catch (error) {
122
122
  expect(error).toBeInstanceOf(BundleError);
123
- expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/p-retry"'?`);
123
+ expect(error.message).toEqual(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/parent-2"'?`);
124
+ }
125
+ finally {
126
+ await cleanup();
127
+ }
128
+ });
129
+ test('Prints a nice error message when user tries importing an npm module and npm support is enabled', async () => {
130
+ expect.assertions(2);
131
+ const { basePath, cleanup, distPath } = await useFixture('imports_npm_module_scheme');
132
+ const sourceDirectory = join(basePath, 'functions');
133
+ const declarations = [
134
+ {
135
+ function: 'func1',
136
+ path: '/func1',
137
+ },
138
+ ];
139
+ try {
140
+ await bundle([sourceDirectory], distPath, declarations, {
141
+ basePath,
142
+ featureFlags: { edge_functions_npm_modules: true },
143
+ });
144
+ }
145
+ catch (error) {
146
+ expect(error).toBeInstanceOf(BundleError);
147
+ expect(error.message).toEqual(`There was an error when loading the 'p-retry' npm module. Support for npm modules in edge functions is an experimental feature. Refer to https://ntl.fyi/edge-functions-npm for more information.`);
124
148
  }
125
149
  finally {
126
150
  await cleanup();
@@ -373,3 +397,27 @@ test('Handles imports with the `node:` prefix', async () => {
373
397
  expect(func1).toBe('ok');
374
398
  await cleanup();
375
399
  });
400
+ test('Loads npm modules from bare specifiers', async () => {
401
+ const { basePath, cleanup, distPath } = await useFixture('imports_npm_module');
402
+ const sourceDirectory = join(basePath, 'functions');
403
+ const declarations = [
404
+ {
405
+ function: 'func1',
406
+ path: '/func1',
407
+ },
408
+ ];
409
+ const vendorDirectory = await tmp.dir();
410
+ await bundle([sourceDirectory], distPath, declarations, {
411
+ basePath,
412
+ featureFlags: { edge_functions_npm_modules: true },
413
+ importMapPaths: [join(basePath, 'import_map.json')],
414
+ vendorDirectory: vendorDirectory.path,
415
+ });
416
+ const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8');
417
+ const manifest = JSON.parse(manifestFile);
418
+ const bundlePath = join(distPath, manifest.bundles[0].asset);
419
+ const { func1 } = await runESZIP(bundlePath, vendorDirectory.path);
420
+ expect(func1).toBe(`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>`);
421
+ await cleanup();
422
+ await rm(vendorDirectory.path, { force: true, recursive: true });
423
+ });
@@ -1,10 +1,12 @@
1
1
  declare const defaultFlags: {
2
2
  edge_functions_fail_unsupported_regex: boolean;
3
+ edge_functions_npm_modules: boolean;
3
4
  };
4
5
  type FeatureFlag = keyof typeof defaultFlags;
5
6
  type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
6
7
  declare const getFlags: (input?: Record<string, boolean>, flags?: {
7
8
  edge_functions_fail_unsupported_regex: boolean;
9
+ edge_functions_npm_modules: boolean;
8
10
  }) => FeatureFlags;
9
11
  export { defaultFlags, getFlags };
10
12
  export type { FeatureFlag, FeatureFlags };
@@ -1,5 +1,6 @@
1
1
  const defaultFlags = {
2
2
  edge_functions_fail_unsupported_regex: false,
3
+ edge_functions_npm_modules: false,
3
4
  };
4
5
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
5
6
  ...result,
@@ -13,6 +13,7 @@ interface BundleESZIPOptions {
13
13
  featureFlags: FeatureFlags;
14
14
  functions: EdgeFunction[];
15
15
  importMap: ImportMap;
16
+ vendorDirectory?: string;
16
17
  }
17
- declare const bundleESZIP: ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, }: BundleESZIPOptions) => Promise<Bundle>;
18
+ declare const bundleESZIP: ({ basePath, buildID, debug, deno, distDirectory, externals, featureFlags, functions, importMap, vendorDirectory, }: BundleESZIPOptions) => Promise<Bundle>;
18
19
  export { bundleESZIP as bundle };
@@ -1,26 +1,29 @@
1
1
  import { join } from 'path';
2
2
  import { pathToFileURL } from 'url';
3
- import { virtualRoot } from '../../shared/consts.js';
3
+ import { virtualRoot, virtualVendorRoot } from '../../shared/consts.js';
4
4
  import { BundleFormat } from '../bundle.js';
5
5
  import { wrapBundleError } from '../bundle_error.js';
6
6
  import { wrapNpmImportError } from '../npm_import_error.js';
7
7
  import { getPackagePath } from '../package_json.js';
8
8
  import { getFileHash } from '../utils/sha256.js';
9
- const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, externals, functions, importMap, }) => {
9
+ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, externals, featureFlags, functions, importMap, vendorDirectory, }) => {
10
10
  const extension = '.eszip';
11
11
  const destPath = join(distDirectory, `${buildID}${extension}`);
12
- const { bundler, importMap: bundlerImportMap } = getESZIPPaths();
13
- // Transforming all paths under `basePath` to use the virtual root prefix.
14
12
  const importMapPrefixes = {
15
13
  [`${pathToFileURL(basePath)}/`]: virtualRoot,
16
14
  };
17
- const importMapData = importMap.getContents(importMapPrefixes);
15
+ if (vendorDirectory !== undefined) {
16
+ importMapPrefixes[`${pathToFileURL(vendorDirectory)}/`] = virtualVendorRoot;
17
+ }
18
+ const { bundler, importMap: bundlerImportMap } = getESZIPPaths();
19
+ const importMapData = JSON.stringify(importMap.getContents(importMapPrefixes));
18
20
  const payload = {
19
21
  basePath,
20
22
  destPath,
21
23
  externals,
22
24
  functions,
23
- importMapData: JSON.stringify(importMapData),
25
+ importMapData,
26
+ vendorDirectory,
24
27
  };
25
28
  const flags = ['--allow-all', '--no-config', `--import-map=${bundlerImportMap}`];
26
29
  if (!debug) {
@@ -30,7 +33,9 @@ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, exte
30
33
  await deno.run(['run', ...flags, bundler, JSON.stringify(payload)], { pipeOutput: true });
31
34
  }
32
35
  catch (error) {
33
- throw wrapBundleError(wrapNpmImportError(error), { format: 'eszip' });
36
+ throw wrapBundleError(wrapNpmImportError(error, Boolean(featureFlags.edge_functions_npm_modules)), {
37
+ format: 'eszip',
38
+ });
34
39
  }
35
40
  const hash = await getFileHash(destPath);
36
41
  return { extension, format: BundleFormat.ESZIP2, hash };
@@ -9,11 +9,14 @@ export interface ImportMapFile {
9
9
  export declare class ImportMap {
10
10
  rootPath: string | null;
11
11
  sources: ImportMapFile[];
12
- constructor(sources?: ImportMapFile[], rootURL?: string | null);
12
+ constructor(sources?: ImportMapFile[], rootPath?: string | null);
13
13
  add(source: ImportMapFile): void;
14
14
  addFile(path: string, logger: Logger): Promise<void>;
15
15
  addFiles(paths: (string | undefined)[], logger: Logger): Promise<void>;
16
16
  static applyPrefixesToImports(imports: Imports, prefixes: Record<string, string>): Imports;
17
+ clone(): ImportMap;
18
+ static convertImportsToURLObjects(imports: Imports): Record<string, URL>;
19
+ static convertScopesToURLObjects(scopes: Record<string, Imports>): Record<string, Record<string, URL>>;
17
20
  static applyPrefixesToPath(path: string, prefixes: Record<string, string>): string;
18
21
  filterImports(imports?: Record<string, URL | null>): Record<string, string>;
19
22
  filterScopes(scopes?: ParsedImportMap['scopes']): Record<string, Imports>;
@@ -21,6 +24,10 @@ export declare class ImportMap {
21
24
  imports: Imports;
22
25
  scopes: {};
23
26
  };
27
+ getContentsWithURLObjects(prefixes?: Record<string, string>): {
28
+ imports: Record<string, URL>;
29
+ scopes: Record<string, Record<string, URL>>;
30
+ };
24
31
  static readFile(path: string, logger: Logger): Promise<ImportMapFile>;
25
32
  resolve(source: ImportMapFile): {
26
33
  imports: Record<string, string>;
@@ -11,8 +11,8 @@ const INTERNAL_IMPORTS = {
11
11
  // ImportMap can take several import map files and merge them into a final
12
12
  // import map object, also adding the internal imports in the right order.
13
13
  export class ImportMap {
14
- constructor(sources = [], rootURL = null) {
15
- this.rootPath = rootURL ? fileURLToPath(rootURL) : null;
14
+ constructor(sources = [], rootPath = null) {
15
+ this.rootPath = rootPath;
16
16
  this.sources = [];
17
17
  sources.forEach((file) => {
18
18
  this.add(file);
@@ -44,6 +44,21 @@ export class ImportMap {
44
44
  [key]: ImportMap.applyPrefixesToPath(value, prefixes),
45
45
  }), {});
46
46
  }
47
+ clone() {
48
+ return new ImportMap(this.sources, this.rootPath);
49
+ }
50
+ static convertImportsToURLObjects(imports) {
51
+ return Object.entries(imports).reduce((acc, [key, value]) => ({
52
+ ...acc,
53
+ [key]: new URL(value),
54
+ }), {});
55
+ }
56
+ static convertScopesToURLObjects(scopes) {
57
+ return Object.entries(scopes).reduce((acc, [key, value]) => ({
58
+ ...acc,
59
+ [key]: ImportMap.convertImportsToURLObjects(value),
60
+ }), {});
61
+ }
47
62
  // Applies a list of prefixes to a given path, returning the replaced path.
48
63
  // For example, given a `path` of `file:///foo/bar/baz.js` and a `prefixes`
49
64
  // object with `{"file:///foo/": "file:///hello/"}`, this method will return
@@ -122,6 +137,16 @@ export class ImportMap {
122
137
  scopes: transformedScopes,
123
138
  };
124
139
  }
140
+ // The same as `getContents`, but the URLs are represented as URL objects
141
+ // instead of strings. This is compatible with the `ParsedImportMap` type
142
+ // from the `@import-maps/resolve` library.
143
+ getContentsWithURLObjects(prefixes = {}) {
144
+ const { imports, scopes } = this.getContents(prefixes);
145
+ return {
146
+ imports: ImportMap.convertImportsToURLObjects(imports),
147
+ scopes: ImportMap.convertScopesToURLObjects(scopes),
148
+ };
149
+ }
125
150
  static async readFile(path, logger) {
126
151
  const baseURL = pathToFileURL(path);
127
152
  try {
@@ -20,11 +20,16 @@ test('Handles import maps with full URLs without specifying a base URL', () => {
20
20
  },
21
21
  };
22
22
  const map = new ImportMap([inputFile1, inputFile2]);
23
- const { imports } = map.getContents();
24
- expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts?v=legacy');
25
- expect(imports['@netlify/edge-functions']).toBe('https://edge.netlify.com/v1/index.ts');
26
- expect(imports['alias:jamstack']).toBe('https://jamstack.org/');
27
- expect(imports['alias:pets']).toBe('https://petsofnetlify.com/');
23
+ const m1 = map.getContents();
24
+ expect(m1.imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts?v=legacy');
25
+ expect(m1.imports['@netlify/edge-functions']).toBe('https://edge.netlify.com/v1/index.ts');
26
+ expect(m1.imports['alias:jamstack']).toBe('https://jamstack.org/');
27
+ expect(m1.imports['alias:pets']).toBe('https://petsofnetlify.com/');
28
+ const m2 = map.getContentsWithURLObjects();
29
+ expect(m2.imports['netlify:edge']).toStrictEqual(new URL('https://edge.netlify.com/v1/index.ts?v=legacy'));
30
+ expect(m2.imports['@netlify/edge-functions']).toStrictEqual(new URL('https://edge.netlify.com/v1/index.ts'));
31
+ expect(m2.imports['alias:jamstack']).toStrictEqual(new URL('https://jamstack.org/'));
32
+ expect(m2.imports['alias:pets']).toStrictEqual(new URL('https://petsofnetlify.com/'));
28
33
  });
29
34
  test('Resolves relative paths to absolute paths if a base path is not provided', () => {
30
35
  const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
@@ -35,11 +40,15 @@ test('Resolves relative paths to absolute paths if a base path is not provided',
35
40
  },
36
41
  };
37
42
  const map = new ImportMap([inputFile1]);
38
- const { imports } = map.getContents();
39
43
  const expectedPath = join(cwd(), 'my-cool-site', 'heart', 'pets');
40
- expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts?v=legacy');
41
- expect(imports['@netlify/edge-functions']).toBe('https://edge.netlify.com/v1/index.ts');
42
- expect(imports['alias:pets']).toBe(`${pathToFileURL(expectedPath).toString()}/`);
44
+ const m1 = map.getContents();
45
+ expect(m1.imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts?v=legacy');
46
+ expect(m1.imports['@netlify/edge-functions']).toBe('https://edge.netlify.com/v1/index.ts');
47
+ expect(m1.imports['alias:pets']).toBe(`${pathToFileURL(expectedPath).toString()}/`);
48
+ const m2 = map.getContentsWithURLObjects();
49
+ expect(m2.imports['netlify:edge']).toStrictEqual(new URL('https://edge.netlify.com/v1/index.ts?v=legacy'));
50
+ expect(m2.imports['@netlify/edge-functions']).toStrictEqual(new URL('https://edge.netlify.com/v1/index.ts'));
51
+ expect(m2.imports['alias:pets']).toStrictEqual(new URL(`${pathToFileURL(expectedPath).toString()}/`));
43
52
  });
44
53
  describe('Returns the fully resolved import map', () => {
45
54
  const inputFile1 = {
@@ -112,7 +121,7 @@ test('Throws when an import map uses a relative path to reference a file outside
112
121
  'alias:file': '../file.js',
113
122
  },
114
123
  };
115
- const map = new ImportMap([inputFile1], pathToFileURL(cwd()).toString());
124
+ const map = new ImportMap([inputFile1], cwd());
116
125
  expect(() => map.getContents()).toThrowError(`Import map cannot reference '${join(cwd(), '..', 'file.js')}' as it's outside of the base directory '${cwd()}'`);
117
126
  });
118
127
  test('Writes import map file to disk', async () => {
@@ -134,3 +143,45 @@ test('Writes import map file to disk', async () => {
134
143
  expect(imports['@netlify/edge-functions']).toBe('https://edge.netlify.com/v1/index.ts');
135
144
  expect(imports['alias:pets']).toBe(pathToFileURL(expectedPath).toString());
136
145
  });
146
+ test('Clones an import map', () => {
147
+ const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
148
+ const inputFile1 = {
149
+ baseURL: pathToFileURL(basePath),
150
+ imports: {
151
+ 'alias:jamstack': 'https://jamstack.org',
152
+ },
153
+ };
154
+ const inputFile2 = {
155
+ baseURL: pathToFileURL(basePath),
156
+ imports: {
157
+ 'alias:pets': 'https://petsofnetlify.com/',
158
+ },
159
+ };
160
+ const map1 = new ImportMap([inputFile1, inputFile2]);
161
+ const map2 = map1.clone();
162
+ map2.add({
163
+ baseURL: pathToFileURL(basePath),
164
+ imports: {
165
+ netlify: 'https://netlify.com',
166
+ },
167
+ });
168
+ expect(map1.getContents()).toStrictEqual({
169
+ imports: {
170
+ 'netlify:edge': 'https://edge.netlify.com/v1/index.ts?v=legacy',
171
+ '@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
172
+ 'alias:jamstack': 'https://jamstack.org/',
173
+ 'alias:pets': 'https://petsofnetlify.com/',
174
+ },
175
+ scopes: {},
176
+ });
177
+ expect(map2.getContents()).toStrictEqual({
178
+ imports: {
179
+ 'netlify:edge': 'https://edge.netlify.com/v1/index.ts?v=legacy',
180
+ '@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
181
+ 'alias:jamstack': 'https://jamstack.org/',
182
+ 'alias:pets': 'https://petsofnetlify.com/',
183
+ netlify: 'https://netlify.com/',
184
+ },
185
+ scopes: {},
186
+ });
187
+ });
@@ -0,0 +1,22 @@
1
+ /// <reference types="node" />
2
+ import { ParsedImportMap } from '@import-maps/resolve';
3
+ import { Plugin } from 'esbuild';
4
+ import { ImportMap } from './import_map.js';
5
+ import { Logger } from './logger.js';
6
+ export declare const getDependencyTrackerPlugin: (specifiers: Set<string>, importMap: ParsedImportMap, baseURL: URL) => Plugin;
7
+ interface VendorNPMSpecifiersOptions {
8
+ basePath: string;
9
+ directory?: string;
10
+ functions: string[];
11
+ importMap: ImportMap;
12
+ logger: Logger;
13
+ }
14
+ export declare const vendorNPMSpecifiers: ({ basePath, directory, functions, importMap, logger, }: VendorNPMSpecifiersOptions) => Promise<{
15
+ cleanup: () => Promise<void>;
16
+ directory: string;
17
+ importMap: {
18
+ baseURL: import("node:url").URL;
19
+ imports: Record<string, string>;
20
+ };
21
+ } | undefined>;
22
+ export {};
@@ -0,0 +1,184 @@
1
+ import { promises as fs } from 'fs';
2
+ import { builtinModules, createRequire } from 'module';
3
+ import path from 'path';
4
+ import { fileURLToPath, pathToFileURL } from 'url';
5
+ import { resolve } from '@import-maps/resolve';
6
+ import { build } from 'esbuild';
7
+ import tmp from 'tmp-promise';
8
+ import { nodePrefix, npmPrefix } from '../shared/consts.js';
9
+ const builtinModulesSet = new Set(builtinModules);
10
+ const require = createRequire(import.meta.url);
11
+ // Workaround for https://github.com/evanw/esbuild/issues/1921.
12
+ const banner = {
13
+ js: `
14
+ import {createRequire as ___nfyCreateRequire} from "node:module";
15
+ import {fileURLToPath as ___nfyFileURLToPath} from "node:url";
16
+ import {dirname as ___nfyPathDirname} from "node:path";
17
+ let __filename=___nfyFileURLToPath(import.meta.url);
18
+ let __dirname=___nfyPathDirname(___nfyFileURLToPath(import.meta.url));
19
+ let require=___nfyCreateRequire(import.meta.url);
20
+ `,
21
+ };
22
+ // esbuild plugin that will traverse the code and look for imports of external
23
+ // dependencies (i.e. Node modules). It stores the specifiers found in the Set
24
+ // provided.
25
+ export const getDependencyTrackerPlugin = (specifiers, importMap, baseURL) => ({
26
+ name: 'dependency-tracker',
27
+ setup(build) {
28
+ build.onResolve({ filter: /^(.*)$/ }, (args) => {
29
+ if (args.kind !== 'import-statement') {
30
+ return;
31
+ }
32
+ const result = {};
33
+ let specifier = args.path;
34
+ // Start by checking whether the specifier matches any import map defined
35
+ // by the user.
36
+ const { matched, resolvedImport } = resolve(specifier, importMap, baseURL);
37
+ // If it does, the resolved import is the specifier we'll evaluate going
38
+ // forward.
39
+ if (matched) {
40
+ specifier = fileURLToPath(resolvedImport).replace(/\\/g, '/');
41
+ result.path = specifier;
42
+ }
43
+ // If the specifier is a Node.js built-in, we don't want to bundle it.
44
+ if (specifier.startsWith(nodePrefix) || builtinModulesSet.has(specifier)) {
45
+ return { external: true };
46
+ }
47
+ // We don't support the `npm:` prefix yet. Mark the specifier as external
48
+ // and the ESZIP bundler will handle the failure.
49
+ if (specifier.startsWith(npmPrefix)) {
50
+ return { external: true };
51
+ }
52
+ const isLocalImport = specifier.startsWith(path.sep) || specifier.startsWith('.');
53
+ // If this is a local import, return so that esbuild visits that path.
54
+ if (isLocalImport) {
55
+ return result;
56
+ }
57
+ const isRemoteURLImport = specifier.startsWith('https://') || specifier.startsWith('http://');
58
+ if (isRemoteURLImport) {
59
+ return { external: true };
60
+ }
61
+ // At this point we know we're dealing with a bare specifier that should
62
+ // be treated as an external module. We first try to resolve it, because
63
+ // in the event that it doesn't exist (e.g. user is referencing a module
64
+ // that they haven't installed) we won't even attempt to bundle it. This
65
+ // lets the ESZIP bundler handle and report the missing import instead of
66
+ // esbuild, which is a better experience for the user.
67
+ try {
68
+ require.resolve(specifier, { paths: [args.resolveDir] });
69
+ specifiers.add(specifier);
70
+ }
71
+ catch {
72
+ // no-op
73
+ }
74
+ // Mark the specifier as external, because we don't want to traverse the
75
+ // entire module tree — i.e. if user code imports module `foo` and that
76
+ // imports `bar`, we only want to add `foo` to the list of specifiers,
77
+ // since the whole module — including its dependencies like `bar` —
78
+ // will be bundled.
79
+ return { external: true };
80
+ });
81
+ },
82
+ });
83
+ export const vendorNPMSpecifiers = async ({ basePath, directory, functions, importMap, logger, }) => {
84
+ const specifiers = new Set();
85
+ // The directories that esbuild will use when resolving Node modules. We must
86
+ // set these manually because esbuild will be operating from a temporary
87
+ // directory that will not live inside the project root, so the normal
88
+ // resolution logic won't work.
89
+ const nodePaths = [path.join(basePath, 'node_modules')];
90
+ // We need to create some files on disk, which we don't want to write to the
91
+ // project directory. If a custom directory has been specified, we use it.
92
+ // Otherwise, create a random temporary directory.
93
+ const temporaryDirectory = directory ? { path: directory } : await tmp.dir();
94
+ // Do a first pass at bundling to gather a list of specifiers that should be
95
+ // loaded as npm dependencies, because they either use the `npm:` prefix or
96
+ // they are bare specifiers. We'll collect them in `specifiers`.
97
+ try {
98
+ await build({
99
+ banner,
100
+ bundle: true,
101
+ entryPoints: functions,
102
+ logLevel: 'error',
103
+ nodePaths,
104
+ outdir: temporaryDirectory.path,
105
+ platform: 'node',
106
+ plugins: [getDependencyTrackerPlugin(specifiers, importMap.getContentsWithURLObjects(), pathToFileURL(basePath))],
107
+ write: false,
108
+ });
109
+ }
110
+ catch (error) {
111
+ logger.system('Could not track dependencies in edge function:', error);
112
+ logger.user('An error occurred when trying to scan your edge functions for npm modules, which is an experimental feature. If you are loading npm modules, please share the errors above in https://ntl.fyi/edge-functions-npm. If you are not loading npm modules, you can ignore this message.');
113
+ }
114
+ // If we found no specifiers, there's nothing left to do here.
115
+ if (specifiers.size === 0) {
116
+ return;
117
+ }
118
+ logger.user('You are using npm modules in Edge Functions, which is an experimental feature. Learn more at https://ntl.fyi/edge-functions-npm.');
119
+ // To bundle an entire module and all its dependencies, create a barrel file
120
+ // where we re-export everything from that specifier. We do this for every
121
+ // specifier, and each of these files will become entry points to esbuild.
122
+ const ops = await Promise.all([...specifiers].map(async (specifier, index) => {
123
+ const code = `import * as mod from "${specifier}"; export default mod.default; export * from "${specifier}";`;
124
+ const filePath = path.join(temporaryDirectory.path, `barrel-${index}.js`);
125
+ await fs.writeFile(filePath, code);
126
+ return { filePath, specifier };
127
+ }));
128
+ const entryPoints = ops.map(({ filePath }) => filePath);
129
+ // Bundle each of the barrel files we created. We'll end up with a compiled
130
+ // version of each of the barrel files, plus any chunks of shared code
131
+ // between them (such that a common module isn't bundled twice).
132
+ await build({
133
+ allowOverwrite: true,
134
+ banner,
135
+ bundle: true,
136
+ entryPoints,
137
+ format: 'esm',
138
+ logLevel: 'error',
139
+ nodePaths,
140
+ outdir: temporaryDirectory.path,
141
+ platform: 'node',
142
+ splitting: true,
143
+ target: 'es2020',
144
+ });
145
+ // Add all Node.js built-ins to the import map, so any unprefixed specifiers
146
+ // (e.g. `process`) resolve to the prefixed versions (e.g. `node:prefix`),
147
+ // which Deno can process.
148
+ const builtIns = builtinModules.reduce((acc, name) => ({
149
+ ...acc,
150
+ [name]: `node:${name}`,
151
+ }), {});
152
+ // Creates an object that is compatible with the `imports` block of an import
153
+ // map, mapping specifiers to the paths of their bundled files on disk. Each
154
+ // specifier gets two entries in the import map, one with the `npm:` prefix
155
+ // and one without, such that both options are supported.
156
+ const newImportMap = {
157
+ baseURL: pathToFileURL(temporaryDirectory.path),
158
+ imports: ops.reduce((acc, op) => {
159
+ const url = pathToFileURL(op.filePath).toString();
160
+ return {
161
+ ...acc,
162
+ [op.specifier]: url,
163
+ };
164
+ }, builtIns),
165
+ };
166
+ const cleanup = async () => {
167
+ // If a custom temporary directory was specified, we leave the cleanup job
168
+ // up to the caller.
169
+ if (directory) {
170
+ return;
171
+ }
172
+ try {
173
+ await fs.rm(temporaryDirectory.path, { force: true, recursive: true });
174
+ }
175
+ catch {
176
+ // no-op
177
+ }
178
+ };
179
+ return {
180
+ cleanup,
181
+ directory: temporaryDirectory.path,
182
+ importMap: newImportMap,
183
+ };
184
+ };
@@ -1,5 +1,5 @@
1
1
  declare class NPMImportError extends Error {
2
- constructor(originalError: Error, moduleName: string);
2
+ constructor(originalError: Error, moduleName: string, supportsNPM: boolean);
3
3
  }
4
- declare const wrapNpmImportError: (input: unknown) => unknown;
4
+ declare const wrapNpmImportError: (input: unknown, supportsNPM: boolean) => unknown;
5
5
  export { NPMImportError, wrapNpmImportError };
@@ -1,23 +1,27 @@
1
1
  class NPMImportError extends Error {
2
- constructor(originalError, moduleName) {
3
- super(`It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/${moduleName}"'?`);
2
+ constructor(originalError, moduleName, supportsNPM) {
3
+ let message = `It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from "https://esm.sh/${moduleName}"'?`;
4
+ if (supportsNPM) {
5
+ message = `There was an error when loading the '${moduleName}' npm module. Support for npm modules in edge functions is an experimental feature. Refer to https://ntl.fyi/edge-functions-npm for more information.`;
6
+ }
7
+ super(message);
4
8
  this.name = 'NPMImportError';
5
9
  this.stack = originalError.stack;
6
10
  // https://github.com/microsoft/TypeScript-wiki/blob/8a66ecaf77118de456f7cd9c56848a40fe29b9b4/Breaking-Changes.md#implicit-any-error-raised-for-un-annotated-callback-arguments-with-no-matching-overload-arguments
7
11
  Object.setPrototypeOf(this, NPMImportError.prototype);
8
12
  }
9
13
  }
10
- const wrapNpmImportError = (input) => {
14
+ const wrapNpmImportError = (input, supportsNPM) => {
11
15
  if (input instanceof Error) {
12
16
  const match = input.message.match(/Relative import path "(.*)" not prefixed with/);
13
17
  if (match !== null) {
14
18
  const [, moduleName] = match;
15
- return new NPMImportError(input, moduleName);
19
+ return new NPMImportError(input, moduleName, supportsNPM);
16
20
  }
17
21
  const schemeMatch = input.message.match(/Error: Module not found "npm:(.*)"/);
18
22
  if (schemeMatch !== null) {
19
23
  const [, moduleName] = schemeMatch;
20
- return new NPMImportError(input, moduleName);
24
+ return new NPMImportError(input, moduleName, supportsNPM);
21
25
  }
22
26
  }
23
27
  return input;
@@ -7,7 +7,7 @@ import { OnAfterDownloadHook, OnBeforeDownloadHook } from '../bridge.js';
7
7
  import { FunctionConfig } from '../config.js';
8
8
  import type { EdgeFunction } from '../edge_function.js';
9
9
  import { LogFunction } from '../logger.js';
10
- type FormatFunction = (name: string) => string;
10
+ export type FormatFunction = (name: string) => string;
11
11
  interface StartServerOptions {
12
12
  getFunctionsConfig?: boolean;
13
13
  }
@@ -17,6 +17,7 @@ interface InspectSettings {
17
17
  address?: string;
18
18
  }
19
19
  interface ServeOptions {
20
+ basePath: string;
20
21
  bootstrapURL: string;
21
22
  certificatePath?: string;
22
23
  debug?: boolean;
@@ -28,12 +29,12 @@ interface ServeOptions {
28
29
  formatExportTypeError?: FormatFunction;
29
30
  formatImportError?: FormatFunction;
30
31
  port: number;
32
+ servePath: string;
31
33
  systemLogger?: LogFunction;
32
34
  }
33
- declare const serve: ({ bootstrapURL, certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMapPaths, onAfterDownload, onBeforeDownload, port, systemLogger, }: ServeOptions) => Promise<(functions: EdgeFunction[], env?: NodeJS.ProcessEnv, options?: StartServerOptions) => Promise<{
35
+ export declare const serve: ({ basePath, bootstrapURL, certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMapPaths, onAfterDownload, onBeforeDownload, port, servePath, systemLogger, }: ServeOptions) => Promise<(functions: EdgeFunction[], env?: NodeJS.ProcessEnv, options?: StartServerOptions) => Promise<{
34
36
  functionsConfig: FunctionConfig[];
35
37
  graph: any;
36
38
  success: boolean;
37
39
  }>>;
38
- export { serve };
39
- export type { FormatFunction };
40
+ export {};
@@ -1,12 +1,12 @@
1
- import { tmpName } from 'tmp-promise';
2
1
  import { DenoBridge } from '../bridge.js';
3
2
  import { getFunctionConfig } from '../config.js';
4
3
  import { generateStage2 } from '../formats/javascript.js';
5
4
  import { ImportMap } from '../import_map.js';
6
5
  import { getLogger } from '../logger.js';
6
+ import { vendorNPMSpecifiers } from '../npm_dependencies.js';
7
7
  import { ensureLatestTypes } from '../types.js';
8
8
  import { killProcess, waitForServer } from './util.js';
9
- const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, formatExportTypeError, formatImportError, importMap, logger, port, }) => {
9
+ const prepareServer = ({ basePath, bootstrapURL, deno, distDirectory, distImportMapPath, flags: denoFlags, formatExportTypeError, formatImportError, importMap: baseImportMap, logger, port, }) => {
10
10
  const processRef = {};
11
11
  const startServer = async (functions, env = {}, options = {}) => {
12
12
  if ((processRef === null || processRef === void 0 ? void 0 : processRef.ps) !== undefined) {
@@ -21,6 +21,17 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
21
21
  formatExportTypeError,
22
22
  formatImportError,
23
23
  });
24
+ const importMap = baseImportMap.clone();
25
+ const vendor = await vendorNPMSpecifiers({
26
+ basePath,
27
+ directory: distDirectory,
28
+ functions: functions.map(({ path }) => path),
29
+ importMap,
30
+ logger,
31
+ });
32
+ if (vendor) {
33
+ importMap.add(vendor.importMap);
34
+ }
24
35
  try {
25
36
  // This command will print a JSON object with all the modules found in
26
37
  // the `stage2Path` file as well as all of their dependencies.
@@ -32,11 +43,12 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
32
43
  catch {
33
44
  // no-op
34
45
  }
35
- const bootstrapFlags = ['--port', port.toString()];
46
+ const extraDenoFlags = [`--import-map=${importMap.toDataURL()}`];
47
+ const applicationFlags = ['--port', port.toString()];
36
48
  // We set `extendEnv: false` to avoid polluting the edge function context
37
49
  // with variables from the user's system, since those will not be available
38
50
  // in the production environment.
39
- await deno.runInBackground(['run', ...denoFlags, stage2Path, ...bootstrapFlags], processRef, {
51
+ await deno.runInBackground(['run', ...denoFlags, ...extraDenoFlags, stage2Path, ...applicationFlags], processRef, {
40
52
  pipeOutput: true,
41
53
  env,
42
54
  extendEnv: false,
@@ -45,6 +57,9 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
45
57
  if (options.getFunctionsConfig) {
46
58
  functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig({ func, importMap, deno, log: logger })));
47
59
  }
60
+ if (distImportMapPath) {
61
+ await importMap.writeToFile(distImportMapPath);
62
+ }
48
63
  const success = await waitForServer(port, processRef.ps);
49
64
  return {
50
65
  functionsConfig,
@@ -54,7 +69,7 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
54
69
  };
55
70
  return startServer;
56
71
  };
57
- const serve = async ({ bootstrapURL, certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMapPaths = [], onAfterDownload, onBeforeDownload, port, systemLogger, }) => {
72
+ export const serve = async ({ basePath, bootstrapURL, certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMapPaths = [], onAfterDownload, onBeforeDownload, port, servePath, systemLogger, }) => {
58
73
  const logger = getLogger(systemLogger, debug);
59
74
  const deno = new DenoBridge({
60
75
  debug,
@@ -62,16 +77,11 @@ const serve = async ({ bootstrapURL, certificatePath, debug, distImportMapPath,
62
77
  onAfterDownload,
63
78
  onBeforeDownload,
64
79
  });
65
- // We need to generate a stage 2 file and write it somewhere. We use a
66
- // temporary directory for that.
67
- const distDirectory = await tmpName();
68
80
  // Wait for the binary to be downloaded if needed.
69
81
  await deno.getBinaryPath();
70
82
  // Downloading latest types if needed.
71
83
  await ensureLatestTypes(deno, logger);
72
- const importMap = new ImportMap();
73
- await importMap.addFiles(importMapPaths, logger);
74
- const flags = ['--allow-all', `--import-map=${importMap.toDataURL()}`, '--no-config'];
84
+ const flags = ['--allow-all', '--no-config'];
75
85
  if (certificatePath) {
76
86
  flags.push(`--cert=${certificatePath}`);
77
87
  }
@@ -89,10 +99,14 @@ const serve = async ({ bootstrapURL, certificatePath, debug, distImportMapPath,
89
99
  flags.push(inspectSettings.address ? `--inspect=${inspectSettings.address}` : '--inspect');
90
100
  }
91
101
  }
102
+ const importMap = new ImportMap();
103
+ await importMap.addFiles(importMapPaths, logger);
92
104
  const server = prepareServer({
105
+ basePath,
93
106
  bootstrapURL,
94
107
  deno,
95
- distDirectory,
108
+ distDirectory: servePath,
109
+ distImportMapPath,
96
110
  flags,
97
111
  formatExportTypeError,
98
112
  formatImportError,
@@ -100,9 +114,5 @@ const serve = async ({ bootstrapURL, certificatePath, debug, distImportMapPath,
100
114
  logger,
101
115
  port,
102
116
  });
103
- if (distImportMapPath) {
104
- await importMap.writeToFile(distImportMapPath);
105
- }
106
117
  return server;
107
118
  };
108
- export { serve };
@@ -1,6 +1,7 @@
1
1
  import { join } from 'path';
2
2
  import getPort from 'get-port';
3
3
  import fetch from 'node-fetch';
4
+ import { tmpName } from 'tmp-promise';
4
5
  import { v4 as uuidv4 } from 'uuid';
5
6
  import { test, expect } from 'vitest';
6
7
  import { fixturesDir } from '../../test/util.js';
@@ -13,10 +14,13 @@ test('Starts a server and serves requests for edge functions', async () => {
13
14
  };
14
15
  const port = await getPort();
15
16
  const importMapPaths = [join(paths.internal, 'import_map.json'), join(paths.user, 'import-map.json')];
17
+ const servePath = await tmpName();
16
18
  const server = await serve({
19
+ basePath,
17
20
  bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
18
21
  importMapPaths,
19
22
  port,
23
+ servePath,
20
24
  });
21
25
  const functions = [
22
26
  {
@@ -1,2 +1,5 @@
1
1
  export declare const importMapSpecifier = "netlify:import-map";
2
+ export declare const nodePrefix = "node:";
3
+ export declare const npmPrefix = "npm:";
2
4
  export declare const virtualRoot = "file:///root/";
5
+ export declare const virtualVendorRoot = "file:///vendor/";
@@ -1,2 +1,5 @@
1
1
  export const importMapSpecifier = 'netlify:import-map';
2
+ export const nodePrefix = 'node:';
3
+ export const npmPrefix = 'npm:';
2
4
  export const virtualRoot = 'file:///root/';
5
+ export const virtualVendorRoot = 'file:///vendor/';
@@ -8,4 +8,5 @@ export interface WriteStage2Options {
8
8
  externals: string[];
9
9
  functions: InputFunction[];
10
10
  importMapData?: string;
11
+ vendorDirectory?: string;
11
12
  }
@@ -7,5 +7,5 @@ declare const useFixture: (fixtureName: string) => Promise<{
7
7
  distPath: string;
8
8
  }>;
9
9
  declare const getRouteMatcher: (manifest: Manifest) => (candidate: string) => import("../node/manifest.js").Route | undefined;
10
- declare const runESZIP: (eszipPath: string) => Promise<any>;
10
+ declare const runESZIP: (eszipPath: string, vendorDirectory?: string) => Promise<any>;
11
11
  export { fixturesDir, getRouteMatcher, testLogger, runESZIP, useFixture };
package/dist/test/util.js CHANGED
@@ -48,7 +48,7 @@ const getRouteMatcher = (manifest) => (candidate) => manifest.routes.find((route
48
48
  const isExcluded = excludedPatterns.some((pattern) => new RegExp(pattern).test(candidate));
49
49
  return !isExcluded;
50
50
  });
51
- const runESZIP = async (eszipPath) => {
51
+ const runESZIP = async (eszipPath, vendorDirectory) => {
52
52
  var _a, _b, _c;
53
53
  const tmpDir = await tmp.dir({ unsafeCleanup: true });
54
54
  // Extract ESZIP into temporary directory.
@@ -68,7 +68,10 @@ const runESZIP = async (eszipPath) => {
68
68
  const importMapPath = join(virtualRootPath, '..', 'import-map');
69
69
  for (const path of [importMapPath, stage2Path]) {
70
70
  const file = await fs.readFile(path, 'utf8');
71
- const normalizedFile = file.replace(/file:\/\/\/root/g, pathToFileURL(virtualRootPath).toString());
71
+ let normalizedFile = file.replace(/file:\/{3}root/g, pathToFileURL(virtualRootPath).toString());
72
+ if (vendorDirectory !== undefined) {
73
+ normalizedFile = normalizedFile.replace(/file:\/{3}vendor/g, pathToFileURL(vendorDirectory).toString());
74
+ }
72
75
  await fs.writeFile(path, normalizedFile);
73
76
  }
74
77
  await fs.rename(stage2Path, `${stage2Path}.js`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "8.19.1",
3
+ "version": "9.0.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -79,6 +79,7 @@
79
79
  "better-ajv-errors": "^1.2.0",
80
80
  "common-path-prefix": "^3.0.0",
81
81
  "env-paths": "^3.0.0",
82
+ "esbuild": "0.19.2",
82
83
  "execa": "^6.0.0",
83
84
  "find-up": "^6.3.0",
84
85
  "get-port": "^6.1.2",
package/shared/consts.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export const importMapSpecifier = 'netlify:import-map'
2
+ export const nodePrefix = 'node:'
3
+ export const npmPrefix = 'npm:'
2
4
  export const virtualRoot = 'file:///root/'
5
+ export const virtualVendorRoot = 'file:///vendor/'
package/shared/stage2.ts CHANGED
@@ -9,4 +9,5 @@ export interface WriteStage2Options {
9
9
  externals: string[]
10
10
  functions: InputFunction[]
11
11
  importMapData?: string
12
+ vendorDirectory?: string
12
13
  }