@netlify/edge-bundler 8.12.3 → 8.13.1
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 +2 -1
- package/dist/node/declaration.js +44 -13
- package/dist/node/declaration.test.js +53 -12
- package/dist/node/downloader.test.js +11 -1
- package/dist/node/feature_flags.d.ts +2 -2
- package/dist/node/feature_flags.js +1 -1
- package/dist/node/finder.d.ts +1 -0
- package/dist/node/finder.js +19 -7
- package/dist/node/finder.test.d.ts +1 -0
- package/dist/node/finder.test.js +17 -0
- package/dist/node/manifest.d.ts +5 -4
- package/dist/node/manifest.js +27 -13
- package/dist/node/manifest.test.js +77 -35
- package/dist/node/validation/manifest/index.d.ts +1 -1
- package/dist/node/validation/manifest/index.js +5 -4
- package/dist/node/validation/manifest/index.test.js +5 -22
- package/dist/node/validation/manifest/schema.js +2 -2
- package/package.json +4 -4
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, featureFlags);
|
|
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) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FunctionConfig, Path } from './config.js';
|
|
2
|
+
import { FeatureFlags } from './feature_flags.js';
|
|
2
3
|
interface BaseDeclaration {
|
|
3
4
|
cache?: string;
|
|
4
5
|
function: string;
|
|
@@ -14,6 +15,6 @@ type DeclarationWithPattern = BaseDeclaration & {
|
|
|
14
15
|
excludedPattern?: string;
|
|
15
16
|
};
|
|
16
17
|
export type Declaration = DeclarationWithPath | DeclarationWithPattern;
|
|
17
|
-
export declare const mergeDeclarations: (tomlDeclarations: Declaration[],
|
|
18
|
+
export declare const mergeDeclarations: (tomlDeclarations: Declaration[], userFunctionsConfig: Record<string, FunctionConfig>, internalFunctionsConfig: Record<string, FunctionConfig>, deployConfigDeclarations: Declaration[], featureFlags?: FeatureFlags) => Declaration[];
|
|
18
19
|
export declare const parsePattern: (pattern: string) => string;
|
|
19
20
|
export {};
|
package/dist/node/declaration.js
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import regexpAST from 'regexp-tree';
|
|
2
|
-
export const mergeDeclarations = (tomlDeclarations,
|
|
2
|
+
export const mergeDeclarations = (tomlDeclarations, userFunctionsConfig, internalFunctionsConfig, deployConfigDeclarations, featureFlags = {}) => {
|
|
3
|
+
const functionsVisited = new Set();
|
|
4
|
+
let declarations = getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited);
|
|
5
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
6
|
+
if (featureFlags.edge_functions_correct_order) {
|
|
7
|
+
declarations = [
|
|
8
|
+
// INTEGRATIONS
|
|
9
|
+
// 1. Declarations from the integrations deploy config
|
|
10
|
+
...getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited),
|
|
11
|
+
// 2. Declarations from the integrations ISC
|
|
12
|
+
...createDeclarationsFromFunctionConfigs(internalFunctionsConfig, functionsVisited),
|
|
13
|
+
// USER
|
|
14
|
+
// 3. Declarations from the users toml config
|
|
15
|
+
...getDeclarationsFromInput(tomlDeclarations, userFunctionsConfig, functionsVisited),
|
|
16
|
+
// 4. Declarations from the users ISC
|
|
17
|
+
...createDeclarationsFromFunctionConfigs(userFunctionsConfig, functionsVisited),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
declarations = [
|
|
22
|
+
...getDeclarationsFromInput(tomlDeclarations, userFunctionsConfig, functionsVisited),
|
|
23
|
+
...getDeclarationsFromInput(deployConfigDeclarations, internalFunctionsConfig, functionsVisited),
|
|
24
|
+
...createDeclarationsFromFunctionConfigs(internalFunctionsConfig, functionsVisited),
|
|
25
|
+
...createDeclarationsFromFunctionConfigs(userFunctionsConfig, functionsVisited),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
return declarations;
|
|
29
|
+
};
|
|
30
|
+
const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsVisited) => {
|
|
3
31
|
var _a;
|
|
4
32
|
const declarations = [];
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// takes precedence.
|
|
10
|
-
for (const declaration of [...tomlDeclarations, ...deployConfigDeclarations]) {
|
|
11
|
-
const config = functionsConfig[declaration.function];
|
|
33
|
+
// For any declaration for which we also have a function configuration object,
|
|
34
|
+
// we replace the path because that object takes precedence.
|
|
35
|
+
for (const declaration of inputDeclarations) {
|
|
36
|
+
const config = functionConfigs[declaration.function];
|
|
12
37
|
if (!config) {
|
|
13
38
|
// If no config is found, add the declaration as is.
|
|
14
39
|
declarations.push(declaration);
|
|
@@ -28,15 +53,21 @@ export const mergeDeclarations = (tomlDeclarations, functionsConfig, deployConfi
|
|
|
28
53
|
}
|
|
29
54
|
functionsVisited.add(declaration.function);
|
|
30
55
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
return declarations;
|
|
57
|
+
};
|
|
58
|
+
const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited) => {
|
|
59
|
+
const declarations = [];
|
|
60
|
+
for (const name in functionConfigs) {
|
|
61
|
+
const { cache, path } = functionConfigs[name];
|
|
35
62
|
// If we have a path specified, create a declaration for each path.
|
|
36
63
|
if (!functionsVisited.has(name) && path) {
|
|
37
64
|
const paths = Array.isArray(path) ? path : [path];
|
|
38
65
|
paths.forEach((singlePath) => {
|
|
39
|
-
|
|
66
|
+
const declaration = { function: name, path: singlePath };
|
|
67
|
+
if (cache) {
|
|
68
|
+
declaration.cache = cache;
|
|
69
|
+
}
|
|
70
|
+
declarations.push(declaration);
|
|
40
71
|
});
|
|
41
72
|
}
|
|
42
73
|
}
|
|
@@ -1,13 +1,54 @@
|
|
|
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('Ensure the order of edge functions with FF', () => {
|
|
5
|
+
const deployConfigDeclarations = [
|
|
6
|
+
{ function: 'framework-manifest-a', path: '/path1' },
|
|
7
|
+
{ function: 'framework-manifest-c', path: '/path3' },
|
|
8
|
+
{ function: 'framework-manifest-b', path: '/path2' },
|
|
9
|
+
];
|
|
10
|
+
const tomlConfig = [
|
|
11
|
+
{ function: 'user-toml-a', path: '/path1' },
|
|
12
|
+
{ function: 'user-toml-c', path: '/path3' },
|
|
13
|
+
{ function: 'user-toml-b', path: '/path2' },
|
|
14
|
+
];
|
|
15
|
+
const userFuncConfig = {
|
|
16
|
+
'user-isc-c': { path: ['/path1', '/path2'] },
|
|
17
|
+
};
|
|
18
|
+
const internalFuncConfig = {
|
|
19
|
+
'framework-isc-c': { path: ['/path1', '/path2'] },
|
|
20
|
+
};
|
|
21
|
+
expect(mergeDeclarations(tomlConfig, userFuncConfig, internalFuncConfig, deployConfigDeclarations, {
|
|
22
|
+
edge_functions_correct_order: true,
|
|
23
|
+
})).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
test('Ensure the order of edge functions without FF', () => {
|
|
26
|
+
const deployConfigDeclarations = [
|
|
27
|
+
{ function: 'framework-manifest-a', path: '/path1' },
|
|
28
|
+
{ function: 'framework-manifest-c', path: '/path3' },
|
|
29
|
+
{ function: 'framework-manifest-b', path: '/path2' },
|
|
30
|
+
];
|
|
31
|
+
const tomlConfig = [
|
|
32
|
+
{ function: 'user-toml-a', path: '/path1' },
|
|
33
|
+
{ function: 'user-toml-c', path: '/path3' },
|
|
34
|
+
{ function: 'user-toml-b', path: '/path2' },
|
|
35
|
+
];
|
|
36
|
+
const userFuncConfig = {
|
|
37
|
+
'user-isc-c': { path: ['/path1', '/path2'] },
|
|
38
|
+
};
|
|
39
|
+
const internalFuncConfig = {
|
|
40
|
+
'framework-isc-c': { path: ['/path1', '/path2'] },
|
|
41
|
+
};
|
|
42
|
+
expect(mergeDeclarations(tomlConfig, userFuncConfig, internalFuncConfig, deployConfigDeclarations, {
|
|
43
|
+
edge_functions_correct_order: false,
|
|
44
|
+
})).toMatchSnapshot();
|
|
45
|
+
});
|
|
5
46
|
test('In-source config takes precedence over netlify.toml config', () => {
|
|
6
47
|
const tomlConfig = [
|
|
7
48
|
{ function: 'geolocation', path: '/geo', cache: 'off' },
|
|
8
49
|
{ function: 'json', path: '/json', cache: 'manual' },
|
|
9
50
|
];
|
|
10
|
-
const
|
|
51
|
+
const userFuncConfig = {
|
|
11
52
|
geolocation: { path: ['/geo-isc', '/*'], cache: 'manual' },
|
|
12
53
|
json: { path: '/json', cache: 'off' },
|
|
13
54
|
};
|
|
@@ -16,7 +57,7 @@ test('In-source config takes precedence over netlify.toml config', () => {
|
|
|
16
57
|
{ function: 'geolocation', path: '/*', cache: 'manual' },
|
|
17
58
|
{ function: 'json', path: '/json', cache: 'off' },
|
|
18
59
|
];
|
|
19
|
-
const declarations = mergeDeclarations(tomlConfig,
|
|
60
|
+
const declarations = mergeDeclarations(tomlConfig, userFuncConfig, {}, deployConfigDeclarations);
|
|
20
61
|
expect(declarations).toEqual(expectedDeclarations);
|
|
21
62
|
});
|
|
22
63
|
test("Declarations don't break if no in-source config is provided", () => {
|
|
@@ -24,7 +65,7 @@ test("Declarations don't break if no in-source config is provided", () => {
|
|
|
24
65
|
{ function: 'geolocation', path: '/geo', cache: 'off' },
|
|
25
66
|
{ function: 'json', path: '/json', cache: 'manual' },
|
|
26
67
|
];
|
|
27
|
-
const
|
|
68
|
+
const userFuncConfig = {
|
|
28
69
|
geolocation: { path: ['/geo-isc'], cache: 'manual' },
|
|
29
70
|
json: {},
|
|
30
71
|
};
|
|
@@ -32,7 +73,7 @@ test("Declarations don't break if no in-source config is provided", () => {
|
|
|
32
73
|
{ function: 'geolocation', path: '/geo-isc', cache: 'manual' },
|
|
33
74
|
{ function: 'json', path: '/json', cache: 'manual' },
|
|
34
75
|
];
|
|
35
|
-
const declarations = mergeDeclarations(tomlConfig,
|
|
76
|
+
const declarations = mergeDeclarations(tomlConfig, userFuncConfig, {}, deployConfigDeclarations);
|
|
36
77
|
expect(declarations).toEqual(expectedDeclarations);
|
|
37
78
|
});
|
|
38
79
|
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 +90,9 @@ test('In-source config works independent of the netlify.toml file if a path is d
|
|
|
49
90
|
{ function: 'json', path: '/json-isc', cache: 'off' },
|
|
50
91
|
];
|
|
51
92
|
const expectedDeclarationsWithoutISCPath = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
|
|
52
|
-
const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, deployConfigDeclarations);
|
|
93
|
+
const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, {}, deployConfigDeclarations);
|
|
53
94
|
expect(declarationsWithISCPath).toEqual(expectedDeclarationsWithISCPath);
|
|
54
|
-
const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, deployConfigDeclarations);
|
|
95
|
+
const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, {}, deployConfigDeclarations);
|
|
55
96
|
expect(declarationsWithoutISCPath).toEqual(expectedDeclarationsWithoutISCPath);
|
|
56
97
|
});
|
|
57
98
|
test('In-source config works if only the cache config property is set', () => {
|
|
@@ -60,7 +101,7 @@ test('In-source config works if only the cache config property is set', () => {
|
|
|
60
101
|
geolocation: { cache: 'manual' },
|
|
61
102
|
};
|
|
62
103
|
const expectedDeclarations = [{ function: 'geolocation', path: '/geo', cache: 'manual' }];
|
|
63
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
104
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
64
105
|
});
|
|
65
106
|
test("In-source config path property works if it's not an array", () => {
|
|
66
107
|
const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
|
|
@@ -68,7 +109,7 @@ test("In-source config path property works if it's not an array", () => {
|
|
|
68
109
|
json: { path: '/json', cache: 'manual' },
|
|
69
110
|
};
|
|
70
111
|
const expectedDeclarations = [{ function: 'json', path: '/json', cache: 'manual' }];
|
|
71
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
112
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
72
113
|
});
|
|
73
114
|
test("In-source config path property works if it's not an array and it's not present in toml or deploy config", () => {
|
|
74
115
|
const tomlConfig = [{ function: 'geolocation', path: '/geo', cache: 'off' }];
|
|
@@ -79,7 +120,7 @@ test("In-source config path property works if it's not an array and it's not pre
|
|
|
79
120
|
{ function: 'geolocation', path: '/geo', cache: 'off' },
|
|
80
121
|
{ function: 'json', path: '/json-isc', cache: 'manual' },
|
|
81
122
|
];
|
|
82
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
123
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
83
124
|
});
|
|
84
125
|
test('In-source config works if path property is an empty array with cache value specified', () => {
|
|
85
126
|
const tomlConfig = [{ function: 'json', path: '/json-toml', cache: 'off' }];
|
|
@@ -87,12 +128,12 @@ test('In-source config works if path property is an empty array with cache value
|
|
|
87
128
|
json: { path: [], cache: 'manual' },
|
|
88
129
|
};
|
|
89
130
|
const expectedDeclarations = [{ function: 'json', path: '/json-toml', cache: 'manual' }];
|
|
90
|
-
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
131
|
+
expect(mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations)).toEqual(expectedDeclarations);
|
|
91
132
|
});
|
|
92
133
|
test('netlify.toml-defined excludedPath are respected', () => {
|
|
93
134
|
const tomlConfig = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
|
|
94
135
|
const funcConfig = {};
|
|
95
136
|
const expectedDeclarations = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }];
|
|
96
|
-
const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations);
|
|
137
|
+
const declarations = mergeDeclarations(tomlConfig, funcConfig, {}, deployConfigDeclarations);
|
|
97
138
|
expect(declarations).toEqual(expectedDeclarations);
|
|
98
139
|
});
|
|
@@ -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);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
declare const defaultFlags: {
|
|
2
|
+
edge_functions_correct_order: boolean;
|
|
2
3
|
edge_functions_fail_unsupported_regex: boolean;
|
|
3
4
|
edge_functions_invalid_config_throw: boolean;
|
|
4
|
-
edge_functions_manifest_validate_slash: boolean;
|
|
5
5
|
};
|
|
6
6
|
type FeatureFlag = keyof typeof defaultFlags;
|
|
7
7
|
type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
|
|
8
8
|
declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
9
|
+
edge_functions_correct_order: boolean;
|
|
9
10
|
edge_functions_fail_unsupported_regex: boolean;
|
|
10
11
|
edge_functions_invalid_config_throw: boolean;
|
|
11
|
-
edge_functions_manifest_validate_slash: boolean;
|
|
12
12
|
}) => FeatureFlags;
|
|
13
13
|
export { defaultFlags, getFlags };
|
|
14
14
|
export type { FeatureFlag, FeatureFlags };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const defaultFlags = {
|
|
2
|
+
edge_functions_correct_order: false,
|
|
2
3
|
edge_functions_fail_unsupported_regex: false,
|
|
3
4
|
edge_functions_invalid_config_throw: false,
|
|
4
|
-
edge_functions_manifest_validate_slash: false,
|
|
5
5
|
};
|
|
6
6
|
const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
|
|
7
7
|
...result,
|
package/dist/node/finder.d.ts
CHANGED
package/dist/node/finder.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
|
-
import { basename, extname, join } from 'path';
|
|
2
|
+
import { basename, extname, join, parse } from 'path';
|
|
3
3
|
import { nonNullable } from './utils/non_nullable.js';
|
|
4
|
-
|
|
4
|
+
// the order of the allowed extensions is also the order we remove duplicates
|
|
5
|
+
// with a lower index meaning a higher precedence over the others
|
|
6
|
+
const ALLOWED_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'];
|
|
7
|
+
export const removeDuplicatesByExtension = (functions) => {
|
|
8
|
+
const seen = new Map();
|
|
9
|
+
return Object.values(functions.reduce((acc, path) => {
|
|
10
|
+
const { ext, name } = parse(path);
|
|
11
|
+
const extIndex = ALLOWED_EXTENSIONS.indexOf(ext);
|
|
12
|
+
if (!seen.has(name) || seen.get(name) > extIndex) {
|
|
13
|
+
seen.set(name, extIndex);
|
|
14
|
+
return { ...acc, [name]: path };
|
|
15
|
+
}
|
|
16
|
+
return acc;
|
|
17
|
+
}, {}));
|
|
18
|
+
};
|
|
5
19
|
const findFunctionInDirectory = async (directory) => {
|
|
6
20
|
const name = basename(directory);
|
|
7
|
-
const candidatePaths = [
|
|
8
|
-
.flatMap((extension) => [`${name}${extension}`, `index${extension}`])
|
|
9
|
-
.map((filename) => join(directory, filename));
|
|
21
|
+
const candidatePaths = ALLOWED_EXTENSIONS.flatMap((extension) => [`${name}${extension}`, `index${extension}`]).map((filename) => join(directory, filename));
|
|
10
22
|
let functionPath;
|
|
11
23
|
for (const candidatePath of candidatePaths) {
|
|
12
24
|
try {
|
|
@@ -35,14 +47,14 @@ const findFunctionInPath = async (path) => {
|
|
|
35
47
|
return findFunctionInDirectory(path);
|
|
36
48
|
}
|
|
37
49
|
const extension = extname(path);
|
|
38
|
-
if (ALLOWED_EXTENSIONS.
|
|
50
|
+
if (ALLOWED_EXTENSIONS.includes(extension)) {
|
|
39
51
|
return { name: basename(path, extension), path };
|
|
40
52
|
}
|
|
41
53
|
};
|
|
42
54
|
const findFunctionsInDirectory = async (baseDirectory) => {
|
|
43
55
|
let items = [];
|
|
44
56
|
try {
|
|
45
|
-
items = await fs.readdir(baseDirectory);
|
|
57
|
+
items = await fs.readdir(baseDirectory).then(removeDuplicatesByExtension);
|
|
46
58
|
}
|
|
47
59
|
catch {
|
|
48
60
|
// no-op
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { removeDuplicatesByExtension } from './finder.js';
|
|
3
|
+
test('filters out any duplicate files based on the extension', () => {
|
|
4
|
+
const functions = [
|
|
5
|
+
'file1.js',
|
|
6
|
+
'file1.ts',
|
|
7
|
+
'file2.tsx',
|
|
8
|
+
'file2.jsx',
|
|
9
|
+
'file3.tsx',
|
|
10
|
+
'file3.js',
|
|
11
|
+
'file4.ts',
|
|
12
|
+
'file5.ts',
|
|
13
|
+
'file5.tsx',
|
|
14
|
+
];
|
|
15
|
+
const expected = ['file1.js', 'file2.jsx', 'file3.js', 'file4.ts', 'file5.ts'];
|
|
16
|
+
expect(removeDuplicatesByExtension(functions)).toStrictEqual(expected);
|
|
17
|
+
});
|
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', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { FeatureFlags } from '../../feature_flags.js';
|
|
2
2
|
import ManifestValidationError from './error.js';
|
|
3
|
-
export declare const validateManifest: (manifestData: unknown,
|
|
3
|
+
export declare const validateManifest: (manifestData: unknown, _featureFlags?: FeatureFlags) => void;
|
|
4
4
|
export { ManifestValidationError };
|
|
@@ -4,13 +4,13 @@ import betterAjvErrors from 'better-ajv-errors';
|
|
|
4
4
|
import ManifestValidationError from './error.js';
|
|
5
5
|
import edgeManifestSchema from './schema.js';
|
|
6
6
|
let manifestValidator;
|
|
7
|
-
const initializeValidator = (
|
|
7
|
+
const initializeValidator = () => {
|
|
8
8
|
if (manifestValidator === undefined) {
|
|
9
9
|
const ajv = new Ajv({ allErrors: true });
|
|
10
10
|
ajvErrors(ajv);
|
|
11
11
|
// regex pattern for manifest route pattern
|
|
12
12
|
// checks if the pattern string starts with ^ and ends with $
|
|
13
|
-
const normalizedPatternRegex =
|
|
13
|
+
const normalizedPatternRegex = /^\^.*\$$/;
|
|
14
14
|
ajv.addFormat('regexPattern', {
|
|
15
15
|
validate: (data) => normalizedPatternRegex.test(data),
|
|
16
16
|
});
|
|
@@ -19,8 +19,9 @@ const initializeValidator = (featureFlags) => {
|
|
|
19
19
|
return manifestValidator;
|
|
20
20
|
};
|
|
21
21
|
// throws on validation error
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
+
export const validateManifest = (manifestData, _featureFlags = {}) => {
|
|
24
|
+
const validate = initializeValidator();
|
|
24
25
|
const valid = validate(manifestData);
|
|
25
26
|
if (!valid) {
|
|
26
27
|
let errorOutput;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { test, expect, describe
|
|
2
|
+
import { test, expect, describe } from 'vitest';
|
|
3
3
|
import { validateManifest, ManifestValidationError } from './index.js';
|
|
4
4
|
// We need to disable all color outputs for the tests as they are different on different platforms, CI, etc.
|
|
5
5
|
// This only works if this is the same instance of chalk that better-ajv-errors uses
|
|
@@ -92,42 +92,25 @@ describe('bundle', () => {
|
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
94
|
describe('route', () => {
|
|
95
|
-
let freshValidateManifest;
|
|
96
|
-
beforeEach(async () => {
|
|
97
|
-
// reset all modules, to get a fresh AJV validator for FF changes
|
|
98
|
-
vi.resetModules();
|
|
99
|
-
const indexImport = await import('./index.js');
|
|
100
|
-
freshValidateManifest = indexImport.validateManifest;
|
|
101
|
-
});
|
|
102
95
|
test('should throw on additional property', () => {
|
|
103
96
|
const manifest = getBaseManifest();
|
|
104
97
|
manifest.routes[0].foo = 'bar';
|
|
105
|
-
expect(() =>
|
|
98
|
+
expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
|
|
106
99
|
});
|
|
107
100
|
test('should throw on invalid pattern', () => {
|
|
108
101
|
const manifest = getBaseManifest();
|
|
109
102
|
manifest.routes[0].pattern = '/^/hello/?$/';
|
|
110
|
-
expect(() =>
|
|
111
|
-
});
|
|
112
|
-
test('should not throw on missing beginning slash without FF', () => {
|
|
113
|
-
const manifest = getBaseManifest();
|
|
114
|
-
manifest.routes[0].pattern = '^hello/?$';
|
|
115
|
-
expect(() => freshValidateManifest(manifest, { edge_functions_manifest_validate_slash: false })).not.toThrowError();
|
|
116
|
-
});
|
|
117
|
-
test('should throw on missing beginning slash with FF', () => {
|
|
118
|
-
const manifest = getBaseManifest();
|
|
119
|
-
manifest.routes[0].pattern = '^hello/?$';
|
|
120
|
-
expect(() => freshValidateManifest(manifest, { edge_functions_manifest_validate_slash: true })).toThrowErrorMatchingSnapshot();
|
|
103
|
+
expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
|
|
121
104
|
});
|
|
122
105
|
test('should throw on missing function', () => {
|
|
123
106
|
const manifest = getBaseManifest();
|
|
124
107
|
delete manifest.routes[0].function;
|
|
125
|
-
expect(() =>
|
|
108
|
+
expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
|
|
126
109
|
});
|
|
127
110
|
test('should throw on missing pattern', () => {
|
|
128
111
|
const manifest = getBaseManifest();
|
|
129
112
|
delete manifest.routes[0].pattern;
|
|
130
|
-
expect(() =>
|
|
113
|
+
expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot();
|
|
131
114
|
});
|
|
132
115
|
});
|
|
133
116
|
// No tests for post_cache_routes as schema shared with routes
|
|
@@ -16,7 +16,7 @@ const routesSchema = {
|
|
|
16
16
|
pattern: {
|
|
17
17
|
type: 'string',
|
|
18
18
|
format: 'regexPattern',
|
|
19
|
-
errorMessage: 'pattern
|
|
19
|
+
errorMessage: 'pattern must be a regex that starts with ^ and ends with $ (e.g. ^/blog/[d]{4}$)',
|
|
20
20
|
},
|
|
21
21
|
generator: { type: 'string' },
|
|
22
22
|
},
|
|
@@ -31,7 +31,7 @@ const functionConfigSchema = {
|
|
|
31
31
|
items: {
|
|
32
32
|
type: 'string',
|
|
33
33
|
format: 'regexPattern',
|
|
34
|
-
errorMessage: 'excluded_patterns
|
|
34
|
+
errorMessage: 'excluded_patterns must be an array of regex that starts with ^ and ends with $ (e.g. ^/blog/[d]{4}$)',
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
on_error: { type: 'string' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.1",
|
|
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.
|
|
61
|
+
"@vitest/coverage-c8": "^0.30.0",
|
|
62
62
|
"archiver": "^5.3.1",
|
|
63
63
|
"chalk": "^4.1.2",
|
|
64
64
|
"cpy": "^9.0.1",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"husky": "^8.0.0",
|
|
67
67
|
"nock": "^13.2.4",
|
|
68
68
|
"tar": "^6.1.11",
|
|
69
|
-
"typescript": "^
|
|
70
|
-
"vitest": "^0.
|
|
69
|
+
"typescript": "^5.0.0",
|
|
70
|
+
"vitest": "^0.30.0"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": "^14.16.0 || >=16.0.0"
|