@netlify/edge-bundler
Advanced tools
Comparing version 8.17.1 to 8.18.0
@@ -171,10 +171,45 @@ import { promises as fs } from 'fs'; | ||
expect(routes.length).toBe(6); | ||
expect(routes[0]).toEqual({ function: 'framework-func2', pattern: '^/framework-func2/?$', excluded_patterns: [] }); | ||
expect(routes[1]).toEqual({ function: 'user-func2', pattern: '^/user-func2/?$', excluded_patterns: [] }); | ||
expect(routes[2]).toEqual({ function: 'framework-func1', pattern: '^/framework-func1/?$', excluded_patterns: [] }); | ||
expect(routes[3]).toEqual({ function: 'user-func1', pattern: '^/user-func1/?$', excluded_patterns: [] }); | ||
expect(routes[4]).toEqual({ function: 'user-func3', pattern: '^/user-func3/?$', excluded_patterns: [] }); | ||
expect(routes[5]).toEqual({ function: 'user-func5', pattern: '^/user-func5(?:/(.*))/?$', excluded_patterns: [] }); | ||
expect(routes[0]).toEqual({ | ||
function: 'framework-func2', | ||
pattern: '^/framework-func2/?$', | ||
excluded_patterns: [], | ||
path: '/framework-func2', | ||
}); | ||
expect(routes[1]).toEqual({ | ||
function: 'user-func2', | ||
pattern: '^/user-func2/?$', | ||
excluded_patterns: [], | ||
path: '/user-func2', | ||
}); | ||
expect(routes[2]).toEqual({ | ||
function: 'framework-func1', | ||
pattern: '^/framework-func1/?$', | ||
excluded_patterns: [], | ||
path: '/framework-func1', | ||
}); | ||
expect(routes[3]).toEqual({ | ||
function: 'user-func1', | ||
pattern: '^/user-func1/?$', | ||
excluded_patterns: [], | ||
path: '/user-func1', | ||
}); | ||
expect(routes[4]).toEqual({ | ||
function: 'user-func3', | ||
pattern: '^/user-func3/?$', | ||
excluded_patterns: [], | ||
path: '/user-func3', | ||
}); | ||
expect(routes[5]).toEqual({ | ||
function: 'user-func5', | ||
pattern: '^/user-func5(?:/(.*))/?$', | ||
excluded_patterns: [], | ||
path: '/user-func5/*', | ||
}); | ||
expect(postCacheRoutes.length).toBe(1); | ||
expect(postCacheRoutes[0]).toEqual({ function: 'user-func4', pattern: '^/user-func4/?$', excluded_patterns: [] }); | ||
expect(postCacheRoutes[0]).toEqual({ | ||
function: 'user-func4', | ||
pattern: '^/user-func4/?$', | ||
excluded_patterns: [], | ||
path: '/user-func4', | ||
}); | ||
expect(Object.keys(functionConfig)).toHaveLength(1); | ||
@@ -181,0 +216,0 @@ expect(functionConfig['user-func5']).toEqual({ |
import { join } from 'path'; | ||
import { pathToFileURL } from 'url'; | ||
import { virtualRoot } from '../../shared/consts.js'; | ||
@@ -12,3 +13,7 @@ import { BundleFormat } from '../bundle.js'; | ||
const { bundler, importMap: bundlerImportMap } = getESZIPPaths(); | ||
const importMapData = JSON.stringify(importMap.getContents(basePath, virtualRoot)); | ||
// Transforming all paths under `basePath` to use the virtual root prefix. | ||
const importMapPrefixes = { | ||
[`${pathToFileURL(basePath)}/`]: virtualRoot, | ||
}; | ||
const importMapData = importMap.getContents(importMapPrefixes); | ||
const payload = { | ||
@@ -19,3 +24,3 @@ basePath, | ||
functions, | ||
importMapData, | ||
importMapData: JSON.stringify(importMapData), | ||
}; | ||
@@ -22,0 +27,0 @@ const flags = ['--allow-all', '--no-config', `--import-map=${bundlerImportMap}`]; |
@@ -0,4 +1,5 @@ | ||
import { ParsedImportMap } from '@import-maps/resolve'; | ||
import { Logger } from './logger.js'; | ||
type Imports = Record<string, string>; | ||
interface ImportMapSource { | ||
export interface ImportMapFile { | ||
baseURL: URL; | ||
@@ -8,16 +9,20 @@ imports: Imports; | ||
} | ||
declare class ImportMap { | ||
sources: ImportMapSource[]; | ||
constructor(sources?: ImportMapSource[]); | ||
static resolve(source: ImportMapSource, basePath?: string, prefix?: string): { | ||
imports: Record<string, string>; | ||
scopes: Record<string, Imports>; | ||
}; | ||
static resolveImports(imports: Record<string, URL | null>, basePath?: string, prefix?: string): Record<string, string>; | ||
static resolvePath(url: URL, basePath?: string, prefix?: string): string; | ||
add(source: ImportMapSource): void; | ||
export declare class ImportMap { | ||
rootPath: string | null; | ||
sources: ImportMapFile[]; | ||
constructor(sources?: ImportMapFile[], rootURL?: string | null); | ||
add(source: ImportMapFile): void; | ||
addFile(path: string, logger: Logger): Promise<void>; | ||
addFiles(paths: (string | undefined)[], logger: Logger): Promise<void>; | ||
getContents(basePath?: string, prefix?: string): { | ||
static applyPrefixesToImports(imports: Imports, prefixes: Record<string, string>): Imports; | ||
static applyPrefixesToPath(path: string, prefixes: Record<string, string>): string; | ||
filterImports(imports?: Record<string, URL | null>): Record<string, string>; | ||
filterScopes(scopes?: ParsedImportMap['scopes']): Record<string, Imports>; | ||
getContents(prefixes?: Record<string, string>): { | ||
imports: Imports; | ||
scopes: {}; | ||
}; | ||
static readFile(path: string, logger: Logger): Promise<ImportMapFile>; | ||
resolve(source: ImportMapFile): { | ||
imports: Record<string, string>; | ||
scopes: Record<string, Imports>; | ||
@@ -28,4 +33,2 @@ }; | ||
} | ||
declare const readFile: (path: string, logger: Logger) => Promise<ImportMapSource>; | ||
export { ImportMap, readFile }; | ||
export type { ImportMapSource as ImportMapFile }; | ||
export {}; |
import { Buffer } from 'buffer'; | ||
import { promises as fs } from 'fs'; | ||
import { dirname, posix, relative, sep } from 'path'; | ||
import { dirname, relative } from 'path'; | ||
import { fileURLToPath, pathToFileURL } from 'url'; | ||
@@ -12,4 +12,5 @@ import { parse } from '@import-maps/resolve'; | ||
// import map object, also adding the internal imports in the right order. | ||
class ImportMap { | ||
constructor(sources = []) { | ||
export class ImportMap { | ||
constructor(sources = [], rootURL = null) { | ||
this.rootPath = rootURL ? fileURLToPath(rootURL) : null; | ||
this.sources = []; | ||
@@ -20,58 +21,2 @@ sources.forEach((file) => { | ||
} | ||
// Transforms an import map by making any relative paths use a different path | ||
// as a base. | ||
static resolve(source, basePath, prefix = 'file://') { | ||
const { baseURL, ...importMap } = source; | ||
const parsedImportMap = parse(importMap, baseURL); | ||
const { imports = {}, scopes = {} } = parsedImportMap; | ||
const resolvedImports = ImportMap.resolveImports(imports, basePath, prefix); | ||
const resolvedScopes = {}; | ||
Object.keys(scopes).forEach((path) => { | ||
const resolvedPath = ImportMap.resolvePath(new URL(path), basePath, prefix); | ||
resolvedScopes[resolvedPath] = ImportMap.resolveImports(scopes[path], basePath, prefix); | ||
}); | ||
return { ...parsedImportMap, imports: resolvedImports, scopes: resolvedScopes }; | ||
} | ||
// Takes an imports object and resolves relative specifiers with a given base | ||
// path and URL prefix. | ||
static resolveImports(imports, basePath, prefix) { | ||
const resolvedImports = {}; | ||
Object.keys(imports).forEach((specifier) => { | ||
const url = imports[specifier]; | ||
// If there's no URL, don't even add the specifier to the final imports. | ||
if (url === null) { | ||
return; | ||
} | ||
// If this is a file URL, we might want to transform it to use another | ||
// base path. | ||
if (url.protocol === 'file:') { | ||
resolvedImports[specifier] = ImportMap.resolvePath(url, basePath, prefix); | ||
return; | ||
} | ||
resolvedImports[specifier] = url.toString(); | ||
}); | ||
return resolvedImports; | ||
} | ||
// Takes a URL, turns it into a path relative to the given base, and prepends | ||
// a prefix (such as the virtual root prefix). | ||
static resolvePath(url, basePath, prefix) { | ||
if (basePath === undefined) { | ||
return url.toString(); | ||
} | ||
const path = fileURLToPath(url); | ||
const relativePath = relative(basePath, path); | ||
if (relativePath.startsWith('..')) { | ||
throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${basePath}'`); | ||
} | ||
// We want to use POSIX paths for the import map regardless of the OS | ||
// we're building in. | ||
let normalizedPath = relativePath.split(sep).join(posix.sep); | ||
// If the original URL had a trailing slash, ensure the normalized path | ||
// has one too. | ||
if (normalizedPath !== '' && url.pathname.endsWith(posix.sep) && !normalizedPath.endsWith(posix.sep)) { | ||
normalizedPath += posix.sep; | ||
} | ||
const newURL = new URL(normalizedPath, prefix); | ||
return newURL.toString(); | ||
} | ||
add(source) { | ||
@@ -81,3 +26,3 @@ this.sources.push(source); | ||
async addFile(path, logger) { | ||
const source = await readFile(path, logger); | ||
const source = await ImportMap.readFile(path, logger); | ||
if (Object.keys(source.imports).length === 0) { | ||
@@ -96,7 +41,69 @@ return; | ||
} | ||
getContents(basePath, prefix) { | ||
// Applies a list of prefixes to an `imports` block, by transforming values | ||
// with the `applyPrefixesToPath` method. | ||
static applyPrefixesToImports(imports, prefixes) { | ||
return Object.entries(imports).reduce((acc, [key, value]) => ({ | ||
...acc, | ||
[key]: ImportMap.applyPrefixesToPath(value, prefixes), | ||
}), {}); | ||
} | ||
// Applies a list of prefixes to a given path, returning the replaced path. | ||
// For example, given a `path` of `file:///foo/bar/baz.js` and a `prefixes` | ||
// object with `{"file:///foo/": "file:///hello/"}`, this method will return | ||
// `file:///hello/bar/baz.js`. If no matching prefix is found, the original | ||
// path is returned. | ||
static applyPrefixesToPath(path, prefixes) { | ||
for (const prefix in prefixes) { | ||
if (path.startsWith(prefix)) { | ||
return path.replace(prefix, prefixes[prefix]); | ||
} | ||
} | ||
return path; | ||
} | ||
// Takes an `imports` object and filters out any entries without a URL. Also, | ||
// it checks whether the import map is referencing a path outside `rootPath`, | ||
// if one is set. | ||
filterImports(imports = {}) { | ||
const filteredImports = {}; | ||
Object.keys(imports).forEach((specifier) => { | ||
const url = imports[specifier]; | ||
// If there's no URL, don't even add the specifier to the final imports. | ||
if (url === null) { | ||
return; | ||
} | ||
if (this.rootPath !== null) { | ||
const path = fileURLToPath(url); | ||
const relativePath = relative(this.rootPath, path); | ||
if (relativePath.startsWith('..')) { | ||
throw new Error(`Import map cannot reference '${path}' as it's outside of the base directory '${this.rootPath}'`); | ||
} | ||
} | ||
filteredImports[specifier] = url.toString(); | ||
}); | ||
return filteredImports; | ||
} | ||
// Takes a `scopes` object and runs all imports through `filterImports`, | ||
// omitting any scopes for which there are no imports. | ||
filterScopes(scopes) { | ||
const filteredScopes = {}; | ||
if (scopes !== undefined) { | ||
Object.keys(scopes).forEach((url) => { | ||
const imports = this.filterImports(scopes[url]); | ||
if (Object.keys(imports).length === 0) { | ||
return; | ||
} | ||
filteredScopes[url] = imports; | ||
}); | ||
} | ||
return filteredScopes; | ||
} | ||
// Returns the import map as a plain object, with any relative paths resolved | ||
// to full URLs. It takes an optional `prefixes` object that specifies a list | ||
// of prefixes to replace path prefixes (see `applyPrefixesToPath`). Prefixes | ||
// will be applied on both `imports` and `scopes`. | ||
getContents(prefixes = {}) { | ||
let imports = {}; | ||
let scopes = {}; | ||
this.sources.forEach((file) => { | ||
const importMap = ImportMap.resolve(file, basePath, prefix); | ||
const importMap = this.resolve(file); | ||
imports = { ...imports, ...importMap.imports }; | ||
@@ -111,7 +118,45 @@ scopes = { ...scopes, ...importMap.scopes }; | ||
}); | ||
const transformedImports = ImportMap.applyPrefixesToImports(imports, prefixes); | ||
const transformedScopes = Object.entries(scopes).reduce((acc, [key, value]) => ({ | ||
...acc, | ||
[ImportMap.applyPrefixesToPath(key, prefixes)]: ImportMap.applyPrefixesToImports(value, prefixes), | ||
}), {}); | ||
return { | ||
imports, | ||
scopes, | ||
imports: transformedImports, | ||
scopes: transformedScopes, | ||
}; | ||
} | ||
static async readFile(path, logger) { | ||
const baseURL = pathToFileURL(path); | ||
try { | ||
const data = await fs.readFile(path, 'utf8'); | ||
const importMap = JSON.parse(data); | ||
return { | ||
...importMap, | ||
baseURL, | ||
}; | ||
} | ||
catch (error) { | ||
if (isFileNotFoundError(error)) { | ||
logger.system(`Did not find an import map file at '${path}'.`); | ||
} | ||
else { | ||
logger.user(`Error while loading import map at '${path}':`, error); | ||
} | ||
} | ||
return { | ||
baseURL, | ||
imports: {}, | ||
}; | ||
} | ||
// Resolves an import map file by transforming all relative paths into full | ||
// URLs. The `baseURL` property of each file is used to resolve all relative | ||
// paths against. | ||
resolve(source) { | ||
const { baseURL, ...importMap } = source; | ||
const parsedImportMap = parse(importMap, baseURL); | ||
const imports = this.filterImports(parsedImportMap.imports); | ||
const scopes = this.filterScopes(parsedImportMap.scopes); | ||
return { ...parsedImportMap, imports, scopes }; | ||
} | ||
toDataURL() { | ||
@@ -129,25 +174,1 @@ const data = JSON.stringify(this.getContents()); | ||
} | ||
const readFile = async (path, logger) => { | ||
const baseURL = pathToFileURL(path); | ||
try { | ||
const data = await fs.readFile(path, 'utf8'); | ||
const importMap = JSON.parse(data); | ||
return { | ||
...importMap, | ||
baseURL, | ||
}; | ||
} | ||
catch (error) { | ||
if (isFileNotFoundError(error)) { | ||
logger.system(`Did not find an import map file at '${path}'.`); | ||
} | ||
else { | ||
logger.user(`Error while loading import map at '${path}':`, error); | ||
} | ||
} | ||
return { | ||
baseURL, | ||
imports: {}, | ||
}; | ||
}; | ||
export { ImportMap, readFile }; |
@@ -6,3 +6,3 @@ import { promises as fs } from 'fs'; | ||
import tmp from 'tmp-promise'; | ||
import { test, expect } from 'vitest'; | ||
import { describe, test, expect } from 'vitest'; | ||
import { ImportMap } from './import_map.js'; | ||
@@ -43,25 +43,67 @@ test('Handles import maps with full URLs without specifying a base URL', () => { | ||
}); | ||
test('Transforms relative paths so that they become relative to the base path', () => { | ||
const basePath = join(cwd(), 'my-cool-site', 'import-map.json'); | ||
describe('Returns the fully resolved import map', () => { | ||
const inputFile1 = { | ||
baseURL: pathToFileURL(basePath), | ||
baseURL: new URL('file:///some/full/path/import-map.json'), | ||
imports: { | ||
'alias:pets': './heart/pets/', | ||
specifier1: 'file:///some/full/path/file.js', | ||
specifier2: './file2.js', | ||
specifier3: 'file:///different/full/path/file3.js', | ||
}, | ||
}; | ||
// Without a prefix. | ||
const map1 = new ImportMap([inputFile1]); | ||
const { imports: imports1 } = map1.getContents(cwd()); | ||
expect(imports1['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts'); | ||
expect(imports1['alias:pets']).toBe('file:///my-cool-site/heart/pets/'); | ||
// With a prefix. | ||
const map2 = new ImportMap([inputFile1]); | ||
const { imports: imports2 } = map2.getContents(cwd(), 'file:///root/'); | ||
expect(imports2['netlify:edge']).toBe('https://edge.netlify.com/v1/index.ts'); | ||
expect(imports2['alias:pets']).toBe('file:///root/my-cool-site/heart/pets/'); | ||
const inputFile2 = { | ||
baseURL: new URL('file:///some/cool/path/import-map.json'), | ||
imports: { | ||
'lib/*': './library/', | ||
}, | ||
scopes: { | ||
'with/scopes/': { | ||
'lib/*': 'https://external.netlify/lib/', | ||
foo: './foo-alias', | ||
bar: 'file:///different/full/path/bar.js', | ||
}, | ||
}, | ||
}; | ||
test('Without prefixes', () => { | ||
const map = new ImportMap([inputFile1, inputFile2]); | ||
const { imports, scopes } = map.getContents(); | ||
expect(imports).toStrictEqual({ | ||
'lib/*': 'file:///some/cool/path/library/', | ||
specifier3: 'file:///different/full/path/file3.js', | ||
specifier2: 'file:///some/full/path/file2.js', | ||
specifier1: 'file:///some/full/path/file.js', | ||
'netlify:edge': 'https://edge.netlify.com/v1/index.ts', | ||
}); | ||
expect(scopes).toStrictEqual({ | ||
'file:///some/cool/path/with/scopes/': { | ||
'lib/*': 'https://external.netlify/lib/', | ||
foo: 'file:///some/cool/path/foo-alias', | ||
bar: 'file:///different/full/path/bar.js', | ||
}, | ||
}); | ||
}); | ||
test('With prefixes', () => { | ||
const map = new ImportMap([inputFile1, inputFile2]); | ||
const { imports, scopes } = map.getContents({ | ||
'file:///some/': 'file:///root/', | ||
'file:///different/': 'file:///vendor/', | ||
}); | ||
expect(imports).toStrictEqual({ | ||
'lib/*': 'file:///root/cool/path/library/', | ||
specifier3: 'file:///vendor/full/path/file3.js', | ||
specifier2: 'file:///root/full/path/file2.js', | ||
specifier1: 'file:///root/full/path/file.js', | ||
'netlify:edge': 'https://edge.netlify.com/v1/index.ts', | ||
}); | ||
expect(scopes).toStrictEqual({ | ||
'file:///root/cool/path/with/scopes/': { | ||
'lib/*': 'https://external.netlify/lib/', | ||
foo: 'file:///root/cool/path/foo-alias', | ||
bar: 'file:///vendor/full/path/bar.js', | ||
}, | ||
}); | ||
}); | ||
}); | ||
test('Throws when an import map uses a relative path to reference a file outside of the base path', () => { | ||
const basePath = join(cwd(), 'my-cool-site'); | ||
const inputFile1 = { | ||
baseURL: pathToFileURL(join(basePath, 'import_map.json')), | ||
baseURL: pathToFileURL(join(cwd(), 'import-map.json')), | ||
imports: { | ||
@@ -71,4 +113,4 @@ 'alias:file': '../file.js', | ||
}; | ||
const map = new ImportMap([inputFile1]); | ||
expect(() => map.getContents(basePath)).toThrowError(`Import map cannot reference '${join(cwd(), 'file.js')}' as it's outside of the base directory '${basePath}'`); | ||
const map = new ImportMap([inputFile1], pathToFileURL(cwd()).toString()); | ||
expect(() => map.getContents()).toThrowError(`Import map cannot reference '${join(cwd(), '..', 'file.js')}' as it's outside of the base directory '${cwd()}'`); | ||
}); | ||
@@ -75,0 +117,0 @@ test('Writes import map file to disk', async () => { |
@@ -11,2 +11,3 @@ import type { Bundle } from './bundle.js'; | ||
excluded_patterns: string[]; | ||
path?: string; | ||
} | ||
@@ -13,0 +14,0 @@ interface EdgeFunctionConfig { |
import { promises as fs } from 'fs'; | ||
import { join } from 'path'; | ||
import globToRegExp from 'glob-to-regexp'; | ||
import { wrapBundleError } from './bundle_error.js'; | ||
import { parsePattern } from './declaration.js'; | ||
@@ -68,2 +69,5 @@ import { getPackageVersion } from './package_json.js'; | ||
}; | ||
if ('path' in declaration) { | ||
route.path = declaration.path; | ||
} | ||
if (declaration.cache === "manual" /* Cache.Manual */) { | ||
@@ -93,11 +97,16 @@ postCacheRoutes.push(route); | ||
if (featureFlags === null || featureFlags === void 0 ? void 0 : featureFlags.edge_functions_path_urlpattern) { | ||
const pattern = new ExtendedURLPattern({ pathname: path }); | ||
// Removing the `^` and `$` delimiters because we'll need to modify what's | ||
// between them. | ||
const source = pattern.regexp.pathname.source.slice(1, -1); | ||
// Wrapping the expression source with `^` and `$`. Also, adding an optional | ||
// trailing slash, so that a declaration of `path: "/foo"` matches requests | ||
// for both `/foo` and `/foo/`. | ||
const normalizedSource = `^${source}\\/?$`; | ||
return normalizedSource; | ||
try { | ||
const pattern = new ExtendedURLPattern({ pathname: path }); | ||
// Removing the `^` and `$` delimiters because we'll need to modify what's | ||
// between them. | ||
const source = pattern.regexp.pathname.source.slice(1, -1); | ||
// Wrapping the expression source with `^` and `$`. Also, adding an optional | ||
// trailing slash, so that a declaration of `path: "/foo"` matches requests | ||
// for both `/foo` and `/foo/`. | ||
const normalizedSource = `^${source}\\/?$`; | ||
return normalizedSource; | ||
} | ||
catch (error) { | ||
throw wrapBundleError(error); | ||
} | ||
} | ||
@@ -104,0 +113,0 @@ // We use the global flag so that `globToRegExp` will not wrap the expression |
@@ -5,2 +5,3 @@ import { env } from 'process'; | ||
import { BundleFormat } from './bundle.js'; | ||
import { BundleError } from './bundle_error.js'; | ||
import { generateManifest } from './manifest.js'; | ||
@@ -25,3 +26,3 @@ test('Generates a manifest with different bundles', () => { | ||
]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }]; | ||
expect(manifest.bundles).toEqual(expectedBundles); | ||
@@ -46,3 +47,3 @@ expect(manifest.routes).toEqual(expectedRoutes); | ||
}); | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }]; | ||
expect(manifest.function_config).toEqual({ | ||
@@ -69,3 +70,3 @@ 'func-1': { name: 'Display Name' }, | ||
}); | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }]; | ||
const expectedFunctionConfig = { 'func-1': { generator: '@netlify/fake-plugin@1.0.0' } }; | ||
@@ -94,8 +95,13 @@ expect(manifest.routes).toEqual(expectedRoutes); | ||
const expectedRoutes = [ | ||
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'] }, | ||
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'] }, | ||
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: ['^/f1/exclude/?$'], path: '/f1/*' }, | ||
{ | ||
function: 'func-2', | ||
pattern: '^/f2(?:/(.*))/?$', | ||
excluded_patterns: ['^/f2/exclude$', '^/f2/exclude-as-well$'], | ||
}, | ||
{ | ||
function: 'func-3', | ||
pattern: '^(?:/(.*))/?$', | ||
excluded_patterns: ['^(?:/((?:.*)(?:/(?:.*))*))?(?:/(.*))\\.html/?$'], | ||
path: '/*', | ||
}, | ||
@@ -123,3 +129,3 @@ ]; | ||
}); | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }]; | ||
expect(manifest.routes).toEqual(expectedRoutes); | ||
@@ -206,2 +212,3 @@ expect(manifest.function_config).toEqual({ | ||
excluded_patterns: [], | ||
path: '/showcases/*', | ||
}, | ||
@@ -212,2 +219,3 @@ { | ||
excluded_patterns: ['^(?:/(.*))/terms-and-conditions/?$'], | ||
path: '/checkout/*', | ||
}, | ||
@@ -227,2 +235,40 @@ ]); | ||
}); | ||
test('URLPattern named groups are supported', () => { | ||
const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }]; | ||
const declarations = [{ function: 'customisation', path: '/products/:productId' }]; | ||
const userFunctionConfig = {}; | ||
const internalFunctionConfig = {}; | ||
const manifest = generateManifest({ | ||
bundles: [], | ||
declarations, | ||
functions, | ||
userFunctionConfig, | ||
internalFunctionConfig, | ||
featureFlags: { edge_functions_path_urlpattern: true }, | ||
}); | ||
expect(manifest.routes).toEqual([ | ||
{ | ||
function: 'customisation', | ||
pattern: '^/products(?:/([^/]+?))/?$', | ||
excluded_patterns: [], | ||
path: '/products/:productId', | ||
}, | ||
]); | ||
const matcher = getRouteMatcher(manifest); | ||
expect(matcher('/products/jigsaw-doweling-jig')).toBeDefined(); | ||
}); | ||
test('Invalid Path patterns throw bundling errors', () => { | ||
const functions = [{ name: 'customisation', path: '/path/to/customisation.ts' }]; | ||
const declarations = [{ function: 'customisation', path: '/https://foo.netlify.app/' }]; | ||
const userFunctionConfig = {}; | ||
const internalFunctionConfig = {}; | ||
expect(() => generateManifest({ | ||
bundles: [], | ||
declarations, | ||
functions, | ||
userFunctionConfig, | ||
internalFunctionConfig, | ||
featureFlags: { edge_functions_path_urlpattern: true }, | ||
})).toThrowError(BundleError); | ||
}); | ||
test('Includes failure modes in manifest', () => { | ||
@@ -259,3 +305,3 @@ const functions = [ | ||
const manifest = generateManifest({ bundles: [bundle1], declarations, functions }); | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }]; | ||
expect(manifest.routes).toEqual(expectedRoutes); | ||
@@ -275,3 +321,3 @@ }); | ||
const manifest = generateManifest({ bundles: [bundle1], declarations, functions }); | ||
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }]; | ||
expect(manifest.routes).toEqual(expectedRoutes); | ||
@@ -283,3 +329,3 @@ }); | ||
const manifest = generateManifest({ bundles: [], declarations, functions }); | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [] }]; | ||
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }]; | ||
expect(manifest.bundles).toEqual([]); | ||
@@ -316,6 +362,8 @@ expect(manifest.routes).toEqual(expectedRoutes); | ||
const expectedPreCacheRoutes = [ | ||
{ function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [] }, | ||
{ function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [] }, | ||
{ function: 'func-1', name: undefined, pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }, | ||
{ function: 'func-2', name: undefined, pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }, | ||
]; | ||
const expectedPostCacheRoutes = [{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [] }]; | ||
const expectedPostCacheRoutes = [ | ||
{ function: 'func-3', name: undefined, pattern: '^/f3/?$', excluded_patterns: [], path: '/f3' }, | ||
]; | ||
expect(manifest.bundles).toEqual(expectedBundles); | ||
@@ -336,4 +384,4 @@ expect(manifest.routes).toEqual(expectedPreCacheRoutes); | ||
const expectedRoutes = [ | ||
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [] }, | ||
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [] }, | ||
{ function: 'func-1', pattern: '^/f1(?:/(.*))/?$', excluded_patterns: [], path: '/f1/*' }, | ||
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excluded_patterns: [], path: '/f2/*' }, | ||
]; | ||
@@ -340,0 +388,0 @@ const layers = [ |
@@ -50,2 +50,5 @@ declare const edgeManifestSchema: { | ||
}; | ||
path: { | ||
type: string; | ||
}; | ||
}; | ||
@@ -83,2 +86,5 @@ additionalProperties: boolean; | ||
}; | ||
path: { | ||
type: string; | ||
}; | ||
}; | ||
@@ -85,0 +91,0 @@ additionalProperties: boolean; |
@@ -31,2 +31,3 @@ const bundlesSchema = { | ||
generator: { type: 'string' }, | ||
path: { type: 'string' }, | ||
}, | ||
@@ -33,0 +34,0 @@ additionalProperties: false, |
{ | ||
"name": "@netlify/edge-bundler", | ||
"version": "8.17.1", | ||
"version": "8.18.0", | ||
"description": "Intelligently prepare Netlify Edge Functions for deployment", | ||
@@ -61,3 +61,3 @@ "type": "module", | ||
"@types/uuid": "^9.0.0", | ||
"@vitest/coverage-v8": "^0.33.0", | ||
"@vitest/coverage-v8": "^0.34.0", | ||
"archiver": "^5.3.1", | ||
@@ -71,3 +71,3 @@ "chalk": "^4.1.2", | ||
"typescript": "^5.0.0", | ||
"vitest": "^0.33.0" | ||
"vitest": "^0.34.0" | ||
}, | ||
@@ -74,0 +74,0 @@ "engines": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3111416
8156