@netlify/edge-bundler 6.1.0 → 7.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.
@@ -9,11 +9,12 @@ interface BundleOptions {
9
9
  debug?: boolean;
10
10
  distImportMapPath?: string;
11
11
  featureFlags?: FeatureFlags;
12
+ importMapPaths?: (string | undefined)[];
12
13
  onAfterDownload?: OnAfterDownloadHook;
13
14
  onBeforeDownload?: OnBeforeDownloadHook;
14
15
  systemLogger?: LogFunction;
15
16
  }
16
- declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
17
+ declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
17
18
  functions: import("./edge_function.js").EdgeFunction[];
18
19
  manifest: import("./manifest.js").Manifest;
19
20
  }>;
@@ -6,16 +6,15 @@ import { importMapSpecifier } from '../shared/consts.js';
6
6
  import { DenoBridge } from './bridge.js';
7
7
  import { getFunctionConfig } from './config.js';
8
8
  import { getDeclarationsFromConfig } from './declaration.js';
9
- import { getConfig as getDenoConfig } from './deno_config.js';
10
9
  import { load as loadDeployConfig } from './deploy_config.js';
11
10
  import { getFlags } from './feature_flags.js';
12
11
  import { findFunctions } from './finder.js';
13
12
  import { bundle as bundleESZIP } from './formats/eszip.js';
14
- import { ImportMap, readFile as readImportMapFile } from './import_map.js';
13
+ import { ImportMap } from './import_map.js';
15
14
  import { getLogger } from './logger.js';
16
15
  import { writeManifest } from './manifest.js';
17
16
  import { ensureLatestTypes } from './types.js';
18
- const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
17
+ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
19
18
  const logger = getLogger(systemLogger, debug);
20
19
  const featureFlags = getFlags(inputFeatureFlags);
