Skip to content

Commit

Permalink
feat!: return declarations without function and unrouted functions (#523
Browse files Browse the repository at this point in the history
)

* feat!: return declarations without function and functions without declaration

BREAKING CHANGE: `generateManifest` exported method now returns an object with a `manifest` property

* refactor: rename to `unroutedFunctions`

* chore: update test name

* feat: add type exports
  • Loading branch information
eduardoboucas committed Nov 6, 2023
1 parent 7d59b81 commit 45f04a7
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 23 deletions.
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

0 comments on commit 45f04a7

Please sign in to comment.