@netlify/edge-bundler 2.1.0 → 2.3.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.
Files changed (41) hide show
  1. package/deno/config.ts +38 -0
  2. package/deno/lib/stage2.test.ts +58 -0
  3. package/dist/node/bootstrap.test.d.ts +1 -0
  4. package/dist/node/bootstrap.test.js +26 -0
  5. package/dist/node/bridge.d.ts +4 -2
  6. package/dist/node/bridge.js +4 -4
  7. package/dist/node/bridge.test.d.ts +1 -0
  8. package/dist/node/bridge.test.js +112 -0
  9. package/dist/node/bundler.d.ts +2 -2
  10. package/dist/node/bundler.js +29 -20
  11. package/dist/node/bundler.test.d.ts +1 -0
  12. package/dist/node/bundler.test.js +246 -0
  13. package/dist/node/config.d.ts +7 -0
  14. package/dist/node/config.js +87 -0
  15. package/dist/node/config.test.d.ts +1 -0
  16. package/dist/node/config.test.js +174 -0
  17. package/dist/node/declaration.d.ts +2 -0
  18. package/dist/node/declaration.js +27 -1
  19. package/dist/node/downloader.test.d.ts +1 -0
  20. package/dist/node/downloader.test.js +115 -0
  21. package/dist/node/feature_flags.js +1 -0
  22. package/dist/node/import_map.test.d.ts +1 -0
  23. package/dist/node/import_map.test.js +33 -0
  24. package/dist/node/logger.test.d.ts +1 -0
  25. package/dist/node/logger.test.js +45 -0
  26. package/dist/node/main.test.d.ts +1 -0
  27. package/dist/node/main.test.js +48 -0
  28. package/dist/node/manifest.test.d.ts +1 -0
  29. package/dist/node/manifest.test.js +65 -0
  30. package/dist/node/package_json.test.d.ts +1 -0
  31. package/dist/node/package_json.test.js +7 -0
  32. package/dist/node/server/server.js +1 -1
  33. package/dist/node/serving.test.d.ts +1 -0
  34. package/dist/node/serving.test.js +31 -0
  35. package/dist/node/stage_2.test.d.ts +1 -0
  36. package/dist/node/stage_2.test.js +48 -0
  37. package/dist/node/types.test.d.ts +1 -0
  38. package/dist/node/types.test.js +61 -0
  39. package/dist/test/util.d.ts +3 -0
  40. package/dist/test/util.js +9 -0
  41. package/package.json +17 -27
