@netlify/edge-bundler 8.8.1 → 8.10.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/dist/node/bridge.test.js +4 -4
- package/dist/node/bundler.d.ts +19 -2
- package/dist/node/bundler.js +20 -10
- package/dist/node/bundler.test.js +18 -30
- package/dist/node/config.d.ts +2 -1
- package/dist/node/config.js +22 -7
- package/dist/node/config.test.js +89 -76
- package/dist/node/deploy_config.test.js +1 -0
- package/dist/node/downloader.test.js +2 -2
- package/dist/node/feature_flags.d.ts +9 -3
- package/dist/node/feature_flags.js +1 -2
- package/dist/node/formats/javascript.js +1 -1
- package/dist/node/main.test.js +2 -2
- package/dist/node/server/server.js +1 -1
- package/dist/node/types.test.js +6 -6
- package/dist/node/validation/manifest/index.test.js +4 -0
- package/package.json +8 -5
package/dist/node/bridge.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
|
-
import
|
|
2
|
+
import { rm } from 'fs/promises';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { platform, env } from 'process';
|
|
5
5
|
import { PassThrough } from 'stream';
|
|
@@ -53,7 +53,7 @@ test('Does not inherit environment variables if `extendEnv` is false', async ()
|
|
|
53
53
|
});
|
|
54
54
|
output = output.trim().replace(/\n+/g, '\n');
|
|
55
55
|
expect(output).toBe('LULU=LALA');
|
|
56
|
-
await
|
|
56
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
57
57
|
});
|
|
58
58
|
test('Does inherit environment variables if `extendEnv` is true', async () => {
|
|
59
59
|
var _a;
|
|
@@ -78,7 +78,7 @@ test('Does inherit environment variables if `extendEnv` is true', async () => {
|
|
|
78
78
|
// lets remove holes, split lines and sort lines by name, as different OSes might order them different
|
|
79
79
|
const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort();
|
|
80
80
|
expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']);
|
|
81
|
-
await
|
|
81
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
82
82
|
});
|
|
83
83
|
test('Does inherit environment variables if `extendEnv` is not set', async () => {
|
|
84
84
|
var _a;
|
|
@@ -103,5 +103,5 @@ test('Does inherit environment variables if `extendEnv` is not set', async () =>
|
|
|
103
103
|
// lets remove holes, split lines and sort lines by name, as different OSes might order them different
|
|
104
104
|
const environmentVariables = output.trim().replace(/\n+/g, '\n').split('\n').sort();
|
|
105
105
|
expect(environmentVariables).toEqual(['LULU=LALA', 'TADA=TUDU']);
|
|
106
|
-
await
|
|
106
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
107
107
|
});
|
package/dist/node/bundler.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
|
|
2
2
|
import { Declaration } from './declaration.js';
|
|
3
|
+
import { EdgeFunction } from './edge_function.js';
|
|
3
4
|
import { FeatureFlags } from './feature_flags.js';
|
|
4
5
|
import { LogFunction } from './logger.js';
|
|
5
6
|
interface BundleOptions {
|
|
@@ -13,10 +14,26 @@ interface BundleOptions {
|
|
|
13
14
|
onAfterDownload?: OnAfterDownloadHook;
|
|
14
15
|
onBeforeDownload?: OnBeforeDownloadHook;
|
|
15
16
|
systemLogger?: LogFunction;
|
|
17
|
+
internalSrcFolder?: string;
|
|
16
18
|
}
|
|
17
|
-
declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, }?: BundleOptions) => Promise<{
|
|
18
|
-
functions:
|
|
19
|
+
declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, }?: BundleOptions) => Promise<{
|
|
20
|
+
functions: EdgeFunction[];
|
|
19
21
|
manifest: import("./manifest.js").Manifest;
|
|
20
22
|
}>;
|
|
23
|
+
export declare const addGeneratorFieldIfMissing: (declaration: Declaration, functions: EdgeFunction[], internalFunctionsPath?: string) => {
|
|
24
|
+
generator: string | undefined;
|
|
25
|
+
cache?: string | undefined;
|
|
26
|
+
function: string;
|
|
27
|
+
name?: string | undefined;
|
|
28
|
+
path: string;
|
|
29
|
+
excludedPath?: string | undefined;
|
|
30
|
+
} | {
|
|
31
|
+
generator: string | undefined;
|
|
32
|
+
cache?: string | undefined;
|
|
33
|
+
function: string;
|
|
34
|
+
name?: string | undefined;
|
|
35
|
+
pattern: string;
|
|
36
|
+
excludedPattern?: string | undefined;
|
|
37
|
+
};
|
|
21
38
|
export { bundle };
|
|
22
39
|
export type { BundleOptions };
|
package/dist/node/bundler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
3
|
import commonPathPrefix from 'common-path-prefix';
|
|
4
|
+
import isPathInside from 'is-path-inside';
|
|
4
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
5
6
|
import { importMapSpecifier } from '../shared/consts.js';
|
|
6
7
|
import { DenoBridge } from './bridge.js';
|
|
@@ -14,7 +15,7 @@ import { ImportMap } from './import_map.js';
|
|
|
14
15
|
import { getLogger } from './logger.js';
|
|
15
16
|
import { writeManifest } from './manifest.js';
|
|
16
17
|
import { ensureLatestTypes } from './types.js';
|
|
17
|
-
const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, } = {}) => {
|
|
18
|
+
const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, } = {}) => {
|
|
18
19
|
const logger = getLogger(systemLogger, debug);
|
|
19
20
|
const featureFlags = getFlags(inputFeatureFlags);
|
|
20
21
|
const options = {
|
|
@@ -24,7 +25,8 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
24
25
|
onAfterDownload,
|
|
25
26
|
onBeforeDownload,
|
|
26
27
|
};
|
|
27
|
-
|
|
28
|
+
const internalFunctionsPath = internalSrcFolder && resolve(internalSrcFolder);
|
|
29
|
+
if (cacheDirectory !== undefined) {
|
|
28
30
|
options.denoDir = join(cacheDirectory, 'deno_dir');
|
|
29
31
|
}
|
|
30
32
|
const deno = new DenoBridge(options);
|
|
@@ -59,17 +61,17 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
59
61
|
// rename the bundles to their permanent names.
|
|
60
62
|
await createFinalBundles([functionBundle], distDirectory, buildID);
|
|
61
63
|
// Retrieving a configuration object for each function.
|
|
62
|
-
const functionsConfig = await Promise.all(functions.map((func) =>
|
|
63
|
-
if (!featureFlags.edge_functions_config_export) {
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
return getFunctionConfig(func, importMap, deno, logger);
|
|
67
|
-
}));
|
|
64
|
+
const functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger, featureFlags)));
|
|
68
65
|
// Creating a hash of function names to configuration objects.
|
|
69
66
|
const functionsWithConfig = functions.reduce((acc, func, index) => ({ ...acc, [func.name]: functionsConfig[index] }), {});
|
|
70
67
|
// Creating a final declarations array by combining the TOML file with the
|
|
71
68
|
// deploy configuration API and the in-source configuration.
|
|
72
|
-
const
|
|
69
|
+
const declarationsFromConfig = getDeclarationsFromConfig(tomlDeclarations, functionsWithConfig, deployConfig);
|
|
70
|
+
// If any declarations are autogenerated and are missing the generator field
|
|
71
|
+
// add a default string.
|
|
72
|
+
const declarations = internalFunctionsPath
|
|
73
|
+
? declarationsFromConfig.map((declaration) => addGeneratorFieldIfMissing(declaration, functions, internalFunctionsPath))
|
|
74
|
+
: declarationsFromConfig;
|
|
73
75
|
const manifest = await writeManifest({
|
|
74
76
|
bundles: [functionBundle],
|
|
75
77
|
declarations,
|
|
@@ -105,4 +107,12 @@ const getBasePath = (sourceDirectories, inputBasePath) => {
|
|
|
105
107
|
}
|
|
106
108
|
return commonPathPrefix(sourceDirectories);
|
|
107
109
|
};
|
|
110
|
+
export const addGeneratorFieldIfMissing = (declaration, functions, internalFunctionsPath) => {
|
|
111
|
+
var _a;
|
|
112
|
+
const fullFuncPath = (_a = functions === null || functions === void 0 ? void 0 : functions.find((func) => func.name === declaration.function)) === null || _a === void 0 ? void 0 : _a.path;
|
|
113
|
+
// If function path is in the internalFunctionsPath, we assume it is autogenerated.
|
|
114
|
+
const isInternal = Boolean(internalFunctionsPath && fullFuncPath && isPathInside(fullFuncPath, internalFunctionsPath));
|
|
115
|
+
const generatorFallback = isInternal ? 'internalFunc' : undefined;
|
|
116
|
+
return { ...declaration, generator: declaration.generator || generatorFallback };
|
|
117
|
+
};
|
|
108
118
|
export { bundle };
|
|
@@ -147,8 +147,8 @@ test('Does not add a custom error property to system errors during bundling', as
|
|
|
147
147
|
expect(error).not.toBeInstanceOf(BundleError);
|
|
148
148
|
}
|
|
149
149
|
});
|
|
150
|
-
test('Uses the cache directory as the `DENO_DIR` value
|
|
151
|
-
expect.assertions(
|
|
150
|
+
test('Uses the cache directory as the `DENO_DIR` value', async () => {
|
|
151
|
+
expect.assertions(3);
|
|
152
152
|
const { basePath, cleanup, distPath } = await useFixture('with_import_maps');
|
|
153
153
|
const sourceDirectory = join(basePath, 'functions');
|
|
154
154
|
const cacheDir = await tmp.dir();
|
|
@@ -163,29 +163,12 @@ test('Uses the cache directory as the `DENO_DIR` value if the `edge_functions_ca
|
|
|
163
163
|
cacheDirectory: cacheDir.path,
|
|
164
164
|
configPath: join(sourceDirectory, 'config.json'),
|
|
165
165
|
};
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
expect(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
await fs.readdir(join(cacheDir.path, 'deno_dir'));
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
expect(error).toBeInstanceOf(Error);
|
|
176
|
-
}
|
|
177
|
-
// Run #2, feature flag on: The directory should be populated.
|
|
178
|
-
const result2 = await bundle([sourceDirectory], distPath, declarations, {
|
|
179
|
-
...options,
|
|
180
|
-
featureFlags: {
|
|
181
|
-
edge_functions_cache_deno_dir: true,
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
const outFiles2 = await fs.readdir(distPath);
|
|
185
|
-
expect(result2.functions.length).toBe(1);
|
|
186
|
-
expect(outFiles2.length).toBe(2);
|
|
187
|
-
const denoDir2 = await fs.readdir(join(cacheDir.path, 'deno_dir'));
|
|
188
|
-
expect(denoDir2.includes('gen')).toBe(true);
|
|
166
|
+
const result = await bundle([sourceDirectory], distPath, declarations, options);
|
|
167
|
+
const outFiles = await fs.readdir(distPath);
|
|
168
|
+
expect(result.functions.length).toBe(1);
|
|
169
|
+
expect(outFiles.length).toBe(2);
|
|
170
|
+
const denoDir = await fs.readdir(join(cacheDir.path, 'deno_dir'));
|
|
171
|
+
expect(denoDir.includes('gen')).toBe(true);
|
|
189
172
|
await cleanup();
|
|
190
173
|
});
|
|
191
174
|
test('Supports import maps with relative paths', async () => {
|
|
@@ -264,7 +247,7 @@ test('Processes a function that imports a custom layer', async () => {
|
|
|
264
247
|
path: '/func1',
|
|
265
248
|
},
|
|
266
249
|
];
|
|
267
|
-
const layer = { name: 'layer
|
|
250
|
+
const layer = { name: 'https://edge-function-layer-template.netlify.app/mod.ts', flag: 'edge-functions-layer-test' };
|
|
268
251
|
const result = await bundle([sourceDirectory], distPath, declarations, {
|
|
269
252
|
basePath,
|
|
270
253
|
configPath: join(sourceDirectory, 'config.json'),
|
|
@@ -292,17 +275,22 @@ test('Loads declarations and import maps from the deploy configuration', async (
|
|
|
292
275
|
const directories = [join(basePath, 'netlify', 'edge-functions'), join(basePath, '.netlify', 'edge-functions')];
|
|
293
276
|
const result = await bundle(directories, distPath, declarations, {
|
|
294
277
|
basePath,
|
|
295
|
-
configPath: join(basePath, '.netlify', 'edge-functions', '
|
|
278
|
+
configPath: join(basePath, '.netlify', 'edge-functions', 'manifest.json'),
|
|
279
|
+
internalSrcFolder: directories[1],
|
|
296
280
|
});
|
|
297
281
|
const generatedFiles = await fs.readdir(distPath);
|
|
298
|
-
expect(result.functions.length).toBe(
|
|
282
|
+
expect(result.functions.length).toBe(3);
|
|
299
283
|
expect(generatedFiles.length).toBe(2);
|
|
300
284
|
const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
|
|
301
285
|
const manifest = JSON.parse(manifestFile);
|
|
302
|
-
const { bundles, function_config: functionConfig } = manifest;
|
|
286
|
+
const { routes, bundles, function_config: functionConfig } = manifest;
|
|
303
287
|
expect(bundles.length).toBe(1);
|
|
304
288
|
expect(bundles[0].format).toBe('eszip2');
|
|
305
289
|
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
290
|
+
expect(routes[0].generator).toBeUndefined();
|
|
291
|
+
expect(routes[1].name).toBe('Function two');
|
|
292
|
+
expect(routes[1].generator).toBe('@netlify/fake-plugin@1.0.0');
|
|
293
|
+
expect(routes[2].generator).toBe('internalFunc');
|
|
306
294
|
// respects excludedPath from deploy config
|
|
307
295
|
expect(functionConfig.func2).toEqual({ excluded_patterns: ['^/func2/skip/?$'] });
|
|
308
296
|
await cleanup();
|
|
@@ -318,7 +306,7 @@ test("Ignores entries in `importMapPaths` that don't point to an existing import
|
|
|
318
306
|
helper: pathToFileURL(join(basePath, 'helper.ts')).toString(),
|
|
319
307
|
},
|
|
320
308
|
scopes: {
|
|
321
|
-
[pathToFileURL(join(sourceDirectory, 'func3')).toString()]: {
|
|
309
|
+
[pathToFileURL(join(sourceDirectory, 'func3/func3.ts')).toString()]: {
|
|
322
310
|
helper: pathToFileURL(join(basePath, 'helper2.ts')).toString(),
|
|
323
311
|
},
|
|
324
312
|
},
|
package/dist/node/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DenoBridge } from './bridge.js';
|
|
2
2
|
import { EdgeFunction } from './edge_function.js';
|
|
3
|
+
import { FeatureFlags } from './feature_flags.js';
|
|
3
4
|
import { ImportMap } from './import_map.js';
|
|
4
5
|
import { Logger } from './logger.js';
|
|
5
6
|
export declare const enum Cache {
|
|
@@ -11,4 +12,4 @@ export interface FunctionConfig {
|
|
|
11
12
|
path?: string | string[];
|
|
12
13
|
excludedPath?: string | string[];
|
|
13
14
|
}
|
|
14
|
-
export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger) => Promise<FunctionConfig>;
|
|
15
|
+
export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger, featureFlags: FeatureFlags) => Promise<FunctionConfig>;
|
package/dist/node/config.js
CHANGED
|
@@ -19,7 +19,7 @@ const getConfigExtractor = () => {
|
|
|
19
19
|
const configExtractorPath = join(packagePath, 'deno', 'config.ts');
|
|
20
20
|
return configExtractorPath;
|
|
21
21
|
};
|
|
22
|
-
export const getFunctionConfig = async (func, importMap, deno, log) => {
|
|
22
|
+
export const getFunctionConfig = async (func, importMap, deno, log, featureFlags) => {
|
|
23
23
|
// The extractor is a Deno script that will import the function and run its
|
|
24
24
|
// `config` export, if one exists.
|
|
25
25
|
const extractorPath = getConfigExtractor();
|
|
@@ -46,7 +46,7 @@ export const getFunctionConfig = async (func, importMap, deno, log) => {
|
|
|
46
46
|
JSON.stringify(ConfigExitCode),
|
|
47
47
|
], { rejectOnExitCode: false });
|
|
48
48
|
if (exitCode !== ConfigExitCode.Success) {
|
|
49
|
-
|
|
49
|
+
handleConfigError(func, exitCode, stderr, log, featureFlags);
|
|
50
50
|
return {};
|
|
51
51
|
}
|
|
52
52
|
if (stdout !== '') {
|
|
@@ -57,27 +57,42 @@ export const getFunctionConfig = async (func, importMap, deno, log) => {
|
|
|
57
57
|
return JSON.parse(collectorData);
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
|
-
|
|
60
|
+
handleConfigError(func, ConfigExitCode.UnhandledError, stderr, log, featureFlags);
|
|
61
61
|
return {};
|
|
62
62
|
}
|
|
63
63
|
finally {
|
|
64
64
|
await collector.cleanup();
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
-
const
|
|
67
|
+
const handleConfigError = (func, exitCode, stderr, log, featureFlags) => {
|
|
68
68
|
switch (exitCode) {
|
|
69
69
|
case ConfigExitCode.ImportError:
|
|
70
|
-
log.user(`Could not load edge function at '${func.path}'`);
|
|
71
70
|
log.user(stderr);
|
|
71
|
+
if (featureFlags.edge_functions_invalid_config_throw) {
|
|
72
|
+
throw new BundleError(new Error(`Could not load edge function at '${func.path}'. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
log.user(`Could not load edge function at '${func.path}'`);
|
|
76
|
+
}
|
|
72
77
|
break;
|
|
73
78
|
case ConfigExitCode.NoConfig:
|
|
74
79
|
log.system(`No in-source config found for edge function at '${func.path}'`);
|
|
75
80
|
break;
|
|
76
81
|
case ConfigExitCode.InvalidExport:
|
|
77
|
-
|
|
82
|
+
if (featureFlags.edge_functions_invalid_config_throw) {
|
|
83
|
+
throw new BundleError(new Error(`The 'config' export in edge function at '${func.path}' must be an object. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
log.user(`'config' export in edge function at '${func.path}' must be an object`);
|
|
87
|
+
}
|
|
78
88
|
break;
|
|
79
89
|
case ConfigExitCode.SerializationError:
|
|
80
|
-
|
|
90
|
+
if (featureFlags.edge_functions_invalid_config_throw) {
|
|
91
|
+
throw new BundleError(new Error(`The 'config' object in the edge function at '${func.path}' must contain primitive values only. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
log.user(`'config' object in edge function at '${func.path}' must contain primitive values only`);
|
|
95
|
+
}
|
|
81
96
|
break;
|
|
82
97
|
case ConfigExitCode.InvalidDefaultExport:
|
|
83
98
|
throw new BundleError(new Error(`Default export in '${func.path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`));
|
package/dist/node/config.test.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, resolve } from 'path';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import { deleteAsync } from 'del';
|
|
5
5
|
import tmp from 'tmp-promise';
|
|
6
|
-
import { test, expect, vi } from 'vitest';
|
|
6
|
+
import { test, expect, vi, describe } from 'vitest';
|
|
7
7
|
import { fixturesDir, useFixture } from '../test/util.js';
|
|
8
8
|
import { DenoBridge } from './bridge.js';
|
|
9
9
|
import { bundle } from './bundler.js';
|
|
@@ -16,102 +16,120 @@ const importMapFile = {
|
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
const invalidDefaultExportErr = (path) => `Default export in '${path}' must be a function. More on the Edge Functions API at https://ntl.fyi/edge-api.`;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Empty config
|
|
32
|
-
{
|
|
33
|
-
expectedConfig: {},
|
|
34
|
-
name: 'func2',
|
|
35
|
-
source: `
|
|
19
|
+
const functions = [
|
|
20
|
+
{
|
|
21
|
+
testName: 'no config',
|
|
22
|
+
expectedConfig: {},
|
|
23
|
+
name: 'func1',
|
|
24
|
+
source: `export default async () => new Response("Hello from function one")`,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
testName: 'empty config',
|
|
28
|
+
expectedConfig: {},
|
|
29
|
+
name: 'func2',
|
|
30
|
+
source: `
|
|
36
31
|
export default async () => new Response("Hello from function two")
|
|
37
32
|
|
|
38
33
|
export const config = {}
|
|
39
34
|
`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
testName: 'config with wrong type (throw)',
|
|
38
|
+
expectedConfig: {},
|
|
39
|
+
name: 'func3',
|
|
40
|
+
source: `
|
|
41
|
+
export default async () => new Response("Hello from function two")
|
|
42
|
+
|
|
43
|
+
export const config = () => ({})
|
|
44
|
+
`,
|
|
45
|
+
error: /^The 'config' export in edge function at '(.*)' must be an object\. More on the Edge Functions API at https:\/\/ntl\.fyi\/edge-api\.$/,
|
|
46
|
+
featureFlags: {
|
|
47
|
+
edge_functions_invalid_config_throw: true,
|
|
40
48
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
testName: 'config with wrong type (log)',
|
|
52
|
+
expectedConfig: {},
|
|
53
|
+
name: 'func3',
|
|
54
|
+
source: `
|
|
46
55
|
export default async () => new Response("Hello from function two")
|
|
47
56
|
|
|
48
57
|
export const config = () => ({})
|
|
49
58
|
`,
|
|
50
|
-
|
|
59
|
+
userLog: /^'config' export in edge function at '(.*)' must be an object$/,
|
|
60
|
+
featureFlags: {
|
|
61
|
+
edge_functions_invalid_config_throw: false,
|
|
51
62
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
testName: 'config with syntax error (throw)',
|
|
66
|
+
expectedConfig: {},
|
|
67
|
+
name: 'func4',
|
|
68
|
+
source: `
|
|
57
69
|
export default async () => new Response("Hello from function two")
|
|
58
70
|
|
|
59
71
|
export const config
|
|
60
72
|
`,
|
|
61
|
-
|
|
73
|
+
error: /^Could not load edge function at '(.*)'\. More on the Edge Functions API at https:\/\/ntl\.fyi\/edge-api\.$/,
|
|
74
|
+
featureFlags: {
|
|
75
|
+
edge_functions_invalid_config_throw: true,
|
|
62
76
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
testName: 'config with syntax error (log)',
|
|
80
|
+
expectedConfig: {},
|
|
81
|
+
name: 'func4',
|
|
82
|
+
source: `
|
|
83
|
+
export default async () => new Response("Hello from function two")
|
|
84
|
+
|
|
85
|
+
export const config
|
|
86
|
+
`,
|
|
87
|
+
userLog: /^Could not load edge function at '(.*)'$/,
|
|
88
|
+
featureFlags: {
|
|
89
|
+
edge_functions_invalid_config_throw: false,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
testName: 'config with `path`',
|
|
94
|
+
expectedConfig: { path: '/home' },
|
|
95
|
+
name: 'func6',
|
|
96
|
+
source: `
|
|
68
97
|
export default async () => new Response("Hello from function three")
|
|
69
98
|
|
|
70
99
|
export const config = { path: "/home" }
|
|
71
100
|
`,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
describe('`getFunctionConfig` extracts configuration properties from function file', () => {
|
|
104
|
+
test.each(functions)('$testName', async (func) => {
|
|
105
|
+
const { path: tmpDir } = await tmp.dir();
|
|
106
|
+
const deno = new DenoBridge({
|
|
107
|
+
cacheDirectory: tmpDir,
|
|
108
|
+
});
|
|
75
109
|
const logger = {
|
|
76
110
|
user: vi.fn().mockResolvedValue(null),
|
|
77
111
|
system: vi.fn().mockResolvedValue(null),
|
|
78
112
|
};
|
|
79
113
|
const path = join(tmpDir, `${func.name}.js`);
|
|
80
114
|
await fs.writeFile(path, func.source);
|
|
81
|
-
const
|
|
115
|
+
const funcCall = () => getFunctionConfig({
|
|
82
116
|
name: func.name,
|
|
83
117
|
path,
|
|
84
|
-
}, new ImportMap([importMapFile]), deno, logger);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
}, new ImportMap([importMapFile]), deno, logger, func.featureFlags || {});
|
|
119
|
+
if (func.error) {
|
|
120
|
+
await expect(funcCall()).rejects.toThrowError(func.error);
|
|
121
|
+
}
|
|
122
|
+
else if (func.userLog) {
|
|
123
|
+
await expect(funcCall()).resolves.not.toThrowError();
|
|
124
|
+
expect(logger.user).toHaveBeenCalledWith(expect.stringMatching(func.userLog));
|
|
88
125
|
}
|
|
89
126
|
else {
|
|
127
|
+
const config = await funcCall();
|
|
128
|
+
expect(config).toEqual(func.expectedConfig);
|
|
90
129
|
expect(logger.user).not.toHaveBeenCalled();
|
|
91
130
|
}
|
|
92
|
-
|
|
93
|
-
await deleteAsync(tmpDir, { force: true });
|
|
94
|
-
});
|
|
95
|
-
test('Ignores function paths from the in-source `config` function if the feature flag is off', async () => {
|
|
96
|
-
const { basePath, cleanup, distPath } = await useFixture('with_config');
|
|
97
|
-
const userDirectory = resolve(basePath, 'netlify', 'edge-functions');
|
|
98
|
-
const internalDirectory = resolve(basePath, '.netlify', 'edge-functions');
|
|
99
|
-
const declarations = [];
|
|
100
|
-
const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
|
|
101
|
-
basePath,
|
|
102
|
-
configPath: join(internalDirectory, 'config.json'),
|
|
131
|
+
await deleteAsync(tmpDir, { force: true });
|
|
103
132
|
});
|
|
104
|
-
const generatedFiles = await fs.readdir(distPath);
|
|
105
|
-
expect(result.functions.length).toBe(7);
|
|
106
|
-
expect(generatedFiles.length).toBe(2);
|
|
107
|
-
const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
|
|
108
|
-
const manifest = JSON.parse(manifestFile);
|
|
109
|
-
const { bundles, routes } = manifest;
|
|
110
|
-
expect(bundles.length).toBe(1);
|
|
111
|
-
expect(bundles[0].format).toBe('eszip2');
|
|
112
|
-
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
113
|
-
expect(routes.length).toBe(0);
|
|
114
|
-
await cleanup();
|
|
115
133
|
});
|
|
116
134
|
test('Loads function paths from the in-source `config` function', async () => {
|
|
117
135
|
const { basePath, cleanup, distPath } = await useFixture('with_config');
|
|
@@ -130,9 +148,6 @@ test('Loads function paths from the in-source `config` function', async () => {
|
|
|
130
148
|
const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
|
|
131
149
|
basePath,
|
|
132
150
|
configPath: join(internalDirectory, 'config.json'),
|
|
133
|
-
featureFlags: {
|
|
134
|
-
edge_functions_config_export: true,
|
|
135
|
-
},
|
|
136
151
|
});
|
|
137
152
|
const generatedFiles = await fs.readdir(distPath);
|
|
138
153
|
expect(result.functions.length).toBe(7);
|
|
@@ -176,12 +191,10 @@ test('Passes validation if default export exists and is a function', async () =>
|
|
|
176
191
|
};
|
|
177
192
|
const path = join(tmpDir, `${func.name}.ts`);
|
|
178
193
|
await fs.writeFile(path, func.source);
|
|
179
|
-
expect(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}, new ImportMap([importMapFile]), deno, logger);
|
|
184
|
-
}).not.toThrow();
|
|
194
|
+
await expect(getFunctionConfig({
|
|
195
|
+
name: func.name,
|
|
196
|
+
path,
|
|
197
|
+
}, new ImportMap([importMapFile]), deno, logger, {})).resolves.not.toThrow();
|
|
185
198
|
await deleteAsync(tmpDir, { force: true });
|
|
186
199
|
});
|
|
187
200
|
test('Fails validation if default export is not function', async () => {
|
|
@@ -205,7 +218,7 @@ test('Fails validation if default export is not function', async () => {
|
|
|
205
218
|
const config = getFunctionConfig({
|
|
206
219
|
name: func.name,
|
|
207
220
|
path,
|
|
208
|
-
}, new ImportMap([importMapFile]), deno, logger);
|
|
221
|
+
}, new ImportMap([importMapFile]), deno, logger, {});
|
|
209
222
|
await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
|
|
210
223
|
await deleteAsync(tmpDir, { force: true });
|
|
211
224
|
});
|
|
@@ -229,7 +242,7 @@ test('Fails validation if default export is not present', async () => {
|
|
|
229
242
|
const config = getFunctionConfig({
|
|
230
243
|
name: func.name,
|
|
231
244
|
path,
|
|
232
|
-
}, new ImportMap([importMapFile]), deno, logger);
|
|
245
|
+
}, new ImportMap([importMapFile]), deno, logger, {});
|
|
233
246
|
await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
|
|
234
247
|
await deleteAsync(tmpDir, { force: true });
|
|
235
248
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rm } from 'fs/promises';
|
|
2
2
|
import { platform } from 'process';
|
|
3
3
|
import { PassThrough } from 'stream';
|
|
4
4
|
import { execa } from 'execa';
|
|
@@ -20,7 +20,7 @@ beforeEach(async (ctx) => {
|
|
|
20
20
|
ctx.tmpDir = tmpDir.path;
|
|
21
21
|
});
|
|
22
22
|
afterEach(async (ctx) => {
|
|
23
|
-
await
|
|
23
|
+
await rm(ctx.tmpDir, { force: true, recursive: true });
|
|
24
24
|
});
|
|
25
25
|
test('tries downloading binary up to 4 times', async (ctx) => {
|
|
26
26
|
nock.disableNetConnect();
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
declare const defaultFlags:
|
|
1
|
+
declare const defaultFlags: {
|
|
2
|
+
edge_functions_fail_unsupported_regex: boolean;
|
|
3
|
+
edge_functions_invalid_config_throw: boolean;
|
|
4
|
+
};
|
|
2
5
|
type FeatureFlag = keyof typeof defaultFlags;
|
|
3
|
-
type FeatureFlags = Record<FeatureFlag, boolean
|
|
4
|
-
declare const getFlags: (input?: Record<string, boolean>, flags?:
|
|
6
|
+
type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
|
|
7
|
+
declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
8
|
+
edge_functions_fail_unsupported_regex: boolean;
|
|
9
|
+
edge_functions_invalid_config_throw: boolean;
|
|
10
|
+
}) => FeatureFlags;
|
|
5
11
|
export { defaultFlags, getFlags };
|
|
6
12
|
export type { FeatureFlag, FeatureFlags };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const defaultFlags = {
|
|
2
|
-
edge_functions_cache_deno_dir: false,
|
|
3
|
-
edge_functions_config_export: false,
|
|
4
2
|
edge_functions_fail_unsupported_regex: false,
|
|
3
|
+
edge_functions_invalid_config_throw: false,
|
|
5
4
|
};
|
|
6
5
|
const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
|
|
7
6
|
...result,
|
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { env } from 'process';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
5
|
import { deleteAsync } from 'del';
|
|
6
|
-
const BOOTSTRAP_LATEST = 'https://
|
|
6
|
+
const BOOTSTRAP_LATEST = 'https://64071f3033a1800007cc20f8--edge.netlify.com/bootstrap/index-combined.ts';
|
|
7
7
|
const defaultFormatExportTypeError = (name) => `The Edge Function "${name}" has failed to load. Does it have a function as the default export?`;
|
|
8
8
|
const defaultFormatImpoortError = (name) => `There was an error with Edge Function "${name}".`;
|
|
9
9
|
const generateStage2 = async ({ distDirectory, fileName, formatExportTypeError, formatImportError, functions, }) => {
|
package/dist/node/main.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
|
-
import
|
|
2
|
+
import { rm } from 'fs/promises';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { platform } from 'process';
|
|
5
5
|
import { PassThrough } from 'stream';
|
|
@@ -43,5 +43,5 @@ test('Downloads the Deno CLI on demand and caches it for subsequent calls', asyn
|
|
|
43
43
|
expect((_d = output2 === null || output2 === void 0 ? void 0 : output2.stdout) !== null && _d !== void 0 ? _d : '').toMatch(expectedOutput);
|
|
44
44
|
expect(beforeDownload).toHaveBeenCalledTimes(1);
|
|
45
45
|
expect(afterDownload).toHaveBeenCalledTimes(1);
|
|
46
|
-
await
|
|
46
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
47
47
|
});
|
|
@@ -42,7 +42,7 @@ const prepareServer = ({ deno, distDirectory, flags: denoFlags, formatExportType
|
|
|
42
42
|
});
|
|
43
43
|
let functionsConfig = [];
|
|
44
44
|
if (options.getFunctionsConfig) {
|
|
45
|
-
functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger)));
|
|
45
|
+
functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger, {})));
|
|
46
46
|
}
|
|
47
47
|
const success = await waitForServer(port, processRef.ps);
|
|
48
48
|
return {
|
package/dist/node/types.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile, rm, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import nock from 'nock';
|
|
4
4
|
import tmp from 'tmp-promise';
|
|
@@ -18,20 +18,20 @@ test('`ensureLatestTypes` updates the Deno CLI cache if the local version of typ
|
|
|
18
18
|
// @ts-expect-error return value not used
|
|
19
19
|
const mock = vi.spyOn(deno, 'run').mockResolvedValue({});
|
|
20
20
|
await ensureLatestTypes(deno, testLogger, mockURL);
|
|
21
|
-
const versionFile = await
|
|
21
|
+
const versionFile = await readFile(join(tmpDir.path, 'types-version.txt'), 'utf8');
|
|
22
22
|
expect(latestVersionMock.isDone()).toBe(true);
|
|
23
23
|
expect(mock).toHaveBeenCalledTimes(1);
|
|
24
24
|
expect(mock).toHaveBeenCalledWith(['cache', '-r', mockURL]);
|
|
25
25
|
expect(versionFile).toBe(mockVersion);
|
|
26
26
|
mock.mockRestore();
|
|
27
|
-
await
|
|
27
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
28
28
|
});
|
|
29
29
|
test('`ensureLatestTypes` does not update the Deno CLI cache if the local version of types is up-to-date', async () => {
|
|
30
30
|
const mockURL = 'https://edge.netlify';
|
|
31
31
|
const mockVersion = '987654321';
|
|
32
32
|
const tmpDir = await tmp.dir();
|
|
33
33
|
const versionFilePath = join(tmpDir.path, 'types-version.txt');
|
|
34
|
-
await
|
|
34
|
+
await writeFile(versionFilePath, mockVersion);
|
|
35
35
|
const latestVersionMock = nock(mockURL).get('/version.txt').reply(200, mockVersion);
|
|
36
36
|
const deno = new DenoBridge({
|
|
37
37
|
cacheDirectory: tmpDir.path,
|
|
@@ -43,7 +43,7 @@ test('`ensureLatestTypes` does not update the Deno CLI cache if the local versio
|
|
|
43
43
|
expect(latestVersionMock.isDone()).toBe(true);
|
|
44
44
|
expect(mock).not.toHaveBeenCalled();
|
|
45
45
|
mock.mockRestore();
|
|
46
|
-
await
|
|
46
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
47
47
|
});
|
|
48
48
|
test('`ensureLatestTypes` does not throw if the types URL is not available', async () => {
|
|
49
49
|
const mockURL = 'https://edge.netlify';
|
|
@@ -59,5 +59,5 @@ test('`ensureLatestTypes` does not throw if the types URL is not available', asy
|
|
|
59
59
|
expect(latestVersionMock.isDone()).toBe(true);
|
|
60
60
|
expect(mock).not.toHaveBeenCalled();
|
|
61
61
|
mock.mockRestore();
|
|
62
|
-
await
|
|
62
|
+
await rm(tmpDir.path, { force: true, recursive: true });
|
|
63
63
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { test, expect, describe } from 'vitest';
|
|
2
3
|
import { validateManifest, ManifestValidationError } from './index.js';
|
|
4
|
+
// We need to disable all color outputs for the tests as they are different on different platforms, CI, etc.
|
|
5
|
+
// This only works if this is the same instance of chalk that better-ajv-errors uses
|
|
6
|
+
chalk.level = 0;
|
|
3
7
|
// Factory so we have a new object per test
|
|
4
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
9
|
const getBaseManifest = () => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.10.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"build:dev": "tsc -w",
|
|
17
18
|
"prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/",
|
|
18
19
|
"prepublishOnly": "npm ci && npm test",
|
|
19
20
|
"prepack": "npm run build",
|
|
@@ -28,10 +29,10 @@
|
|
|
28
29
|
"format:fix:prettier": "cross-env-shell prettier --write $npm_package_config_prettier",
|
|
29
30
|
"test:dev": "run-s test:dev:*",
|
|
30
31
|
"test:ci": "run-s test:ci:*",
|
|
31
|
-
"test:dev:vitest": "
|
|
32
|
-
"test:dev:vitest:watch": "
|
|
32
|
+
"test:dev:vitest": "vitest run",
|
|
33
|
+
"test:dev:vitest:watch": "vitest watch",
|
|
33
34
|
"test:dev:deno": "deno test --allow-all deno",
|
|
34
|
-
"test:ci:vitest": "
|
|
35
|
+
"test:ci:vitest": "vitest run",
|
|
35
36
|
"test:ci:deno": "deno test --allow-all deno",
|
|
36
37
|
"test:integration": "node --experimental-modules test/integration/test.js"
|
|
37
38
|
},
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@commitlint/cli": "^17.0.0",
|
|
54
55
|
"@commitlint/config-conventional": "^17.0.0",
|
|
55
|
-
"@netlify/eslint-config-node": "^
|
|
56
|
+
"@netlify/eslint-config-node": "^7.0.1",
|
|
56
57
|
"@types/glob-to-regexp": "^0.4.1",
|
|
57
58
|
"@types/node": "^14.18.32",
|
|
58
59
|
"@types/semver": "^7.3.9",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"@types/uuid": "^9.0.0",
|
|
61
62
|
"@vitest/coverage-c8": "^0.25.0",
|
|
62
63
|
"archiver": "^5.3.1",
|
|
64
|
+
"chalk": "^4.1.2",
|
|
63
65
|
"cpy": "^9.0.1",
|
|
64
66
|
"cross-env": "^7.0.3",
|
|
65
67
|
"husky": "^8.0.0",
|
|
@@ -84,6 +86,7 @@
|
|
|
84
86
|
"find-up": "^6.3.0",
|
|
85
87
|
"get-port": "^6.1.2",
|
|
86
88
|
"glob-to-regexp": "^0.4.1",
|
|
89
|
+
"is-path-inside": "^4.0.0",
|
|
87
90
|
"jsonc-parser": "^3.2.0",
|
|
88
91
|
"node-fetch": "^3.1.1",
|
|
89
92
|
"node-stream-zip": "^1.15.0",
|