@netlify/edge-bundler 8.17.1 → 8.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno/lib/consts.ts +2 -1
- package/deno/lib/stage2.ts +7 -2
- package/dist/node/config.d.ts +2 -0
- package/dist/node/config.test.js +46 -8
- package/dist/node/declaration.d.ts +2 -1
- package/dist/node/declaration.js +4 -1
- package/dist/node/feature_flags.d.ts +0 -2
- package/dist/node/feature_flags.js +0 -1
- package/dist/node/formats/eszip.js +7 -2
- package/dist/node/import_map.d.ts +18 -15
- package/dist/node/import_map.js +110 -88
- package/dist/node/import_map.test.js +63 -19
- package/dist/node/manifest.d.ts +2 -0
- package/dist/node/manifest.js +31 -17
- package/dist/node/manifest.test.js +60 -21
- package/dist/node/validation/manifest/schema.d.ts +20 -0
- package/dist/node/validation/manifest/schema.js +5 -0
- package/package.json +3 -4
package/deno/lib/consts.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const LEGACY_PUBLIC_SPECIFIER = 'netlify:edge'
|
|
2
|
+
export const PUBLIC_SPECIFIER = '@netlify/edge-functions'
|
|
2
3
|
export const STAGE1_SPECIFIER = 'netlify:bootstrap-stage1'
|
|
3
4
|
export const STAGE2_SPECIFIER = 'netlify:bootstrap-stage2'
|
|
4
5
|
export const virtualRoot = 'file:///root/'
|
package/deno/lib/stage2.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'https://deno.land/std@0.177.0/path/mod.ts'
|
|
|
4
4
|
|
|
5
5
|
import type { InputFunction, WriteStage2Options } from '../../shared/stage2.ts'
|
|
6
6
|
import { importMapSpecifier, virtualRoot } from '../../shared/consts.ts'
|
|
7
|
-
import { PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts'
|
|
7
|
+
import { LEGACY_PUBLIC_SPECIFIER, PUBLIC_SPECIFIER, STAGE2_SPECIFIER } from './consts.ts'
|
|
8
8
|
import { inlineModule, loadFromVirtualRoot, loadWithRetry } from './common.ts'
|
|
9
9
|
|
|
10
10
|
interface FunctionReference {
|
|
@@ -75,7 +75,12 @@ const stage2Loader = (basePath: string, functions: InputFunction[], externals: S
|
|
|
75
75
|
return inlineModule(specifier, importMapData)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
if (
|
|
78
|
+
if (
|
|
79
|
+
specifier === LEGACY_PUBLIC_SPECIFIER ||
|
|
80
|
+
specifier === PUBLIC_SPECIFIER ||
|
|
81
|
+
externals.has(specifier) ||
|
|
82
|
+
specifier.startsWith('node:')
|
|
83
|
+
) {
|
|
79
84
|
return {
|
|
80
85
|
kind: 'external',
|
|
81
86
|
specifier,
|
package/dist/node/config.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare const enum Cache {
|
|
|
6
6
|
Off = "off",
|
|
7
7
|
Manual = "manual"
|
|
8
8
|
}
|
|
9
|
+
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
|
9
10
|
export type Path = `/${string}`;
|
|
10
11
|
export type OnError = 'fail' | 'bypass' | Path;
|
|
11
12
|
export declare const isValidOnError: (value: unknown) => value is OnError;
|
|
@@ -16,6 +17,7 @@ export interface FunctionConfig {
|
|
|
16
17
|
onError?: OnError;
|
|
17
18
|
name?: string;
|
|
18
19
|
generator?: string;
|
|
20
|
+
method?: HTTPMethod | HTTPMethod[];
|
|
19
21
|
}
|
|
20
22
|
export declare const getFunctionConfig: ({ func, importMap, deno, bootstrapURL, log, }: {
|
|
21
23
|
func: EdgeFunction;
|
package/dist/node/config.test.js
CHANGED
|
@@ -151,12 +151,12 @@ test('Loads function paths from the in-source `config` function', async () => {
|
|
|
151
151
|
{
|
|
152
152
|
function: 'user-func2',
|
|
153
153
|
path: '/user-func2',
|
|
154
|
+
method: ['PATCH'],
|
|
154
155
|
},
|
|
155
156
|
];
|
|
156
157
|
const result = await bundle([internalDirectory, userDirectory], distPath, declarations, {
|
|
157
158
|
basePath,
|
|
158
159
|
configPath: join(internalDirectory, 'config.json'),
|
|
159
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
160
160
|
});
|
|
161
161
|
const generatedFiles = await fs.readdir(distPath);
|
|
162
162
|
expect(result.functions.length).toBe(7);
|
|
@@ -168,14 +168,52 @@ test('Loads function paths from the in-source `config` function', async () => {
|
|
|
168
168
|
expect(bundles[0].format).toBe('eszip2');
|
|
169
169
|
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
170
170
|
expect(routes.length).toBe(6);
|
|
171
|
-
expect(routes[0]).toEqual({
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
expect(routes[0]).toEqual({
|
|
172
|
+
function: 'framework-func2',
|
|
173
|
+
pattern: '^/framework-func2/?$',
|
|
174
|
+
excluded_patterns: [],
|
|
175
|
+
path: '/framework-func2',
|
|
176
|
+
});
|
|
177
|
+
expect(routes[1]).toEqual({
|
|
178
|
+
function: 'user-func2',
|
|
179
|
+
pattern: '^/user-func2/?$',
|
|
180
|
+
excluded_patterns: [],
|
|
181
|
+
path: '/user-func2',
|
|
182
|
+
methods: ['PATCH'],
|
|
183
|
+
});
|
|
184
|
+
expect(routes[2]).toEqual({
|
|
185
|
+
function: 'framework-func1',
|
|
186
|
+
pattern: '^/framework-func1/?$',
|
|
187
|
+
excluded_patterns: [],
|
|
188
|
+
path: '/framework-func1',
|
|
189
|
+
});
|
|
190
|
+
expect(routes[3]).toEqual({
|
|
191
|
+
function: 'user-func1',
|
|
192
|
+
pattern: '^/user-func1/?$',
|
|
193
|
+
excluded_patterns: [],
|
|
194
|
+
path: '/user-func1',
|
|
195
|
+
});
|
|
196
|
+
expect(routes[4]).toEqual({
|
|
197
|
+
function: 'user-func3',
|
|
198
|
+
pattern: '^/user-func3/?$',
|
|
199
|
+
excluded_patterns: [],
|
|
200
|
+
path: '/user-func3',
|
|
201
|
+
});
|
|
202
|
+
expect(routes[5]).toEqual({
|
|
203
|
+
function: 'user-func5',
|
|
204
|
+
pattern: '^/user-func5(?:/(.*))/?$',
|
|
205
|
+
excluded_patterns: [],
|
|
206
|
+
path: '/user-func5/*',
|
|
207
|
+
methods: ['GET'],
|
|
208
|
+
});
|
|
177
209
|
expect(postCacheRoutes.length).toBe(1);
|
|
178
|
-
expect(postCacheRoutes[0]).toEqual({
|
|
210
|
+
expect(postCacheRoutes[0]).toEqual({
|
|
211
|
+
function: 'user-func4',
|
|
212
|
+
pattern: '^/user-func4/?$',
|
|
213
|
+
excluded_patterns: [],
|
|
214
|
+
path: '/user-func4',
|
|
215
|
+
methods: ['POST', 'PUT'],
|
|
216
|
+
});
|
|
179
217
|
expect(Object.keys(functionConfig)).toHaveLength(1);
|
|
180
218
|
expect(functionConfig['user-func5']).toEqual({
|
|
181
219
|
excluded_patterns: ['^/user-func5/excluded/?$'],
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { FunctionConfig, Path } from './config.js';
|
|
1
|
+
import { FunctionConfig, HTTPMethod, Path } from './config.js';
|
|
2
2
|
import { FeatureFlags } from './feature_flags.js';
|
|
3
3
|
interface BaseDeclaration {
|
|
4
4
|
cache?: string;
|
|
5
5
|
function: string;
|
|
6
|
+
method?: HTTPMethod | HTTPMethod[];
|
|
6
7
|
name?: string;
|
|
7
8
|
generator?: string;
|
|
8
9
|
}
|
package/dist/node/declaration.js
CHANGED
|
@@ -48,7 +48,7 @@ const getDeclarationsFromInput = (inputDeclarations, functionConfigs, functionsV
|
|
|
48
48
|
const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited) => {
|
|
49
49
|
const declarations = [];
|
|
50
50
|
for (const name in functionConfigs) {
|
|
51
|
-
const { cache, path } = functionConfigs[name];
|
|
51
|
+
const { cache, path, method } = functionConfigs[name];
|
|
52
52
|
// If we have a path specified, create a declaration for each path.
|
|
53
53
|
if (!functionsVisited.has(name) && path) {
|
|
54
54
|
const paths = Array.isArray(path) ? path : [path];
|
|
@@ -57,6 +57,9 @@ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited
|
|
|
57
57
|
if (cache) {
|
|
58
58
|
declaration.cache = cache;
|
|
59
59
|
}
|
|
60
|
+
if (method) {
|
|
61
|
+
declaration.method = method;
|
|
62
|
+
}
|
|
60
63
|
declarations.push(declaration);
|
|
61
64
|
});
|
|
62
65
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
declare const defaultFlags: {
|
|
2
2
|
edge_functions_fail_unsupported_regex: boolean;
|
|
3
|
-
edge_functions_path_urlpattern: boolean;
|
|
4
3
|
};
|
|
5
4
|
type FeatureFlag = keyof typeof defaultFlags;
|
|
6
5
|
type FeatureFlags = Partial<Record<FeatureFlag, boolean>>;
|
|
7
6
|
declare const getFlags: (input?: Record<string, boolean>, flags?: {
|
|
8
7
|
edge_functions_fail_unsupported_regex: boolean;
|
|
9
|
-
edge_functions_path_urlpattern: boolean;
|
|
10
8
|
}) => FeatureFlags;
|
|
11
9
|
export { defaultFlags, getFlags };
|
|
12
10
|
export type { FeatureFlag, FeatureFlags };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
2
3
|
import { virtualRoot } from '../../shared/consts.js';
|
|
3
4
|
import { BundleFormat } from '../bundle.js';
|
|
4
5
|
import { wrapBundleError } from '../bundle_error.js';
|
|
@@ -9,13 +10,17 @@ const bundleESZIP = async ({ basePath, buildID, debug, deno, distDirectory, exte
|
|
|
9
10
|
const extension = '.eszip';
|
|
10
11
|
const destPath = join(distDirectory, `${buildID}${extension}`);
|
|
11
12
|
const { bundler, importMap: bundlerImportMap } = getESZIPPaths();
|
|
12
|
-
|
|
13
|
+
// Transforming all paths under `basePath` to use the virtual root prefix.
|
|
14
|
+
const importMapPrefixes = {
|
|
15
|
+
[`${pathToFileURL(basePath)}/`]: virtualRoot,
|
|
16
|
+
};
|
|
17
|
+
const importMapData = importMap.getContents(importMapPrefixes);
|
|
13
18
|
const payload = {
|
|
14
19
|
basePath,
|
|
15
20
|
destPath,
|
|
16
21
|
externals,
|
|
17
22
|
functions,
|
|
18
|
-
importMapData,
|
|
23
|
+
importMapData: JSON.stringify(importMapData),
|
|
19
24
|
};
|
|
20
25
|
const flags = ['--allow-all', '--no-config', `--import-map=${bundlerImportMap}`];
|
|
21
26
|
if (!debug) {
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
+
import { ParsedImportMap } from '@import-maps/resolve';
|
|
1
2
|
import { Logger } from './logger.js';
|
|
2
3
|
type Imports = Record<string, string>;
|
|
3
|
-
interface
|
|
4
|
+
export interface ImportMapFile {
|
|
4
5
|
baseURL: URL;
|
|
5
6
|
imports: Imports;
|
|
6
7
|
scopes?: Record<string, Imports>;
|
|
7
8
|
}
|
|
8
|
-
declare class ImportMap {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
scopes: Record<string, Imports>;
|
|
14
|
-
};
|
|
15
|
-
static resolveImports(imports: Record<string, URL | null>, basePath?: string, prefix?: string): Record<string, string>;
|
|
16
|
-
static resolvePath(url: URL, basePath?: string, prefix?: string): string;
|
|
17
|
-
add(source: ImportMapSource): void;
|
|
9
|
+
export declare class ImportMap {
|
|
10
|
+
rootPath: string | null;
|
|
11
|
+
sources: ImportMapFile[];
|
|
12
|
+
constructor(sources?: ImportMapFile[], rootURL?: string | null);
|
|
13
|
+
add(source: ImportMapFile): void;
|
|
18
14
|
addFile(path: string, logger: Logger): Promise<void>;
|
|
19
15
|
addFiles(paths: (string | undefined)[], logger: Logger): Promise<void>;
|
|
20
|
-
|
|
16
|
+
static applyPrefixesToImports(imports: Imports, prefixes: Record<string, string>): Imports;
|
|
17
|
+
static applyPrefixesToPath(path: string, prefixes: Record<string, string>): string;
|
|
18
|
+
filterImports(imports?: Record<string, URL | null>): Record<string, string>;
|
|
19
|
+
filterScopes(scopes?: ParsedImportMap['scopes']): Record<string, Imports>;
|
|
20
|
+
getContents(prefixes?: Record<string, string>): {
|
|
21
21
|
imports: Imports;
|
|
22
|
+
scopes: {};
|
|
23
|
+
};
|
|
24
|
+
static readFile(path: string, logger: Logger): Promise<ImportMapFile>;
|
|
25
|
+
resolve(source: ImportMapFile): {
|
|
26
|
+
imports: Record<string, string>;
|
|
22
27
|
scopes: Record<string, Imports>;
|
|
23
28
|
};
|
|
24
29
|
toDataURL(): string;
|
|
25
30
|
writeToFile(path: string): Promise<void>;
|
|
26
31
|
}
|
|
27
|
-
|
|
28
|
-
export { ImportMap, readFile };
|
|
29
|
-
export type { ImportMapSource as ImportMapFile };
|
|
32
|
+
export {};
|
package/dist/node/import_map.js
CHANGED
|
@@ -1,82 +1,28 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
|
-
import { dirname,
|
|
3
|
+
import { dirname, relative } from 'path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
5
|
import { parse } from '@import-maps/resolve';
|
|
6
6
|
import { isFileNotFoundError } from './utils/error.js';
|
|
7
7
|
const INTERNAL_IMPORTS = {
|
|
8
|
+
'@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
|
|
8
9
|
'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
|
|
9
10
|
};
|
|
10
11
|
// ImportMap can take several import map files and merge them into a final
|
|
11
12
|
// import map object, also adding the internal imports in the right order.
|
|
12
|
-
class ImportMap {
|
|
13
|
-
constructor(sources = []) {
|
|
13
|
+
export class ImportMap {
|
|
14
|
+
constructor(sources = [], rootURL = null) {
|
|
15
|
+
this.rootPath = rootURL ? fileURLToPath(rootURL) : null;
|
|
14
16
|
this.sources = [];
|
|
15
17
|
sources.forEach((file) => {
|
|
16
18
|
this.add(file);
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
|
-
// Transforms an import map by making any relative paths use a different path
|
|
20
|
-
// as a base.
|
|
21
|
-
static resolve(source, basePath, prefix = 'file://') {
|
|
22
|
-
const { baseURL, ...importMap } = source;
|
|
23
|
-
const parsedImportMap = parse(importMap, baseURL);
|
|
24
|
-
const { imports = {}, scopes = {} } = parsedImportMap;
|
|
25
|
-
const resolvedImports = ImportMap.resolveImports(imports, basePath, prefix);
|
|
26
|
-
const resolvedScopes = {};
|
|
27
|
-
Object.keys(scopes).forEach((path) => {
|
|
28
|
-
const resolvedPath = ImportMap.resolvePath(new URL(path), basePath, prefix);
|
|
29
|
-
resolvedScopes[resolvedPath] = ImportMap.resolveImports(scopes[path], basePath, prefix);
|
|
30
|
-
});
|
|
31
|
-
return { ...parsedImportMap, imports: resolvedImports, scopes: resolvedScopes };
|
|
32
|
-
}
|
|
33
|
-
// Takes an imports object and resolves relative specifiers with a given base
|
|
34
|
-
// path and URL prefix.
|
|
35
|
-
static resolveImports(imports, basePath, prefix) {
|
|
36
|
-
const resolvedImports = {};
|
|
37
|
-
Object.keys(imports).forEach((specifier) => {
|
|
38
|
-
const url = imports[specifier];
|
|
39
|
-
// If there's no URL, don't even add the specifier to the final imports.
|
|
40
|
-
if (url === null) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
// If this is a file URL, we might want to transform it to use another
|
|
44
|
-
// base path.
|
|
45
|
-
if (url.protocol === 'file:') {
|
|
46
|
-
resolvedImports[specifier] = ImportMap.resolvePath(url, basePath, prefix);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
resolvedImports[specifier] = url.toString();
|
|
50
|
-
});
|
|
51
|
-
return resolvedImports;
|
|
52
|
-
}
|
|
53
|
-
// Takes a URL, turns it into a path relative to the given base, and prepends
|
|
54
|
-
// a prefix (such as the virtual root prefix).
|
|
55
|
-
static resolvePath(url, basePath, prefix) {
|
|
56
|
-
if (basePath === undefined) {
|
|
57
|
-
return url.toString();
|
|
58
|
-
}
|
|
59
|
-
const path = fileURLToPath(url);
|
|
60
|
-
const relativePath = relative(basePath, path);
|
|
61
|
-
if (relativePath.startsWith('..')) {
|
|
62
|
-
throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${basePath}'`);
|
|
63
|
-
}
|
|
64
|
-
// We want to use POSIX paths for the import map regardless of the OS
|
|
65
|
-
// we're building in.
|
|
66
|
-
let normalizedPath = relativePath.split(sep).join(posix.sep);
|
|
67
|
-
// If the original URL had a trailing slash, ensure the normalized path
|
|
68
|
-
// has one too.
|
|
69
|
-
if (normalizedPath !== '' && url.pathname.endsWith(posix.sep) && !normalizedPath.endsWith(posix.sep)) {
|
|
70
|
-
normalizedPath += posix.sep;
|
|
71
|
-
}
|
|
72
|
-
const newURL = new URL(normalizedPath, prefix);
|
|
73
|
-
return newURL.toString();
|
|
74
|
-
}
|
|
75
21
|
add(source) {
|
|
76
22
|
this.sources.push(source);
|
|
77
23
|
}
|
|
78
24
|
async addFile(path, logger) {
|
|
79
|
-
const source = await readFile(path, logger);
|
|
25
|
+
const source = await ImportMap.readFile(path, logger);
|
|
80
26
|
if (Object.keys(source.imports).length === 0) {
|
|
81
27
|
return;
|
|
82
28
|
}
|
|
@@ -90,11 +36,73 @@ class ImportMap {
|
|
|
90
36
|
await this.addFile(path, logger);
|
|
91
37
|
}
|
|
92
38
|
}
|
|
93
|
-
|
|
39
|
+
// Applies a list of prefixes to an `imports` block, by transforming values
|
|
40
|
+
// with the `applyPrefixesToPath` method.
|
|
41
|
+
static applyPrefixesToImports(imports, prefixes) {
|
|
42
|
+
return Object.entries(imports).reduce((acc, [key, value]) => ({
|
|
43
|
+
...acc,
|
|
44
|
+
[key]: ImportMap.applyPrefixesToPath(value, prefixes),
|
|
45
|
+
}), {});
|
|
46
|
+
}
|
|
47
|
+
// Applies a list of prefixes to a given path, returning the replaced path.
|
|
48
|
+
// For example, given a `path` of `file:///foo/bar/baz.js` and a `prefixes`
|
|
49
|
+
// object with `{"file:///foo/": "file:///hello/"}`, this method will return
|
|
50
|
+
// `file:///hello/bar/baz.js`. If no matching prefix is found, the original
|
|
51
|
+
// path is returned.
|
|
52
|
+
static applyPrefixesToPath(path, prefixes) {
|
|
53
|
+
for (const prefix in prefixes) {
|
|
54
|
+
if (path.startsWith(prefix)) {
|
|
55
|
+
return path.replace(prefix, prefixes[prefix]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return path;
|
|
59
|
+
}
|
|
60
|
+
// Takes an `imports` object and filters out any entries without a URL. Also,
|
|
61
|
+
// it checks whether the import map is referencing a path outside `rootPath`,
|
|
62
|
+
// if one is set.
|
|
63
|
+
filterImports(imports = {}) {
|
|
64
|
+
const filteredImports = {};
|
|
65
|
+
Object.keys(imports).forEach((specifier) => {
|
|
66
|
+
const url = imports[specifier];
|
|
67
|
+
// If there's no URL, don't even add the specifier to the final imports.
|
|
68
|
+
if (url === null) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (this.rootPath !== null) {
|
|
72
|
+
const path = fileURLToPath(url);
|
|
73
|
+
const relativePath = relative(this.rootPath, path);
|
|
74
|
+
if (relativePath.startsWith('..')) {
|
|
75
|
+
throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${this.rootPath}'`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
filteredImports[specifier] = url.toString();
|
|
79
|
+
});
|
|
80
|
+
return filteredImports;
|
|
81
|
+
}
|
|
82
|
+
// Takes a `scopes` object and runs all imports through `filterImports`,
|
|
83
|
+
// omitting any scopes for which there are no imports.
|
|
84
|
+
filterScopes(scopes) {
|
|
85
|
+
const filteredScopes = {};
|
|
86
|
+
if (scopes !== undefined) {
|
|
87
|
+
Object.keys(scopes).forEach((url) => {
|
|
88
|
+
const imports = this.filterImports(scopes[url]);
|
|
89
|
+
if (Object.keys(imports).length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
filteredScopes[url] = imports;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return filteredScopes;
|
|
96
|
+
}
|
|
97
|
+
// Returns the import map as a plain object, with any relative paths resolved
|
|
98
|
+
// to full URLs. It takes an optional `prefixes` object that specifies a list
|
|
99
|
+
// of prefixes to replace path prefixes (see `applyPrefixesToPath`). Prefixes
|
|
100
|
+
// will be applied on both `imports` and `scopes`.
|
|
101
|
+
getContents(prefixes = {}) {
|
|
94
102
|
let imports = {};
|
|
95
103
|
let scopes = {};
|
|
96
104
|
this.sources.forEach((file) => {
|
|
97
|
-
const importMap =
|
|
105
|
+
const importMap = this.resolve(file);
|
|
98
106
|
imports = { ...imports, ...importMap.imports };
|
|
99
107
|
scopes = { ...scopes, ...importMap.scopes };
|
|
100
108
|
});
|
|
@@ -104,11 +112,49 @@ class ImportMap {
|
|
|
104
112
|
const [specifier, url] = internalImport;
|
|
105
113
|
imports[specifier] = url;
|
|
106
114
|
});
|
|
115
|
+
const transformedImports = ImportMap.applyPrefixesToImports(imports, prefixes);
|
|
116
|
+
const transformedScopes = Object.entries(scopes).reduce((acc, [key, value]) => ({
|
|
117
|
+
...acc,
|
|
118
|
+
[ImportMap.applyPrefixesToPath(key, prefixes)]: ImportMap.applyPrefixesToImports(value, prefixes),
|
|
119
|
+
}), {});
|
|
120
|
+
return {
|
|
121
|
+
imports: transformedImports,
|
|
122
|
+
scopes: transformedScopes,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
static async readFile(path, logger) {
|
|
126
|
+
const baseURL = pathToFileURL(path);
|
|
127
|
+
try {
|
|
128
|
+
const data = await fs.readFile(path, 'utf8');
|
|
129
|
+
const importMap = JSON.parse(data);
|
|
130
|
+
return {
|
|
131
|
+
...importMap,
|
|
132
|
+
baseURL,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (isFileNotFoundError(error)) {
|
|
137
|
+
logger.system(`Did not find an import map file at '${path}'.`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
logger.user(`Error while loading import map at '${path}':`, error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
107
143
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
baseURL,
|
|
145
|
+
imports: {},
|
|
110
146
|
};
|
|
111
147
|
}
|
|
148
|
+
// Resolves an import map file by transforming all relative paths into full
|
|
149
|
+
// URLs. The `baseURL` property of each file is used to resolve all relative
|
|
150
|
+
// paths against.
|
|
151
|
+
resolve(source) {
|
|
152
|
+
const { baseURL, ...importMap } = source;
|
|
153
|
+
const parsedImportMap = parse(importMap, baseURL);
|
|
154
|
+
const imports = this.filterImports(parsedImportMap.imports);
|
|
155
|
+
const scopes = this.filterScopes(parsedImportMap.scopes);
|
|
156
|
+
return { ...parsedImportMap, imports, scopes };
|
|
157
|
+
}
|
|
112
158
|
toDataURL() {
|
|
113
159
|
const data = JSON.stringify(this.getContents());
|
|
114
160
|
const encodedImportMap = Buffer.from(data).toString('base64');
|
|
@@ -121,27 +167,3 @@ class ImportMap {
|
|
|
121
167
|
await fs.writeFile(path, JSON.stringify(contents));
|
|
122
168
|
}
|
|
123
169
|
}
|
|
124
|
-
const readFile = async (path, logger) => {
|
|
125
|
-
const baseURL = pathToFileURL(path);
|
|
126
|
-
try {
|
|
127
|
-
const data = await fs.readFile(path, 'utf8');
|
|
128
|
-
const importMap = JSON.parse(data);
|
|
129
|
-
return {
|
|
130
|
-
...importMap,
|
|
131
|
-
baseURL,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
if (isFileNotFoundError(error)) {
|
|
136
|
-
logger.system(`Did not find an import map file at '${path}'.`);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
logger.user(`Error while loading import map at '${path}':`, error);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
baseURL,
|
|
144
|
-
imports: {},
|
|
145
|
-
};
|
|
146
|
-
};
|
|
147
|
-
export { ImportMap, readFile };
|
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { cwd } from 'process';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
5
|
import tmp from 'tmp-promise';
|
|
6
|
-
import { test, expect } from 'vitest';
|
|
6
|
+
import { describe, test, expect } from 'vitest';
|
|
7
7
|
import { ImportMap } from './import_map.js';
|
|
8
8
|
test('Handles import maps with full URLs without specifying a base URL', () => {
|
|
9
9
|
const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
|
|
@@ -39,35 +39,79 @@ test('Resolves relative paths to absolute paths if a base path is not provided',
|
|
|
39
39
|
expect(imports['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts');
|
|
40
40
|
expect(imports['alias:pets']).toBe(`${pathToFileURL(expectedPath).toString()}/`);
|
|
41
41
|
});
|
|
42
|
-
|
|
43
|
-
const basePath = join(cwd(), 'my-cool-site', 'import-map.json');
|
|
42
|
+
describe('Returns the fully resolved import map', () => {
|
|
44
43
|
const inputFile1 = {
|
|
45
|
-
baseURL:
|
|
44
|
+
baseURL: new URL('file:///some/full/path/import-map.json'),
|
|
46
45
|
imports: {
|
|
47
|
-
|
|
46
|
+
specifier1: 'file:///some/full/path/file.js',
|
|
47
|
+
specifier2: './file2.js',
|
|
48
|
+
specifier3: 'file:///different/full/path/file3.js',
|
|
48
49
|
},
|
|
49
50
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
const inputFile2 = {
|
|
52
|
+
baseURL: new URL('file:///some/cool/path/import-map.json'),
|
|
53
|
+
imports: {
|
|
54
|
+
'lib/*': './library/',
|
|
55
|
+
},
|
|
56
|
+
scopes: {
|
|
57
|
+
'with/scopes/': {
|
|
58
|
+
'lib/*': 'https://external.netlify/lib/',
|
|
59
|
+
foo: './foo-alias',
|
|
60
|
+
bar: 'file:///different/full/path/bar.js',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
test('Without prefixes', () => {
|
|
65
|
+
const map = new ImportMap([inputFile1, inputFile2]);
|
|
66
|
+
const { imports, scopes } = map.getContents();
|
|
67
|
+
expect(imports).toStrictEqual({
|
|
68
|
+
'lib/*': 'file:///some/cool/path/library/',
|
|
69
|
+
specifier3: 'file:///different/full/path/file3.js',
|
|
70
|
+
specifier2: 'file:///some/full/path/file2.js',
|
|
71
|
+
specifier1: 'file:///some/full/path/file.js',
|
|
72
|
+
'@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
|
|
73
|
+
'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
|
|
74
|
+
});
|
|
75
|
+
expect(scopes).toStrictEqual({
|
|
76
|
+
'file:///some/cool/path/with/scopes/': {
|
|
77
|
+
'lib/*': 'https://external.netlify/lib/',
|
|
78
|
+
foo: 'file:///some/cool/path/foo-alias',
|
|
79
|
+
bar: 'file:///different/full/path/bar.js',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
test('With prefixes', () => {
|
|
84
|
+
const map = new ImportMap([inputFile1, inputFile2]);
|
|
85
|
+
const { imports, scopes } = map.getContents({
|
|
86
|
+
'file:///some/': 'file:///root/',
|
|
87
|
+
'file:///different/': 'file:///vendor/',
|
|
88
|
+
});
|
|
89
|
+
expect(imports).toStrictEqual({
|
|
90
|
+
'lib/*': 'file:///root/cool/path/library/',
|
|
91
|
+
specifier3: 'file:///vendor/full/path/file3.js',
|
|
92
|
+
specifier2: 'file:///root/full/path/file2.js',
|
|
93
|
+
specifier1: 'file:///root/full/path/file.js',
|
|
94
|
+
'@netlify/edge-functions': 'https://edge.netlify.com/v1/index.ts',
|
|
95
|
+
'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
|
|
96
|
+
});
|
|
97
|
+
expect(scopes).toStrictEqual({
|
|
98
|
+
'file:///root/cool/path/with/scopes/': {
|
|
99
|
+
'lib/*': 'https://external.netlify/lib/',
|
|
100
|
+
foo: 'file:///root/cool/path/foo-alias',
|
|
101
|
+
bar: 'file:///vendor/full/path/bar.js',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
60
105
|
});
|
|
61
106
|
test('Throws when an import map uses a relative path to reference a file outside of the base path', () => {
|
|
62
|
-
const basePath = join(cwd(), 'my-cool-site');
|
|
63
107
|
const inputFile1 = {
|
|
64
|
-
baseURL: pathToFileURL(join(
|
|
108
|
+
baseURL: pathToFileURL(join(cwd(), 'import-map.json')),
|
|
65
109
|
imports: {
|
|
66
110
|
'alias:file': '../file.js',
|
|
67
111
|
},
|
|
68
112
|
};
|
|
69
|
-
const map = new ImportMap([inputFile1]);
|
|
70
|
-
expect(() => map.getContents(
|
|
113
|
+
const map = new ImportMap([inputFile1], pathToFileURL(cwd()).toString());
|
|
114
|
+
expect(() => map.getContents()).toThrowError(`Import map cannot reference '${join(cwd(), '..', 'file.js')}' as it's outside of the base directory '${cwd()}'`);
|
|
71
115
|
});
|
|
72
116
|
test('Writes import map file to disk', async () => {
|
|
73
117
|
const file = await tmp.file();
|
package/dist/node/manifest.d.ts
CHANGED
package/dist/node/manifest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import
|
|
3
|
+
import { wrapBundleError } from './bundle_error.js';
|
|
4
4
|
import { parsePattern } from './declaration.js';
|
|
5
5
|
import { getPackageVersion } from './package_json.js';
|
|
6
6
|
import { nonNullable } from './utils/non_nullable.js';
|
|
@@ -26,13 +26,26 @@ const sanitizeEdgeFunctionConfig = (config) => {
|
|
|
26
26
|
}
|
|
27
27
|
return newConfig;
|
|
28
28
|
};
|
|
29
|
-
const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath
|
|
29
|
+
const addExcludedPatterns = (name, manifestFunctionConfig, excludedPath) => {
|
|
30
30
|
if (excludedPath) {
|
|
31
31
|
const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath];
|
|
32
|
-
const excludedPatterns = paths.map((path) => pathToRegularExpression(path
|
|
32
|
+
const excludedPatterns = paths.map((path) => pathToRegularExpression(path)).map(serializePattern);
|
|
33
33
|
manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns);
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes method names into arrays of uppercase strings.
|
|
38
|
+
* (e.g. "get" becomes ["GET"])
|
|
39
|
+
*/
|
|
40
|
+
const normalizeMethods = (method, name) => {
|
|
41
|
+
const methods = Array.isArray(method) ? method : [method];
|
|
42
|
+
return methods.map((method) => {
|
|
43
|
+
if (typeof method !== 'string') {
|
|
44
|
+
throw new TypeError(`Could not parse method declaration of function '${name}'. Expecting HTTP Method, got ${method}`);
|
|
45
|
+
}
|
|
46
|
+
return method.toUpperCase();
|
|
47
|
+
});
|
|
48
|
+
};
|
|
36
49
|
const generateManifest = ({ bundles = [], declarations = [], featureFlags, functions, userFunctionConfig = {}, internalFunctionConfig = {}, importMap, layers = [], }) => {
|
|
37
50
|
const preCacheRoutes = [];
|
|
38
51
|
const postCacheRoutes = [];
|
|
@@ -42,7 +55,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
42
55
|
if (manifestFunctionConfig[name] === undefined) {
|
|
43
56
|
continue;
|
|
44
57
|
}
|
|
45
|
-
addExcludedPatterns(name, manifestFunctionConfig, excludedPath
|
|
58
|
+
addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
|
|
46
59
|
manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError };
|
|
47
60
|
}
|
|
48
61
|
for (const [name, { excludedPath, path, onError, ...rest }] of Object.entries(internalFunctionConfig)) {
|
|
@@ -50,7 +63,7 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
50
63
|
if (manifestFunctionConfig[name] === undefined) {
|
|
51
64
|
continue;
|
|
52
65
|
}
|
|
53
|
-
addExcludedPatterns(name, manifestFunctionConfig, excludedPath
|
|
66
|
+
addExcludedPatterns(name, manifestFunctionConfig, excludedPath);
|
|
54
67
|
manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError, ...rest };
|
|
55
68
|
}
|
|
56
69
|
declarations.forEach((declaration) => {
|
|
@@ -65,6 +78,12 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
65
78
|
pattern: serializePattern(pattern),
|
|
66
79
|
excluded_patterns: excludedPattern.map(serializePattern),
|
|
67
80
|
};
|
|
81
|
+
if ('method' in declaration) {
|
|
82
|
+
route.methods = normalizeMethods(declaration.method, func.name);
|
|
83
|
+
}
|
|
84
|
+
if ('path' in declaration) {
|
|
85
|
+
route.path = declaration.path;
|
|
86
|
+
}
|
|
68
87
|
if (declaration.cache === "manual" /* Cache.Manual */) {
|
|
69
88
|
postCacheRoutes.push(route);
|
|
70
89
|
}
|
|
@@ -87,8 +106,8 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
87
106
|
};
|
|
88
107
|
return manifest;
|
|
89
108
|
};
|
|
90
|
-
const pathToRegularExpression = (path
|
|
91
|
-
|
|
109
|
+
const pathToRegularExpression = (path) => {
|
|
110
|
+
try {
|
|
92
111
|
const pattern = new ExtendedURLPattern({ pathname: path });
|
|
93
112
|
// Removing the `^` and `$` delimiters because we'll need to modify what's
|
|
94
113
|
// between them.
|
|
@@ -99,14 +118,9 @@ const pathToRegularExpression = (path, featureFlags) => {
|
|
|
99
118
|
const normalizedSource = `^${source}\\/?$`;
|
|
100
119
|
return normalizedSource;
|
|
101
120
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Wrapping the expression source with `^` and `$`. Also, adding an optional
|
|
106
|
-
// trailing slash, so that a declaration of `path: "/foo"` matches requests
|
|
107
|
-
// for both `/foo` and `/foo/`.
|
|
108
|
-
const normalizedSource = `^${regularExpression.source}\\/?$`;
|
|
109
|
-
return normalizedSource;
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw wrapBundleError(error);
|
|
123
|
+
}
|
|
110
124
|
};
|
|
111
125
|
const getRegularExpression = (declaration, featureFlags) => {
|
|
112
126
|
if ('pattern' in declaration) {
|
|
@@ -122,7 +136,7 @@ const getRegularExpression = (declaration, featureFlags) => {
|
|
|
122
136
|
return declaration.pattern;
|
|
123
137
|
}
|
|
124
138
|
}
|
|
125
|
-
return pathToRegularExpression(declaration.path
|
|
139
|
+
return pathToRegularExpression(declaration.path);
|
|
126
140
|
};
|
|
127
141
|
const getExcludedRegularExpressions = (declaration, featureFlags) => {
|
|
128
142
|
if ('excludedPattern' in declaration && declaration.excludedPattern) {
|
|
@@ -144,7 +158,7 @@ const getExcludedRegularExpressions = (declaration, featureFlags) => {
|
|
|
144
158
|
}
|
|
145
159
|
if ('path' in declaration && declaration.excludedPath) {
|
|
146
160
|
const paths = Array.isArray(declaration.excludedPath) ? declaration.excludedPath : [declaration.excludedPath];
|
|
147
|
-
return paths.map((path) => pathToRegularExpression(path
|
|
161
|
+
return paths.map((path) => pathToRegularExpression(path));
|
|
148
162
|
}
|
|
149
163
|
return [];
|
|
150
164
|
};
|
|
@@ -2,6 +2,7 @@ import { env } from 'process';
|
|
|
2
2
|
import { test, expect, vi } from 'vitest';
|
|
3
3
|
import { getRouteMatcher } from '../test/util.js';
|
|
4
4
|
import { BundleFormat } from './bundle.js';
|
|
5
|
+
import { BundleError } from './bundle_error.js';
|
|
5
6
|
import { generateManifest } from './manifest.js';
|
|
6
7
|
test('Generates a manifest with different bundles', () => {
|
|
7
8
|
const bundle1 = {
|
|
@@ -21,7 +22,7 @@ test('Generates a manifest with different bundles', () => {
|
|
|
21
22
|
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
|
|
22
23
|
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
|
|
23
24
|
];
|
|
24
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
|
|
25
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
25
26
|
expect(manifest.bundles).toEqual(expectedBundles);
|
|
26
27
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
27
28
|
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
@@ -39,9 +40,8 @@ test('Generates a manifest with display names', () => {
|
|
|
39
40
|
declarations,
|
|
40
41
|
functions,
|
|
41
42
|
internalFunctionConfig,
|
|
42
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
43
43
|
});
|
|
44
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
|
|
44
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
45
45
|
expect(manifest.function_config).toEqual({
|
|
46
46
|
'func-1': { name: 'Display Name' },
|
|
47
47
|
});
|
|
@@ -61,9 +61,8 @@ test('Generates a manifest with a generator field', () => {
|
|
|
61
61
|
declarations,
|
|
62
62
|
functions,
|
|
63
63
|
internalFunctionConfig,
|
|
64
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
65
64
|
});
|
|
66
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
|
|
65
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
67
66
|
const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
|
|
68
67
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
69
68
|
expect(manifest.function_config).toEqual(expectedFunctionConfig);
|
|
@@ -84,15 +83,19 @@ test('Generates a manifest with excluded paths and patterns', () => {
|
|
|
84
83
|
bundles: [],
|
|
85
84
|
declarations,
|
|
86
85
|
functions,
|
|
87
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
88
86
|
});
|
|
89
87
|
const expectedRoutes = [
|
|
90
|
-
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'] },
|
|
91
|
-
{
|
|
88
|
+
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'], path: '/f1/*' },
|
|
89
|
+
{
|
|
90
|
+
function: 'func-2',
|
|
91
|
+
pattern: '^/f2(?:/(.*))/?$',
|
|
92
|
+
excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'],
|
|
93
|
+
},
|
|
92
94
|
{
|
|
93
95
|
function: 'func-3',
|
|
94
96
|
pattern: '^(?:/(.*))/?$',
|
|
95
97
|
excluded_patterns: ['^(?:/((?:.*)(?:/(?:.*))*))?(?:/(.*))\\.html/?$'],
|
|
98
|
+
path: '/*',
|
|
96
99
|
},
|
|
97
100
|
];
|
|
98
101
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
@@ -113,9 +116,8 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
|
|
|
113
116
|
declarations,
|
|
114
117
|
functions,
|
|
115
118
|
userFunctionConfig,
|
|
116
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
117
119
|
});
|
|
118
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
|
|
120
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
119
121
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
120
122
|
expect(manifest.function_config).toEqual({
|
|
121
123
|
'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
|
|
@@ -190,18 +192,19 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
|
|
|
190
192
|
functions,
|
|
191
193
|
userFunctionConfig,
|
|
192
194
|
internalFunctionConfig,
|
|
193
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
194
195
|
});
|
|
195
196
|
expect(manifest.routes).toEqual([
|
|
196
197
|
{
|
|
197
198
|
function: 'customisation',
|
|
198
199
|
pattern: '^/showcases(?:/(.*))/?$',
|
|
199
200
|
excluded_patterns: [],
|
|
201
|
+
path: '/showcases/*',
|
|
200
202
|
},
|
|
201
203
|
{
|
|
202
204
|
function: 'customisation',
|
|
203
205
|
pattern: '^/checkout(?:/(.*))/?$',
|
|
204
206
|
excluded_patterns: ['^(?:/(.*))/terms-and-conditions/?$'],
|
|
207
|
+
path: '/checkout/*',
|
|
205
208
|
},
|
|
206
209
|
]);
|
|
207
210
|
expect(manifest.function_config).toEqual({
|
|
@@ -216,6 +219,42 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
|
|
|
216
219
|
expect(matcher('/checkout/scrooge-mc-duck-animation.css')).toBeUndefined();
|
|
217
220
|
expect(matcher('/showcases/boho-style/expensive-chair.jpg')).toBeUndefined();
|
|
218
221
|
});
|
|
222
|
+
test('URLPattern named groups are supported', () => {
|
|
223
|
+
const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }];
|
|
224
|
+
const declarations = [{ function: 'customisation', path: '/products/:productId' }];
|
|
225
|
+
const userFunctionConfig = {};
|
|
226
|
+
const internalFunctionConfig = {};
|
|
227
|
+
const manifest = generateManifest({
|
|
228
|
+
bundles: [],
|
|
229
|
+
declarations,
|
|
230
|
+
functions,
|
|
231
|
+
userFunctionConfig,
|
|
232
|
+
internalFunctionConfig,
|
|
233
|
+
});
|
|
234
|
+
expect(manifest.routes).toEqual([
|
|
235
|
+
{
|
|
236
|
+
function: 'customisation',
|
|
237
|
+
pattern: '^/products(?:/([^/]+?))/?$',
|
|
238
|
+
excluded_patterns: [],
|
|
239
|
+
path: '/products/:productId',
|
|
240
|
+
},
|
|
241
|
+
]);
|
|
242
|
+
const matcher = getRouteMatcher(manifest);
|
|
243
|
+
expect(matcher('/products/jigsaw-doweling-jig')).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
test('Invalid Path patterns throw bundling errors', () => {
|
|
246
|
+
const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }];
|
|
247
|
+
const declarations = [{ function: 'customisation', path: '/https://foo.netlify.app/' }];
|
|
248
|
+
const userFunctionConfig = {};
|
|
249
|
+
const internalFunctionConfig = {};
|
|
250
|
+
expect(() => generateManifest({
|
|
251
|
+
bundles: [],
|
|
252
|
+
declarations,
|
|
253
|
+
functions,
|
|
254
|
+
userFunctionConfig,
|
|
255
|
+
internalFunctionConfig,
|
|
256
|
+
})).toThrowError(BundleError);
|
|
257
|
+
});
|
|
219
258
|
test('Includes failure modes in manifest', () => {
|
|
220
259
|
const functions = [
|
|
221
260
|
{ name: 'func-1', path: '/path/to/func-1.ts' },
|
|
@@ -247,7 +286,7 @@ test('Excludes functions for which there are function files but no matching conf
|
|
|
247
286
|
];
|
|
248
287
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
249
288
|
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
250
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
|
|
289
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
251
290
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
252
291
|
});
|
|
253
292
|
test('Excludes functions for which there are config declarations but no matching function files', () => {
|
|
@@ -262,14 +301,14 @@ test('Excludes functions for which there are config declarations but no matching
|
|
|
262
301
|
{ function: 'func-2', path: '/f2' },
|
|
263
302
|
];
|
|
264
303
|
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
265
|
-
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [] }];
|
|
304
|
+
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }];
|
|
266
305
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
267
306
|
});
|
|
268
307
|
test('Generates a manifest without bundles', () => {
|
|
269
308
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
270
309
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
271
310
|
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
272
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
|
|
311
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
273
312
|
expect(manifest.bundles).toEqual([]);
|
|
274
313
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
275
314
|
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
@@ -301,10 +340,12 @@ test('Generates a manifest with pre and post-cache routes', () => {
|
|
|
301
340
|
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
|
|
302
341
|
];
|
|
303
342
|
const expectedPreCacheRoutes = [
|
|
304
|
-
{ function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [] },
|
|
305
|
-
{ function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [] },
|
|
343
|
+
{ function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' },
|
|
344
|
+
{ function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' },
|
|
345
|
+
];
|
|
346
|
+
const expectedPostCacheRoutes = [
|
|
347
|
+
{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [], path: '/f3' },
|
|
306
348
|
];
|
|
307
|
-
const expectedPostCacheRoutes = [{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [] }];
|
|
308
349
|
expect(manifest.bundles).toEqual(expectedBundles);
|
|
309
350
|
expect(manifest.routes).toEqual(expectedPreCacheRoutes);
|
|
310
351
|
expect(manifest.post_cache_routes).toEqual(expectedPostCacheRoutes);
|
|
@@ -320,8 +361,8 @@ test('Generates a manifest with layers', () => {
|
|
|
320
361
|
{ function: 'func-2', path: '/f2/*' },
|
|
321
362
|
];
|
|
322
363
|
const expectedRoutes = [
|
|
323
|
-
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] },
|
|
324
|
-
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [] },
|
|
364
|
+
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' },
|
|
365
|
+
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [], path: '/f2/*' },
|
|
325
366
|
];
|
|
326
367
|
const layers = [
|
|
327
368
|
{
|
|
@@ -333,14 +374,12 @@ test('Generates a manifest with layers', () => {
|
|
|
333
374
|
bundles: [],
|
|
334
375
|
declarations,
|
|
335
376
|
functions,
|
|
336
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
337
377
|
});
|
|
338
378
|
const manifest2 = generateManifest({
|
|
339
379
|
bundles: [],
|
|
340
380
|
declarations,
|
|
341
381
|
functions,
|
|
342
382
|
layers,
|
|
343
|
-
featureFlags: { edge_functions_path_urlpattern: true },
|
|
344
383
|
});
|
|
345
384
|
expect(manifest1.routes).toEqual(expectedRoutes);
|
|
346
385
|
expect(manifest1.layers).toEqual([]);
|
|
@@ -47,6 +47,16 @@ declare const edgeManifestSchema: {
|
|
|
47
47
|
generator: {
|
|
48
48
|
type: string;
|
|
49
49
|
};
|
|
50
|
+
path: {
|
|
51
|
+
type: string;
|
|
52
|
+
};
|
|
53
|
+
methods: {
|
|
54
|
+
type: string;
|
|
55
|
+
items: {
|
|
56
|
+
type: string;
|
|
57
|
+
enum: string[];
|
|
58
|
+
};
|
|
59
|
+
};
|
|
50
60
|
};
|
|
51
61
|
additionalProperties: boolean;
|
|
52
62
|
};
|
|
@@ -79,6 +89,16 @@ declare const edgeManifestSchema: {
|
|
|
79
89
|
generator: {
|
|
80
90
|
type: string;
|
|
81
91
|
};
|
|
92
|
+
path: {
|
|
93
|
+
type: string;
|
|
94
|
+
};
|
|
95
|
+
methods: {
|
|
96
|
+
type: string;
|
|
97
|
+
items: {
|
|
98
|
+
type: string;
|
|
99
|
+
enum: string[];
|
|
100
|
+
};
|
|
101
|
+
};
|
|
82
102
|
};
|
|
83
103
|
additionalProperties: boolean;
|
|
84
104
|
};
|
|
@@ -28,6 +28,11 @@ const routesSchema = {
|
|
|
28
28
|
},
|
|
29
29
|
excluded_patterns: excludedPatternsSchema,
|
|
30
30
|
generator: { type: 'string' },
|
|
31
|
+
path: { type: 'string' },
|
|
32
|
+
methods: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] },
|
|
35
|
+
},
|
|
31
36
|
},
|
|
32
37
|
additionalProperties: false,
|
|
33
38
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.19.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-v8": "^0.
|
|
61
|
+
"@vitest/coverage-v8": "^0.34.0",
|
|
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": "^5.0.0",
|
|
70
|
-
"vitest": "^0.
|
|
70
|
+
"vitest": "^0.34.0"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": "^14.16.0 || >=16.0.0"
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
"execa": "^6.0.0",
|
|
83
83
|
"find-up": "^6.3.0",
|
|
84
84
|
"get-port": "^6.1.2",
|
|
85
|
-
"glob-to-regexp": "^0.4.1",
|
|
86
85
|
"is-path-inside": "^4.0.0",
|
|
87
86
|
"jsonc-parser": "^3.2.0",
|
|
88
87
|
"node-fetch": "^3.1.1",
|