package/deno/config.ts ADDED
@@ -0,0 +1,38 @@
1
+ const [functionURL, collectorURL, rawExitCodes] = Deno.args
2
+ const exitCodes = JSON.parse(rawExitCodes)
3
+
4
+ let func
5
+
6
+ try {
7
+ func = await import(functionURL)
8
+ } catch {
9
+ Deno.exit(exitCodes.ImportError)
10
+ }
11
+
12
+ if (func.config === undefined) {
13
+ Deno.exit(exitCodes.NoConfig)
14
+ }
15
+
16
+ if (typeof func.config !== 'function') {
17
+ Deno.exit(exitCodes.InvalidExport)
18
+ }
19
+
20
+ let config
21
+
22
+ try {
23
+ config = await func.config()
24
+ } catch (error) {
25
+ console.error(error)
26
+
27
+ Deno.exit(exitCodes.RuntimeError)
28
+ }
29
+
30
+ try {
31
+ const result = JSON.stringify(config)
32
+
33
+ await Deno.writeTextFile(new URL(collectorURL), result)
34
+ } catch {
35
+ Deno.exit(exitCodes.SerializationError)
36
+ }
37
+
38
+ Deno.exit(exitCodes.Success)
@@ -0,0 +1,58 @@
1
+ import { assertEquals, assertStringIncludes } from 'https://deno.land/std@0.127.0/testing/asserts.ts'
2
+
3
+ import { join } from 'https://deno.land/std@0.155.0/path/mod.ts'
4
+ import { pathToFileURL } from 'https://deno.land/std@0.155.0/node/url.ts'
5
+
6
+ import { getStage2Entry } from './stage2.ts'
7
+ import { virtualRoot } from './consts.ts'
8
+
9
+ Deno.test('`getStage2Entry` returns a valid stage 2 file', async () => {
10
+ const directory = await Deno.makeTempDir()
11
+ const functions = [
12
+ {
13
+ name: 'func1',
14
+ path: join(directory, 'func1.ts'),
15
+ response: 'Hello from function 1',
16
+ },
17
+ {
18
+ name: 'func2',
19
+ path: join(directory, 'func2.ts'),
20
+ response: 'Hello from function 2',
21
+ },
22
+ ]
23
+
24
+ for (const func of functions) {
25
+ const contents = `export default async () => new Response(${JSON.stringify(func.response)})`
26
+
27
+ await Deno.writeTextFile(func.path, contents)
28
+ }
29
+
30
+ const baseURL = pathToFileURL(directory)
31
+ const stage2 = getStage2Entry(
32
+ directory,
33
+ functions.map(({ name, path }) => ({ name, path })),
34
+ )
35
+
36
+ // Ensuring that the stage 2 paths have the virtual root before we strip it.
37
+ assertStringIncludes(stage2, virtualRoot)
38
+
39
+ // Replacing the virtual root with the URL of the temporary directory so that
40
+ // we can actually import the module.
41
+ const normalizedStage2 = stage2.replaceAll(virtualRoot, `${baseURL.href}/`)
42
+
43
+ const stage2Path = join(directory, 'stage2.ts')
44
+ const stage2URL = pathToFileURL(stage2Path)
45
+
46
+ await Deno.writeTextFile(stage2Path, normalizedStage2)
47
+
48
+ const mod = await import(stage2URL.href)
49
+
50
+ await Deno.remove(directory, { recursive: true })
51
+
52
+ for (const func of functions) {
53
+ const result = await mod.functions[func.name]()
54
+
55
+ assertEquals(await result.text(), func.response)
56
+ assertEquals(mod.metadata.functions[func.name].url, pathToFileURL(func.path).toString())
57
+ }
58
+ })
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { Buffer } from 'buffer';
2
+ import { env } from 'process';
3
+ import fetch, { Headers } from 'node-fetch';
4
+ import { test, expect } from 'vitest';
5
+ import { getBootstrapURL } from './formats/javascript.js';
6
+ test('Imports the bootstrap layer from a valid URL', async () => {
7
+ const importURL = new URL(getBootstrapURL());
8
+ const headers = new Headers();
9
+ // `node-fetch` doesn't let us send credentials as part of the URL, so we
10
+ // have to transform them into an `Authorization` header.
11
+ if (importURL.username) {
12
+ const auth = Buffer.from(`${importURL.username}:${importURL.password}`);
13
+ importURL.username = '';
14
+ importURL.password = '';
15
+ headers.set('Authorization', `Basic ${auth.toString('base64')}`);
16
+ }
17
+ const canonicalURL = importURL.toString();
18
+ const { status } = await fetch(canonicalURL, { headers });
19
+ expect(status).toBe(200);
20
+ });
21
+ test('Imports the bootstrap layer from the URL present in the `NETLIFY_EDGE_BOOTSTRAP` environment variable, if present', () => {
22
+ const mockURL = 'https://example.com/boot.ts';
23
+ env.NETLIFY_EDGE_BOOTSTRAP = mockURL;
24
+ expect(getBootstrapURL()).toBe(mockURL);
25
+ env.NETLIFY_EDGE_BOOTSTRAP = undefined;
26
+ });
@@ -1,6 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import { ExecaChildProcess } from 'execa';
3
3
  import { Logger } from './logger.js';
