@netlify/edge-bundler 8.12.3 → 8.13.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/bundler.d.ts +1 -17
- package/dist/node/bundler.js +38 -20
- package/dist/node/bundler.test.js +13 -7
- package/dist/node/config.d.ts +3 -1
- package/dist/node/config.test.js +19 -0
- package/dist/node/declaration.d.ts +1 -1
- package/dist/node/declaration.js +9 -5
- package/dist/node/declaration.test.js +28 -12
- package/dist/node/downloader.test.js +11 -1
- package/dist/node/manifest.d.ts +5 -4
- package/dist/node/manifest.js +27 -13
- package/dist/node/manifest.test.js +77 -35
- package/package.json +3 -3
package/dist/node/bundler.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js';
|
|
2
2
|
import { Declaration } from './declaration.js';
|
|
3
|
-
import { EdgeFunction } from './edge_function.js';
|
|
4
3
|
import { FeatureFlags } from './feature_flags.js';
|
|
5
4
|
import { LogFunction } from './logger.js';
|
|
6
5
|
interface BundleOptions {
|
|
@@ -17,23 +16,8 @@ interface BundleOptions {
|
|
|
17
16
|
internalSrcFolder?: string;
|
|
18
17
|
}
|
|
19
18
|
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
|
+
functions: import("./edge_function.js").EdgeFunction[];
|
|
21
20
|
manifest: import("./manifest.js").Manifest;
|
|
22
21
|
}>;
|
|
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
|
-
};
|
|
38
22
|
export { bundle };
|
|
39
23
|
export type { BundleOptions };
|
package/dist/node/bundler.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'path';
|
|
3
3
|
import commonPathPrefix from 'common-path-prefix';
|
|
4
|
-
import isPathInside from 'is-path-inside';
|
|
5
4
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
5
|
import { importMapSpecifier } from '../shared/consts.js';
|
|
7
6
|
import { DenoBridge } from './bridge.js';
|
|
@@ -25,7 +24,6 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
25
24
|
onAfterDownload,
|
|
26
25
|
onBeforeDownload,
|
|
27
26
|
};
|
|
28
|
-
const internalFunctionsPath = internalSrcFolder && resolve(internalSrcFolder);
|
|
29
27
|
if (cacheDirectory !== undefined) {
|
|
30
28
|
options.denoDir = join(cacheDirectory, 'deno_dir');
|
|
31
29
|
}
|
|
@@ -42,9 +40,12 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
42
40
|
// Layers are marked as externals in the ESZIP, so that those specifiers are
|
|
43
41
|
// not actually included in the bundle.
|
|
44
42
|
const externals = deployConfig.layers.map((layer) => layer.name);
|
|
43
|
+
const userSourceDirectories = sourceDirectories.filter((dir) => dir !== internalSrcFolder);
|
|
45
44
|
const importMap = new ImportMap();
|
|
46
45
|
await importMap.addFiles([deployConfig === null || deployConfig === void 0 ? void 0 : deployConfig.importMap, ...importMapPaths], logger);
|
|
47
|
-
const
|
|
46
|
+
const userFunctions = userSourceDirectories.length === 0 ? [] : await findFunctions(userSourceDirectories);
|
|
47
|
+
const internalFunctions = internalSrcFolder ? await findFunctions([internalSrcFolder]) : [];
|
|
48
|
+
const functions = [...internalFunctions, ...userFunctions];
|
|
48
49
|
const functionBundle = await bundleESZIP({
|
|
49
50
|
basePath,
|
|
50
51
|
buildID,
|
|
@@ -61,24 +62,27 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
61
62
|
// rename the bundles to their permanent names.
|
|
62
63
|
await createFinalBundles([functionBundle], distDirectory, buildID);
|
|
63
64
|
// Retrieving a configuration object for each function.
|
|
64
|
-
|
|
65
|
+
// Run `getFunctionConfig` in parallel as it is a non-trivial operation and spins up deno
|
|
66
|
+
const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger, featureFlags)]);
|
|
67
|
+
const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger, featureFlags)]);
|
|
65
68
|
// Creating a hash of function names to configuration objects.
|
|
66
|
-
const
|
|
69
|
+
const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
|
|
70
|
+
const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
|
|
67
71
|
// Creating a final declarations array by combining the TOML file with the
|
|
68
72
|
// deploy configuration API and the in-source configuration.
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
: declarationsFromConfig;
|
|
73
|
+
const declarations = mergeDeclarations(tomlDeclarations, userFunctionsWithConfig, internalFunctionsWithConfig, deployConfig.declarations);
|
|
74
|
+
const internalFunctionConfig = createFunctionConfig({
|
|
75
|
+
internalFunctionsWithConfig,
|
|
76
|
+
declarations,
|
|
77
|
+
});
|
|
75
78
|
const manifest = await writeManifest({
|
|
76
79
|
bundles: [functionBundle],
|
|
77
80
|
declarations,
|
|
78
81
|
distDirectory,
|
|
79
82
|
featureFlags,
|
|
80
83
|
functions,
|
|
81
|
-
|
|
84
|
+
userFunctionConfig: userFunctionsWithConfig,
|
|
85
|
+
internalFunctionConfig,
|
|
82
86
|
importMap: importMapSpecifier,
|
|
83
87
|
layers: deployConfig.layers,
|
|
84
88
|
});
|
|
@@ -107,12 +111,26 @@ const getBasePath = (sourceDirectories, inputBasePath) => {
|
|
|
107
111
|
}
|
|
108
112
|
return commonPathPrefix(sourceDirectories);
|
|
109
113
|
};
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// We used to allow the `name` and `generator` fields to be defined at the
|
|
115
|
+
// declaration level. We want these properties to live at the function level
|
|
116
|
+
// in their config object, so we translate that for backwards-compatibility.
|
|
117
|
+
const mergeWithDeclarationConfig = ({ functionName, config, declarations }) => {
|
|
118
|
+
const declaration = declarations === null || declarations === void 0 ? void 0 : declarations.find((decl) => decl.function === functionName);
|
|
119
|
+
return {
|
|
120
|
+
...config,
|
|
121
|
+
name: (declaration === null || declaration === void 0 ? void 0 : declaration.name) || config.name,
|
|
122
|
+
generator: (declaration === null || declaration === void 0 ? void 0 : declaration.generator) || config.generator,
|
|
123
|
+
};
|
|
117
124
|
};
|
|
125
|
+
const addGeneratorFallback = (config) => ({
|
|
126
|
+
...config,
|
|
127
|
+
generator: config.generator || 'internalFunc',
|
|
128
|
+
});
|
|
129
|
+
const createFunctionConfig = ({ internalFunctionsWithConfig, declarations }) => Object.entries(internalFunctionsWithConfig).reduce((acc, [functionName, config]) => {
|
|
130
|
+
const mergedConfigFields = mergeWithDeclarationConfig({ functionName, config, declarations });
|
|
131
|
+
return {
|
|
132
|
+
...acc,
|
|
133
|
+
[functionName]: addGeneratorFallback(mergedConfigFields),
|
|
134
|
+
};
|
|
135
|
+
}, {});
|
|
118
136
|
export { bundle };
|
|
@@ -264,7 +264,7 @@ test('Processes a function that imports a custom layer', async () => {
|
|
|
264
264
|
expect(layers).toEqual([layer]);
|
|
265
265
|
await cleanup();
|
|
266
266
|
});
|
|
267
|
-
test('Loads declarations and import maps from the deploy configuration', async () => {
|
|
267
|
+
test('Loads declarations and import maps from the deploy configuration and in-source config', async () => {
|
|
268
268
|
const { basePath, cleanup, distPath } = await useFixture('with_deploy_config');
|
|
269
269
|
const declarations = [
|
|
270
270
|
{
|
|
@@ -283,16 +283,22 @@ test('Loads declarations and import maps from the deploy configuration', async (
|
|
|
283
283
|
expect(generatedFiles.length).toBe(2);
|
|
284
284
|
const manifestFile = await fs.readFile(resolve(distPath, 'manifest.json'), 'utf8');
|
|
285
285
|
const manifest = JSON.parse(manifestFile);
|
|
286
|
-
const {
|
|
286
|
+
const { bundles, function_config: functionConfig } = manifest;
|
|
287
287
|
expect(bundles.length).toBe(1);
|
|
288
288
|
expect(bundles[0].format).toBe('eszip2');
|
|
289
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');
|
|
294
290
|
// respects excludedPath from deploy config
|
|
295
|
-
expect(functionConfig.func2).toEqual({
|
|
291
|
+
expect(functionConfig.func2).toEqual({
|
|
292
|
+
excluded_patterns: ['^/func2/skip/?$'],
|
|
293
|
+
name: 'Function two',
|
|
294
|
+
generator: '@netlify/fake-plugin@1.0.0',
|
|
295
|
+
});
|
|
296
|
+
// respects in-source config
|
|
297
|
+
expect(functionConfig.func3).toEqual({
|
|
298
|
+
name: 'in-config-function',
|
|
299
|
+
on_error: 'bypass',
|
|
300
|
+
generator: 'internalFunc',
|
|
301
|
+
});
|
|
296
302
|
await cleanup();
|
|
297
303
|
});
|
|
298
304
|
test("Ignores entries in `importMapPaths` that don't point to an existing import map file", async () => {
|
package/dist/node/config.d.ts
CHANGED
|
@@ -8,12 +8,14 @@ export declare const enum Cache {
|
|
|
8
8
|
Manual = "manual"
|
|
9
9
|
}
|
|
10
10
|
export type Path = `/${string}`;
|
|
11
|
-
export type OnError = 'fail' | 'bypass' |
|
|
11
|
+
export type OnError = 'fail' | 'bypass' | Path;
|
|
12
12
|
export declare const isValidOnError: (value: unknown) => value is OnError;
|
|
13
13
|
export interface FunctionConfig {
|
|
14
14
|
cache?: Cache;
|
|
15
15
|
path?: Path | Path[];
|
|
16
16
|
excludedPath?: Path | Path[];
|
|
17
17
|
onError?: OnError;
|
|
18
|
+
name?: string;
|
|
19
|
+
generator?: string;
|
|
18
20
|
}
|
|
19
21
|
export declare const getFunctionConfig: (func: EdgeFunction, importMap: ImportMap, deno: DenoBridge, log: Logger, featureFlags: FeatureFlags) => Promise<FunctionConfig>;
|
package/dist/node/config.test.js
CHANGED
|
@@ -117,6 +117,25 @@ const functions = [
|
|
|
117
117
|
export const config = { path: "/home" }
|
|
118
118
|
`,
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
testName: 'config with path, generator, name and onError`',
|
|
122
|
+
expectedConfig: {
|
|
123
|
+
path: '/home',
|
|
124
|
+
generator: '@netlify/fake-plugin@1.0.0',
|
|
125
|
+
name: 'a displayName',
|
|
126
|
+
onError: 'bypass',
|
|
127
|
+
},
|
|
128
|
+
name: 'func6',
|
|
129
|
+
source: `
|
|
130
|
+
export default async () => new Response("Hello from function three")
|
|
131
|
+
|
|
132
|
+
export const config = { path: "/home",
|
|
133
|
+
generator: '@netlify/fake-plugin@1.0.0',
|
|
134
|
+
name: 'a displayName',
|
|
135
|
+
onError: 'bypass',
|
|
136
|
+
}
|
|
137
|
+
`,
|
|
138
|
+
},
|
|
120
139
|
];
|
|
121
140
|
describe('`getFunctionConfig` extracts configuration properties from function file', () => {
|
|
122
141
|
test.each(functions)('$testName', async (func) => {
|
|
@@ -14,6 +14,6 @@ type DeclarationWithPattern = BaseDeclaration & {
|
|
|
14
14
|
excludedPattern?: string;
|
|
15
15
|
};
|
|
16
16
|
export type Declaration = DeclarationWithPath | DeclarationWithPattern;
|
|
17
|
-
export declare const mergeDeclarations: (tomlDeclarations: Declaration[],
|
|
17
|
+
export declare const mergeDeclarations: (tomlDeclarations: Declaration[], userFunctionsConfig: Record<string, FunctionConfig>, internalFunctionsConfig: Record<string, FunctionConfig>, deployConfigDeclarations: Declaration[]) => Declaration[];
|
|
18
18
|
export declare const parsePattern: (pattern: string) => string;
|
|
19
19
|
export {};
|
package/dist/node/declaration.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import regexpAST from 'regexp-tree';
|
|
2
|
-
export const mergeDeclarations = (tomlDeclarations,
|
|
2
|
+
export const mergeDeclarations = (tomlDeclarations, userFunctionsConfig, internalFunctionsConfig, deployConfigDeclarations) => {
|
|
3
3
|
var _a;
|
|
4
4
|
const declarations = [];
|
|
5
5
|
const functionsVisited = new Set();
|
|
@@ -8,7 +8,7 @@ export const mergeDeclarations = (tomlDeclarations, functionsConfig, deployConfi
|
|
|
8
8
|
// a function configuration object, we replace the path because that object
|
|
9
9
|
// takes precedence.
|
|
10
10
|
for (const declaration of [...tomlDeclarations, ...deployConfigDeclarations]) {
|
|
11
|
-
const config =
|
|
11
|
+
const config = userFunctionsConfig[declaration.function] || internalFunctionsConfig[declaration.function];
|
|
12
12
|
if (!config) {
|
|
13
13
|
// If no config is found, add the declaration as is.
|
|
14
14
|
declarations.push(declaration);
|
|
@@ -30,13 +30,17 @@ export const mergeDeclarations = (tomlDeclarations, functionsConfig, deployConfi
|
|
|
30
30
|
}
|
|
31
31
|
// Finally, we must create declarations for functions that are not declared
|
|
32
32
|
// in the TOML at all.
|
|
33
|
-
for (const name in
|
|
34
|
-
const { cache, path } =
|
|
33
|
+
for (const name in { ...internalFunctionsConfig, ...userFunctionsConfig }) {
|
|
34
|
+
const { cache, path } = internalFunctionsConfig[name] || userFunctionsConfig[name];
|
|
35
35
|
// If we have a path specified, create a declaration for each path.
|
|
36
36
|
if (!functionsVisited.has(name) && path) {
|
|
37
37
|
const paths = Array.isArray(path) ? path : [path];
|
|
38
38
|
paths.forEach((singlePath) => {
|
|
39
|
-
|
|
39
|
+
const declaration = { function: name, path: singlePath };
|
|
40
|
+
if (cache) {
|
|
41
|
+
declaration.cache = cache;
|
|
42
|
+
}
|
|
43
|
+
declarations.push(declaration);
|
|
40
44
|
});
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { test, expect } from 'vitest';
|
|
2
2
|
import { mergeDeclarations } from './declaration.js';
|
|
3
|
-
// TODO: Add tests with the deploy config.
|
|
4
3
|
const deployConfigDeclarations = [];
|
|
4
|
+
test('Deploy config takes precedence over user config', () => {
|
|
5
|
+
const deployConfigDeclarations = [
|
|
6
|
+
{ function: 'framework-a', path: '/path1' },
|
|
7
|
+
{ function: 'framework-b', path: '/path2' },
|
|
8
|
+
];
|
|
9
|
+
const tomlConfig = [
|
|
10
|
+
{ function: 'user-a', path: '/path1' },
|
|
11
|
+
{ function: 'user-b', path: '/path2' },
|
|
12
|
+
];
|
|
13
|
+
const userFuncConfig = {
|
|
14
|
+
'user-c': { path: ['/path1', '/path2'] },
|
|
15
|
+
};
|
|
16
|
+
const internalFuncConfig = {
|
|
17
|
+
'framework-c': { path: ['/path1', '/path2'] },
|
|
18
|
+
};
|
|
19
|
+
expect(mergeDeclarations(tomlConfig, userFuncConfig, internalFuncConfig, deployConfigDeclarations)).toMatchSnapshot();
|
|
20
|
+
});
|
|
5
21
|
test('In-source config takes precedence over netlify.toml config', () => {
|
|
6
22
|
const tomlConfig = [
|
|
7
23
|
{ function: 'geolocation', path: '/geo', cache: 'off' },
|
|
8
24
|
{ function: 'json', path: '/json', cache: 'manual' },
|
|
9
25
|
];
|
|
10
|
-
const
|
|
26
|
+
const userFuncConfig = {
|
|
11
27
|
geolocation: { path: ['/geo-isc', '/*'], cache: 'manual' },
|
|
12
28
|
json: { path: '/json', cache: 'off' },
|
|
13
29
|
};
|
|
@@ -16,7 +32,7 @@ test('In-source config takes precedence over netlify.toml config', () => {
|
|
|
16
32
|
{ function: 'geolocation', path: '/*', cache: 'manual' },
|
|
17
33
|
{ function: 'json', path: '/json', cache: 'off' },
|
|
18
34
|
];
|
|
19
|
-
const declarations = mergeDeclarations(tomlConfig,
|
|
35
|
+
const declarations = mergeDeclarations(tomlConfig, userFuncConfig, {}, deployConfigDeclarations);
|
|
20
36
|
expect(declarations).toEqual(expectedDeclarations);
|
|
21
37
|
});
|
|
22
38
|
test("Declarations don't break if no in-source config is provided", () => {
|
|
@@ -24,7 +40,7 @@ test("Declarations don't break if no in-source config is provided", () => {
|
|
|
24
40
|
{ function: 'geolocation', path: '/geo', cache: 'off' },
|
|
25
41
|
{ function: 'json', path: '/json', cache: 'manual' },
|
|
26
42
|
];
|
|
27
|
-
const
|
|
43
|
+
const userFuncConfig = {
|
|
28
44
|
geolocation: { path: ['/geo-isc'], cache: 'manual' },
|
|
29
45
|
json: {},
|
|
30
46
|
};
|
|
@@ -32,7 +48,7 @@ test("Declarations don't break if no in-source config is provided", () => {
|
|
|
32
48
|
{ function: 'geolocation', path: '/geo-isc', cache: 'manual' },
|
|
33
49
|
{ function: 'json', path: '/json', cache: 'manual' },
|
|
34
50
|
];
|
|
35
|
-
const declarations = mergeDeclarations(tomlConfig,
|
|
51
|
+
const declarations = mergeDeclarations(tomlConfig, userFuncConfig, {}, deployConfigDeclarations);
|
|
36
52
|
expect(declarations).toEqual(expectedDeclarations);
|
|
37
53
|
});
|
|
38
54
|
test('In-source config works independent of the netlify.toml file if a path is defined and otherwise if no path is set', () => {
|
|
@@ -49,9 +65,9 @@ test('In-source config works independent of the netlify.toml file if a path is d
|
|
|
49
65
|
{ function: 'json', path: '/json-isc', cache: 'off' },
|
|
50
66
|
];
|
|
51
67
|
const expectedDeclarationsWithoutISCPath = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
|
|
52
|
-
const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, deployConfigDeclarations);
|
|
68
|
+
const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, {}, deployConfigDeclarations);
|
|
53
69
|
expect(declarationsWithISCPath).toEqual(expectedDeclarationsWithISCPath);
|
|
54
|
-
const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, deployConfigDeclarations);
|
|
70
|
+
const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, {}, deployConfigDeclarations);
|
|
55
71
|
expect(declarationsWithoutISCPath).toEqual(expectedDeclarationsWithoutISCPath);
|
|
56
72
|
});
|
|
57
73
|
test('In-source config works if only the cache config property is set', () => {
|
|
@@ -60,7 +76,7 @@ test('In-source config works if only the cache config property is set', () => {
|
|
|
60
76
|
geolocation: { cache: 'manual' },
|
|
61
77
|
};
|
|
62
78
|
const expectedDeclarations = [{ function: 'geolocation', path: '/geo', cache: 'manual' }];
|
|
63
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
79
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
64
80
|
});
|
|
65
81
|
test("In-source config path property works if it's not an array", () => {
|
|
66
82
|
const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
|
|
@@ -68,7 +84,7 @@ test("In-source config path property works if it's not an array", () => {
|
|
|
68
84
|
json: { path: '/json', cache: 'manual' },
|
|
69
85
|
};
|
|
70
86
|
const expectedDeclarations = [{ function: 'json', path: '/json', cache: 'manual' }];
|
|
71
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
87
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
72
88
|
});
|
|
73
89
|
test("In-source config path property works if it's not an array and it's not present in toml or deploy config", () => {
|
|
74
90
|
const tomlConfig = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
|
|
@@ -79,7 +95,7 @@ test("In-source config path property works if it's not an array and it's not pre
|
|
|
79
95
|
{ function: 'geolocation', path: '/geo', cache: 'off' },
|
|
80
96
|
{ function: 'json', path: '/json-isc', cache: 'manual' },
|
|
81
97
|
];
|
|
82
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
98
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
83
99
|
});
|
|
84
100
|
test('In-source config works if path property is an empty array with cache value specified', () => {
|
|
85
101
|
const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
|
|
@@ -87,12 +103,12 @@ test('In-source config works if path property is an empty array with cache value
|
|
|
87
103
|
json: { path: [], cache: 'manual' },
|
|
88
104
|
};
|
|
89
105
|
const expectedDeclarations = [{ function: 'json', path: '/json-toml', cache: 'manual' }];
|
|
90
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
106
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
91
107
|
});
|
|
92
108
|
test('netlify.toml-defined excludedPath are respected', () => {
|
|
93
109
|
const tomlConfig = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
|
|
94
110
|
const funcConfig = {};
|
|
95
111
|
const expectedDeclarations = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
|
|
96
|
-
const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations);
|
|
112
|
+
const declarations = mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations);
|
|
97
113
|
expect(declarations).toEqual(expectedDeclarations);
|
|
98
114
|
});
|
|
@@ -4,10 +4,20 @@ import { PassThrough } from 'stream';
|
|
|
4
4
|
import { execa } from 'execa';
|
|
5
5
|
import nock from 'nock';
|
|
6
6
|
import tmp from 'tmp-promise';
|
|
7
|
-
import { beforeEach, afterEach, test, expect } from 'vitest';
|
|
7
|
+
import { beforeEach, afterEach, test, expect, vi } from 'vitest';
|
|
8
8
|
import { fixturesDir, testLogger } from '../test/util.js';
|
|
9
9
|
import { download } from './downloader.js';
|
|
10
10
|
import { getPlatformTarget } from './platform.js';
|
|
11
|
+
// This changes the defaults for p-retry
|
|
12
|
+
// minTimeout 1000 -> 10
|
|
13
|
+
// factor 2 -> 1
|
|
14
|
+
// This reduces the wait time in the tests from `2s, 4s, 8s` to `10ms, 10ms, 10ms` for 3 retries
|
|
15
|
+
vi.mock('p-retry', async (importOriginal) => {
|
|
16
|
+
const pRetry = (await importOriginal());
|
|
17
|
+
return {
|
|
18
|
+
default: (func, options) => pRetry.default(func, { minTimeout: 10, factor: 1, ...options }),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
11
21
|
const streamError = () => {
|
|
12
22
|
const stream = new PassThrough();
|
|
13
23
|
setTimeout(() => stream.emit('data', 'zipcontent'), 100);
|
package/dist/node/manifest.d.ts
CHANGED
|
@@ -6,13 +6,13 @@ import { FeatureFlags } from './feature_flags.js';
|
|
|
6
6
|
import { Layer } from './layer.js';
|
|
7
7
|
interface Route {
|
|
8
8
|
function: string;
|
|
9
|
-
name?: string;
|
|
10
9
|
pattern: string;
|
|
11
|
-
generator?: string;
|
|
12
10
|
}
|
|
13
11
|
interface EdgeFunctionConfig {
|
|
14
12
|
excluded_patterns: string[];
|
|
15
13
|
on_error?: string;
|
|
14
|
+
generator?: string;
|
|
15
|
+
name?: string;
|
|
16
16
|
}
|
|
17
17
|
interface Manifest {
|
|
18
18
|
bundler_version: string;
|
|
@@ -34,11 +34,12 @@ interface GenerateManifestOptions {
|
|
|
34
34
|
declarations?: Declaration[];
|
|
35
35
|
featureFlags?: FeatureFlags;
|
|
36
36
|
functions: EdgeFunction[];
|
|
37
|
-
functionConfig?: Record<string, FunctionConfig>;
|
|
38
37
|
importMap?: string;
|
|
38
|
+
internalFunctionConfig?: Record<string, FunctionConfig>;
|
|
39
39
|
layers?: Layer[];
|
|
40
|
+
userFunctionConfig?: Record<string, FunctionConfig>;
|
|
40
41
|
}
|
|
41
|
-
declare const generateManifest: ({ bundles, declarations, featureFlags, functions,
|
|
42
|
+
declare const generateManifest: ({ bundles, declarations, featureFlags, functions, userFunctionConfig, internalFunctionConfig, importMap, layers, }: GenerateManifestOptions) => Manifest;
|
|
42
43
|
interface WriteManifestOptions extends GenerateManifestOptions {
|
|
43
44
|
distDirectory: string;
|
|
44
45
|
}
|
package/dist/node/manifest.js
CHANGED
|
@@ -4,6 +4,12 @@ import globToRegExp from 'glob-to-regexp';
|
|
|
4
4
|
import { parsePattern } from './declaration.js';
|
|
5
5
|
import { getPackageVersion } from './package_json.js';
|
|
6
6
|
import { nonNullable } from './utils/non_nullable.js';
|
|
7
|
+
const removeEmptyConfigValues = (functionConfig) => Object.entries(functionConfig).reduce((acc, [key, value]) => {
|
|
8
|
+
if (value && !(Array.isArray(value) && value.length === 0)) {
|
|
9
|
+
return { ...acc, [key]: value };
|
|
10
|
+
}
|
|
11
|
+
return acc;
|
|
12
|
+
}, {});
|
|
7
13
|
// JavaScript regular expressions are converted to strings with leading and
|
|
8
14
|
// trailing slashes, so any slashes inside the expression itself are escaped
|
|
9
15
|
// as `//`. This function deserializes that back into a single slash, which
|
|
@@ -12,29 +18,39 @@ const serializePattern = (pattern) => pattern.replace(/\\\//g, '/');
|
|
|
12
18
|
const sanitizeEdgeFunctionConfig = (config) => {
|
|
13
19
|
const newConfig = {};
|
|
14
20
|
for (const [name, functionConfig] of Object.entries(config)) {
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
const newFunctionConfig = removeEmptyConfigValues(functionConfig);
|
|
22
|
+
if (Object.keys(newFunctionConfig).length !== 0) {
|
|
23
|
+
newConfig[name] = newFunctionConfig;
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
26
|
return newConfig;
|
|
20
27
|
};
|
|
21
|
-
const
|
|
28
|
+
const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath) => {
|
|
29
|
+
if (excludedPath) {
|
|
30
|
+
const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
|
|
31
|
+
const excludedPatterns = paths.map(pathToRegularExpression).map(serializePattern);
|
|
32
|
+
manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const generateManifest = ({ bundles = [], declarations = [], featureFlags, functions, userFunctionConfig = {}, internalFunctionConfig = {}, importMap, layers = [], }) => {
|
|
22
36
|
const preCacheRoutes = [];
|
|
23
37
|
const postCacheRoutes = [];
|
|
24
38
|
const manifestFunctionConfig = Object.fromEntries(functions.map(({ name }) => [name, { excluded_patterns: [] }]));
|
|
25
|
-
for (const [name, { excludedPath, onError }] of Object.entries(
|
|
39
|
+
for (const [name, { excludedPath, onError }] of Object.entries(userFunctionConfig)) {
|
|
26
40
|
// If the config block is for a function that is not defined, discard it.
|
|
27
41
|
if (manifestFunctionConfig[name] === undefined) {
|
|
28
42
|
continue;
|
|
29
43
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
44
|
+
addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
|
|
45
|
+
manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError };
|
|
46
|
+
}
|
|
47
|
+
for (const [name, { excludedPath, path, onError, ...rest }] of Object.entries(internalFunctionConfig)) {
|
|
48
|
+
// If the config block is for a function that is not defined, discard it.
|
|
49
|
+
if (manifestFunctionConfig[name] === undefined) {
|
|
50
|
+
continue;
|
|
37
51
|
}
|
|
52
|
+
addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
|
|
53
|
+
manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError, ...rest };
|
|
38
54
|
}
|
|
39
55
|
declarations.forEach((declaration) => {
|
|
40
56
|
const func = functions.find(({ name }) => declaration.function === name);
|
|
@@ -44,9 +60,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
44
60
|
const pattern = getRegularExpression(declaration, featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex);
|
|
45
61
|
const route = {
|
|
46
62
|
function: func.name,
|
|
47
|
-
name: declaration.name,
|
|
48
63
|
pattern: serializePattern(pattern),
|
|
49
|
-
generator: declaration.generator,
|
|
50
64
|
};
|
|
51
65
|
const excludedPattern = getExcludedRegularExpression(declaration, featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_fail_unsupported_regex);
|
|
52
66
|
if (excludedPattern) {
|
|
@@ -26,63 +26,105 @@ test('Generates a manifest with different bundles', () => {
|
|
|
26
26
|
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
27
27
|
});
|
|
28
28
|
test('Generates a manifest with display names', () => {
|
|
29
|
-
const functions = [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
];
|
|
29
|
+
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
30
|
+
const declarations = [{ function: 'func-1', path: '/f1/*' }];
|
|
31
|
+
const internalFunctionConfig = {
|
|
32
|
+
'func-1': {
|
|
33
|
+
name: 'Display Name',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const manifest = generateManifest({ bundles: [], declarations, functions, internalFunctionConfig });
|
|
37
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$' }];
|
|
38
|
+
expect(manifest.function_config).toEqual({
|
|
39
|
+
'func-1': { name: 'Display Name' },
|
|
40
|
+
});
|
|
42
41
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
43
42
|
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
44
43
|
});
|
|
45
44
|
test('Generates a manifest with a generator field', () => {
|
|
45
|
+
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
46
|
+
const declarations = [{ function: 'func-1', path: '/f1/*' }];
|
|
47
|
+
const internalFunctionConfig = {
|
|
48
|
+
'func-1': {
|
|
49
|
+
generator: '@netlify/fake-plugin@1.0.0',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const manifest = generateManifest({ bundles: [], declarations, functions, internalFunctionConfig });
|
|
53
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/.*/?$' }];
|
|
54
|
+
const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
|
|
55
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
56
|
+
expect(manifest.function_config).toEqual(expectedFunctionConfig);
|
|
57
|
+
});
|
|
58
|
+
test('Generates a manifest with excluded paths and patterns', () => {
|
|
46
59
|
const functions = [
|
|
47
60
|
{ name: 'func-1', path: '/path/to/func-1.ts' },
|
|
48
61
|
{ name: 'func-2', path: '/path/to/func-2.ts' },
|
|
49
|
-
{ name: 'func-3', path: '/path/to/func-3.ts' },
|
|
50
62
|
];
|
|
51
63
|
const declarations = [
|
|
52
|
-
{ function: 'func-1',
|
|
53
|
-
{ function: 'func-2',
|
|
54
|
-
{ function: 'func-3', generator: '@netlify/fake-plugin@1.0.0', cache: 'manual', path: '/f3' },
|
|
64
|
+
{ function: 'func-1', path: '/f1/*', excludedPath: '/f1/exclude' },
|
|
65
|
+
{ function: 'func-2', pattern: '^/f2/.*/?$', excludedPattern: '^/f2/exclude$' },
|
|
55
66
|
];
|
|
56
67
|
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
57
68
|
const expectedRoutes = [
|
|
58
|
-
{ function: 'func-1',
|
|
69
|
+
{ function: 'func-1', pattern: '^/f1/.*/?$' },
|
|
59
70
|
{ function: 'func-2', pattern: '^/f2/.*/?$' },
|
|
60
71
|
];
|
|
61
|
-
const expectedPostCacheRoutes = [{ function: 'func-3', generator: '@netlify/fake-plugin@1.0.0', pattern: '^/f3/?$' }];
|
|
62
72
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
63
|
-
expect(manifest.
|
|
73
|
+
expect(manifest.function_config).toEqual({
|
|
74
|
+
'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
|
|
75
|
+
'func-2': { excluded_patterns: ['^/f2/exclude$'] },
|
|
76
|
+
});
|
|
64
77
|
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
65
78
|
});
|
|
66
|
-
test('
|
|
79
|
+
test('Filters out internal in-source configurations in user created functions', () => {
|
|
67
80
|
const functions = [
|
|
68
81
|
{ name: 'func-1', path: '/path/to/func-1.ts' },
|
|
69
82
|
{ name: 'func-2', path: '/path/to/func-2.ts' },
|
|
70
83
|
];
|
|
71
84
|
const declarations = [
|
|
72
|
-
{ function: 'func-1',
|
|
73
|
-
{ function: 'func-2', pattern: '^/f2/.*/?$', excludedPattern: '^/f2/exclude$' },
|
|
74
|
-
];
|
|
75
|
-
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
76
|
-
const expectedRoutes = [
|
|
77
|
-
{ function: 'func-1', name: 'Display Name', pattern: '^/f1/.*/?$' },
|
|
85
|
+
{ function: 'func-1', path: '/f1/*' },
|
|
78
86
|
{ function: 'func-2', pattern: '^/f2/.*/?$' },
|
|
79
87
|
];
|
|
80
|
-
|
|
88
|
+
const userFunctionConfig = {
|
|
89
|
+
'func-1': {
|
|
90
|
+
onError: '/custom-error',
|
|
91
|
+
cache: "manual" /* Cache.Manual */,
|
|
92
|
+
excludedPath: '/f1/exclude',
|
|
93
|
+
path: '/path/to/func-1.ts',
|
|
94
|
+
name: 'User function',
|
|
95
|
+
generator: 'fake-generator',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const internalFunctionConfig = {
|
|
99
|
+
'func-2': {
|
|
100
|
+
onError: 'bypass',
|
|
101
|
+
cache: "off" /* Cache.Off */,
|
|
102
|
+
excludedPath: '/f2/exclude',
|
|
103
|
+
path: '/path/to/func-2.ts',
|
|
104
|
+
name: 'Internal function',
|
|
105
|
+
generator: 'internal-generator',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const manifest = generateManifest({
|
|
109
|
+
bundles: [],
|
|
110
|
+
declarations,
|
|
111
|
+
functions,
|
|
112
|
+
userFunctionConfig,
|
|
113
|
+
internalFunctionConfig,
|
|
114
|
+
});
|
|
81
115
|
expect(manifest.function_config).toEqual({
|
|
82
|
-
'func-1': {
|
|
83
|
-
|
|
116
|
+
'func-1': {
|
|
117
|
+
on_error: '/custom-error',
|
|
118
|
+
excluded_patterns: ['^/f1/exclude/?$'],
|
|
119
|
+
},
|
|
120
|
+
'func-2': {
|
|
121
|
+
on_error: 'bypass',
|
|
122
|
+
cache: "off" /* Cache.Off */,
|
|
123
|
+
name: 'Internal function',
|
|
124
|
+
generator: 'internal-generator',
|
|
125
|
+
excluded_patterns: ['^/f2/exclude/?$'],
|
|
126
|
+
},
|
|
84
127
|
});
|
|
85
|
-
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
86
128
|
});
|
|
87
129
|
test('Includes failure modes in manifest', () => {
|
|
88
130
|
const functions = [
|
|
@@ -90,17 +132,17 @@ test('Includes failure modes in manifest', () => {
|
|
|
90
132
|
{ name: 'func-2', path: '/path/to/func-2.ts' },
|
|
91
133
|
];
|
|
92
134
|
const declarations = [
|
|
93
|
-
{ function: 'func-1',
|
|
135
|
+
{ function: 'func-1', path: '/f1/*' },
|
|
94
136
|
{ function: 'func-2', pattern: '^/f2/.*/?$' },
|
|
95
137
|
];
|
|
96
|
-
const
|
|
138
|
+
const userFunctionConfig = {
|
|
97
139
|
'func-1': {
|
|
98
140
|
onError: '/custom-error',
|
|
99
141
|
},
|
|
100
142
|
};
|
|
101
|
-
const manifest = generateManifest({ bundles: [], declarations, functions,
|
|
143
|
+
const manifest = generateManifest({ bundles: [], declarations, functions, userFunctionConfig });
|
|
102
144
|
expect(manifest.function_config).toEqual({
|
|
103
|
-
'func-1': {
|
|
145
|
+
'func-1': { on_error: '/custom-error' },
|
|
104
146
|
});
|
|
105
147
|
});
|
|
106
148
|
test('Excludes functions for which there are function files but no matching config declarations', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@types/node": "^14.18.32",
|
|
59
59
|
"@types/semver": "^7.3.9",
|
|
60
60
|
"@types/uuid": "^9.0.0",
|
|
61
|
-
"@vitest/coverage-c8": "^0.29.
|
|
61
|
+
"@vitest/coverage-c8": "^0.29.7",
|
|
62
62
|
"archiver": "^5.3.1",
|
|
63
63
|
"chalk": "^4.1.2",
|
|
64
64
|
"cpy": "^9.0.1",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"nock": "^13.2.4",
|
|
68
68
|
"tar": "^6.1.11",
|
|
69
69
|
"typescript": "^4.5.4",
|
|
70
|
-
"vitest": "^0.29.
|
|
70
|
+
"vitest": "^0.29.7"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": "^14.16.0 || >=16.0.0"
|