@netlify/edge-bundler 2.2.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.
- package/deno/config.ts +38 -0
- package/deno/lib/stage2.test.ts +58 -0
- package/dist/node/bootstrap.test.d.ts +1 -0
- package/dist/node/bootstrap.test.js +26 -0
- package/dist/node/bridge.d.ts +2 -1
- package/dist/node/bridge.js +2 -2
- package/dist/node/bridge.test.d.ts +1 -0
- package/dist/node/bridge.test.js +112 -0
- package/dist/node/bundler.d.ts +2 -2
- package/dist/node/bundler.js +29 -20
- package/dist/node/bundler.test.d.ts +1 -0
- package/dist/node/bundler.test.js +246 -0
- package/dist/node/config.d.ts +7 -0
- package/dist/node/config.js +87 -0
- package/dist/node/config.test.d.ts +1 -0
- package/dist/node/config.test.js +174 -0
- package/dist/node/declaration.d.ts +2 -0
- package/dist/node/declaration.js +27 -1
- package/dist/node/downloader.test.d.ts +1 -0
- package/dist/node/downloader.test.js +115 -0
- package/dist/node/feature_flags.js +1 -0
- package/dist/node/import_map.test.d.ts +1 -0
- package/dist/node/import_map.test.js +33 -0
- package/dist/node/logger.test.d.ts +1 -0
- package/dist/node/logger.test.js +45 -0
- package/dist/node/main.test.d.ts +1 -0
- package/dist/node/main.test.js +48 -0
- package/dist/node/manifest.test.d.ts +1 -0
- package/dist/node/manifest.test.js +65 -0
- package/dist/node/package_json.test.d.ts +1 -0
- package/dist/node/package_json.test.js +7 -0
- package/dist/node/serving.test.d.ts +1 -0
- package/dist/node/serving.test.js +31 -0
- package/dist/node/stage_2.test.d.ts +1 -0
- package/dist/node/stage_2.test.js +48 -0
- package/dist/node/types.test.d.ts +1 -0
- package/dist/node/types.test.js +61 -0
- package/dist/test/util.d.ts +3 -0
- package/dist/test/util.js +9 -0
- package/package.json +17 -28
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
|
+
});
|
package/dist/node/bridge.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ interface RunOptions {
|
|
|
21
21
|
pipeOutput?: boolean;
|
|
22
22
|
env?: NodeJS.ProcessEnv;
|
|
23
23
|
extendEnv?: boolean;
|
|
24
|
+
rejectOnExitCode?: boolean;
|
|
24
25
|
}
|
|
25
26
|
declare class DenoBridge {
|
|
26
27
|
cacheDirectory: string;
|
|
@@ -46,7 +47,7 @@ declare class DenoBridge {
|
|
|
46
47
|
path: string;
|
|
47
48
|
}>;
|
|
48
49
|
getEnvironmentVariables(inputEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
49
|
-
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>>;
|
|
50
51
|
runInBackground(args: string[], ref?: ProcessRef, { pipeOutput, env: inputEnv, extendEnv }?: RunOptions): Promise<void>;
|
|
51
52
|
}
|
|
52
53
|
export { DENO_VERSION_RANGE, DenoBridge };
|
package/dist/node/bridge.js
CHANGED
|
@@ -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
|
|
@@ -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
|
+
});
|
package/dist/node/bundler.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
|
|
2
|
-
import
|
|
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,
|
|
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
|
}>;
|
package/dist/node/bundler.js
CHANGED
|
@@ -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
|
|
15
|
-
const bundleOps = [];
|
|
16
|
+
const createBundle = ({ basePath, buildID, debug, deno, distDirectory, functions, importMap, featureFlags, }) => {
|
|
16
17
|
if (featureFlags.edge_functions_produce_eszip) {
|
|
17
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,
|
|
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
|
|
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(
|
|
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>;
|