4
+ declare const DENO_VERSION_RANGE = "^1.22.0";
4
5
  declare type OnBeforeDownloadHook = () => void | Promise<void>;
5
6
  declare type OnAfterDownloadHook = (error?: Error) => void | Promise<void>;
6
7
  interface DenoOptions {
@@ -20,6 +21,7 @@ interface RunOptions {
20
21
  pipeOutput?: boolean;
21
22
  env?: NodeJS.ProcessEnv;
22
23
  extendEnv?: boolean;
24
+ rejectOnExitCode?: boolean;
23
25
  }
24
26
  declare class DenoBridge {
25
27
  cacheDirectory: string;
@@ -45,8 +47,8 @@ declare class DenoBridge {
45
47
  path: string;
46
48
  }>;
47
49
  getEnvironmentVariables(inputEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
48
- run(args: string[], { pipeOutput, env: inputEnv, extendEnv }?: RunOptions): Promise<import("execa").ExecaReturnValue<string>>;
50
+ run(args: string[], { pipeOutput, env: inputEnv, extendEnv, rejectOnExitCode }?: RunOptions): Promise<import("execa").ExecaReturnValue<string>>;
49
51
  runInBackground(args: string[], ref?: ProcessRef, { pipeOutput, env: inputEnv, extendEnv }?: RunOptions): Promise<void>;
50
52
  }
51
- export { DenoBridge };
53
+ export { DENO_VERSION_RANGE, DenoBridge };
52
54
  export type { DenoOptions, OnAfterDownloadHook, OnBeforeDownloadHook, ProcessRef };
@@ -9,7 +9,7 @@ import { getPathInHome } from './home_path.js';
9
9
  import { getLogger } from './logger.js';
10
10
  import { getBinaryExtension } from './platform.js';
11
11
  const DENO_VERSION_FILE = 'version.txt';
12
- const DENO_VERSION_RANGE = '^1.20.3';
12
+ const DENO_VERSION_RANGE = '^1.22.0';
13
13
  class DenoBridge {
14
14
  constructor(options) {
15
15
  var _a, _b, _c, _d, _e;
@@ -131,10 +131,10 @@ class DenoBridge {
131
131
  }
132
132
  // Runs the Deno CLI in the background and returns a reference to the child
133
133
  // process, awaiting its execution.
134
- async run(args, { pipeOutput, env: inputEnv, extendEnv = true } = {}) {
134
+ async run(args, { pipeOutput, env: inputEnv, extendEnv = true, rejectOnExitCode = true } = {}) {
135
135
  const { path: binaryPath } = await this.getBinaryPath();
136
136
  const env = this.getEnvironmentVariables(inputEnv);
137
- const options = { env, extendEnv };
137
+ const options = { env, extendEnv, reject: rejectOnExitCode };
138
138
  return DenoBridge.runWithBinary(binaryPath, args, options, pipeOutput);
139
139
  }
140
140
  // Runs the Deno CLI in the background, assigning a reference of the child
@@ -150,4 +150,4 @@ class DenoBridge {
150
150
  }
151
151
  }
152
152
  }
153
- export { DenoBridge };
153
+ export { DENO_VERSION_RANGE, DenoBridge };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ import { Buffer } from 'buffer';
2
+ import fs from 'fs';
3
+ import { createRequire } from 'module';
4
+ import { platform, env } from 'process';
5
+ import { PassThrough } from 'stream';
6
+ import nock from 'nock';
7
+ import semver from 'semver';
8
+ import { spy } from 'sinon';
9
+ import tmp from 'tmp-promise';
10
+ import { test, expect } from 'vitest';
11
+ import { DenoBridge, DENO_VERSION_RANGE } from './bridge.js';
12
+ import { getPlatformTarget } from './platform.js';
13
+ const require = createRequire(import.meta.url);
14
+ const archiver = require('archiver');
15
+ const getMockDenoBridge = function (tmpDir, mockBinaryOutput) {
16
+ var _a, _b;
17
+ const latestVersion = (_b = (_a = semver.minVersion(DENO_VERSION_RANGE)) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : '';
18
+ const data = new PassThrough();
19
+ const archive = archiver('zip', { zlib: { level: 9 } });
20
+ archive.pipe(data);
21
+ archive.append(Buffer.from(mockBinaryOutput.replace(/@@@latestVersion@@@/g, latestVersion)), {
22
+ name: platform === 'win32' ? 'deno.exe' : 'deno',
23
+ });
24
+ archive.finalize();
25
+ const target = getPlatformTarget();
26
+ nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`);
27
+ nock('https://dl.deno.land')
28
+ .get(`/release/v${latestVersion}/deno-${target}.zip`)
29
+ .reply(200, () => data);
30
+ const beforeDownload = spy();
31
+ const afterDownload = spy();
32
+ return new DenoBridge({
33
+ cacheDirectory: tmpDir.path,
34
+ onBeforeDownload: beforeDownload,
35
+ onAfterDownload: afterDownload,
36
+ useGlobal: false,
37
+ });
38
+ };
39
+ test('Does not inherit environment variables if `extendEnv` is false', async () => {
40
+ var _a;
41
+ const tmpDir = await tmp.dir();
42
+ const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
43
+
44
+ if [ "$1" = "test" ]
45
+ then
46
+ env
47
+ else
48
+ echo "deno @@@latestVersion@@@"
49
+ fi`);
50
+ // The environment sets some variables so let us see what they are and remove them from the result
51
+ const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: false });
52
+ env.TADA = 'TUDU';
53
+ const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: false });
54
+ let output = (_a = result === null || result === void 0 ? void 0 : result.stdout) !== null && _a !== void 0 ? _a : '';
55
+ delete env.TADA;
56
+ referenceOutput === null || referenceOutput === void 0 ? void 0 : referenceOutput.stdout.split('\n').forEach((line) => {
57
+ output = output.replace(line.trim(), '');
58
+ });
59
+ output = output.trim().replace(/\n+/g, '\n');
60
+ expect(output).toBe('LULU=LALA');
61
+ await fs.promises.rmdir(tmpDir.path, { recursive: true });
62
+ });
63
+ test('Does inherit environment variables if `extendEnv` is true', async () => {
64
+ var _a;
65
+ const tmpDir = await tmp.dir();
66
+ const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
67
+
68
+ if [ "$1" = "test" ]
69
+ then
70
+ env
71
+ else
72
+ echo "deno @@@latestVersion@@@"
73
+ fi`);
74
+ // The environment sets some variables so let us see what they are and remove them from the result
75
+ const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true });
76
+ env.TADA = 'TUDU';
77
+ const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: true });
78
+ let output = (_a = result === null || result === void 0 ? void 0 : result.stdout) !== null && _a !== void 0 ? _a : '';
79
+ delete env.TADA;
80
+ referenceOutput === null || referenceOutput === void 0 ? void 0 : referenceOutput.stdout.split('\n').forEach((line) => {
81
+ output = output.replace(line.trim(), '');
82
+ });
83
+ // lets remove holes, split lines and sort lines by name, as different OSes might order them different
84
+ const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort();
85
+ expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']);
86
+ await fs.promises.rmdir(tmpDir.path, { recursive: true });
87
+ });
88
+ test('Does inherit environment variables if `extendEnv` is not set', async () => {
89
+ var _a;
90
+ const tmpDir = await tmp.dir();
91
+ const deno = getMockDenoBridge(tmpDir, `#!/usr/bin/env sh
92
+
93
+ if [ "$1" = "test" ]
94
+ then
95
+ env
96
+ else
97
+ echo "deno @@@latestVersion@@@"
98
+ fi`);
99
+ // The environment sets some variables so let us see what they are and remove them from the result
100
+ const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true });
101
+ env.TADA = 'TUDU';
102
+ const result = await deno.run(['test'], { env: { LULU: 'LALA' } });
103
+ let output = (_a = result === null || result === void 0 ? void 0 : result.stdout) !== null && _a !== void 0 ? _a : '';
104
+ delete env.TADA;
105
+ referenceOutput === null || referenceOutput === void 0 ? void 0 : referenceOutput.stdout.split('\n').forEach((line) => {
106
+ output = output.replace(line.trim(), '');
107
+ });
108
+ // lets remove holes, split lines and sort lines by name, as different OSes might order them different
109
+ const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort();
110
+ expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']);
111
+ await fs.promises.rmdir(tmpDir.path, { recursive: true });
112
+ });
@@ -1,5 +1,5 @@
1
1
  import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
2
- import type { Declaration } from './declaration.js';
2
+ import { Declaration } from './declaration.js';
3
3
  import { EdgeFunction } from './edge_function.js';
4
4
  import { FeatureFlags } from './feature_flags.js';
5
5
  import { ImportMapFile } from './import_map.js';
@@ -15,7 +15,7 @@ interface BundleOptions {
15
15
  onBeforeDownload?: OnBeforeDownloadHook;
16
16
  systemLogger?: LogFunction;
17
17
  }
18
- declare const bundle: (sourceDirectories: string[], distDirectory: string, declarations?: Declaration[], { basePath, cacheDirectory, debug, distImportMapPath, featureFlags, importMaps, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
18
+ declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath, cacheDirectory, debug, distImportMapPath, featureFlags, importMaps, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
19
19
  functions: EdgeFunction[];
20
20
  manifest: import("./manifest.js").Manifest;
21
21
  }>;
@@ -3,6 +3,8 @@ import { join } from 'path';
3
3
  import commonPathPrefix from 'common-path-prefix';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
  import { DenoBridge } from './bridge.js';
6
+ import { getFunctionConfig } from './config.js';
7
+ import { getDeclarationsFromConfig } from './declaration.js';
6
8
  import { getFlags } from './feature_flags.js';
7
9
  import { findFunctions } from './finder.js';
8
10
  import { bundle as bundleESZIP } from './formats/eszip.js';
@@ -11,10 +13,9 @@ import { ImportMap } from './import_map.js';
11
13
  import { getLogger } from './logger.js';
12
14
  import { writeManifest } from './manifest.js';
13
15
  import { ensureLatestTypes } from './types.js';
14
- const createBundleOps = ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, featureFlags, }) => {
15
- const bundleOps = [];
16
+ const createBundle = ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, featureFlags, }) => {
16
17
  if (featureFlags.edge_functions_produce_eszip) {
17
- bundleOps.push(bundleESZIP({
18
+ return bundleESZIP({
18
19
  basePath,
19
20
  buildID,
20
21
  debug,
@@ -22,21 +23,18 @@ const createBundleOps = ({ basePath, buildID, debug, deno, distDirectory, functi
22
23
  distDirectory,
23
24
  functions,
24
25
  importMap,
25
- }));
26
+ });
26
27
  }
27
- else {
28
- bundleOps.push(bundleJS({
29
- buildID,
30
- debug,
31
- deno,
32
- distDirectory,
33
- functions,
34
- importMap,
35
- }));
36
- }
37
- return bundleOps;
28
+ return bundleJS({
29
+ buildID,
30
+ debug,
31
+ deno,
32
+ distDirectory,
33
+ functions,
34
+ importMap,
35
+ });
38
36
  };
39
- const bundle = async (sourceDirectories, distDirectory, declarations = [], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
37
+ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMaps, onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
40
38
  const logger = getLogger(systemLogger, debug);
41
39
  const featureFlags = getFlags(inputFeatureFlags);
42
40
  const options = {
@@ -60,7 +58,7 @@ const bundle = async (sourceDirectories, distDirectory, declarations = [], { bas
60
58
  // if any.
61
59
  const importMap = new ImportMap(importMaps);
62
60
  const functions = await findFunctions(sourceDirectories);
63
- const bundleOps = createBundleOps({
61
+ const functionBundle = await createBundle({
64
62
  basePath,
65
63
  buildID,
66
64
  debug,
@@ -70,13 +68,24 @@ const bundle = async (sourceDirectories, distDirectory, declarations = [], { bas
70
68
  importMap,
71
69
  featureFlags,
72
70
  });
73
- const bundles = await Promise.all(bundleOps);
74
71
  // The final file name of the bundles contains a SHA256 hash of the contents,
75
72
  // which we can only compute now that the files have been generated. So let's
76
73
  // rename the bundles to their permanent names.
77
- await createFinalBundles(bundles, distDirectory, buildID);
74
+ await createFinalBundles([functionBundle], distDirectory, buildID);
75
+ // Retrieving a configuration object for each function.
76
+ const functionsConfig = await Promise.all(functions.map((func) => {
77
+ if (!featureFlags.edge_functions_config_export) {
78
+ return {};
79
+ }
80
+ return getFunctionConfig(func, deno, logger);
81
+ }));
82
+ // Creating a hash of function names to configuration objects.
83
+ const functionsWithConfig = functions.reduce((acc, func, index) => ({ ...acc, [func.name]: functionsConfig[index] }), {});
84
+ // Creating a final declarations array by combining the TOML entries with the
85
+ // function configuration objects.
86
+ const declarations = getDeclarationsFromConfig(tomlDeclarations, functionsWithConfig);
78
87
  const manifest = await writeManifest({
79
- bundles,
88
+ bundles: [functionBundle],
80
89
  declarations,
81
90
  distDirectory,
82
91
  functions,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,246 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import process from 'process';
4
+ import { pathToFileURL } from 'url';
5
+ import del from 'del';
6
+ import tmp from 'tmp-promise';
7
+ import { test, expect } from 'vitest';
8
+ import { fixturesDir } from '../test/util.js';
9
+ import { BundleError } from './bundle_error.js';
10
+ import { bundle } from './bundler.js';
11
+ test('Produces a JavaScript bundle and a manifest file', async () => {
12
+ const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
13
+ const tmpDir = await tmp.dir();
14
+ const declarations = [
15
+ {
16
+ function: 'func1',
17
+ path: '/func1',
18
+ },
19
+ ];
20
+ const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
21
+ basePath: fixturesDir,
22
+ importMaps: [
23
+ {
24
+ baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
25
+ imports: {
26
+ 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
27
+ },
28
+ },
29
+ ],
30
+ });
31
+ const generatedFiles = await fs.readdir(tmpDir.path);
32
+ expect(result.functions.length).toBe(1);
33
+ expect(generatedFiles.length).toBe(2);
34
+ const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
35
+ const manifest = JSON.parse(manifestFile);
36
+ const { bundles } = manifest;
37
+ expect(bundles.length).toBe(1);
38
+ expect(bundles[0].format).toBe('js');
39
+ expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
40
+ expect(result.manifest).toEqual(manifest);
41
+ await fs.rmdir(tmpDir.path, { recursive: true });
42
+ }, 10000);
43
+ test('Produces only a ESZIP bundle when the `edge_functions_produce_eszip` feature flag is set', async () => {
44
+ const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
45
+ const tmpDir = await tmp.dir();
46
+ const declarations = [
47
+ {
48
+ function: 'func1',
49
+ path: '/func1',
50
+ },
51
+ ];
52
+ const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
53
+ basePath: fixturesDir,
54
+ featureFlags: {
55
+ edge_functions_produce_eszip: true,
56
+ },
57
+ importMaps: [
58
+ {
59
+ baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
60
+ imports: {
61
+ 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
62
+ },
63
+ },
64
+ ],
65
+ });
66
+ const generatedFiles = await fs.readdir(tmpDir.path);
67
+ expect(result.functions.length).toBe(1);
68
+ expect(generatedFiles.length).toBe(2);
69
+ const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
70
+ const manifest = JSON.parse(manifestFile);
71
+ const { bundles } = manifest;
72
+ expect(bundles.length).toBe(1);
73
+ expect(bundles[0].format).toBe('eszip2');
74
+ expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
75
+ await fs.rmdir(tmpDir.path, { recursive: true });
76
+ }, 10000);
77
+ test('Adds a custom error property to user errors during bundling', async () => {
78
+ expect.assertions(2);
79
+ const sourceDirectory = resolve(fixturesDir, 'invalid_functions', 'functions');
80
+ const tmpDir = await tmp.dir();
81
+ const declarations = [
82
+ {
83
+ function: 'func1',
84
+ path: '/func1',
85
+ },
86
+ ];
87
+ try {
88
+ await bundle([sourceDirectory], tmpDir.path, declarations);
89
+ }
90
+ catch (error) {
91
+ expect(error).toBeInstanceOf(BundleError);
92
+ expect(error.customErrorInfo).toEqual({
93
+ location: {
94
+ format: 'javascript',
95
+ runtime: 'deno',
96
+ },
97
+ type: 'functionsBundling',
98
+ });
99
+ }
100
+ }, 10000);
101
+ test('Does not add a custom error property to system errors during bundling', async () => {
102
+ expect.assertions(1);
103
+ try {
104
+ // @ts-expect-error Sending bad input to `bundle` to force a system error.
105
+ await bundle([123, 321], tmpDir.path, declarations);
106
+ }
107
+ catch (error) {
108
+ expect(error).not.toBeInstanceOf(BundleError);
109
+ }
110
+ });
111
+ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_cache_deno_dir` feature flag is set', async () => {
112
+ expect.assertions(7);
113
+ const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
114
+ const outDir = await tmp.dir();
115
+ const cacheDir = await tmp.dir();
116
+ const declarations = [
117
+ {
118
+ function: 'func1',
119
+ path: '/func1',
120
+ },
121
+ ];
122
+ const options = {
123
+ basePath: fixturesDir,
124
+ cacheDirectory: cacheDir.path,
125
+ importMaps: [
126
+ {
127
+ baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
128
+ imports: {
129
+ 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
130
+ },
131
+ },
132
+ ],
133
+ };
134
+ // Run #1, feature flag off: The directory should not be populated.
135
+ const result1 = await bundle([sourceDirectory], outDir.path, declarations, options);
136
+ const outFiles1 = await fs.readdir(outDir.path);
137
+ expect(result1.functions.length).toBe(1);
138
+ expect(outFiles1.length).toBe(2);
139
+ try {
140
+ await fs.readdir(join(cacheDir.path, 'deno_dir'));
141
+ }
142
+ catch (error) {
143
+ expect(error).toBeInstanceOf(Error);
144
+ }
145
+ // Run #2, feature flag on: The directory should be populated.
146
+ const result2 = await bundle([sourceDirectory], outDir.path, declarations, {
147
+ ...options,
148
+ featureFlags: {
149
+ edge_functions_cache_deno_dir: true,
150
+ },
151
+ });
152
+ const outFiles2 = await fs.readdir(outDir.path);
153
+ expect(result2.functions.length).toBe(1);
154
+ expect(outFiles2.length).toBe(2);
155
+ const denoDir2 = await fs.readdir(join(cacheDir.path, 'deno_dir'));
156
+ expect(denoDir2.includes('deps')).toBe(true);
157
+ expect(denoDir2.includes('gen')).toBe(true);
158
+ await fs.rmdir(outDir.path, { recursive: true });
159
+ }, 10000);
160
+ test('Supports import maps with relative paths', async () => {
161
+ const sourceDirectory = resolve(fixturesDir, 'with_import_maps', 'functions');
162
+ const tmpDir = await tmp.dir();
163
+ const declarations = [
164
+ {
165
+ function: 'func1',
166
+ path: '/func1',
167
+ },
168
+ ];
169
+ const result = await bundle([sourceDirectory], tmpDir.path, declarations, {
170
+ basePath: fixturesDir,
171
+ featureFlags: {
172
+ edge_functions_produce_eszip: true,
173
+ },
174
+ importMaps: [
175
+ {
176
+ baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
177
+ imports: {
178
+ 'alias:helper': './helper.ts',
179
+ },
180
+ },
181
+ ],
182
+ });
183
+ const generatedFiles = await fs.readdir(tmpDir.path);
184
+ expect(result.functions.length).toBe(1);
185
+ expect(generatedFiles.length).toBe(2);
186
+ const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
187
+ const manifest = JSON.parse(manifestFile);
188
+ const { bundles } = manifest;
189
+ expect(bundles.length).toBe(1);
190
+ expect(bundles[0].format).toBe('eszip2');
191
+ expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
192
+ await fs.rmdir(tmpDir.path, { recursive: true });
193
+ }, 10000);
194
+ test('Ignores any user-defined `deno.json` files', async () => {
195
+ const fixtureDir = join(fixturesDir, 'with_import_maps');
196
+ const tmpDir = await tmp.dir();
197
+ const declarations = [
198
+ {
199
+ function: 'func1',
200
+ path: '/func1',
201
+ },
202
+ ];
203
+ // Creating an import map file that rewires the URL of the Deno registry to
204
+ // an invalid location.
205
+ const importMapFile = await tmp.file();
206
+ const importMap = {
207
+ imports: {
208
+ 'https://deno.land/': 'https://black.hole/',
209
+ },
210
+ };
211
+ await fs.writeFile(importMapFile.path, JSON.stringify(importMap));
212
+ // Deno configuration files need to be in the current working directory.
213
+ // There's not a great way for us to set the working directory of the `deno`
214
+ // process that we'll run, so our best bet is to write the file to whatever
215
+ // is the current working directory now and then clean it up.
216
+ const denoConfigPath = join(process.cwd(), 'deno.json');
217
+ const denoConfig = {
218
+ importMap: importMapFile.path,
219
+ };
220
+ try {
221
+ await fs.access(denoConfigPath);
222
+ throw new Error(`The file at '${denoConfigPath} would be overwritten by this test. Please move the file to a different location and try again.'`);
223
+ }
224
+ catch (error) {
225
+ // @ts-expect-error Error is not typed
226
+ if (error.code !== 'ENOENT') {
227
+ throw error;
228
+ }
229
+ }
230
+ await fs.writeFile(denoConfigPath, JSON.stringify(denoConfig));
231
+ expect(() => bundle([join(fixtureDir, 'functions')], tmpDir.path, declarations, {
232
+ basePath: fixturesDir,
233
+ featureFlags: {
234
+ edge_functions_produce_eszip: true,
235
+ },
236
+ importMaps: [
237
+ {
238
+ baseURL: pathToFileURL(join(fixturesDir, 'import-map.json')),
239
+ imports: {
240
+ 'alias:helper': pathToFileURL(join(fixturesDir, 'helper.ts')).toString(),
241
+ },
242
+ },
243
+ ],
244
+ })).not.toThrow();
245
+ await del([tmpDir.path, denoConfigPath, importMapFile.path], { force: true });
246
+ }, 10000);
@@ -0,0 +1,7 @@
1
+ import { DenoBridge } from './bridge.js';
2
+ import { EdgeFunction } from './edge_function.js';
3
+ import { Logger } from './logger.js';
4
+ export interface FunctionConfig {
5
+ path?: string;
6
+ }
7
+ export declare const getFunctionConfig: (func: EdgeFunction, deno: DenoBridge, log: Logger) => Promise<FunctionConfig>;