@netlify/edge-bundler 8.17.0 → 8.18.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/config.ts +4 -1
- package/dist/node/bundler.d.ts +2 -1
- package/dist/node/bundler.js +3 -3
- package/dist/node/config.d.ts +7 -1
- package/dist/node/config.js +2 -1
- package/dist/node/config.test.js +79 -19
- package/dist/node/declaration.js +6 -1
- package/dist/node/declaration.test.js +6 -0
- package/dist/node/formats/eszip.js +7 -2
- package/dist/node/import_map.d.ts +18 -15
- package/dist/node/import_map.js +109 -88
- package/dist/node/import_map.test.js +61 -19
- package/dist/node/manifest.d.ts +1 -0
- package/dist/node/manifest.js +18 -9
- package/dist/node/manifest.test.js +62 -14
- package/dist/node/server/server.js +1 -1
- package/dist/node/server/server.test.js +16 -1
- package/dist/node/validation/manifest/schema.d.ts +6 -0
- package/dist/node/validation/manifest/schema.js +1 -0
- package/package.json +3 -3
package/deno/config.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const [functionURL, collectorURL, rawExitCodes] = Deno.args
|
|
1
|
+
const [functionURL, collectorURL, bootstrapURL, rawExitCodes] = Deno.args
|
|
2
2
|
const exitCodes = JSON.parse(rawExitCodes)
|
|
3
3
|
|
|
4
|
+
const { Netlify } = await import(bootstrapURL)
|
|
5
|
+
globalThis.Netlify = Netlify
|
|
6
|
+
|
|
4
7
|
let func
|
|
5
8
|
|
|
6
9
|
try {
|
package/dist/node/bundler.d.ts
CHANGED
|
@@ -14,8 +14,9 @@ interface BundleOptions {
|
|
|
14
14
|
onBeforeDownload?: OnBeforeDownloadHook;
|
|
15
15
|
systemLogger?: LogFunction;
|
|
16
16
|
internalSrcFolder?: string;
|
|
17
|
+
bootstrapURL?: string;
|
|
17
18
|
}
|
|
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<{
|
|
19
|
+
declare const bundle: (sourceDirectories: string[], distDirectory: string, tomlDeclarations?: Declaration[], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths, onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, bootstrapURL, }?: BundleOptions) => Promise<{
|
|
19
20
|
functions: import("./edge_function.js").EdgeFunction[];
|
|
20
21
|
manifest: import("./manifest.js").Manifest;
|
|
21
22
|
}>;
|
package/dist/node/bundler.js
CHANGED
|
@@ -14,7 +14,7 @@ import { ImportMap } from './import_map.js';
|
|
|
14
14
|
import { getLogger } from './logger.js';
|
|
15
15
|
import { writeManifest } from './manifest.js';
|
|
16
16
|
import { ensureLatestTypes } from './types.js';
|
|
17
|
-
const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, } = {}) => {
|
|
17
|
+
const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], { basePath: inputBasePath, cacheDirectory, configPath, debug, distImportMapPath, featureFlags: inputFeatureFlags, importMapPaths = [], onAfterDownload, onBeforeDownload, systemLogger, internalSrcFolder, bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts', } = {}) => {
|
|
18
18
|
const logger = getLogger(systemLogger, debug);
|
|
19
19
|
const featureFlags = getFlags(inputFeatureFlags);
|
|
20
20
|
const options = {
|
|
@@ -63,8 +63,8 @@ const bundle = async (sourceDirectories, distDirectory, tomlDeclarations = [], {
|
|
|
63
63
|
await createFinalBundles([functionBundle], distDirectory, buildID);
|
|
64
64
|
// Retrieving a configuration object for each function.
|
|
65
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)]);
|
|
67
|
-
const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig(func, importMap, deno, logger)]);
|
|
66
|
+
const internalConfigPromises = internalFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger, bootstrapURL })]);
|
|
67
|
+
const userConfigPromises = userFunctions.map(async (func) => [func.name, await getFunctionConfig({ func, importMap, deno, log: logger, bootstrapURL })]);
|
|
68
68
|
// Creating a hash of function names to configuration objects.
|
|
69
69
|
const internalFunctionsWithConfig = Object.fromEntries(await Promise.all(internalConfigPromises));
|
|
70
70
|
const userFunctionsWithConfig = Object.fromEntries(await Promise.all(userConfigPromises));
|
package/dist/node/config.d.ts
CHANGED
|
@@ -17,4 +17,10 @@ export interface FunctionConfig {
|
|
|
17
17
|
name?: string;
|
|
18
18
|
generator?: string;
|
|
19
19
|
}
|
|
20
|
-
export declare const getFunctionConfig: (func
|
|
20
|
+
export declare const getFunctionConfig: ({ func, importMap, deno, bootstrapURL, log, }: {
|
|
21
|
+
func: EdgeFunction;
|
|
22
|
+
importMap: ImportMap;
|
|
23
|
+
deno: DenoBridge;
|
|
24
|
+
bootstrapURL: string;
|
|
25
|
+
log: Logger;
|
|
26
|
+
}) => Promise<FunctionConfig>;
|
package/dist/node/config.js
CHANGED
|
@@ -26,7 +26,7 @@ const getConfigExtractor = () => {
|
|
|
26
26
|
const configExtractorPath = join(packagePath, 'deno', 'config.ts');
|
|
27
27
|
return configExtractorPath;
|
|
28
28
|
};
|
|
29
|
-
export const getFunctionConfig = async (func, importMap, deno, log) => {
|
|
29
|
+
export const getFunctionConfig = async ({ func, importMap, deno, bootstrapURL, log, }) => {
|
|
30
30
|
// The extractor is a Deno script that will import the function and run its
|
|
31
31
|
// `config` export, if one exists.
|
|
32
32
|
const extractorPath = getConfigExtractor();
|
|
@@ -50,6 +50,7 @@ export const getFunctionConfig = async (func, importMap, deno, log) => {
|
|
|
50
50
|
extractorPath,
|
|
51
51
|
pathToFileURL(func.path).href,
|
|
52
52
|
pathToFileURL(collector.path).href,
|
|
53
|
+
bootstrapURL,
|
|
53
54
|
JSON.stringify(ConfigExitCode),
|
|
54
55
|
], { rejectOnExitCode: false });
|
|
55
56
|
if (exitCode !== ConfigExitCode.Success) {
|
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
|
+
const bootstrapURL = 'https://edge.netlify.com/bootstrap/index-combined.ts';
|
|
12
13
|
const importMapFile = {
|
|
13
14
|
baseURL: new URL('file:///some/path/import-map.json'),
|
|
14
15
|
imports: {
|
|
@@ -114,9 +115,15 @@ describe('`getFunctionConfig` extracts configuration properties from function fi
|
|
|
114
115
|
const path = join(tmpDir, `${func.name}.js`);
|
|
115
116
|
await fs.writeFile(path, func.source);
|
|
116
117
|
const funcCall = () => getFunctionConfig({
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
func: {
|
|
119
|
+
name: func.name,
|
|
120
|
+
path,
|
|
121
|
+
},
|
|
122
|
+
importMap: new ImportMap([importMapFile]),
|
|
123
|
+
deno,
|
|
124
|
+
log: logger,
|
|
125
|
+
bootstrapURL,
|
|
126
|
+
});
|
|
120
127
|
if (func.error) {
|
|
121
128
|
await expect(funcCall()).rejects.toThrowError(func.error);
|
|
122
129
|
}
|
|
@@ -161,14 +168,49 @@ test('Loads function paths from the in-source `config` function', async () => {
|
|
|
161
168
|
expect(bundles[0].format).toBe('eszip2');
|
|
162
169
|
expect(generatedFiles.includes(bundles[0].asset)).toBe(true);
|
|
163
170
|
expect(routes.length).toBe(6);
|
|
164
|
-
expect(routes[0]).toEqual({
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
});
|
|
183
|
+
expect(routes[2]).toEqual({
|
|
184
|
+
function: 'framework-func1',
|
|
185
|
+
pattern: '^/framework-func1/?$',
|
|
186
|
+
excluded_patterns: [],
|
|
187
|
+
path: '/framework-func1',
|
|
188
|
+
});
|
|
189
|
+
expect(routes[3]).toEqual({
|
|
190
|
+
function: 'user-func1',
|
|
191
|
+
pattern: '^/user-func1/?$',
|
|
192
|
+
excluded_patterns: [],
|
|
193
|
+
path: '/user-func1',
|
|
194
|
+
});
|
|
195
|
+
expect(routes[4]).toEqual({
|
|
196
|
+
function: 'user-func3',
|
|
197
|
+
pattern: '^/user-func3/?$',
|
|
198
|
+
excluded_patterns: [],
|
|
199
|
+
path: '/user-func3',
|
|
200
|
+
});
|
|
201
|
+
expect(routes[5]).toEqual({
|
|
202
|
+
function: 'user-func5',
|
|
203
|
+
pattern: '^/user-func5(?:/(.*))/?$',
|
|
204
|
+
excluded_patterns: [],
|
|
205
|
+
path: '/user-func5/*',
|
|
206
|
+
});
|
|
170
207
|
expect(postCacheRoutes.length).toBe(1);
|
|
171
|
-
expect(postCacheRoutes[0]).toEqual({
|
|
208
|
+
expect(postCacheRoutes[0]).toEqual({
|
|
209
|
+
function: 'user-func4',
|
|
210
|
+
pattern: '^/user-func4/?$',
|
|
211
|
+
excluded_patterns: [],
|
|
212
|
+
path: '/user-func4',
|
|
213
|
+
});
|
|
172
214
|
expect(Object.keys(functionConfig)).toHaveLength(1);
|
|
173
215
|
expect(functionConfig['user-func5']).toEqual({
|
|
174
216
|
excluded_patterns: ['^/user-func5/excluded/?$'],
|
|
@@ -194,9 +236,15 @@ test('Passes validation if default export exists and is a function', async () =>
|
|
|
194
236
|
const path = join(tmpDir, `${func.name}.ts`);
|
|
195
237
|
await fs.writeFile(path, func.source);
|
|
196
238
|
await expect(getFunctionConfig({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
239
|
+
func: {
|
|
240
|
+
name: func.name,
|
|
241
|
+
path,
|
|
242
|
+
},
|
|
243
|
+
importMap: new ImportMap([importMapFile]),
|
|
244
|
+
deno,
|
|
245
|
+
log: logger,
|
|
246
|
+
bootstrapURL,
|
|
247
|
+
})).resolves.not.toThrow();
|
|
200
248
|
await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
|
|
201
249
|
});
|
|
202
250
|
test('Fails validation if default export is not function', async () => {
|
|
@@ -218,9 +266,15 @@ test('Fails validation if default export is not function', async () => {
|
|
|
218
266
|
const path = join(tmpDir, `${func.name}.ts`);
|
|
219
267
|
await fs.writeFile(path, func.source);
|
|
220
268
|
const config = getFunctionConfig({
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
269
|
+
func: {
|
|
270
|
+
name: func.name,
|
|
271
|
+
path,
|
|
272
|
+
},
|
|
273
|
+
importMap: new ImportMap([importMapFile]),
|
|
274
|
+
deno,
|
|
275
|
+
log: logger,
|
|
276
|
+
bootstrapURL,
|
|
277
|
+
});
|
|
224
278
|
await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
|
|
225
279
|
await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
|
|
226
280
|
});
|
|
@@ -242,9 +296,15 @@ test('Fails validation if default export is not present', async () => {
|
|
|
242
296
|
const path = join(tmpDir, `${func.name}.ts`);
|
|
243
297
|
await fs.writeFile(path, func.source);
|
|
244
298
|
const config = getFunctionConfig({
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
299
|
+
func: {
|
|
300
|
+
name: func.name,
|
|
301
|
+
path,
|
|
302
|
+
},
|
|
303
|
+
importMap: new ImportMap([importMapFile]),
|
|
304
|
+
deno,
|
|
305
|
+
log: logger,
|
|
306
|
+
bootstrapURL,
|
|
307
|
+
});
|
|
248
308
|
await expect(config).rejects.toThrowError(invalidDefaultExportErr(path));
|
|
249
309
|
await rm(tmpDir, { force: true, recursive: true, maxRetries: 10 });
|
|
250
310
|
});
|
package/dist/node/declaration.js
CHANGED
|
@@ -66,7 +66,12 @@ const createDeclarationsFromFunctionConfigs = (functionConfigs, functionsVisited
|
|
|
66
66
|
// Validates and normalizes a pattern so that it's a valid regular expression
|
|
67
67
|
// in Go, which is the engine used by our edge nodes.
|
|
68
68
|
export const parsePattern = (pattern) => {
|
|
69
|
-
|
|
69
|
+
let enclosedPattern = pattern;
|
|
70
|
+
if (!pattern.startsWith('^'))
|
|
71
|
+
enclosedPattern = `^${enclosedPattern}`;
|
|
72
|
+
if (!pattern.endsWith('$'))
|
|
73
|
+
enclosedPattern = `${enclosedPattern}$`;
|
|
74
|
+
const regexp = new RegExp(enclosedPattern);
|
|
70
75
|
const newRegexp = regexpAST.transform(regexp, {
|
|
71
76
|
Assertion(path) {
|
|
72
77
|
// Lookaheads are not supported. If we find one, throw an error.
|
|
@@ -126,3 +126,9 @@ test('Escapes front slashes in a regex pattern', () => {
|
|
|
126
126
|
const actual = parsePattern(regexPattern);
|
|
127
127
|
expect(actual).toEqual(expected);
|
|
128
128
|
});
|
|
129
|
+
test('Ensures pattern match on the whole path', () => {
|
|
130
|
+
const regexPattern = '/foo/.*/bar';
|
|
131
|
+
const expected = '^\\/foo\\/.*\\/bar$';
|
|
132
|
+
const actual = parsePattern(regexPattern);
|
|
133
|
+
expect(actual).toEqual(expected);
|
|
134
|
+
});
|
|
@@ -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,6 +1,6 @@
|
|
|
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';
|
|
@@ -9,74 +9,19 @@ const INTERNAL_IMPORTS = {
|
|
|
9
9
|
};
|
|
10
10
|
// ImportMap can take several import map files and merge them into a final
|
|
11
11
|
// import map object, also adding the internal imports in the right order.
|
|
12
|
-
class ImportMap {
|
|
13
|
-
constructor(sources = []) {
|
|
12
|
+
export class ImportMap {
|
|
13
|
+
constructor(sources = [], rootURL = null) {
|
|
14
|
+
this.rootPath = rootURL ? fileURLToPath(rootURL) : null;
|
|
14
15
|
this.sources = [];
|
|
15
16
|
sources.forEach((file) => {
|
|
16
17
|
this.add(file);
|
|
17
18
|
});
|
|
18
19
|
}
|
|
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
20
|
add(source) {
|
|
76
21
|
this.sources.push(source);
|
|
77
22
|
}
|
|
78
23
|
async addFile(path, logger) {
|
|
79
|
-
const source = await readFile(path, logger);
|
|
24
|
+
const source = await ImportMap.readFile(path, logger);
|
|
80
25
|
if (Object.keys(source.imports).length === 0) {
|
|
81
26
|
return;
|
|
82
27
|
}
|
|
@@ -90,11 +35,73 @@ class ImportMap {
|
|
|
90
35
|
await this.addFile(path, logger);
|
|
91
36
|
}
|
|
92
37
|
}
|
|
93
|
-
|
|
38
|
+
// Applies a list of prefixes to an `imports` block, by transforming values
|
|
39
|
+
// with the `applyPrefixesToPath` method.
|
|
40
|
+
static applyPrefixesToImports(imports, prefixes) {
|
|
41
|
+
return Object.entries(imports).reduce((acc, [key, value]) => ({
|
|
42
|
+
...acc,
|
|
43
|
+
[key]: ImportMap.applyPrefixesToPath(value, prefixes),
|
|
44
|
+
}), {});
|
|
45
|
+
}
|
|
46
|
+
// Applies a list of prefixes to a given path, returning the replaced path.
|
|
47
|
+
// For example, given a `path` of `file:///foo/bar/baz.js` and a `prefixes`
|
|
48
|
+
// object with `{"file:///foo/": "file:///hello/"}`, this method will return
|
|
49
|
+
// `file:///hello/bar/baz.js`. If no matching prefix is found, the original
|
|
50
|
+
// path is returned.
|
|
51
|
+
static applyPrefixesToPath(path, prefixes) {
|
|
52
|
+
for (const prefix in prefixes) {
|
|
53
|
+
if (path.startsWith(prefix)) {
|
|
54
|
+
return path.replace(prefix, prefixes[prefix]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return path;
|
|
58
|
+
}
|
|
59
|
+
// Takes an `imports` object and filters out any entries without a URL. Also,
|
|
60
|
+
// it checks whether the import map is referencing a path outside `rootPath`,
|
|
61
|
+
// if one is set.
|
|
62
|
+
filterImports(imports = {}) {
|
|
63
|
+
const filteredImports = {};
|
|
64
|
+
Object.keys(imports).forEach((specifier) => {
|
|
65
|
+
const url = imports[specifier];
|
|
66
|
+
// If there's no URL, don't even add the specifier to the final imports.
|
|
67
|
+
if (url === null) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (this.rootPath !== null) {
|
|
71
|
+
const path = fileURLToPath(url);
|
|
72
|
+
const relativePath = relative(this.rootPath, path);
|
|
73
|
+
if (relativePath.startsWith('..')) {
|
|
74
|
+
throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${this.rootPath}'`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
filteredImports[specifier] = url.toString();
|
|
78
|
+
});
|
|
79
|
+
return filteredImports;
|
|
80
|
+
}
|
|
81
|
+
// Takes a `scopes` object and runs all imports through `filterImports`,
|
|
82
|
+
// omitting any scopes for which there are no imports.
|
|
83
|
+
filterScopes(scopes) {
|
|
84
|
+
const filteredScopes = {};
|
|
85
|
+
if (scopes !== undefined) {
|
|
86
|
+
Object.keys(scopes).forEach((url) => {
|
|
87
|
+
const imports = this.filterImports(scopes[url]);
|
|
88
|
+
if (Object.keys(imports).length === 0) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
filteredScopes[url] = imports;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return filteredScopes;
|
|
95
|
+
}
|
|
96
|
+
// Returns the import map as a plain object, with any relative paths resolved
|
|
97
|
+
// to full URLs. It takes an optional `prefixes` object that specifies a list
|
|
98
|
+
// of prefixes to replace path prefixes (see `applyPrefixesToPath`). Prefixes
|
|
99
|
+
// will be applied on both `imports` and `scopes`.
|
|
100
|
+
getContents(prefixes = {}) {
|
|
94
101
|
let imports = {};
|
|
95
102
|
let scopes = {};
|
|
96
103
|
this.sources.forEach((file) => {
|
|
97
|
-
const importMap =
|
|
104
|
+
const importMap = this.resolve(file);
|
|
98
105
|
imports = { ...imports, ...importMap.imports };
|
|
99
106
|
scopes = { ...scopes, ...importMap.scopes };
|
|
100
107
|
});
|
|
@@ -104,11 +111,49 @@ class ImportMap {
|
|
|
104
111
|
const [specifier, url] = internalImport;
|
|
105
112
|
imports[specifier] = url;
|
|
106
113
|
});
|
|
114
|
+
const transformedImports = ImportMap.applyPrefixesToImports(imports, prefixes);
|
|
115
|
+
const transformedScopes = Object.entries(scopes).reduce((acc, [key, value]) => ({
|
|
116
|
+
...acc,
|
|
117
|
+
[ImportMap.applyPrefixesToPath(key, prefixes)]: ImportMap.applyPrefixesToImports(value, prefixes),
|
|
118
|
+
}), {});
|
|
119
|
+
return {
|
|
120
|
+
imports: transformedImports,
|
|
121
|
+
scopes: transformedScopes,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
static async readFile(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
|
+
}
|
|
107
142
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
baseURL,
|
|
144
|
+
imports: {},
|
|
110
145
|
};
|
|
111
146
|
}
|
|
147
|
+
// Resolves an import map file by transforming all relative paths into full
|
|
148
|
+
// URLs. The `baseURL` property of each file is used to resolve all relative
|
|
149
|
+
// paths against.
|
|
150
|
+
resolve(source) {
|
|
151
|
+
const { baseURL, ...importMap } = source;
|
|
152
|
+
const parsedImportMap = parse(importMap, baseURL);
|
|
153
|
+
const imports = this.filterImports(parsedImportMap.imports);
|
|
154
|
+
const scopes = this.filterScopes(parsedImportMap.scopes);
|
|
155
|
+
return { ...parsedImportMap, imports, scopes };
|
|
156
|
+
}
|
|
112
157
|
toDataURL() {
|
|
113
158
|
const data = JSON.stringify(this.getContents());
|
|
114
159
|
const encodedImportMap = Buffer.from(data).toString('base64');
|
|
@@ -121,27 +166,3 @@ class ImportMap {
|
|
|
121
166
|
await fs.writeFile(path, JSON.stringify(contents));
|
|
122
167
|
}
|
|
123
168
|
}
|
|
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,77 @@ 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': 'https://edge.netlify.com/v1/index.ts',
|
|
73
|
+
});
|
|
74
|
+
expect(scopes).toStrictEqual({
|
|
75
|
+
'file:///some/cool/path/with/scopes/': {
|
|
76
|
+
'lib/*': 'https://external.netlify/lib/',
|
|
77
|
+
foo: 'file:///some/cool/path/foo-alias',
|
|
78
|
+
bar: 'file:///different/full/path/bar.js',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
test('With prefixes', () => {
|
|
83
|
+
const map = new ImportMap([inputFile1, inputFile2]);
|
|
84
|
+
const { imports, scopes } = map.getContents({
|
|
85
|
+
'file:///some/': 'file:///root/',
|
|
86
|
+
'file:///different/': 'file:///vendor/',
|
|
87
|
+
});
|
|
88
|
+
expect(imports).toStrictEqual({
|
|
89
|
+
'lib/*': 'file:///root/cool/path/library/',
|
|
90
|
+
specifier3: 'file:///vendor/full/path/file3.js',
|
|
91
|
+
specifier2: 'file:///root/full/path/file2.js',
|
|
92
|
+
specifier1: 'file:///root/full/path/file.js',
|
|
93
|
+
'netlify:edge': 'https://edge.netlify.com/v1/index.ts',
|
|
94
|
+
});
|
|
95
|
+
expect(scopes).toStrictEqual({
|
|
96
|
+
'file:///root/cool/path/with/scopes/': {
|
|
97
|
+
'lib/*': 'https://external.netlify/lib/',
|
|
98
|
+
foo: 'file:///root/cool/path/foo-alias',
|
|
99
|
+
bar: 'file:///vendor/full/path/bar.js',
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
60
103
|
});
|
|
61
104
|
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
105
|
const inputFile1 = {
|
|
64
|
-
baseURL: pathToFileURL(join(
|
|
106
|
+
baseURL: pathToFileURL(join(cwd(), 'import-map.json')),
|
|
65
107
|
imports: {
|
|
66
108
|
'alias:file': '../file.js',
|
|
67
109
|
},
|
|
68
110
|
};
|
|
69
|
-
const map = new ImportMap([inputFile1]);
|
|
70
|
-
expect(() => map.getContents(
|
|
111
|
+
const map = new ImportMap([inputFile1], pathToFileURL(cwd()).toString());
|
|
112
|
+
expect(() => map.getContents()).toThrowError(`Import map cannot reference '${join(cwd(), '..', 'file.js')}' as it's outside of the base directory '${cwd()}'`);
|
|
71
113
|
});
|
|
72
114
|
test('Writes import map file to disk', async () => {
|
|
73
115
|
const file = await tmp.file();
|
package/dist/node/manifest.d.ts
CHANGED
package/dist/node/manifest.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import globToRegExp from 'glob-to-regexp';
|
|
4
|
+
import { wrapBundleError } from './bundle_error.js';
|
|
4
5
|
import { parsePattern } from './declaration.js';
|
|
5
6
|
import { getPackageVersion } from './package_json.js';
|
|
6
7
|
import { nonNullable } from './utils/non_nullable.js';
|
|
@@ -65,6 +66,9 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
65
66
|
pattern: serializePattern(pattern),
|
|
66
67
|
excluded_patterns: excludedPattern.map(serializePattern),
|
|
67
68
|
};
|
|
69
|
+
if ('path' in declaration) {
|
|
70
|
+
route.path = declaration.path;
|
|
71
|
+
}
|
|
68
72
|
if (declaration.cache === "manual" /* Cache.Manual */) {
|
|
69
73
|
postCacheRoutes.push(route);
|
|
70
74
|
}
|
|
@@ -89,15 +93,20 @@ const generateManifest = ({ bundles = [], declarations = [], featureFlags, funct
|
|
|
89
93
|
};
|
|
90
94
|
const pathToRegularExpression = (path, featureFlags) => {
|
|
91
95
|
if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_path_urlpattern) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
try {
|
|
97
|
+
const pattern = new ExtendedURLPattern({ pathname: path });
|
|
98
|
+
// Removing the `^` and `$` delimiters because we'll need to modify what's
|
|
99
|
+
// between them.
|
|
100
|
+
const source = pattern.regexp.pathname.source.slice(1, -1);
|
|
101
|
+
// Wrapping the expression source with `^` and `$`. Also, adding an optional
|
|
102
|
+
// trailing slash, so that a declaration of `path: "/foo"` matches requests
|
|
103
|
+
// for both `/foo` and `/foo/`.
|
|
104
|
+
const normalizedSource = `^${source}\\/?$`;
|
|
105
|
+
return normalizedSource;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw wrapBundleError(error);
|
|
109
|
+
}
|
|
101
110
|
}
|
|
102
111
|
// We use the global flag so that `globToRegExp` will not wrap the expression
|
|
103
112
|
// with `^` and `$`. We'll do that ourselves.
|
|
@@ -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);
|
|
@@ -41,7 +42,7 @@ test('Generates a manifest with display names', () => {
|
|
|
41
42
|
internalFunctionConfig,
|
|
42
43
|
featureFlags: { edge_functions_path_urlpattern: true },
|
|
43
44
|
});
|
|
44
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
|
|
45
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
45
46
|
expect(manifest.function_config).toEqual({
|
|
46
47
|
'func-1': { name: 'Display Name' },
|
|
47
48
|
});
|
|
@@ -63,7 +64,7 @@ test('Generates a manifest with a generator field', () => {
|
|
|
63
64
|
internalFunctionConfig,
|
|
64
65
|
featureFlags: { edge_functions_path_urlpattern: true },
|
|
65
66
|
});
|
|
66
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
|
|
67
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
67
68
|
const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } };
|
|
68
69
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
69
70
|
expect(manifest.function_config).toEqual(expectedFunctionConfig);
|
|
@@ -87,12 +88,17 @@ test('Generates a manifest with excluded paths and patterns', () => {
|
|
|
87
88
|
featureFlags: { edge_functions_path_urlpattern: true },
|
|
88
89
|
});
|
|
89
90
|
const expectedRoutes = [
|
|
90
|
-
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'] },
|
|
91
|
-
{
|
|
91
|
+
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'], path: '/f1/*' },
|
|
92
|
+
{
|
|
93
|
+
function: 'func-2',
|
|
94
|
+
pattern: '^/f2(?:/(.*))/?$',
|
|
95
|
+
excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'],
|
|
96
|
+
},
|
|
92
97
|
{
|
|
93
98
|
function: 'func-3',
|
|
94
99
|
pattern: '^(?:/(.*))/?$',
|
|
95
100
|
excluded_patterns: ['^(?:/((?:.*)(?:/(?:.*))*))?(?:/(.*))\\.html/?$'],
|
|
101
|
+
path: '/*',
|
|
96
102
|
},
|
|
97
103
|
];
|
|
98
104
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
@@ -115,7 +121,7 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
|
|
|
115
121
|
userFunctionConfig,
|
|
116
122
|
featureFlags: { edge_functions_path_urlpattern: true },
|
|
117
123
|
});
|
|
118
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }];
|
|
124
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }];
|
|
119
125
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
120
126
|
expect(manifest.function_config).toEqual({
|
|
121
127
|
'func-1': { excluded_patterns: ['^/f1/exclude/?$'] },
|
|
@@ -197,11 +203,13 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
|
|
|
197
203
|
function: 'customisation',
|
|
198
204
|
pattern: '^/showcases(?:/(.*))/?$',
|
|
199
205
|
excluded_patterns: [],
|
|
206
|
+
path: '/showcases/*',
|
|
200
207
|
},
|
|
201
208
|
{
|
|
202
209
|
function: 'customisation',
|
|
203
210
|
pattern: '^/checkout(?:/(.*))/?$',
|
|
204
211
|
excluded_patterns: ['^(?:/(.*))/terms-and-conditions/?$'],
|
|
212
|
+
path: '/checkout/*',
|
|
205
213
|
},
|
|
206
214
|
]);
|
|
207
215
|
expect(manifest.function_config).toEqual({
|
|
@@ -216,6 +224,44 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
|
|
|
216
224
|
expect(matcher('/checkout/scrooge-mc-duck-animation.css')).toBeUndefined();
|
|
217
225
|
expect(matcher('/showcases/boho-style/expensive-chair.jpg')).toBeUndefined();
|
|
218
226
|
});
|
|
227
|
+
test('URLPattern named groups are supported', () => {
|
|
228
|
+
const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }];
|
|
229
|
+
const declarations = [{ function: 'customisation', path: '/products/:productId' }];
|
|
230
|
+
const userFunctionConfig = {};
|
|
231
|
+
const internalFunctionConfig = {};
|
|
232
|
+
const manifest = generateManifest({
|
|
233
|
+
bundles: [],
|
|
234
|
+
declarations,
|
|
235
|
+
functions,
|
|
236
|
+
userFunctionConfig,
|
|
237
|
+
internalFunctionConfig,
|
|
238
|
+
featureFlags: { edge_functions_path_urlpattern: true },
|
|
239
|
+
});
|
|
240
|
+
expect(manifest.routes).toEqual([
|
|
241
|
+
{
|
|
242
|
+
function: 'customisation',
|
|
243
|
+
pattern: '^/products(?:/([^/]+?))/?$',
|
|
244
|
+
excluded_patterns: [],
|
|
245
|
+
path: '/products/:productId',
|
|
246
|
+
},
|
|
247
|
+
]);
|
|
248
|
+
const matcher = getRouteMatcher(manifest);
|
|
249
|
+
expect(matcher('/products/jigsaw-doweling-jig')).toBeDefined();
|
|
250
|
+
});
|
|
251
|
+
test('Invalid Path patterns throw bundling errors', () => {
|
|
252
|
+
const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }];
|
|
253
|
+
const declarations = [{ function: 'customisation', path: '/https://foo.netlify.app/' }];
|
|
254
|
+
const userFunctionConfig = {};
|
|
255
|
+
const internalFunctionConfig = {};
|
|
256
|
+
expect(() => generateManifest({
|
|
257
|
+
bundles: [],
|
|
258
|
+
declarations,
|
|
259
|
+
functions,
|
|
260
|
+
userFunctionConfig,
|
|
261
|
+
internalFunctionConfig,
|
|
262
|
+
featureFlags: { edge_functions_path_urlpattern: true },
|
|
263
|
+
})).toThrowError(BundleError);
|
|
264
|
+
});
|
|
219
265
|
test('Includes failure modes in manifest', () => {
|
|
220
266
|
const functions = [
|
|
221
267
|
{ name: 'func-1', path: '/path/to/func-1.ts' },
|
|
@@ -247,7 +293,7 @@ test('Excludes functions for which there are function files but no matching conf
|
|
|
247
293
|
];
|
|
248
294
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
249
295
|
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
250
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
|
|
296
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
251
297
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
252
298
|
});
|
|
253
299
|
test('Excludes functions for which there are config declarations but no matching function files', () => {
|
|
@@ -262,14 +308,14 @@ test('Excludes functions for which there are config declarations but no matching
|
|
|
262
308
|
{ function: 'func-2', path: '/f2' },
|
|
263
309
|
];
|
|
264
310
|
const manifest = generateManifest({ bundles: [bundle1], declarations, functions });
|
|
265
|
-
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [] }];
|
|
311
|
+
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }];
|
|
266
312
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
267
313
|
});
|
|
268
314
|
test('Generates a manifest without bundles', () => {
|
|
269
315
|
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }];
|
|
270
316
|
const declarations = [{ function: 'func-1', path: '/f1' }];
|
|
271
317
|
const manifest = generateManifest({ bundles: [], declarations, functions });
|
|
272
|
-
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }];
|
|
318
|
+
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }];
|
|
273
319
|
expect(manifest.bundles).toEqual([]);
|
|
274
320
|
expect(manifest.routes).toEqual(expectedRoutes);
|
|
275
321
|
expect(manifest.bundler_version).toBe(env.npm_package_version);
|
|
@@ -301,10 +347,12 @@ test('Generates a manifest with pre and post-cache routes', () => {
|
|
|
301
347
|
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
|
|
302
348
|
];
|
|
303
349
|
const expectedPreCacheRoutes = [
|
|
304
|
-
{ function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [] },
|
|
305
|
-
{ function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [] },
|
|
350
|
+
{ function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' },
|
|
351
|
+
{ function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' },
|
|
352
|
+
];
|
|
353
|
+
const expectedPostCacheRoutes = [
|
|
354
|
+
{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [], path: '/f3' },
|
|
306
355
|
];
|
|
307
|
-
const expectedPostCacheRoutes = [{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [] }];
|
|
308
356
|
expect(manifest.bundles).toEqual(expectedBundles);
|
|
309
357
|
expect(manifest.routes).toEqual(expectedPreCacheRoutes);
|
|
310
358
|
expect(manifest.post_cache_routes).toEqual(expectedPostCacheRoutes);
|
|
@@ -320,8 +368,8 @@ test('Generates a manifest with layers', () => {
|
|
|
320
368
|
{ function: 'func-2', path: '/f2/*' },
|
|
321
369
|
];
|
|
322
370
|
const expectedRoutes = [
|
|
323
|
-
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] },
|
|
324
|
-
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [] },
|
|
371
|
+
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' },
|
|
372
|
+
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [], path: '/f2/*' },
|
|
325
373
|
];
|
|
326
374
|
const layers = [
|
|
327
375
|
{
|
|
@@ -43,7 +43,7 @@ const prepareServer = ({ bootstrapURL, deno, distDirectory, flags: denoFlags, fo
|
|
|
43
43
|
});
|
|
44
44
|
let functionsConfig = [];
|
|
45
45
|
if (options.getFunctionsConfig) {
|
|
46
|
-
functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig(func, importMap, deno, logger)));
|
|
46
|
+
functionsConfig = await Promise.all(functions.map((func) => getFunctionConfig({ func, importMap, deno, bootstrapURL, log: logger })));
|
|
47
47
|
}
|
|
48
48
|
const success = await waitForServer(port, processRef.ps);
|
|
49
49
|
return {
|
|
@@ -27,6 +27,10 @@ test('Starts a server and serves requests for edge functions', async () => {
|
|
|
27
27
|
name: 'greet',
|
|
28
28
|
path: join(paths.internal, 'greet.ts'),
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
name: 'global_netlify',
|
|
32
|
+
path: join(paths.user, 'global_netlify.ts'),
|
|
33
|
+
},
|
|
30
34
|
];
|
|
31
35
|
const options = {
|
|
32
36
|
getFunctionsConfig: true,
|
|
@@ -35,7 +39,7 @@ test('Starts a server and serves requests for edge functions', async () => {
|
|
|
35
39
|
very_secret_secret: 'i love netlify',
|
|
36
40
|
}, options);
|
|
37
41
|
expect(success).toBe(true);
|
|
38
|
-
expect(functionsConfig).toEqual([{ path: '/my-function' }, {}]);
|
|
42
|
+
expect(functionsConfig).toEqual([{ path: '/my-function' }, {}, { path: '/global-netlify' }]);
|
|
39
43
|
for (const key in functions) {
|
|
40
44
|
const graphEntry = graph === null || graph === void 0 ? void 0 : graph.modules.some(
|
|
41
45
|
// @ts-expect-error TODO: Module graph is currently not typed
|
|
@@ -60,4 +64,15 @@ test('Starts a server and serves requests for edge functions', async () => {
|
|
|
60
64
|
});
|
|
61
65
|
expect(response2.status).toBe(200);
|
|
62
66
|
expect(await response2.text()).toBe('HELLO!');
|
|
67
|
+
const response3 = await fetch(`http://0.0.0.0:${port}/global-netlify`, {
|
|
68
|
+
headers: {
|
|
69
|
+
'x-nf-edge-functions': 'global_netlify',
|
|
70
|
+
'x-ef-passthrough': 'passthrough',
|
|
71
|
+
'X-NF-Request-ID': uuidv4(),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
expect(await response3.json()).toEqual({
|
|
75
|
+
global: 'i love netlify',
|
|
76
|
+
local: 'i love netlify',
|
|
77
|
+
});
|
|
63
78
|
});
|
|
@@ -47,6 +47,9 @@ declare const edgeManifestSchema: {
|
|
|
47
47
|
generator: {
|
|
48
48
|
type: string;
|
|
49
49
|
};
|
|
50
|
+
path: {
|
|
51
|
+
type: string;
|
|
52
|
+
};
|
|
50
53
|
};
|
|
51
54
|
additionalProperties: boolean;
|
|
52
55
|
};
|
|
@@ -79,6 +82,9 @@ declare const edgeManifestSchema: {
|
|
|
79
82
|
generator: {
|
|
80
83
|
type: string;
|
|
81
84
|
};
|
|
85
|
+
path: {
|
|
86
|
+
type: string;
|
|
87
|
+
};
|
|
82
88
|
};
|
|
83
89
|
additionalProperties: boolean;
|
|
84
90
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/edge-bundler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.18.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"
|