@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import tmp from 'tmp-promise';
|
|
5
|
+
import { getPackagePath } from './package_json.js';
|
|
6
|
+
// eslint-disable-next-line no-shadow
|
|
7
|
+
var ConfigExitCode;
|
|
8
|
+
(function (ConfigExitCode) {
|
|
9
|
+
ConfigExitCode[ConfigExitCode["Success"] = 0] = "Success";
|
|
10
|
+
ConfigExitCode[ConfigExitCode["UnhandledError"] = 1] = "UnhandledError";
|
|
11
|
+
ConfigExitCode[ConfigExitCode["ImportError"] = 2] = "ImportError";
|
|
12
|
+
ConfigExitCode[ConfigExitCode["NoConfig"] = 3] = "NoConfig";
|
|
13
|
+
ConfigExitCode[ConfigExitCode["InvalidExport"] = 4] = "InvalidExport";
|
|
14
|
+
ConfigExitCode[ConfigExitCode["RuntimeError"] = 5] = "RuntimeError";
|
|
15
|
+
ConfigExitCode[ConfigExitCode["SerializationError"] = 6] = "SerializationError";
|
|
16
|
+
})(ConfigExitCode || (ConfigExitCode = {}));
|
|
17
|
+
const getConfigExtractor = () => {
|
|
18
|
+
const packagePath = getPackagePath();
|
|
19
|
+
const configExtractorPath = join(packagePath, 'deno', 'config.ts');
|
|
20
|
+
return configExtractorPath;
|
|
21
|
+
};
|
|
22
|
+
export const getFunctionConfig = async (func, deno, log) => {
|
|
23
|
+
// The extractor is a Deno script that will import the function and run its
|
|
24
|
+
// `config` export, if one exists.
|
|
25
|
+
const extractorPath = getConfigExtractor();
|
|
26
|
+
// We need to collect the output of the config function, which should be a
|
|
27
|
+
// JSON object. Rather than printing it to stdout, the extractor will write
|
|
28
|
+
// it to a temporary file, which we then read in the Node side. This allows
|
|
29
|
+
// the config function to write to stdout and stderr without that interfering
|
|
30
|
+
// with the extractor.
|
|
31
|
+
const collector = await tmp.file();
|
|
32
|
+
// The extractor will use its exit code to signal different error scenarios,
|
|
33
|
+
// based on the list of exit codes we send as an argument. We then capture
|
|
34
|
+
// the exit code to know exactly what happened and guide people accordingly.
|
|
35
|
+
const { exitCode, stderr, stdout } = await deno.run([
|
|
36
|
+
'run',
|
|
37
|
+
'--allow-read',
|
|
38
|
+
`--allow-write=${collector.path}`,
|
|
39
|
+
'--quiet',
|
|
40
|
+
extractorPath,
|
|
41
|
+
pathToFileURL(func.path).href,
|
|
42
|
+
pathToFileURL(collector.path).href,
|
|
43
|
+
JSON.stringify(ConfigExitCode),
|
|
44
|
+
], { rejectOnExitCode: false });
|
|
45
|
+
if (exitCode !== ConfigExitCode.Success) {
|
|
46
|
+
logConfigError(func, exitCode, stderr, log);
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
if (stdout !== '') {
|
|
50
|
+
log.user(stdout);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const collectorData = await fs.readFile(collector.path, 'utf8');
|
|
54
|
+
return JSON.parse(collectorData);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
logConfigError(func, ConfigExitCode.UnhandledError, stderr, log);
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await collector.cleanup();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
const logConfigError = (func, exitCode, stderr, log) => {
|
|
65
|
+
switch (exitCode) {
|
|
66
|
+
case ConfigExitCode.ImportError:
|
|
67
|
+
log.user(`Could not load edge function at '${func.path}'`);
|
|
68
|
+
log.system(stderr);
|
|
69
|
+
break;
|
|
70
|
+
case ConfigExitCode.NoConfig:
|
|
71
|
+
log.system(`No in-source config found for edge function at '${func.path}'`);
|
|
72
|
+
break;
|
|
73
|
+
case ConfigExitCode.InvalidExport:
|
|
74
|
+
log.user(`'config' export in edge function at '${func.path}' must be a function`);
|
|
75
|
+
break;
|
|
76
|
+
case ConfigExitCode.RuntimeError:
|
|
77
|
+
log.user(`Error while running 'config' function in edge function at '${func.path}'`);
|
|
78
|
+
log.user(stderr);
|
|
79
|
+
break;
|
|
80
|
+
case ConfigExitCode.SerializationError:
|
|
81
|
+
log.user(`'config' function in edge function at '${func.path}' must return an object with primitive values only`);
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
log.user(`Could not load configuration for edge function at '${func.path}'`);
|
|
85
|
+
log.user(stderr);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import del from 'del';
|
|
4
|
+
import { stub } from 'sinon';
|
|
5
|
+
import tmp from 'tmp-promise';
|
|
6
|
+
import { test, expect } from 'vitest';
|
|
7
|
+
import { fixturesDir } from '../test/util.js';
|
|
8
|
+
import { DenoBridge } from './bridge.js';
|
|
9
|
+
import { bundle } from './bundler.js';
|
|
10
|
+
import { getFunctionConfig } from './config.js';
|
|
11
|
+
test('`getFunctionConfig` extracts configuration properties from function file', async () => {
|
|
12
|
+
const { path: tmpDir } = await tmp.dir();
|
|
13
|
+
const deno = new DenoBridge({
|
|
14
|
+
cacheDirectory: tmpDir,
|
|
15
|
+
});
|
|
16
|
+
const functions = [
|
|
17
|
+
// No config
|
|
18
|
+
{
|
|
19
|
+
expectedConfig: {},
|
|
20
|
+
name: 'func1',
|
|
21
|
+
source: `export default async () => new Response("Hello from function one")`,
|
|
22
|
+
},
|
|
23
|
+
// Empty config
|
|
24
|
+
{
|
|
25
|
+
expectedConfig: {},
|
|
26
|
+
name: 'func2',
|
|
27
|
+
source: `
|
|
28
|
+
export default async () => new Response("Hello from function two")
|
|
29
|
+
|
|
30
|
+
export const config = () => ({})
|
|
31
|
+
`,
|
|
32
|
+
},
|
|
33
|
+
// Config with the wrong type
|
|
34
|
+
{
|
|
35
|
+
expectedConfig: {},
|
|
36
|
+
name: 'func3',
|
|
37
|
+
source: `
|
|
38
|
+
export default async () => new Response("Hello from function two")
|
|
39
|
+
|
|
40
|
+
export const config = {}
|
|
41
|
+
`,
|
|
42
|
+
userLog: /^'config' export in edge function at '(.*)' must be a function$/,
|
|
43
|
+
},
|
|
44
|
+
// Config with a syntax error
|
|
45
|
+
{
|
|
46
|
+
expectedConfig: {},
|
|
47
|
+
name: 'func4',
|
|
48
|
+
source: `
|
|
49
|
+
export default async () => new Response("Hello from function two")
|
|
50
|
+
|
|
51
|
+
export const config
|
|
52
|
+
`,
|
|
53
|
+
userLog: /^Could not load edge function at '(.*)'$/,
|
|
54
|
+
},
|
|
55
|
+
// Config that throws
|
|
56
|
+
{
|
|
57
|
+
expectedConfig: {},
|
|
58
|
+
name: 'func5',
|
|
59
|
+
source: `
|
|
60
|
+
export default async () => new Response("Hello from function two")
|
|
61
|
+
|
|
62
|
+
export const config = () => {
|
|
63
|
+
throw new Error('uh-oh')
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
userLog: /^Error while running 'config' function in edge function at '(.*)'$/,
|
|
67
|
+
},
|
|
68
|
+
// Config with `path`
|
|
69
|
+
{
|
|
70
|
+
expectedConfig: { path: '/home' },
|
|
71
|
+
name: 'func6',
|
|
72
|
+
source: `
|
|
73
|
+
export default async () => new Response("Hello from function three")
|
|
74
|
+
|
|
75
|
+
export const config = () => ({ path: "/home" })
|
|
76
|
+
`,
|
|
77
|
+
},
|
|
78
|
+
// Config that prints to stdout
|
|
79
|
+
{
|
|
80
|
+
expectedConfig: { path: '/home' },
|
|
81
|
+
name: 'func7',
|
|
82
|
+
source: `
|
|
83
|
+
export default async () => new Response("Hello from function three")
|
|
84
|
+
|
|
85
|
+
export const config = () => {
|
|
86
|
+
console.log("Hello from config!")
|
|
87
|
+
|
|
88
|
+
return { path: "/home" }
|
|
89
|
+
}
|
|
90
|
+
`,
|
|
91
|
+
userLog: /^Hello from config!$/,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
for (const func of functions) {
|
|
95
|
+
const logger = {
|
|
96
|
+
user: stub().resolves(),
|
|
97
|
+
system: stub().resolves(),
|
|
98
|
+
};
|
|
99
|
+
const path = join(tmpDir, `${func.name}.js`);
|
|
100
|
+
await fs.writeFile(path, func.source);
|
|
101
|
+
const config = await getFunctionConfig({
|
|
102
|
+
name: func.name,
|
|
103
|
+
path,
|
|
104
|
+
}, deno, logger);
|
|
105
|
+
expect(config).toEqual(func.expectedConfig);
|
|
106
|
+
if (func.userLog) {
|
|
107
|
+
expect(logger.user.firstCall.firstArg).toMatch(func.userLog);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
expect(logger.user.callCount).toBe(0);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await del(tmpDir, { force: true });
|
|
114
|
+
});
|
|
115
|
+
test('Ignores function paths from the in-source `config` function if the feature flag is off', async () => {
|
|
116
|
+
const userDirectory = resolve(fixturesDir, 'with_config', 'netlify', 'edge-functions');
|
|
117
|
+
const internalDirectory = resolve(fixturesDir, 'with_config', '.netlify', 'edge-functions');
|
|
118
|
+
const tmpDir = await tmp.dir();
|
|
119
|
+
const declarations = [];
|
|
120
|
+
const result = await bundle([internalDirectory, userDirectory], tmpDir.path, declarations, {
|
|
121
|
+
basePath: fixturesDir,
|
|
122
|
+
featureFlags: {
|
|
123
|
+
edge_functions_produce_eszip: true,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const generatedFiles = await fs.readdir(tmpDir.path);
|
|
127
|
+
expect(result.functions.length).toBe(4);
|
|
128
|
+
expect(generatedFiles.length).toBe(2);
|
|
129
|
+
const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
|
|
130
|
+
const manifest = JSON.parse(manifestFile);
|
|
131
|
+
const { bundles, routes } = manifest;
|
|
132
|
+
expect(bundles.length).toBe(1);
|
|
133
|
+
expect(bundles[0].format).toBe('eszip2');
|
|
134
|
+
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
135
|
+
expect(routes.length).toBe(0);
|
|
136
|
+
await fs.rmdir(tmpDir.path, { recursive: true });
|
|
137
|
+
});
|
|
138
|
+
test('Loads function paths from the in-source `config` function', async () => {
|
|
139
|
+
const userDirectory = resolve(fixturesDir, 'with_config', 'netlify', 'edge-functions');
|
|
140
|
+
const internalDirectory = resolve(fixturesDir, 'with_config', '.netlify', 'edge-functions');
|
|
141
|
+
const tmpDir = await tmp.dir();
|
|
142
|
+
const declarations = [
|
|
143
|
+
{
|
|
144
|
+
function: 'framework-func2',
|
|
145
|
+
path: '/framework-func2',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
function: 'user-func2',
|
|
149
|
+
path: '/user-func2',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
const result = await bundle([internalDirectory, userDirectory], tmpDir.path, declarations, {
|
|
153
|
+
basePath: fixturesDir,
|
|
154
|
+
featureFlags: {
|
|
155
|
+
edge_functions_config_export: true,
|
|
156
|
+
edge_functions_produce_eszip: true,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const generatedFiles = await fs.readdir(tmpDir.path);
|
|
160
|
+
expect(result.functions.length).toBe(4);
|
|
161
|
+
expect(generatedFiles.length).toBe(2);
|
|
162
|
+
const manifestFile = await fs.readFile(resolve(tmpDir.path, 'manifest.json'), 'utf8');
|
|
163
|
+
const manifest = JSON.parse(manifestFile);
|
|
164
|
+
const { bundles, routes } = manifest;
|
|
165
|
+
expect(bundles.length).toBe(1);
|
|
166
|
+
expect(bundles[0].format).toBe('eszip2');
|
|
167
|
+
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
168
|
+
expect(routes.length).toBe(4);
|
|
169
|
+
expect(routes[0]).toEqual({ function: 'framework-func2', pattern: '^/framework-func2/?$' });
|
|
170
|
+
expect(routes[1]).toEqual({ function: 'user-func2', pattern: '^/user-func2/?$' });
|
|
171
|
+
expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$' });
|
|
172
|
+
expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$' });
|
|
173
|
+
await fs.rmdir(tmpDir.path, { recursive: true });
|
|
174
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FunctionConfig } from './config.js';
|
|
1
2
|
interface DeclarationWithPath {
|
|
2
3
|
function: string;
|
|
3
4
|
path: string;
|
|
@@ -7,4 +8,5 @@ interface DeclarationWithPattern {
|
|
|
7
8
|
pattern: string;
|
|
8
9
|
}
|
|
9
10
|
declare type Declaration = DeclarationWithPath | DeclarationWithPattern;
|
|
11
|
+
export declare const getDeclarationsFromConfig: (tomlDeclarations: Declaration[], functionsConfig: Record<string, FunctionConfig>) => Declaration[];
|
|
10
12
|
export { Declaration, DeclarationWithPath, DeclarationWithPattern };
|
package/dist/node/declaration.js
CHANGED
|
@@ -1 +1,27 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export const getDeclarationsFromConfig = (tomlDeclarations, functionsConfig) => {
|
|
2
|
+
var _a;
|
|
3
|
+
const declarations = [];
|
|
4
|
+
const functionsVisited = new Set();
|
|
5
|
+
// We start by iterating over all the TOML declarations. For any declaration
|
|
6
|
+
// for which we also have a function configuration object, we replace the
|
|
7
|
+
// path because that object takes precedence.
|
|
8
|
+
for (const declaration of tomlDeclarations) {
|
|
9
|
+
const { path } = (_a = functionsConfig[declaration.function]) !== null && _a !== void 0 ? _a : {};
|
|
10
|
+
if (path) {
|
|
11
|
+
functionsVisited.add(declaration.function);
|
|
12
|
+
declarations.push({ function: declaration.function, path });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
declarations.push(declaration);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Finally, we must create declarations for functions that are not declared
|
|
19
|
+
// in the TOML at all.
|
|
20
|
+
for (const name in functionsConfig) {
|
|
21
|
+
const { path } = functionsConfig[name];
|
|
22
|
+
if (!functionsVisited.has(name) && path) {
|
|
23
|
+
declarations.push({ function: name, path });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return declarations;
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { platform } from 'process';
|
|
3
|
+
import { PassThrough } from 'stream';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import nock from 'nock';
|
|
6
|
+
import tmp from 'tmp-promise';
|
|
7
|
+
import { beforeEach, afterEach, test, expect } from 'vitest';
|
|
8
|
+
import { fixturesDir, testLogger } from '../test/util.js';
|
|
9
|
+
import { download } from './downloader.js';
|
|
10
|
+
import { getPlatformTarget } from './platform.js';
|
|
11
|
+
const streamError = () => {
|
|
12
|
+
const stream = new PassThrough();
|
|
13
|
+
setTimeout(() => stream.emit('data', 'zipcontent'), 100);
|
|
14
|
+
setTimeout(() => stream.emit('error', new Error('stream error')), 200);
|
|
15
|
+
return stream;
|
|
16
|
+
};
|
|
17
|
+
beforeEach(async (ctx) => {
|
|
18
|
+
const tmpDir = await tmp.dir();
|
|
19
|
+
// eslint-disable-next-line no-param-reassign
|
|
20
|
+
ctx.tmpDir = tmpDir.path;
|
|
21
|
+
});
|
|
22
|
+
afterEach(async (ctx) => {
|
|
23
|
+
await fs.rmdir(ctx.tmpDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
test('tries downloading binary up to 4 times', async (ctx) => {
|
|
26
|
+
nock.disableNetConnect();
|
|
27
|
+
const version = '99.99.99';
|
|
28
|
+
const mockURL = 'https://dl.deno.land:443';
|
|
29
|
+
const target = getPlatformTarget();
|
|
30
|
+
const zipPath = `/release/v${version}/deno-${target}.zip`;
|
|
31
|
+
const latestVersionMock = nock(mockURL)
|
|
32
|
+
.get('/release-latest.txt')
|
|
33
|
+
.reply(200, `v${version}\n`)
|
|
34
|
+
// first attempt
|
|
35
|
+
.get(zipPath)
|
|
36
|
+
.reply(500)
|
|
37
|
+
// second attempt
|
|
38
|
+
.get(zipPath)
|
|
39
|
+
.reply(500)
|
|
40
|
+
// third attempt
|
|
41
|
+
.get(zipPath)
|
|
42
|
+
.reply(500)
|
|
43
|
+
// fourth attempt
|
|
44
|
+
.get(zipPath)
|
|
45
|
+
// 1 second delay
|
|
46
|
+
.delayBody(1000)
|
|
47
|
+
.replyWithFile(200, platform === 'win32' ? `${fixturesDir}/deno.win.zip` : `${fixturesDir}/deno.zip`, {
|
|
48
|
+
'Content-Type': 'application/zip',
|
|
49
|
+
});
|
|
50
|
+
const deno = await download(ctx.tmpDir, `^${version}`, testLogger);
|
|
51
|
+
expect(latestVersionMock.isDone()).toBe(true);
|
|
52
|
+
expect(deno).toBeTruthy();
|
|
53
|
+
const res = await execa(deno);
|
|
54
|
+
expect(res.stdout).toBe('hello');
|
|
55
|
+
});
|
|
56
|
+
test('fails downloading binary after 4th time', async (ctx) => {
|
|
57
|
+
expect.assertions(2);
|
|
58
|
+
nock.disableNetConnect();
|
|
59
|
+
const version = '99.99.99';
|
|
60
|
+
const mockURL = 'https://dl.deno.land:443';
|
|
61
|
+
const target = getPlatformTarget();
|
|
62
|
+
const zipPath = `/release/v${version}/deno-${target}.zip`;
|
|
63
|
+
const latestVersionMock = nock(mockURL)
|
|
64
|
+
.get('/release-latest.txt')
|
|
65
|
+
.reply(200, `v${version}\n`)
|
|
66
|
+
// first attempt
|
|
67
|
+
.get(zipPath)
|
|
68
|
+
.reply(500)
|
|
69
|
+
// second attempt
|
|
70
|
+
.get(zipPath)
|
|
71
|
+
.reply(500)
|
|
72
|
+
// third attempt
|
|
73
|
+
.get(zipPath)
|
|
74
|
+
.reply(500)
|
|
75
|
+
// fourth attempt
|
|
76
|
+
.get(zipPath)
|
|
77
|
+
.reply(500);
|
|
78
|
+
try {
|
|
79
|
+
await download(ctx.tmpDir, `^${version}`, testLogger);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
expect(error).toMatch(/Download failed with status code 500/);
|
|
83
|
+
}
|
|
84
|
+
expect(latestVersionMock.isDone()).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
test('fails downloading if response stream throws error', async (ctx) => {
|
|
87
|
+
expect.assertions(2);
|
|
88
|
+
nock.disableNetConnect();
|
|
89
|
+
const version = '99.99.99';
|
|
90
|
+
const mockURL = 'https://dl.deno.land:443';
|
|
91
|
+
const target = getPlatformTarget();
|
|
92
|
+
const zipPath = `/release/v${version}/deno-${target}.zip`;
|
|
93
|
+
const latestVersionMock = nock(mockURL)
|
|
94
|
+
.get('/release-latest.txt')
|
|
95
|
+
.reply(200, `v${version}\n`)
|
|
96
|
+
// first attempt
|
|
97
|
+
.get(zipPath)
|
|
98
|
+
.reply(200, streamError)
|
|
99
|
+
// second attempt
|
|
100
|
+
.get(zipPath)
|
|
101
|
+
.reply(200, streamError)
|
|
102
|
+
// third attempt
|
|
103
|
+
.get(zipPath)
|
|
104
|
+
.reply(200, streamError)
|
|
105
|
+
// fourth attempt
|
|
106
|
+
.get(zipPath)
|
|
107
|
+
.reply(200, streamError);
|
|
108
|
+
try {
|
|
109
|
+
await download(ctx.tmpDir, `^${version}`, testLogger);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
expect(error).toMatch(/stream error/);
|
|
113
|
+
}
|
|
114
|
+
expect(latestVersionMock.isDone()).toBe(true);
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { ImportMap } from './import_map.js';
|
|
3
|
+
test('Handles import maps with full URLs without specifying a base URL', () => {
|
|
4
|
+
const inputFile1 = {
|
|
5
|
+
baseURL: new URL('file:///some/path/import-map.json'),
|
|
6
|
+
imports: {
|
|
7
|
+
'alias:jamstack': 'https://jamstack.org',
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const inputFile2 = {
|
|
11
|
+
baseURL: new URL('file:///some/path/import-map.json'),
|
|
12
|
+
imports: {
|
|
13
|
+
'alias:pets': 'https://petsofnetlify.com/',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const map = new ImportMap([inputFile1, inputFile2]);
|
|
17
|
+
const { imports } = JSON.parse(map.getContents());
|
|
18
|
+
expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
|
|
19
|
+
expect(imports['alias:jamstack']).toBe('https://jamstack.org/');
|
|
20
|
+
expect(imports['alias:pets']).toBe('https://petsofnetlify.com/');
|
|
21
|
+
});
|
|
22
|
+
test('Handles import maps with relative paths', () => {
|
|
23
|
+
const inputFile1 = {
|
|
24
|
+
baseURL: new URL('file:///Users/jane-doe/my-site/import-map.json'),
|
|
25
|
+
imports: {
|
|
26
|
+
'alias:pets': './heart/pets/',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const map = new ImportMap([inputFile1]);
|
|
30
|
+
const { imports } = JSON.parse(map.getContents());
|
|
31
|
+
expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
|
|
32
|
+
expect(imports['alias:pets']).toBe('file:///Users/jane-doe/my-site/heart/pets/');
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { stub } from 'sinon';
|
|
2
|
+
import { afterEach, test, expect } from 'vitest';
|
|
3
|
+
import { getLogger } from './logger.js';
|
|
4
|
+
const consoleLog = console.log;
|
|
5
|
+
const noopLogger = () => {
|
|
6
|
+
// no-op
|
|
7
|
+
};
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
// Restoring global `console.log`.
|
|
10
|
+
console.log = consoleLog;
|
|
11
|
+
});
|
|
12
|
+
test('Prints user logs to stdout', () => {
|
|
13
|
+
const mockConsoleLog = stub();
|
|
14
|
+
console.log = mockConsoleLog;
|
|
15
|
+
const logger1 = getLogger(noopLogger, true);
|
|
16
|
+
const logger2 = getLogger(noopLogger, false);
|
|
17
|
+
logger1.user('Hello with `debug: true`');
|
|
18
|
+
logger2.user('Hello with `debug: false`');
|
|
19
|
+
expect(mockConsoleLog.callCount).toBe(2);
|
|
20
|
+
expect(mockConsoleLog.firstCall.firstArg).toBe('Hello with `debug: true`');
|
|
21
|
+
expect(mockConsoleLog.secondCall.firstArg).toBe('Hello with `debug: false`');
|
|
22
|
+
});
|
|
23
|
+
test('Prints system logs to the system logger provided', () => {
|
|
24
|
+
const mockSystemLog = stub();
|
|
25
|
+
const mockConsoleLog = stub();
|
|
26
|
+
console.log = mockSystemLog;
|
|
27
|
+
const logger1 = getLogger(mockSystemLog, true);
|
|
28
|
+
const logger2 = getLogger(mockSystemLog, false);
|
|
29
|
+
logger1.system('Hello with `debug: true`');
|
|
30
|
+
logger2.system('Hello with `debug: false`');
|
|
31
|
+
expect(mockConsoleLog.callCount).toBe(0);
|
|
32
|
+
expect(mockSystemLog.callCount).toBe(2);
|
|
33
|
+
expect(mockSystemLog.firstCall.firstArg).toBe('Hello with `debug: true`');
|
|
34
|
+
expect(mockSystemLog.secondCall.firstArg).toBe('Hello with `debug: false`');
|
|
35
|
+
});
|
|
36
|
+
test('Prints system logs to stdout if there is no system logger provided and `debug` is enabled', () => {
|
|
37
|
+
const mockConsoleLog = stub();
|
|
38
|
+
console.log = mockConsoleLog;
|
|
39
|
+
const logger1 = getLogger(undefined, true);
|
|
40
|
+
const logger2 = getLogger(undefined, false);
|
|
41
|
+
logger1.system('Hello with `debug: true`');
|
|
42
|
+
logger2.system('Hello with `debug: false`');
|
|
43
|
+
expect(mockConsoleLog.callCount).toBe(1);
|
|
44
|
+
expect(mockConsoleLog.firstCall.firstArg).toBe('Hello with `debug: true`');
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { platform } 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
|
+
test('Downloads the Deno CLI on demand and caches it for subsequent calls', async () => {
|
|
16
|
+
var _a, _b, _c, _d;
|
|
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 mockBinaryOutput = `#!/usr/bin/env sh\n\necho "deno ${latestVersion}"`;
|
|
19
|
+
const data = new PassThrough();
|
|
20
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
21
|
+
archive.pipe(data);
|
|
22
|
+
archive.append(Buffer.from(mockBinaryOutput), { name: platform === 'win32' ? 'deno.exe' : 'deno' });
|
|
23
|
+
archive.finalize();
|
|
24
|
+
const target = getPlatformTarget();
|
|
25
|
+
const latestReleaseMock = nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`);
|
|
26
|
+
const downloadMock = nock('https://dl.deno.land')
|
|
27
|
+
.get(`/release/v${latestVersion}/deno-${target}.zip`)
|
|
28
|
+
.reply(200, () => data);
|
|
29
|
+
const tmpDir = await tmp.dir();
|
|
30
|
+
const beforeDownload = spy();
|
|
31
|
+
const afterDownload = spy();
|
|
32
|
+
const deno = new DenoBridge({
|
|
33
|
+
cacheDirectory: tmpDir.path,
|
|
34
|
+
onBeforeDownload: beforeDownload,
|
|
35
|
+
onAfterDownload: afterDownload,
|
|
36
|
+
useGlobal: false,
|
|
37
|
+
});
|
|
38
|
+
const output1 = await deno.run(['help']);
|
|
39
|
+
const output2 = await deno.run(['help']);
|
|
40
|
+
const expectedOutput = /^deno [\d.]+/;
|
|
41
|
+
expect(latestReleaseMock.isDone()).toBe(true);
|
|
42
|
+
expect(downloadMock.isDone()).toBe(true);
|
|
43
|
+
expect((_c = output1 === null || output1 === void 0 ? void 0 : output1.stdout) !== null && _c !== void 0 ? _c : '').toMatch(expectedOutput);
|
|
44
|
+
expect((_d = output2 === null || output2 === void 0 ? void 0 : output2.stdout) !== null && _d !== void 0 ? _d : '').toMatch(expectedOutput);
|
|
45
|
+
expect(beforeDownload.callCount).toBe(1);
|
|
46
|
+
expect(afterDownload.callCount).toBe(1);
|
|
47
|
+
await fs.promises.rmdir(tmpDir.path, { recursive: true });
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { env } from 'process';
|
|
2
|
+
import { test, expect } from 'vitest';
|
|
3
|
+
import { generateManifest } from './manifest.js';
|
|
4
|
+
test('Generates a manifest with different bundles', () => {
|
|
5
|
+
const bundle1 = {
|
|
6
|
+
extension: '.ext1',
|
|
7
|
+
format: 'format1',
|
|
8
|
+
hash: '123456',
|
|
9
|
+
};
|
|
10
|
+
const bundle2 = {
|
|
11
|
+
extension: '.ext2',
|
|
12
|
+
format: 'format2',
|
|
13
|
+
hash: '654321',
|
|
14
|
+
};
|
|
15
|
+
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
16
|
+
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
17
|
+
const manifest = generateManifest({ bundles: [bundle1, bundle2], declarations, functions });
|
|
18
|
+
const expectedBundles = [
|
|
19
|
+
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
|
|
20
|
+
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
|
|
21
|
+
];
|
|
22
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$' }];
|
|
23
|
+
expect(manifest.bundles).toEqual(expectedBundles);
|
|
24
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
25
|
+
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
26
|
+
});
|
|
27
|
+
test('Excludes functions for which there are function files but no matching config declarations', () => {
|
|
28
|
+
const bundle1 = {
|
|
29
|
+
extension: '.ext2',
|
|
30
|
+
format: 'format1',
|
|
31
|
+
hash: '123456',
|
|
32
|
+
};
|
|
33
|
+
const functions = [
|
|
34
|
+
{ name: 'func-1', path: '/path/to/func-1.ts' },
|
|
35
|
+
{ name: 'func-2', path: '/path/to/func-2.ts' },
|
|
36
|
+
];
|
|
37
|
+
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
38
|
+
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
39
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$' }];
|
|
40
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
41
|
+
});
|
|
42
|
+
test('Excludes functions for which there are config declarations but no matching function files', () => {
|
|
43
|
+
const bundle1 = {
|
|
44
|
+
extension: '.ext2',
|
|
45
|
+
format: 'format1',
|
|
46
|
+
hash: '123456',
|
|
47
|
+
};
|
|
48
|
+
const functions = [{ name: 'func-2', path: '/path/to/func-2.ts' }];
|
|
49
|
+
const declarations = [
|
|
50
|
+
{ function: 'func-1', path: '/f1' },
|
|
51
|
+
{ function: 'func-2', path: '/f2' },
|
|
52
|
+
];
|
|
53
|
+
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
54
|
+
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$' }];
|
|
55
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
56
|
+
});
|
|
57
|
+
test('Generates a manifest without bundles', () => {
|
|
58
|
+
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
59
|
+
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
60
|
+
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
61
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$' }];
|
|
62
|
+
expect(manifest.bundles).toEqual([]);
|
|
63
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
64
|
+
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
65
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { test, expect } from 'vitest';
|
|
3
|
+
import { getPackageVersion } from './package_json.js';
|
|
4
|
+
test('`getPackageVersion` returns the package version`', () => {
|
|
5
|
+
const version = getPackageVersion();
|
|
6
|
+
expect(semver.valid(version)).not.toBeNull();
|
|
7
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|