21
20
  const options = {
@@ -42,19 +41,7 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
42
41
  // not actually included in the bundle.
43
42
  const externals = deployConfig.layers.map((layer) => layer.name);
44
43
  const importMap = new ImportMap();
45
- if (deployConfig.importMap) {
46
- importMap.add(deployConfig.importMap);
47
- }
48
- if (featureFlags.edge_functions_read_deno_config) {
49
- // Look for a Deno config file and read it if one exists.
50
- const denoConfig = await getDenoConfig(logger, basePath);
51
- // If the Deno config file defines an import map, read the file and add the
52
- // imports to the global import map.
53
- if (denoConfig === null || denoConfig === void 0 ? void 0 : denoConfig.importMap) {
54
- const importMapFile = await readImportMapFile(denoConfig.importMap);
55
- importMap.add(importMapFile);
56
- }
57
- }
44
+ await importMap.addFiles([deployConfig === null || deployConfig === void 0 ? void 0 : deployConfig.importMap, ...importMapPaths], logger);
58
45
  const functions = await findFunctions(sourceDirectories);
59
46
  const functionBundle = await bundleESZIP({
60
47
  basePath,
@@ -3,12 +3,12 @@ import { join, resolve } from 'path';
3
3
  import process from 'process';
4
4
  import { deleteAsync } from 'del';
5
5
  import tmp from 'tmp-promise';
6
- import { test, expect } from 'vitest';
6
+ import { test, expect, vi } from 'vitest';
7
7
  import { importMapSpecifier } from '../shared/consts.js';
8
- import { useFixture } from '../test/util.js';
8
+ import { runESZIP, useFixture } from '../test/util.js';
9
9
  import { BundleError } from './bundle_error.js';
10
10
  import { bundle } from './bundler.js';
11
- import { isNodeError } from './utils/error.js';
11
+ import { isFileNotFoundError } from './utils/error.js';
12
12
  import { validateManifest } from './validation/manifest/index.js';
13
13
  test('Produces an ESZIP bundle', async () => {
14
14
  const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
@@ -23,12 +23,10 @@ test('Produces an ESZIP bundle', async () => {
23
23
  const result = await bundle([userDirectory, internalDirectory], distPath, declarations, {
24
24
  basePath,
25
25
  configPath: join(internalDirectory, 'config.json'),
26
- featureFlags: {
27
- edge_functions_read_deno_config: true,
28
- },
26
+ importMapPaths: [join(userDirectory, 'import_map.json')],
29
27
  });
30
28
  const generatedFiles = await fs.readdir(distPath);
31
- expect(result.functions.length).toBe(2);
29
+ expect(result.functions.length).toBe(3);
32
30
  expect(generatedFiles.length).toBe(2);
33
31
  const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
34
32
  const manifest = JSON.parse(manifestFile);
@@ -38,6 +36,11 @@ test('Produces an ESZIP bundle', async () => {
38
36
  expect(bundles[0].format).toBe('eszip2');
39
37
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
40
38
  expect(importMapURL).toBe(importMapSpecifier);
39
+ const bundlePath = join(distPath, bundles[0].asset);
40
+ const { func1, func2, func3 } = await runESZIP(bundlePath);
41
+ expect(func1).toBe('HELLO, JANE DOE!');
42
+ expect(func2).toBe('Jane Doe');
43
+ expect(func3).toBe('hello, netlify!');
41
44
  await cleanup();
42
45
  });
43
46
  test('Uses the vendored eszip module instead of fetching it from deno.land', async () => {
@@ -218,7 +221,7 @@ test('Ignores any user-defined `deno.json` files', async () => {
218
221
  throw new Error(`The file at '${denoConfigPath} would be overwritten by this test. Please move the file to a different location and try again.'`);
219
222
  }
220
223
  catch (error) {
221
- if (isNodeError(error) && error.code !== 'ENOENT') {
224
+ if (!isFileNotFoundError(error)) {
222
225
  throw error;
223
226
  }
224
227
  }
@@ -280,3 +283,26 @@ test('Loads declarations and import maps from the deploy configuration', async (
280
283
  expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
281
284
  await cleanup();
282
285
  });
286
+ test("Ignores entries in `importMapPaths` that don't point to an existing import map file", async () => {
287
+ const systemLogger = vi.fn();
288
+ const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
289
+ const sourceDirectory = join(basePath, 'functions');
290
+ const importMapPath = join(distPath, 'some-file-that-does-not-exist.json');
291
+ const declarations = [
292
+ {
293
+ function: 'func1',
294
+ path: '/func1',
295
+ },
296
+ ];
297
+ const result = await bundle([sourceDirectory], distPath, declarations, {
298
+ basePath,
299
+ configPath: join(sourceDirectory, 'config.json'),
300
+ importMapPaths: [importMapPath],
301
+ systemLogger,
302
+ });
303
+ const generatedFiles = await fs.readdir(distPath);
304
+ expect(result.functions.length).toBe(1);
305
+ expect(generatedFiles.length).toBe(2);
306
+ expect(systemLogger).toHaveBeenCalledWith(`Did not find an import map file at '${importMapPath}'.`);
307
+ await cleanup();
308
+ });
@@ -1,10 +1,9 @@
1
1
  import type { Declaration } from './declaration.js';
2
- import { ImportMapFile } from './import_map.js';
3
2
  import type { Layer } from './layer.js';
4
3
  import type { Logger } from './logger.js';
5
4
  export interface DeployConfig {
6
5
  declarations: Declaration[];
7
- importMap?: ImportMapFile;
6
+ importMap?: string;
8
7
  layers: Layer[];
9
8
  }
10
9
  export declare const load: (path: string | undefined, logger: Logger) => Promise<DeployConfig>;
@@ -1,7 +1,6 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { dirname, resolve } from 'path';
3
- import { readFile as readImportMap } from './import_map.js';
4
- import { isNodeError } from './utils/error.js';
3
+ import { isFileNotFoundError } from './utils/error.js';
5
4
  export const load = async (path, logger) => {
6
5
  if (path === undefined) {
7
6
  return {
@@ -15,7 +14,7 @@ export const load = async (path, logger) => {
15
14
  return parse(config, path);
16
15
  }
17
16
  catch (error) {
18
- if (isNodeError(error) && error.code !== 'ENOENT') {
17
+ if (!isFileNotFoundError(error)) {
19
18
  logger.system('Error while parsing internal edge functions manifest:', error);
20
19
  }
21
20
  }
@@ -24,7 +23,7 @@ export const load = async (path, logger) => {
24
23
  layers: [],
25
24
  };
26
25
  };
27
- const parse = async (data, path) => {
26
+ const parse = (data, path) => {
28
27
  var _a, _b;
29
28
  if (data.version !== 1) {
30
29
  throw new Error(`Unsupported file version: ${data.version}`);
@@ -35,11 +34,7 @@ const parse = async (data, path) => {
35
34
  };
36
35
  if (data.import_map) {
37
36
  const importMapPath = resolve(dirname(path), data.import_map);
38
- const importMap = await readImportMap(importMapPath);
39
- return {
40
- ...config,
41
- importMap,
42
- };
37
+ config.importMap = importMapPath;
43
38
  }
44
39
  return config;
45
40
  };
@@ -1,7 +1,6 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { cwd } from 'process';
4
- import { pathToFileURL } from 'url';
5
4
  import tmp from 'tmp-promise';
6
5
  import { test, expect } from 'vitest';
7
6
  import { load } from './deploy_config.js';
@@ -14,8 +13,7 @@ test('Returns an empty config object if there is no file at the given path', asy
14
13
  expect(config.layers).toEqual([]);
15
14
  });
16
15
  test('Returns a config object with declarations, layers, and import map', async () => {
17
- var _a, _b;
18
- const importMapFile = await tmp.file();
16
+ const importMapFile = await tmp.file({ postfix: '.json' });
19
17
  const importMap = {
20
18
  imports: {
21
19
  'https://deno.land/': 'https://black.hole/',
@@ -42,9 +40,8 @@ test('Returns a config object with declarations, layers, and import map', async
42
40
  };
43
41
  await fs.writeFile(configFile.path, JSON.stringify(config));
44
42
  const parsedConfig = await load(configFile.path, logger);
43
+ await importMapFile.cleanup();
45
44
  expect(parsedConfig.declarations).toEqual(config.functions);
46
45
  expect(parsedConfig.layers).toEqual(config.layers);
47
- expect(parsedConfig.importMap).toBeTruthy();
48
- expect((_a = parsedConfig.importMap) === null || _a === void 0 ? void 0 : _a.baseURL).toEqual(pathToFileURL(importMapFile.path));
49
- expect((_b = parsedConfig.importMap) === null || _b === void 0 ? void 0 : _b.imports).toEqual(importMap.imports);
46
+ expect(parsedConfig.importMap).toBe(importMapFile.path);
50
47
  });
@@ -1,7 +1,6 @@
1
1
  const defaultFlags = {
2
2
  edge_functions_cache_deno_dir: false,
3
3
  edge_functions_config_export: false,
4
- edge_functions_read_deno_config: false,
5
4
  };
6
5
  const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
7
6
  ...result,
@@ -1,22 +1,29 @@
1
- interface ImportMapFile {
1
+ import { Logger } from './logger.js';
2
+ type Imports = Record<string, string>;
3
+ interface ImportMapSource {
2
4
  baseURL: URL;
3
- imports: Record<string, string>;
4
- scopes?: Record<string, Record<string, string>>;
5
+ imports: Imports;
6
+ scopes?: Record<string, Imports>;
5
7
  }
6
8
  declare class ImportMap {
7
- files: ImportMapFile[];
8
- constructor(files?: ImportMapFile[]);
9
- static resolve(importMapFile: ImportMapFile, basePath?: string, prefix?: string): {
9
+ sources: ImportMapSource[];
10
+ constructor(sources?: ImportMapSource[]);
11
+ static resolve(source: ImportMapSource, basePath?: string, prefix?: string): {
10
12
  imports: Record<string, string>;
11
- scopes?: import("@import-maps/resolve/types/src/types").ParsedScopesMap | undefined;
13
+ scopes: Record<string, Imports>;
12
14
  };
13
- add(file: ImportMapFile): void;
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;
18
+ addFile(path: string, logger: Logger): Promise<void>;
19
+ addFiles(paths: (string | undefined)[], logger: Logger): Promise<void>;
14
20
  getContents(basePath?: string, prefix?: string): {
15
- imports: Record<string, string>;
21
+ imports: Imports;
22
+ scopes: Record<string, Imports>;
16
23
  };
17
24
  toDataURL(): string;
18
25
  writeToFile(path: string): Promise<void>;
19
26
  }
20
- declare const readFile: (path: string) => Promise<ImportMapFile>;
27
+ declare const readFile: (path: string, logger: Logger) => Promise<ImportMapSource>;
21
28
  export { ImportMap, readFile };
22
- export type { ImportMapFile };
29
+ export type { ImportMapSource as ImportMapFile };
@@ -3,25 +3,37 @@ import { promises as fs } from 'fs';
3
3
  import { dirname, posix, relative, sep } from 'path';
4
4
  import { fileURLToPath, pathToFileURL } from 'url';
5
5
  import { parse } from '@import-maps/resolve';
6
+ import { isFileNotFoundError } from './utils/error.js';
6
7
  const INTERNAL_IMPORTS = {
7
8
  'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
8
9
  };
9
10
  // ImportMap can take several import map files and merge them into a final
10
11
  // import map object, also adding the internal imports in the right order.
11
12
  class ImportMap {
12
- constructor(files = []) {
13
- this.files = [];
14
- files.forEach((file) => {
13
+ constructor(sources = []) {
14
+ this.sources = [];
15
+ sources.forEach((file) => {
15
16
  this.add(file);
16
17
  });
17
18
  }
18
19
  // Transforms an import map by making any relative paths use a different path
19
20
  // as a base.
20
- static resolve(importMapFile, basePath, prefix = 'file://') {
21
- const { baseURL, ...importMap } = importMapFile;
21
+ static resolve(source, basePath, prefix = 'file://') {
22
+ const { baseURL, ...importMap } = source;
22
23
  const parsedImportMap = parse(importMap, baseURL);
23
- const { imports = {} } = parsedImportMap;
24
- const newImports = {};
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 = {};
25
37
  Object.keys(imports).forEach((specifier) => {
26
38
  const url = imports[specifier];
27
39
  // If there's no URL, don't even add the specifier to the final imports.
@@ -29,37 +41,62 @@ class ImportMap {
29
41
  return;
30
42
  }
31
43
  // If this is a file URL, we might want to transform it to use another
32
- // base path, as long as one is provided.
33
- if (url.protocol === 'file:' && basePath !== undefined) {
34
- const path = fileURLToPath(url);
35
- const relativePath = relative(basePath, path);
36
- if (relativePath.startsWith('..')) {
37
- throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${basePath}'`);
38
- }
39
- // We want to use POSIX paths for the import map regardless of the OS
40
- // we're building in.
41
- let normalizedPath = relativePath.split(sep).join(posix.sep);
42
- // If the original URL had a trailing slash, ensure the normalized path
43
- // has one too.
44
- if (normalizedPath !== '' && url.pathname.endsWith(posix.sep) && !normalizedPath.endsWith(posix.sep)) {
45
- normalizedPath += posix.sep;
46
- }
47
- const newURL = new URL(normalizedPath, prefix);
48
- newImports[specifier] = newURL.toString();
44
+ // base path.
45
+ if (url.protocol === 'file:') {
46
+ resolvedImports[specifier] = ImportMap.resolvePath(url, basePath, prefix);
49
47
  return;
50
48
  }
51
- newImports[specifier] = url.toString();
49
+ resolvedImports[specifier] = url.toString();
52
50
  });
53
- return { ...parsedImportMap, imports: newImports };
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
+ add(source) {
76
+ this.sources.push(source);
54
77
  }
55
- add(file) {
56
- this.files.push(file);
78
+ async addFile(path, logger) {
79
+ const source = await readFile(path, logger);
80
+ if (Object.keys(source.imports).length === 0) {
81
+ return;
82
+ }
83
+ return this.add(source);
84
+ }
85
+ async addFiles(paths, logger) {
86
+ for (const path of paths) {
87
+ if (path === undefined) {
88
+ return;
89
+ }
90
+ await this.addFile(path, logger);
91
+ }
57
92
  }
58
93
  getContents(basePath, prefix) {
59
94
  let imports = {};
60
- this.files.forEach((file) => {
95
+ let scopes = {};
96
+ this.sources.forEach((file) => {
61
97
  const importMap = ImportMap.resolve(file, basePath, prefix);
62
98
  imports = { ...imports, ...importMap.imports };
99
+ scopes = { ...scopes, ...importMap.scopes };
63
100
  });
64
101
  // Internal imports must come last, because we need to guarantee that
65
102
  // `netlify:edge` isn't user-defined.
@@ -69,6 +106,7 @@ class ImportMap {
69
106
  });
70
107
  return {
71
108
  imports,
109
+ scopes,
72
110
  };
73
111
  }
74
112
  toDataURL() {
@@ -83,7 +121,7 @@ class ImportMap {
83
121
  await fs.writeFile(path, JSON.stringify(contents));
84
122
  }
85
123
  }
86
- const readFile = async (path) => {
124
+ const readFile = async (path, logger) => {
87
125
  const baseURL = pathToFileURL(path);
88
126
  try {
89
127
  const data = await fs.readFile(path, 'utf8');
@@ -93,8 +131,13 @@ const readFile = async (path) => {
93
131
  baseURL,
94
132
  };
95
133
  }
96
- catch {
97
- // no-op
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
+ }
98
141
  }
99
142
  return {
100
143
  baseURL,
@@ -6,7 +6,6 @@
6
6
  import { OnAfterDownloadHook, OnBeforeDownloadHook } from '../bridge.js';
7
7
  import { FunctionConfig } from '../config.js';
8
8
  import type { EdgeFunction } from '../edge_function.js';
9
- import { ImportMapFile } from '../import_map.js';
10
9
  import { LogFunction } from '../logger.js';
11
10
  type FormatFunction = (name: string) => string;
12
11
  interface StartServerOptions {
@@ -18,12 +17,11 @@ interface InspectSettings {
18
17
  address?: string;
19
18
  }
20
19
  interface ServeOptions {
21
- basePath: string;
22
20
  certificatePath?: string;
23
21
  debug?: boolean;
24
22
  distImportMapPath?: string;
25
23
  inspectSettings?: InspectSettings;
26
- importMaps?: ImportMapFile[];
24
+ importMapPaths?: string[];
27
25
  onAfterDownload?: OnAfterDownloadHook;
28
26
  onBeforeDownload?: OnBeforeDownloadHook;
29
27
  formatExportTypeError?: FormatFunction;
@@ -31,7 +29,7 @@ interface ServeOptions {
31
29
  port: number;
32
30
  systemLogger?: LogFunction;
33
31
  }
34
- declare const serve: ({ basePath, certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMaps, onAfterDownload, onBeforeDownload, port, systemLogger, }: ServeOptions) => Promise<(functions: EdgeFunction[], env?: NodeJS.ProcessEnv, options?: StartServerOptions) => Promise<{
32
+ declare const serve: ({ certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMapPaths, onAfterDownload, onBeforeDownload, port, systemLogger, }: ServeOptions) => Promise<(functions: EdgeFunction[], env?: NodeJS.ProcessEnv, options?: StartServerOptions) => Promise<{
35
33
  functionsConfig: FunctionConfig[];
36
34
  graph: any;
37
35
  success: boolean;
@@ -1,9 +1,8 @@
1
1
  import { tmpName } from 'tmp-promise';
2
2
  import { DenoBridge } from '../bridge.js';
3
3
  import { getFunctionConfig } from '../config.js';
4
- import { getConfig as getDenoConfig } from '../deno_config.js';
5
4
  import { generateStage2 } from '../formats/javascript.js';
6
- import { ImportMap, readFile as readImportMapFile } from '../import_map.js';
5
+ import { ImportMap } from '../import_map.js';
7
6
  import { getLogger } from '../logger.js';
8
7
  import { ensureLatestTypes } from '../types.js';
9
8
  import { killProcess, waitForServer } from './util.js';
@@ -54,7 +53,7 @@ const prepareServer = ({ deno, distDirectory, flags: denoFlags, formatExportType
54
53
  };
55
54
  return startServer;
56
55
  };
57
- const serve = async ({ basePath, certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMaps, onAfterDownload, onBeforeDownload, port, systemLogger, }) => {
56
+ const serve = async ({ certificatePath, debug, distImportMapPath, inspectSettings, formatExportTypeError, formatImportError, importMapPaths = [], onAfterDownload, onBeforeDownload, port, systemLogger, }) => {
58
57
  const logger = getLogger(systemLogger, debug);
59
58
  const deno = new DenoBridge({
60
59
  debug,
@@ -69,17 +68,8 @@ const serve = async ({ basePath, certificatePath, debug, distImportMapPath, insp
69
68
  await deno.getBinaryPath();
70
69
  // Downloading latest types if needed.
71
70
  await ensureLatestTypes(deno, logger);
72
- // Creating an ImportMap instance with any import maps supplied by the user,
73
- // if any.
74
- const importMap = new ImportMap(importMaps);
75
- // Look for a Deno config file and read it if one exists.
76
- const denoConfig = await getDenoConfig(logger, basePath);
77
- // If the Deno config file defines an import map, read the file and add the
78
- // imports to the global import map.
79
- if (denoConfig === null || denoConfig === void 0 ? void 0 : denoConfig.importMap) {
80
- const importMapFile = await readImportMapFile(denoConfig.importMap);
81
- importMap.add(importMapFile);
82
- }
71
+ const importMap = new ImportMap();
72
+ await importMap.addFiles(importMapPaths, logger);
83
73
  const flags = ['--allow-all', '--unstable', `--import-map=${importMap.toDataURL()}`, '--no-config'];
84
74
  if (certificatePath) {
85
75
  flags.push(`--cert=${certificatePath}`);
@@ -1,5 +1,4 @@
1
1
  import { join } from 'path';
2
- import { pathToFileURL } from 'url';
3
2
  import getPort from 'get-port';
4
3
  import fetch from 'node-fetch';
5
4
  import { v4 as uuidv4 } from 'uuid';
@@ -8,30 +7,24 @@ import { fixturesDir } from '../../test/util.js';
8
7
  import { serve } from '../index.js';
9
8
  test('Starts a server and serves requests for edge functions', async () => {
10
9
  const basePath = join(fixturesDir, 'serve_test');
11
- const functionPaths = {
12
- internal: join(basePath, '.netlify', 'edge-functions', 'greet.ts'),
13
- user: join(basePath, 'netlify', 'edge-functions', 'echo_env.ts'),
10
+ const paths = {
11
+ internal: join(basePath, '.netlify', 'edge-functions'),
12
+ user: join(basePath, 'netlify', 'edge-functions'),
14
13
  };
15
14
  const port = await getPort();
16
- const internalImportMap = {
17
- baseURL: pathToFileURL(functionPaths.internal),
18
- imports: {
19
- 'internal-helper': '../../helper.ts',
20
- },
21
- };
15
+ const importMapPaths = [join(paths.internal, 'import_map.json'), join(paths.user, 'import-map.json')];
22
16
  const server = await serve({
23
- basePath,
24
- importMaps: [internalImportMap],
17
+ importMapPaths,
25
18
  port,
26
19
  });
27
20
  const functions = [
28
21
  {
29
22
  name: 'echo_env',
30
- path: functionPaths.user,
23
+ path: join(paths.user, 'echo_env.ts'),
31
24
  },
32
25
  {
33
26
  name: 'greet',
34
- path: functionPaths.internal,
27
+ path: join(paths.internal, 'greet.ts'),
35
28
  },
36
29
  ];
37
30
  const options = {
@@ -42,10 +35,10 @@ test('Starts a server and serves requests for edge functions', async () => {
42
35
  }, options);
43
36
  expect(success).toBe(true);
44
37
  expect(functionsConfig).toEqual([{ path: '/my-function' }, {}]);
45
- for (const key in functionPaths) {
38
+ for (const key in functions) {
46
39
  const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(
47
40
  // @ts-expect-error TODO: Module graph is currently not typed
48
- ({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functionPaths[key]);
41
+ ({ kind, mediaType, local }) => kind === 'esm' && mediaType === 'TypeScript' && local === functions[key].path);
49
42
  expect(graphEntry).toBe(true);
50
43
  }
51
44
  const response1 = await fetch(`http://0.0.0.0:${port}/foo`, {
@@ -1,2 +1,3 @@
1
1
  /// <reference types="node" />
2
2
  export declare const isNodeError: (error: any) => error is NodeJS.ErrnoException;
3
+ export declare const isFileNotFoundError: (error: any) => boolean;
@@ -1,2 +1,4 @@
1
1
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
2
  export const isNodeError = (error) => error instanceof Error;
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export const isFileNotFoundError = (error) => isNodeError(error) && error.code === 'ENOENT';
@@ -5,4 +5,5 @@ declare const useFixture: (fixtureName: string) => Promise<{
5
5
  cleanup: () => Promise<void>;
6
6
  distPath: string;
7
7
  }>;
8
- export { fixturesDir, testLogger, useFixture };
8
+ declare const runESZIP: (eszipPath: string) => Promise<any>;
9
+ export { fixturesDir, testLogger, runESZIP, useFixture };
package/dist/test/util.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import { join, resolve } from 'path';
3
- import { fileURLToPath } from 'url';
3
+ import { stderr, stdout } from 'process';
4
+ import { fileURLToPath, pathToFileURL } from 'url';
5
+ import { execa } from 'execa';
4
6
  import tmp from 'tmp-promise';
5
7
  import { getLogger } from '../node/logger.js';
6
8
  const testLogger = getLogger(() => {
@@ -20,4 +22,49 @@ const useFixture = async (fixtureName) => {
20
22
  distPath,
21
23
  };
22
24
  };
23
- export { fixturesDir, testLogger, useFixture };
25
+ const inspectFunction = (path) => `
26
+ import { functions } from "${pathToFileURL(path)}.js";
27
+
28
+ const responses = {};
29
+
30
+ for (const functionName in functions) {
31
+ const req = new Request("https://test.netlify");
32
+ const res = await functions[functionName](req);
33
+
34
+ responses[functionName] = await res.text();
35
+ }
36
+
37
+ console.log(JSON.stringify(responses));
38
+ `;
39
+ const runESZIP = async (eszipPath) => {
40
+ var _a, _b, _c;
41
+ const tmpDir = await tmp.dir({ unsafeCleanup: true });
42
+ // Extract ESZIP into temporary directory.
43
+ const extractCommand = execa('deno', [
44
+ 'run',
45
+ '--allow-all',
46
+ 'https://deno.land/x/eszip@v0.28.0/eszip.ts',
47
+ 'x',
48
+ eszipPath,
49
+ tmpDir.path,
50
+ ]);
51
+ (_a = extractCommand.stderr) === null || _a === void 0 ? void 0 : _a.pipe(stderr);
52
+ (_b = extractCommand.stdout) === null || _b === void 0 ? void 0 : _b.pipe(stdout);
53
+ await extractCommand;
54
+ const virtualRootPath = join(tmpDir.path, 'source', 'root');
55
+ const stage2Path = join(virtualRootPath, '..', 'bootstrap-stage2');
56
+ const importMapPath = join(virtualRootPath, '..', 'import-map');
57
+ for (const path of [importMapPath, stage2Path]) {
58
+ const file = await fs.readFile(path, 'utf8');
59
+ const normalizedFile = file.replace(/file:\/\/\/root/g, pathToFileURL(virtualRootPath).toString());
60
+ await fs.writeFile(path, normalizedFile);
61
+ }
62
+ await fs.rename(stage2Path, `${stage2Path}.js`);
63
+ // Run function that imports the extracted stage 2 and invokes each function.
64
+ const evalCommand = execa('deno', ['eval', '--no-check', '--import-map', importMapPath, inspectFunction(stage2Path)]);
65
+ (_c = evalCommand.stderr) === null || _c === void 0 ? void 0 : _c.pipe(stderr);
66
+ const result = await evalCommand;
67
+ await tmpDir.cleanup();
68
+ return JSON.parse(result.stdout);
69
+ };
70
+ export { fixturesDir, testLogger, runESZIP, useFixture };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/edge-bundler",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "Intelligently prepare Netlify Edge Functions for deployment",
5
5
  "type": "module",
6
6
  "main": "./dist/node/index.js",
@@ -1,6 +0,0 @@
1
- import { Logger } from './logger.js';
2
- interface DenoConfigFile {
3
- importMap?: string;
4
- }
5
- export declare const getConfig: (logger: Logger, basePath?: string) => Promise<DenoConfigFile | undefined>;
6
- export {};
@@ -1,43 +0,0 @@
1
- import { promises as fs } from 'fs';
2
- import { join, resolve } from 'path';
3
- import { parse as parseJSONC } from 'jsonc-parser';
4
- import { isNodeError } from './utils/error.js';
5
- const filenames = ['deno.json', 'deno.jsonc'];
6
- export const getConfig = async (logger, basePath) => {
7
- if (basePath === undefined) {
8
- logger.system('No base path specified, will not attempt to read Deno config');
9
- return;
10
- }
11
- for (const filename of filenames) {
12
- const candidatePath = join(basePath, filename);
13
- const config = await getConfigFromFile(candidatePath);
14
- if (config !== undefined) {
15
- logger.system('Loaded Deno config file from path', candidatePath);
16
- return normalizeConfig(config, basePath);
17
- }
18
- }
19
- logger.system('No Deno config file found at base path', basePath);
20
- };
21
- const getConfigFromFile = async (filePath) => {
22
- try {
23
- const data = await fs.readFile(filePath, 'utf8');
24
- const config = parseJSONC(data);
25
- return config;
26
- }
27
- catch (error) {
28
- if (isNodeError(error) && error.code === 'ENOENT') {
29
- return;
30
- }
31
- return {};
32
- }
33
- };
34
- const normalizeConfig = (rawConfig, basePath) => {
35
- const config = {};
36
- if (rawConfig.importMap) {
37
- if (typeof rawConfig.importMap !== 'string') {
38
- throw new TypeError(`'importMap' property in Deno config must be a string`);
39
- }
40
- config.importMap = resolve(basePath, rawConfig.importMap);
41
- }
42
- return config;
43
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,92 +0,0 @@
1
- import { promises as fs } from 'fs';
2
- import { join } from 'path';
3
- import tmp from 'tmp-promise';
4
- import { expect, test } from 'vitest';
5
- import { testLogger } from '../test/util.js';
6
- import { getConfig } from './deno_config.js';
7
- test('Returns `undefined` if no config file is found', async () => {
8
- const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
9
- const config = await getConfig(testLogger, path);
10
- expect(config).toBeUndefined();
11
- await cleanup();
12
- });
13
- test('Returns an empty object if the config file cannot be parsed', async () => {
14
- const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
15
- const configPath = join(path, 'deno.json');
16
- await fs.writeFile(configPath, '{');
17
- const config = await getConfig(testLogger, path);
18
- expect(config).toEqual({});
19
- await cleanup();
20
- });
21
- test('Throws a type error if the `importMap` contains anything other than a string', async () => {
22
- const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
23
- const configPath = join(path, 'deno.json');
24
- const data = JSON.stringify({ importMap: { imports: { foo: './bar/' } } });
25
- await fs.writeFile(configPath, data);
26
- await expect(getConfig(testLogger, path)).rejects.toThrowError(TypeError);
27
- await cleanup();
28
- });
29
- test('Excludes unsupported properties', async () => {
30
- const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
31
- const configPath = join(path, 'deno.json');
32
- const data = JSON.stringify({
33
- compilerOptions: {
34
- allowJs: true,
35
- lib: ['deno.window'],
36
- strict: true,
37
- },
38
- importMap: 'import_map.json',
39
- lint: {
40
- files: {
41
- include: ['src/'],
42
- exclude: ['src/testdata/'],
43
- },
44
- rules: {
45
- tags: ['recommended'],
46
- include: ['ban-untagged-todo'],
47
- exclude: ['no-unused-vars'],
48
- },
49
- },
50
- fmt: {
51
- files: {
52
- include: ['src/'],
53
- exclude: ['src/testdata/'],
54
- },
55
- options: {
56
- useTabs: true,
57
- lineWidth: 80,
58
- indentWidth: 4,
59
- singleQuote: true,
60
- proseWrap: 'preserve',
61
- },
62
- },
63
- test: {
64
- files: {
65
- include: ['src/'],
66
- exclude: ['src/testdata/'],
67
- },
68
- },
69
- });
70
- await fs.writeFile(configPath, data);
71
- const config = await getConfig(testLogger, path);
72
- expect(Object.keys(config !== null && config !== void 0 ? config : {})).toEqual(['importMap']);
73
- await cleanup();
74
- });
75
- test('Resolves `importMap` into an absolute path', async () => {
76
- const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
77
- const configPath = join(path, 'deno.json');
78
- const data = JSON.stringify({ importMap: 'import_map.json' });
79
- await fs.writeFile(configPath, data);
80
- const config = await getConfig(testLogger, path);
81
- expect(config).toEqual({ importMap: join(path, 'import_map.json') });
82
- await cleanup();
83
- });
84
- test('Supports JSONC', async () => {
85
- const { cleanup, path } = await tmp.dir({ unsafeCleanup: true });
86
- const configPath = join(path, 'deno.jsonc');
87
- const data = JSON.stringify({ importMap: 'import_map.json' });
88
- await fs.writeFile(configPath, `// This is a comment\n${data}`);
89
- const config = await getConfig(testLogger, path);
90
- expect(config).toEqual({ importMap: join(path, 'import_map.json') });
91
- await cleanup();
92
- });