Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: return declarations without function and unrouted functions #523

Merged
merged 5 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export { Declaration, mergeDeclarations } from './declaration.js'
export type { EdgeFunction } from './edge_function.js'
export { findFunctions as find } from './finder.js'
export { generateManifest } from './manifest.js'
export type { EdgeFunctionConfig, Manifest } from './manifest.js'
export { serve } from './server/server.js'
export { validateManifest, ManifestValidationError } from './validation/manifest/index.js'
63 changes: 47 additions & 16 deletions node/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ test('Generates a manifest with different bundles', () => {
}
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations: Declaration[] = [{ function: 'func-1', path: '/f1' }]
const manifest = generateManifest({ bundles: [bundle1, bundle2], declarations, functions })
const { manifest } = generateManifest({ bundles: [bundle1, bundle2], declarations, functions })

const expectedBundles = [
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
Expand All @@ -45,7 +45,7 @@ test('Generates a manifest with display names', () => {
name: 'Display Name',
},
}
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand All @@ -69,7 +69,7 @@ test('Generates a manifest with a generator field', () => {
generator: '@netlify/fake-plugin@1.0.0',
},
}
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand All @@ -93,7 +93,7 @@ test('Generates a manifest with excluded paths and patterns', () => {
{ function: 'func-2', pattern: '^/f2(?:/(.*))/?$', excludedPattern: ['^/f2/exclude$', '^/f2/exclude-as-well$'] },
{ function: 'func-3', path: '/*', excludedPath: '/**/*.html' },
]
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand Down Expand Up @@ -129,7 +129,7 @@ test('TOML-defined paths can be combined with ISC-defined excluded paths', () =>
const userFunctionConfig: Record<string, FunctionConfig> = {
'func-1': { excludedPath: '/f1/exclude' },
}
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand Down Expand Up @@ -173,7 +173,7 @@ test('Filters out internal in-source configurations in user created functions',
generator: 'internal-generator',
},
}
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand Down Expand Up @@ -207,7 +207,7 @@ test('excludedPath from ISC goes into function_config, TOML goes into routes', (
},
}
const internalFunctionConfig: Record<string, FunctionConfig> = {}
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand Down Expand Up @@ -248,7 +248,7 @@ test('URLPattern named groups are supported', () => {
const declarations: Declaration[] = [{ function: 'customisation', path: '/products/:productId' }]
const userFunctionConfig: Record<string, FunctionConfig> = {}
const internalFunctionConfig: Record<string, FunctionConfig> = {}
const manifest = generateManifest({
const { manifest } = generateManifest({
bundles: [],
declarations,
functions,
Expand Down Expand Up @@ -300,7 +300,7 @@ test('Includes failure modes in manifest', () => {
onError: '/custom-error',
},
}
const manifest = generateManifest({ bundles: [], declarations, functions, userFunctionConfig })
const { manifest } = generateManifest({ bundles: [], declarations, functions, userFunctionConfig })
expect(manifest.function_config).toEqual({
'func-1': { on_error: '/custom-error' },
})
Expand All @@ -317,7 +317,7 @@ test('Excludes functions for which there are function files but no matching conf
{ name: 'func-2', path: '/path/to/func-2.ts' },
]
const declarations: Declaration[] = [{ function: 'func-1', path: '/f1' }]
const manifest = generateManifest({ bundles: [bundle1], declarations, functions })
const { manifest } = generateManifest({ bundles: [bundle1], declarations, functions })

const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }]

Expand All @@ -335,7 +335,7 @@ test('Excludes functions for which there are config declarations but no matching
{ function: 'func-1', path: '/f1' },
{ function: 'func-2', path: '/f2' },
]
const manifest = generateManifest({ bundles: [bundle1], declarations, functions })
const { manifest } = generateManifest({ bundles: [bundle1], declarations, functions })

const expectedRoutes = [{ function: 'func-2', pattern: '^/f2/?$', excluded_patterns: [], path: '/f2' }]

Expand All @@ -345,7 +345,7 @@ test('Excludes functions for which there are config declarations but no matching
test('Generates a manifest without bundles', () => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations: Declaration[] = [{ function: 'func-1', path: '/f1' }]
const manifest = generateManifest({ bundles: [], declarations, functions })
const { manifest } = generateManifest({ bundles: [], declarations, functions })

const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }]

Expand Down Expand Up @@ -375,7 +375,7 @@ test('Generates a manifest with pre and post-cache routes', () => {
{ function: 'func-2', cache: 'not_a_supported_value', path: '/f2' },
{ function: 'func-3', cache: 'manual', path: '/f3' },
]
const manifest = generateManifest({ bundles: [bundle1, bundle2], declarations, functions })
const { manifest } = generateManifest({ bundles: [bundle1, bundle2], declarations, functions })

const expectedBundles = [
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
Expand Down Expand Up @@ -414,12 +414,12 @@ test('Generates a manifest with layers', () => {
flag: 'edge_functions_onion_layer',
},
]
const manifest1 = generateManifest({
const { manifest: manifest1 } = generateManifest({
bundles: [],
declarations,
functions,
})
const manifest2 = generateManifest({
const { manifest: manifest2 } = generateManifest({
bundles: [],
declarations,
functions,
Expand Down Expand Up @@ -451,7 +451,38 @@ test('Throws an error if the regular expression contains a negative lookahead',
test('Converts named capture groups to unnamed capture groups in regular expressions', () => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations = [{ function: 'func-1', pattern: '^/(?<name>\\w+)$' }]
const manifest = generateManifest({ bundles: [], declarations, functions })
const { manifest } = generateManifest({ bundles: [], declarations, functions })

expect(manifest.routes).toEqual([{ function: 'func-1', pattern: '^/(\\w+)$', excluded_patterns: [] }])
})

test('Returns functions without a declaration and unrouted functions', () => {
const bundle = {
extension: '.ext1',
format: BundleFormat.ESZIP2,
hash: '123456',
}
const functions = [
{ name: 'func-1', path: '/path/to/func-1.ts' },
{ name: 'func-2', path: '/path/to/func-2.ts' },
{ name: 'func-4', path: '/path/to/func-4.ts' },
]
const declarations: Declaration[] = [
{ function: 'func-1', path: '/f1' },
{ function: 'func-3', path: '/f3' },

// @ts-expect-error Error is expected due to neither `path` or `pattern`
// being present.
{ function: 'func-4', name: 'Some name' },
]
const { declarationsWithoutFunction, manifest, unroutedFunctions } = generateManifest({
bundles: [bundle],
declarations,
functions,
})
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1/?$', excluded_patterns: [], path: '/f1' }]

expect(manifest.routes).toEqual(expectedRoutes)
expect(declarationsWithoutFunction).toEqual(['func-3'])
expect(unroutedFunctions).toEqual(['func-2', 'func-4'])
})
34 changes: 27 additions & 7 deletions node/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface Route {
methods?: string[]
}

interface EdgeFunctionConfig {
export interface EdgeFunctionConfig {
excluded_patterns: string[]
on_error?: string
generator?: string
Expand Down Expand Up @@ -82,7 +82,7 @@ const addExcludedPatterns = (
) => {
if (excludedPath) {
const paths = Array.isArray(excludedPath) ? excludedPath : [excludedPath]
const excludedPatterns = paths.map((path) => pathToRegularExpression(path)).map(serializePattern)
const excludedPatterns = paths.map(pathToRegularExpression).filter(nonNullable).map(serializePattern)

manifestFunctionConfig[name].excluded_patterns.push(...excludedPatterns)
}
Expand Down Expand Up @@ -119,12 +119,15 @@ const generateManifest = ({
const manifestFunctionConfig: Manifest['function_config'] = Object.fromEntries(
functions.map(({ name }) => [name, { excluded_patterns: [] }]),
)
const routedFunctions = new Set<string>()
const declarationsWithoutFunction = new Set<string>()

for (const [name, { excludedPath, onError }] of Object.entries(userFunctionConfig)) {
// If the config block is for a function that is not defined, discard it.
if (manifestFunctionConfig[name] === undefined) {
continue
}

addExcludedPatterns(name, manifestFunctionConfig, excludedPath)

manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError }
Expand All @@ -135,6 +138,7 @@ const generateManifest = ({
if (manifestFunctionConfig[name] === undefined) {
continue
}

addExcludedPatterns(name, manifestFunctionConfig, excludedPath)

manifestFunctionConfig[name] = { ...manifestFunctionConfig[name], on_error: onError, ...rest }
Expand All @@ -144,12 +148,22 @@ const generateManifest = ({
const func = functions.find(({ name }) => declaration.function === name)

if (func === undefined) {
declarationsWithoutFunction.add(declaration.function)

return
}

const pattern = getRegularExpression(declaration)
const excludedPattern = getExcludedRegularExpressions(declaration)

// If there is no `pattern`, the declaration will never be triggered, so we
// can discard it.
if (!pattern) {
return
}

routedFunctions.add(declaration.function)

const excludedPattern = getExcludedRegularExpressions(declaration)
const route: Route = {
function: func.name,
pattern: serializePattern(pattern),
Expand Down Expand Up @@ -183,11 +197,16 @@ const generateManifest = ({
import_map: importMap,
function_config: sanitizeEdgeFunctionConfig(manifestFunctionConfig),
}
const unroutedFunctions = functions.filter(({ name }) => !routedFunctions.has(name)).map(({ name }) => name)

return manifest
return { declarationsWithoutFunction: [...declarationsWithoutFunction], manifest, unroutedFunctions }
}

const pathToRegularExpression = (path: string) => {
if (!path) {
return null
}

try {
const pattern = new ExtendedURLPattern({ pathname: path })

Expand All @@ -206,7 +225,7 @@ const pathToRegularExpression = (path: string) => {
}
}

const getRegularExpression = (declaration: Declaration): string => {
const getRegularExpression = (declaration: Declaration) => {
if ('pattern' in declaration) {
try {
return parsePattern(declaration.pattern)
Expand Down Expand Up @@ -242,7 +261,8 @@ const getExcludedRegularExpressions = (declaration: Declaration): string[] => {

if ('path' in declaration && declaration.excludedPath) {
const paths = Array.isArray(declaration.excludedPath) ? declaration.excludedPath : [declaration.excludedPath]
return paths.map((path) => pathToRegularExpression(path))

return paths.map(pathToRegularExpression).filter(nonNullable)
}

return []
Expand All @@ -253,7 +273,7 @@ interface WriteManifestOptions extends GenerateManifestOptions {
}

const writeManifest = async ({ distDirectory, ...rest }: WriteManifestOptions) => {
const manifest = generateManifest(rest)
const { manifest } = generateManifest(rest)
const manifestPath = join(distDirectory, 'manifest.json')

await fs.writeFile(manifestPath, JSON.stringify(manifest))
Expand Down