@netlify/edge-bundler 11.2.2 → 11.4.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/config.d.ts +2 -0
- package/dist/node/config.test.js +62 -4
- package/dist/node/declaration.d.ts +2 -3
- package/dist/node/declaration.js +3 -33
- package/dist/node/declaration.test.js +4 -7
- package/dist/node/feature_flags.d.ts +2 -6
- package/dist/node/feature_flags.js +1 -3
- package/dist/node/manifest.d.ts +20 -1
- package/dist/node/manifest.js +45 -12
- package/dist/node/manifest.test.js +83 -13
- package/dist/node/rate_limit.d.ts +25 -0
- package/dist/node/rate_limit.js +14 -0
- package/package.json +2 -3
package/dist/node/config.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DenoBridge } from './bridge.js';
|
|
|
2
2
|
import { EdgeFunction } from './edge_function.js';
|
|
3
3
|
import { ImportMap } from './import_map.js';
|
|
4
4
|
import { Logger } from './logger.js';
|
|
5
|
+
import { RateLimit } from './rate_limit.js';
|
|
5
6
|
export declare const enum Cache {
|
|
6
7
|
Off = "off",
|
|
7
8
|
Manual = "manual"
|
|
@@ -18,6 +19,7 @@ export interface FunctionConfig {
|
|
|
18
19
|
name?: string;
|
|
19
20
|
generator?: string;
|
|
20
21
|
method?: HTTPMethod | HTTPMethod[];
|
|
22
|
+
rateLimit?: RateLimit;
|
|
21
23
|
}
|
|
22
24
|
export declare const getFunctionConfig: ({ func, importMap, deno, log, }: {
|
|
23
25
|
func: EdgeFunction;
|
package/dist/node/config.test.js
CHANGED
|
@@ -9,6 +9,7 @@ import { DenoBridge } from './bridge.js';
|
|
|
9
9
|
import { bundle } from './bundler.js';
|
|
10
10
|
import { getFunctionConfig } from './config.js';
|
|
11
11
|
import { ImportMap } from './import_map.js';
|
|
12
|
+
import { RateLimitAction, RateLimitAggregator } from './rate_limit.js';
|
|
12
13
|
const importMapFile = {
|
|
13
14
|
baseURL: new URL('file:///some/path/import-map.json'),
|
|
14
15
|
imports: {
|
|
@@ -64,7 +65,7 @@ const functions = [
|
|
|
64
65
|
},
|
|
65
66
|
{
|
|
66
67
|
testName: 'config with wrong onError',
|
|
67
|
-
name: '
|
|
68
|
+
name: 'func6',
|
|
68
69
|
source: `
|
|
69
70
|
export default async () => new Response("Hello from function two")
|
|
70
71
|
export const config = { onError: "foo" }
|
|
@@ -74,7 +75,7 @@ const functions = [
|
|
|
74
75
|
{
|
|
75
76
|
testName: 'config with `path`',
|
|
76
77
|
expectedConfig: { path: '/home' },
|
|
77
|
-
name: '
|
|
78
|
+
name: 'func7',
|
|
78
79
|
source: `
|
|
79
80
|
export default async () => new Response("Hello from function three")
|
|
80
81
|
|
|
@@ -89,17 +90,74 @@ const functions = [
|
|
|
89
90
|
name: 'a displayName',
|
|
90
91
|
onError: 'bypass',
|
|
91
92
|
},
|
|
92
|
-
name: '
|
|
93
|
+
name: 'func8',
|
|
93
94
|
source: `
|
|
94
95
|
export default async () => new Response("Hello from function three")
|
|
95
96
|
|
|
96
|
-
export const config = {
|
|
97
|
+
export const config = {
|
|
98
|
+
path: "/home",
|
|
97
99
|
generator: '@netlify/fake-plugin@1.0.0',
|
|
98
100
|
name: 'a displayName',
|
|
99
101
|
onError: 'bypass',
|
|
100
102
|
}
|
|
101
103
|
`,
|
|
102
104
|
},
|
|
105
|
+
{
|
|
106
|
+
testName: 'config with ratelimit',
|
|
107
|
+
expectedConfig: {
|
|
108
|
+
path: '/ratelimit',
|
|
109
|
+
name: 'a limit rate',
|
|
110
|
+
rateLimit: {
|
|
111
|
+
windowSize: 10,
|
|
112
|
+
windowLimit: 100,
|
|
113
|
+
aggregateBy: [RateLimitAggregator.IP, RateLimitAggregator.Domain],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
name: 'func9',
|
|
117
|
+
source: `
|
|
118
|
+
export default async () => new Response("Rate my limits")
|
|
119
|
+
|
|
120
|
+
export const config = {
|
|
121
|
+
path: "/ratelimit",
|
|
122
|
+
rateLimit: {
|
|
123
|
+
windowSize: 10,
|
|
124
|
+
windowLimit: 100,
|
|
125
|
+
aggregateBy: ["ip", "domain"],
|
|
126
|
+
},
|
|
127
|
+
name: 'a limit rate',
|
|
128
|
+
}
|
|
129
|
+
`,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
testName: 'config with rewrite',
|
|
133
|
+
expectedConfig: {
|
|
134
|
+
path: '/rewrite',
|
|
135
|
+
name: 'a limit rewrite',
|
|
136
|
+
rateLimit: {
|
|
137
|
+
action: RateLimitAction.Rewrite,
|
|
138
|
+
to: '/rewritten',
|
|
139
|
+
windowSize: 20,
|
|
140
|
+
windowLimit: 200,
|
|
141
|
+
aggregateBy: [RateLimitAggregator.IP, RateLimitAggregator.Domain],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
name: 'func9',
|
|
145
|
+
source: `
|
|
146
|
+
export default async () => new Response("Rate my limits")
|
|
147
|
+
|
|
148
|
+
export const config = {
|
|
149
|
+
path: "/rewrite",
|
|
150
|
+
rateLimit: {
|
|
151
|
+
action: "rewrite",
|
|
152
|
+
to: "/rewritten",
|
|
153
|
+
windowSize: 20,
|
|
154
|
+
windowLimit: 200,
|
|
155
|
+
aggregateBy: ["ip", "domain"],
|
|
156
|
+
},
|
|
157
|
+
name: 'a limit rewrite',
|
|
158
|
+
}
|
|
159
|
+
`,
|
|
160
|
+
},
|
|
103
161
|
];
|
|
104
162
|
describe('`getFunctionConfig` extracts configuration properties from function file', () => {
|
|
105
163
|
test.each(functions)('$testName', async (func) => {
|
|
@@ -19,8 +19,7 @@ export type Declaration = DeclarationWithPath | DeclarationWithPattern;
|
|
|
19
19
|
export declare const mergeDeclarations: (tomlDeclarations: Declaration[], userFunctionsConfig: Record<string, FunctionConfig>, internalFunctionsConfig: Record<string, FunctionConfig>, deployConfigDeclarations: Declaration[], _featureFlags?: FeatureFlags) => Declaration[];
|
|
20
20
|
/**
|
|
21
21
|
* Normalizes a regular expression, ensuring it has a leading `^` and trailing
|
|
22
|
-
* `$` characters.
|
|
23
|
-
* needed.
|
|
22
|
+
* `$` characters.
|
|
24
23
|
*/
|
|
25
|
-
export declare const
|
|
24
|
+
export declare const normalizePattern: (pattern: string) => string;
|
|
26
25
|
export {};
|
package/dist/node/declaration.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import regexpAST from 'regexp-tree';
|
|
2
1
|
export const mergeDeclarations = (tomlDeclarations, userFunctionsConfig, internalFunctionsConfig, deployConfigDeclarations,
|
|
3
2
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4
3
|
_featureFlags = {}) => {
|
|
@@ -68,10 +67,9 @@ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited
|
|
|
68
67
|
};
|
|
69
68
|
/**
|
|
70
69
|
* Normalizes a regular expression, ensuring it has a leading `^` and trailing
|
|
71
|
-
* `$` characters.
|
|
72
|
-
* needed.
|
|
70
|
+
* `$` characters.
|
|
73
71
|
*/
|
|
74
|
-
export const
|
|
72
|
+
export const normalizePattern = (pattern) => {
|
|
75
73
|
let enclosedPattern = pattern;
|
|
76
74
|
if (!pattern.startsWith('^')) {
|
|
77
75
|
enclosedPattern = `^${enclosedPattern}`;
|
|
@@ -80,34 +78,6 @@ export const parsePattern = (pattern, pcreRegexpEngine) => {
|
|
|
80
78
|
enclosedPattern = `${enclosedPattern}$`;
|
|
81
79
|
}
|
|
82
80
|
const regexp = new RegExp(enclosedPattern);
|
|
83
|
-
const regexpString = pcreRegexpEngine ? regexp.toString() : transformPCRERegexp(regexp);
|
|
84
81
|
// Strip leading and forward slashes.
|
|
85
|
-
return
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* Transforms a PCRE regular expression into a RE2 expression, compatible
|
|
89
|
-
* with the Go engine used in our edge nodes.
|
|
90
|
-
*/
|
|
91
|
-
const transformPCRERegexp = (regexp) => {
|
|
92
|
-
const newRegexp = regexpAST.transform(regexp, {
|
|
93
|
-
Assertion(path) {
|
|
94
|
-
// Lookaheads are not supported. If we find one, throw an error.
|
|
95
|
-
if (path.node.kind === 'Lookahead') {
|
|
96
|
-
throw new Error('Regular expressions with lookaheads are not supported');
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
Group(path) {
|
|
100
|
-
// Named captured groups in JavaScript use a different syntax than in Go.
|
|
101
|
-
// If we find one, convert it to an unnamed capture group, which is valid
|
|
102
|
-
// in both engines.
|
|
103
|
-
if ('name' in path.node && path.node.name !== undefined) {
|
|
104
|
-
path.replace({
|
|
105
|
-
...path.node,
|
|
106
|
-
name: undefined,
|
|
107
|
-
nameRaw: undefined,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
return newRegexp.toString();
|
|
82
|
+
return regexp.toString().slice(1, -1);
|
|
113
83
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test, expect } from 'vitest';
|
|
2
|
-
import { mergeDeclarations,
|
|
2
|
+
import { mergeDeclarations, normalizePattern } from './declaration.js';
|
|
3
3
|
const deployConfigDeclarations = [];
|
|
4
4
|
test('Ensure the order of edge functions with FF', () => {
|
|
5
5
|
const deployConfigDeclarations = [
|
|
@@ -117,18 +117,15 @@ test('netlify.toml-defined excludedPath are respected', () => {
|
|
|
117
117
|
test('Does not escape front slashes in a regex pattern if they are already escaped', () => {
|
|
118
118
|
const regexPattern = '^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))\\/shows(?:\\/(.*))(.json)?[\\/#\\?]?$';
|
|
119
119
|
const expected = '^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))\\/shows(?:\\/(.*))(.json)?[\\/#\\?]?$';
|
|
120
|
-
expect(
|
|
121
|
-
expect(parsePattern(regexPattern, true)).toEqual(expected);
|
|
120
|
+
expect(normalizePattern(regexPattern)).toEqual(expected);
|
|
122
121
|
});
|
|
123
122
|
test('Escapes front slashes in a regex pattern', () => {
|
|
124
123
|
const regexPattern = '^(?:/(_next/data/[^/]{1,}))?(?:/([^/.]{1,}))/shows(?:/(.*))(.json)?[/#\\?]?$';
|
|
125
124
|
const expected = '^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/([^/.]{1,}))\\/shows(?:\\/(.*))(.json)?[/#\\?]?$';
|
|
126
|
-
expect(
|
|
127
|
-
expect(parsePattern(regexPattern, true)).toEqual(expected);
|
|
125
|
+
expect(normalizePattern(regexPattern)).toEqual(expected);
|
|
128
126
|
});
|
|
129
127
|
test('Ensures pattern match on the whole path', () => {
|
|
130
128
|
const regexPattern = '/foo/.*/bar';
|
|
131
129
|
const expected = '^\\/foo\\/.*\\/bar$';
|
|
132
|
-
expect(
|
|
133
|
-
expect(parsePattern(regexPattern, true)).toEqual(expected);
|
|
130
|
+
expect(normalizePattern(regexPattern)).toEqual(expected);
|
|
134
131
|
});
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
declare const defaultFlags: {
|
|
2
|
-
edge_bundler_pcre_regexp: boolean;
|
|
3
|
-
};
|
|
1
|
+
declare const defaultFlags: {};
|
|
4
2
|
type FeatureFlag = keyof typeof defaultFlags;
|
|
5
3
|
type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
|
|
6
|
-
declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
7
|
-
edge_bundler_pcre_regexp: boolean;
|
|
8
|
-
}) => FeatureFlags;
|
|
4
|
+
declare const getFlags: (input?: Record<string, boolean>, flags?: {}) => FeatureFlags;
|
|
9
5
|
export { defaultFlags, getFlags };
|
|
10
6
|
export type { FeatureFlag, FeatureFlags };
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
const defaultFlags = {
|
|
2
|
-
edge_bundler_pcre_regexp: false,
|
|
3
|
-
};
|
|
1
|
+
const defaultFlags = {};
|
|
4
2
|
const getFlags = (input = {}, flags = defaultFlags) => Object.entries(flags).reduce((result, [key, defaultValue]) => ({
|
|
5
3
|
...result,
|
|
6
4
|
[key]: input[key] === undefined ? defaultValue : input[key],
|
package/dist/node/manifest.d.ts
CHANGED
|
@@ -11,11 +11,30 @@ interface Route {
|
|
|
11
11
|
path?: string;
|
|
12
12
|
methods?: string[];
|
|
13
13
|
}
|
|
14
|
+
interface TrafficRules {
|
|
15
|
+
action: {
|
|
16
|
+
type: string;
|
|
17
|
+
config: {
|
|
18
|
+
rate_limit_config: {
|
|
19
|
+
algorithm: string;
|
|
20
|
+
window_size: number;
|
|
21
|
+
window_limit: number;
|
|
22
|
+
};
|
|
23
|
+
aggregate: {
|
|
24
|
+
keys: {
|
|
25
|
+
type: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
to?: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
14
32
|
export interface EdgeFunctionConfig {
|
|
15
33
|
excluded_patterns: string[];
|
|
16
34
|
on_error?: string;
|
|
17
35
|
generator?: string;
|
|
18
36
|
name?: string;
|
|
37
|
+
traffic_rules?: TrafficRules;
|
|
19
38
|
}
|
|
20
39
|
interface Manifest {
|
|
21
40
|
bundler_version: string;
|
|
@@ -42,7 +61,7 @@ interface GenerateManifestOptions {
|
|
|
42
61
|
layers?: Layer[];
|
|
43
62
|
userFunctionConfig?: Record<string, FunctionConfig>;
|
|
44
63
|
}
|
|
45
|
-
declare const generateManifest: ({ bundles, declarations,
|
|
64
|
+
declare const generateManifest: ({ bundles, declarations, functions, userFunctionConfig, internalFunctionConfig, importMap, layers, }: GenerateManifestOptions) => {
|
|
46
65
|
declarationsWithoutFunction: string[];
|
|
47
66
|
manifest: Manifest;
|
|
48
67
|
unroutedFunctions: string[];
|
package/dist/node/manifest.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { wrapBundleError } from './bundle_error.js';
|
|
4
|
-
import {
|
|
4
|
+
import { normalizePattern } from './declaration.js';
|
|
5
5
|
import { getPackageVersion } from './package_json.js';
|
|
6
|
+
import { RateLimitAction, RateLimitAlgorithm, RateLimitAggregator } from './rate_limit.js';
|
|
6
7
|
import { nonNullable } from './utils/non_nullable.js';
|
|
7
8
|
import { ExtendedURLPattern } from './utils/urlpattern.js';
|
|
8
9
|
const removeEmptyConfigValues = (functionConfig) => Object.entries(functionConfig).reduce((acc, [key, value]) => {
|
|
@@ -46,27 +47,36 @@ const normalizeMethods = (method, name) => {
|
|
|
46
47
|
return method.toUpperCase();
|
|
47
48
|
});
|
|
48
49
|
};
|
|
49
|
-
const generateManifest = ({ bundles = [], declarations = [],
|
|
50
|
+
const generateManifest = ({ bundles = [], declarations = [], functions, userFunctionConfig = {}, internalFunctionConfig = {}, importMap, layers = [], }) => {
|
|
50
51
|
const preCacheRoutes = [];
|
|
51
52
|
const postCacheRoutes = [];
|
|
52
53
|
const manifestFunctionConfig = Object.fromEntries(functions.map(({ name }) => [name, { excluded_patterns: [] }]));
|
|
53
54
|
const routedFunctions = new Set();
|
|
54
55
|
const declarationsWithoutFunction = new Set();
|
|
55
|
-
for (const [name, { excludedPath, onError }] of Object.entries(userFunctionConfig)) {
|
|
56
|
+
for (const [name, { excludedPath, onError, rateLimit }] of Object.entries(userFunctionConfig)) {
|
|
56
57
|
// If the config block is for a function that is not defined, discard it.
|
|
57
58
|
if (manifestFunctionConfig[name] === undefined) {
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
60
61
|
addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
|
|
61
|
-
manifestFunctionConfig[name] = {
|
|
62
|
+
manifestFunctionConfig[name] = {
|
|
63
|
+
...manifestFunctionConfig[name],
|
|
64
|
+
on_error: onError,
|
|
65
|
+
traffic_rules: getTrafficRulesConfig(rateLimit),
|
|
66
|
+
};
|
|
62
67
|
}
|
|
63
|
-
for (const [name, { excludedPath, path, onError, ...rest }] of Object.entries(internalFunctionConfig)) {
|
|
68
|
+
for (const [name, { excludedPath, path, onError, rateLimit, ...rest }] of Object.entries(internalFunctionConfig)) {
|
|
64
69
|
// If the config block is for a function that is not defined, discard it.
|
|
65
70
|
if (manifestFunctionConfig[name] === undefined) {
|
|
66
71
|
continue;
|
|
67
72
|
}
|
|
68
73
|
addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
|
|
69
|
-
manifestFunctionConfig[name] = {
|
|
74
|
+
manifestFunctionConfig[name] = {
|
|
75
|
+
...manifestFunctionConfig[name],
|
|
76
|
+
on_error: onError,
|
|
77
|
+
traffic_rules: getTrafficRulesConfig(rateLimit),
|
|
78
|
+
...rest,
|
|
79
|
+
};
|
|
70
80
|
}
|
|
71
81
|
declarations.forEach((declaration) => {
|
|
72
82
|
const func = functions.find(({ name }) => declaration.function === name);
|
|
@@ -74,14 +84,14 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
74
84
|
declarationsWithoutFunction.add(declaration.function);
|
|
75
85
|
return;
|
|
76
86
|
}
|
|
77
|
-
const pattern = getRegularExpression(declaration
|
|
87
|
+
const pattern = getRegularExpression(declaration);
|
|
78
88
|
// If there is no `pattern`, the declaration will never be triggered, so we
|
|
79
89
|
// can discard it.
|
|
80
90
|
if (!pattern) {
|
|
81
91
|
return;
|
|
82
92
|
}
|
|
83
93
|
routedFunctions.add(declaration.function);
|
|
84
|
-
const excludedPattern = getExcludedRegularExpressions(declaration
|
|
94
|
+
const excludedPattern = getExcludedRegularExpressions(declaration);
|
|
85
95
|
const route = {
|
|
86
96
|
function: func.name,
|
|
87
97
|
pattern: serializePattern(pattern),
|
|
@@ -116,6 +126,29 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
116
126
|
const unroutedFunctions = functions.filter(({ name }) => !routedFunctions.has(name)).map(({ name }) => name);
|
|
117
127
|
return { declarationsWithoutFunction: [...declarationsWithoutFunction], manifest, unroutedFunctions };
|
|
118
128
|
};
|
|
129
|
+
const getTrafficRulesConfig = (rl) => {
|
|
130
|
+
if (rl === undefined) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const rateLimitAgg = Array.isArray(rl.aggregateBy) ? rl.aggregateBy : [RateLimitAggregator.Domain];
|
|
134
|
+
const rewriteConfig = 'to' in rl && typeof rl.to === 'string' ? { to: rl.to } : undefined;
|
|
135
|
+
return {
|
|
136
|
+
action: {
|
|
137
|
+
type: rl.action || RateLimitAction.Limit,
|
|
138
|
+
config: {
|
|
139
|
+
...rewriteConfig,
|
|
140
|
+
rate_limit_config: {
|
|
141
|
+
window_limit: rl.windowLimit,
|
|
142
|
+
window_size: rl.windowSize,
|
|
143
|
+
algorithm: RateLimitAlgorithm.SlidingWindow,
|
|
144
|
+
},
|
|
145
|
+
aggregate: {
|
|
146
|
+
keys: rateLimitAgg.map((agg) => ({ type: agg })),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
};
|
|
119
152
|
const pathToRegularExpression = (path) => {
|
|
120
153
|
if (!path) {
|
|
121
154
|
return null;
|
|
@@ -135,10 +168,10 @@ const pathToRegularExpression = (path) => {
|
|
|
135
168
|
throw wrapBundleError(error);
|
|
136
169
|
}
|
|
137
170
|
};
|
|
138
|
-
const getRegularExpression = (declaration
|
|
171
|
+
const getRegularExpression = (declaration) => {
|
|
139
172
|
if ('pattern' in declaration) {
|
|
140
173
|
try {
|
|
141
|
-
return
|
|
174
|
+
return normalizePattern(declaration.pattern);
|
|
142
175
|
}
|
|
143
176
|
catch (error) {
|
|
144
177
|
throw wrapBundleError(new Error(`Could not parse path declaration of function '${declaration.function}': ${error.message}`));
|
|
@@ -146,14 +179,14 @@ const getRegularExpression = (declaration, pcreRegexpEngine) => {
|
|
|
146
179
|
}
|
|
147
180
|
return pathToRegularExpression(declaration.path);
|
|
148
181
|
};
|
|
149
|
-
const getExcludedRegularExpressions = (declaration
|
|
182
|
+
const getExcludedRegularExpressions = (declaration) => {
|
|
150
183
|
if ('excludedPattern' in declaration && declaration.excludedPattern) {
|
|
151
184
|
const excludedPatterns = Array.isArray(declaration.excludedPattern)
|
|
152
185
|
? declaration.excludedPattern
|
|
153
186
|
: [declaration.excludedPattern];
|
|
154
187
|
return excludedPatterns.map((excludedPattern) => {
|
|
155
188
|
try {
|
|
156
|
-
return
|
|
189
|
+
return normalizePattern(excludedPattern);
|
|
157
190
|
}
|
|
158
191
|
catch (error) {
|
|
159
192
|
throw wrapBundleError(new Error(`Could not parse path declaration of function '${declaration.function}': ${error.message}`));
|
|
@@ -4,6 +4,7 @@ import { getRouteMatcher } from '../test/util.js';
|
|
|
4
4
|
import { BundleFormat } from './bundle.js';
|
|
5
5
|
import { BundleError } from './bundle_error.js';
|
|
6
6
|
import { generateManifest } from './manifest.js';
|
|
7
|
+
import { RateLimitAction, RateLimitAggregator } from './rate_limit.js';
|
|
7
8
|
test('Generates a manifest with different bundles', () => {
|
|
8
9
|
const bundle1 = {
|
|
9
10
|
extension: '.ext1',
|
|
@@ -386,22 +387,12 @@ test('Generates a manifest with layers', () => {
|
|
|
386
387
|
expect(manifest2.routes).toEqual(expectedRoutes);
|
|
387
388
|
expect(manifest2.layers).toEqual(layers);
|
|
388
389
|
});
|
|
389
|
-
test('
|
|
390
|
-
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
391
|
-
const declarations = [{ function: 'func-1', pattern: '^\\/((?!api|_next\\/static|_next\\/image|favicon.ico).*)$' }];
|
|
392
|
-
expect(() => generateManifest({
|
|
393
|
-
bundles: [],
|
|
394
|
-
declarations,
|
|
395
|
-
functions,
|
|
396
|
-
})).toThrowError(/^Could not parse path declaration of function 'func-1': Regular expressions with lookaheads are not supported$/);
|
|
397
|
-
});
|
|
398
|
-
test('Accepts regular expressions with lookaheads if support for the PCRE engine is enabled', () => {
|
|
390
|
+
test('Accepts regular expressions with lookaheads', () => {
|
|
399
391
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
400
392
|
const declarations = [{ function: 'func-1', pattern: '^\\/((?!api|_next\\/static|_next\\/image|favicon.ico).*)$' }];
|
|
401
393
|
const { manifest } = generateManifest({
|
|
402
394
|
bundles: [],
|
|
403
395
|
declarations,
|
|
404
|
-
featureFlags: { edge_bundler_pcre_regexp: true },
|
|
405
396
|
functions,
|
|
406
397
|
});
|
|
407
398
|
const [route] = manifest.routes;
|
|
@@ -409,11 +400,11 @@ test('Accepts regular expressions with lookaheads if support for the PCRE engine
|
|
|
409
400
|
expect(regexp.test('/foo')).toBeTruthy();
|
|
410
401
|
expect(regexp.test('/_next/static/foo')).toBeFalsy();
|
|
411
402
|
});
|
|
412
|
-
test('
|
|
403
|
+
test('Accepts regular expressions with named capture groups', () => {
|
|
413
404
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
414
405
|
const declarations = [{ function: 'func-1', pattern: '^/(?<name>\\w+)$' }];
|
|
415
406
|
const { manifest } = generateManifest({ bundles: [], declarations, functions });
|
|
416
|
-
expect(manifest.routes).toEqual([{ function: 'func-1', pattern: '^/(
|
|
407
|
+
expect(manifest.routes).toEqual([{ function: 'func-1', pattern: '^/(?<name>\\w+)$', excluded_patterns: [] }]);
|
|
417
408
|
});
|
|
418
409
|
test('Returns functions without a declaration and unrouted functions', () => {
|
|
419
410
|
const bundle = {
|
|
@@ -443,3 +434,82 @@ test('Returns functions without a declaration and unrouted functions', () => {
|
|
|
443
434
|
expect(declarationsWithoutFunction).toEqual(['func-3']);
|
|
444
435
|
expect(unroutedFunctions).toEqual(['func-2', 'func-4']);
|
|
445
436
|
});
|
|
437
|
+
test('Generates a manifest with rate limit config', () => {
|
|
438
|
+
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
439
|
+
const declarations = [{ function: 'func-1', path: '/f1/*' }];
|
|
440
|
+
const userFunctionConfig = {
|
|
441
|
+
'func-1': { rateLimit: { windowLimit: 100, windowSize: 60 } },
|
|
442
|
+
};
|
|
443
|
+
const { manifest } = generateManifest({
|
|
444
|
+
bundles: [],
|
|
445
|
+
declarations,
|
|
446
|
+
functions,
|
|
447
|
+
userFunctionConfig,
|
|
448
|
+
});
|
|
449
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
450
|
+
const expectedFunctionConfig = {
|
|
451
|
+
'func-1': {
|
|
452
|
+
traffic_rules: {
|
|
453
|
+
action: {
|
|
454
|
+
type: 'rate_limit',
|
|
455
|
+
config: {
|
|
456
|
+
rate_limit_config: {
|
|
457
|
+
window_limit: 100,
|
|
458
|
+
window_size: 60,
|
|
459
|
+
algorithm: 'sliding_window',
|
|
460
|
+
},
|
|
461
|
+
aggregate: {
|
|
462
|
+
keys: [{ type: 'domain' }],
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
470
|
+
expect(manifest.function_config).toEqual(expectedFunctionConfig);
|
|
471
|
+
});
|
|
472
|
+
test('Generates a manifest with rewrite config', () => {
|
|
473
|
+
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
474
|
+
const declarations = [{ function: 'func-1', path: '/f1/*' }];
|
|
475
|
+
const userFunctionConfig = {
|
|
476
|
+
'func-1': {
|
|
477
|
+
rateLimit: {
|
|
478
|
+
action: RateLimitAction.Rewrite,
|
|
479
|
+
to: '/new_path',
|
|
480
|
+
windowLimit: 100,
|
|
481
|
+
windowSize: 60,
|
|
482
|
+
aggregateBy: [RateLimitAggregator.Domain, RateLimitAggregator.IP],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
const { manifest } = generateManifest({
|
|
487
|
+
bundles: [],
|
|
488
|
+
declarations,
|
|
489
|
+
functions,
|
|
490
|
+
userFunctionConfig,
|
|
491
|
+
});
|
|
492
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
493
|
+
const expectedFunctionConfig = {
|
|
494
|
+
'func-1': {
|
|
495
|
+
traffic_rules: {
|
|
496
|
+
action: {
|
|
497
|
+
type: 'rewrite',
|
|
498
|
+
config: {
|
|
499
|
+
to: '/new_path',
|
|
500
|
+
rate_limit_config: {
|
|
501
|
+
window_limit: 100,
|
|
502
|
+
window_size: 60,
|
|
503
|
+
algorithm: 'sliding_window',
|
|
504
|
+
},
|
|
505
|
+
aggregate: {
|
|
506
|
+
keys: [{ type: 'domain' }, { type: 'ip' }],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
expect(manifest.routes).toEqual(expectedRoutes);
|
|
514
|
+
expect(manifest.function_config).toEqual(expectedFunctionConfig);
|
|
515
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare enum RateLimitAlgorithm {
|
|
2
|
+
SlidingWindow = "sliding_window"
|
|
3
|
+
}
|
|
4
|
+
export declare enum RateLimitAggregator {
|
|
5
|
+
Domain = "domain",
|
|
6
|
+
IP = "ip"
|
|
7
|
+
}
|
|
8
|
+
export declare enum RateLimitAction {
|
|
9
|
+
Limit = "rate_limit",
|
|
10
|
+
Rewrite = "rewrite"
|
|
11
|
+
}
|
|
12
|
+
interface SlidingWindow {
|
|
13
|
+
windowLimit: number;
|
|
14
|
+
windowSize: number;
|
|
15
|
+
}
|
|
16
|
+
export type RewriteActionConfig = SlidingWindow & {
|
|
17
|
+
to: string;
|
|
18
|
+
};
|
|
19
|
+
interface RateLimitConfig {
|
|
20
|
+
action?: RateLimitAction;
|
|
21
|
+
aggregateBy?: RateLimitAggregator | RateLimitAggregator[];
|
|
22
|
+
algorithm?: RateLimitAlgorithm;
|
|
23
|
+
}
|
|
24
|
+
export type RateLimit = RateLimitConfig & (SlidingWindow | RewriteActionConfig);
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export var RateLimitAlgorithm;
|
|
2
|
+
(function (RateLimitAlgorithm) {
|
|
3
|
+
RateLimitAlgorithm["SlidingWindow"] = "sliding_window";
|
|
4
|
+
})(RateLimitAlgorithm || (RateLimitAlgorithm = {}));
|
|
5
|
+
export var RateLimitAggregator;
|
|
6
|
+
(function (RateLimitAggregator) {
|
|
7
|
+
RateLimitAggregator["Domain"] = "domain";
|
|
8
|
+
RateLimitAggregator["IP"] = "ip";
|
|
9
|
+
})(RateLimitAggregator || (RateLimitAggregator = {}));
|
|
10
|
+
export var RateLimitAction;
|
|
11
|
+
(function (RateLimitAction) {
|
|
12
|
+
RateLimitAction["Limit"] = "rate_limit";
|
|
13
|
+
RateLimitAction["Rewrite"] = "rewrite";
|
|
14
|
+
})(RateLimitAction || (RateLimitAction = {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.4.0",
|
|
4
4
|
"description": "Intelligently prepare Netlify Edge Functions for deployment",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/node/index.js",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"better-ajv-errors": "^1.2.0",
|
|
82
82
|
"common-path-prefix": "^3.0.0",
|
|
83
83
|
"env-paths": "^3.0.0",
|
|
84
|
-
"esbuild": "0.
|
|
84
|
+
"esbuild": "0.20.2",
|
|
85
85
|
"execa": "^6.0.0",
|
|
86
86
|
"find-up": "^6.3.0",
|
|
87
87
|
"get-package-name": "^2.2.0",
|
|
@@ -93,7 +93,6 @@
|
|
|
93
93
|
"p-retry": "^5.1.1",
|
|
94
94
|
"p-wait-for": "^4.1.0",
|
|
95
95
|
"path-key": "^4.0.0",
|
|
96
|
-
"regexp-tree": "^0.1.24",
|
|
97
96
|
"semver": "^7.3.8",
|
|
98
97
|
"tmp-promise": "^3.0.3",
|
|
99
98
|
"urlpattern-polyfill": "8.0.2",
|