Skip to content

Commit

Permalink
feat: export mergeDeclarations function (#334)
Browse files Browse the repository at this point in the history
* feat: export `mergeDeclarations` function

* chore: fix linting error

* chore: remove ESLint directives

* refactor: remove `NETLIFY_EDGE_BOOTSTRAP` var

* refactor: remove exports

* chore: fix test

* chore: update test

* chore: update bootstrap URL in test

* chore: update .eslintrc.cjs

Co-authored-by: Daniel Tschinder <231804+danez@users.noreply.github.com>

---------

Co-authored-by: Daniel Tschinder <231804+danez@users.noreply.github.com>
  • Loading branch information
eduardoboucas and danez committed Mar 13, 2023
1 parent 0074102 commit a3b01c0
Show file tree
Hide file tree
Showing 11 changed files with 45 additions and 81 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = {
'no-shadow': 'off',
'no-use-before-define': 'off',
'unicorn/prefer-json-parse-buffer': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }],
},
overrides: [
...overrides,
Expand Down
38 changes: 0 additions & 38 deletions node/bootstrap.test.ts

This file was deleted.

4 changes: 2 additions & 2 deletions node/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { importMapSpecifier } from '../shared/consts.js'
import { DenoBridge, DenoOptions, OnAfterDownloadHook, OnBeforeDownloadHook } from './bridge.js'
import type { Bundle } from './bundle.js'
import { FunctionConfig, getFunctionConfig } from './config.js'
import { Declaration, getDeclarationsFromConfig } from './declaration.js'
import { Declaration, mergeDeclarations } from './declaration.js'
import { load as loadDeployConfig } from './deploy_config.js'
import { EdgeFunction } from './edge_function.js'
import { FeatureFlags, getFlags } from './feature_flags.js'
Expand Down Expand Up @@ -119,7 +119,7 @@ const bundle = async (

// Creating a final declarations array by combining the TOML file with the
// deploy configuration API and the in-source configuration.
const declarationsFromConfig = getDeclarationsFromConfig(tomlDeclarations, functionsWithConfig, deployConfig)
const declarationsFromConfig = mergeDeclarations(tomlDeclarations, functionsWithConfig, deployConfig.declarations)

// If any declarations are autogenerated and are missing the generator field
// add a default string.
Expand Down
25 changes: 11 additions & 14 deletions node/declaration.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { test, expect } from 'vitest'

import { FunctionConfig } from './config.js'
import { getDeclarationsFromConfig } from './declaration.js'
import { Declaration, mergeDeclarations } from './declaration.js'

// TODO: Add tests with the deploy config.
const deployConfig = {
declarations: [],
layers: [],
}
const deployConfigDeclarations: Declaration[] = []

test('In-source config takes precedence over netlify.toml config', () => {
const tomlConfig = [
Expand All @@ -26,7 +23,7 @@ test('In-source config takes precedence over netlify.toml config', () => {
{ function: 'json', path: '/json', cache: 'off' },
]

const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)
const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)

expect(declarations).toEqual(expectedDeclarations)
})
Expand All @@ -47,7 +44,7 @@ test("Declarations don't break if no in-source config is provided", () => {
{ function: 'json', path: '/json', cache: 'manual' },
]

const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)
const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)

expect(declarations).toEqual(expectedDeclarations)
})
Expand All @@ -71,10 +68,10 @@ test('In-source config works independent of the netlify.toml file if a path is d

const expectedDeclarationsWithoutISCPath = [{ function: 'geolocation', path: '/geo', cache: 'off' }]

const declarationsWithISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithPath, deployConfig)
const declarationsWithISCPath = mergeDeclarations(tomlConfig, funcConfigWithPath, deployConfigDeclarations)
expect(declarationsWithISCPath).toEqual(expectedDeclarationsWithISCPath)

const declarationsWithoutISCPath = getDeclarationsFromConfig(tomlConfig, funcConfigWithoutPath, deployConfig)
const declarationsWithoutISCPath = mergeDeclarations(tomlConfig, funcConfigWithoutPath, deployConfigDeclarations)
expect(declarationsWithoutISCPath).toEqual(expectedDeclarationsWithoutISCPath)
})

Expand All @@ -87,7 +84,7 @@ test('In-source config works if only the cache config property is set', () => {

const expectedDeclarations = [{ function: 'geolocation', path: '/geo', cache: 'manual' }]

expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations)
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations)
})

test("In-source config path property works if it's not an array", () => {
Expand All @@ -99,7 +96,7 @@ test("In-source config path property works if it's not an array", () => {

const expectedDeclarations = [{ function: 'json', path: '/json', cache: 'manual' }]

expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations)
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations)
})

test("In-source config path property works if it's not an array and it's not present in toml or deploy config", () => {
Expand All @@ -113,7 +110,7 @@ test("In-source config path property works if it's not an array and it's not pre
{ function: 'json', path: '/json-isc', cache: 'manual' },
]

expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations)
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations)
})

test('In-source config works if path property is an empty array with cache value specified', () => {
Expand All @@ -125,7 +122,7 @@ test('In-source config works if path property is an empty array with cache value

const expectedDeclarations = [{ function: 'json', path: '/json-toml', cache: 'manual' }]

expect(getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)).toEqual(expectedDeclarations)
expect(mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)).toEqual(expectedDeclarations)
})

test('netlify.toml-defined excludedPath are respected', () => {
Expand All @@ -135,7 +132,7 @@ test('netlify.toml-defined excludedPath are respected', () => {

const expectedDeclarations = [{ function: 'geolocation', path: '/geo/*', excludedPath: '/geo/exclude' }]

const declarations = getDeclarationsFromConfig(tomlConfig, funcConfig, deployConfig)
const declarations = mergeDeclarations(tomlConfig, funcConfig, deployConfigDeclarations)

expect(declarations).toEqual(expectedDeclarations)
})
25 changes: 10 additions & 15 deletions node/declaration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import regexpAST from 'regexp-tree'

import { FunctionConfig } from './config.js'
import type { DeployConfig } from './deploy_config.js'

interface BaseDeclaration {
cache?: string
Expand All @@ -20,12 +19,12 @@ type DeclarationWithPattern = BaseDeclaration & {
excludedPattern?: string
}

type Declaration = DeclarationWithPath | DeclarationWithPattern
export type Declaration = DeclarationWithPath | DeclarationWithPattern

export const getDeclarationsFromConfig = (
export const mergeDeclarations = (
tomlDeclarations: Declaration[],
functionsConfig: Record<string, FunctionConfig>,
deployConfig: DeployConfig,
deployConfigDeclarations: Declaration[],
) => {
const declarations: Declaration[] = []
const functionsVisited: Set<string> = new Set()
Expand All @@ -34,26 +33,24 @@ export const getDeclarationsFromConfig = (
// the deploy configuration file. For any declaration for which we also have
// a function configuration object, we replace the path because that object
// takes precedence.
for (const declaration of [...tomlDeclarations, ...deployConfig.declarations]) {
for (const declaration of [...tomlDeclarations, ...deployConfigDeclarations]) {
const config = functionsConfig[declaration.function]

// If no config is found, add the declaration as is
if (!config) {
// If no config is found, add the declaration as is.
declarations.push(declaration)

// If we have a path specified as either a string or non-empty array
// create a declaration for each path
} else if (config.path?.length) {
// If we have a path specified as either a string or non-empty array,
// create a declaration for each path.
const paths = Array.isArray(config.path) ? config.path : [config.path]

paths.forEach((path) => {
declarations.push({ ...declaration, cache: config.cache, path })
})

// With an in-source config without a path, add the config to the declaration
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// With an in-source config without a path, add the config to the declaration.
const { path, excludedPath, ...rest } = config

declarations.push({ ...declaration, ...rest })
}

Expand All @@ -65,7 +62,7 @@ export const getDeclarationsFromConfig = (
for (const name in functionsConfig) {
const { cache, path } = functionsConfig[name]

// If we have path specified create a declaration for each path
// If we have a path specified, create a declaration for each path.
if (!functionsVisited.has(name) && path) {
const paths = Array.isArray(path) ? path : [path]

Expand Down Expand Up @@ -108,5 +105,3 @@ export const parsePattern = (pattern: string) => {
// Strip leading and forward slashes.
return regex.toString().slice(1, -1)
}

export { Declaration, DeclarationWithPath, DeclarationWithPattern }
15 changes: 7 additions & 8 deletions node/formats/javascript.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { env } from 'process'
import { pathToFileURL } from 'url'

import { deleteAsync } from 'del'

import { EdgeFunction } from '../edge_function.js'
import type { FormatFunction } from '../server/server.js'

const BOOTSTRAP_LATEST = 'https://640b5b066a2b9b0008e88cb0--edge.netlify.com/bootstrap/index-combined.ts'

const defaultFormatExportTypeError: FormatFunction = (name) =>
`The Edge Function "${name}" has failed to load. Does it have a function as the default export?`

const defaultFormatImpoortError: FormatFunction = (name) => `There was an error with Edge Function "${name}".`

interface GenerateStage2Options {
bootstrapURL: string
distDirectory: string
fileName: string
formatExportTypeError?: FormatFunction
Expand All @@ -24,6 +22,7 @@ interface GenerateStage2Options {
}

const generateStage2 = async ({
bootstrapURL,
distDirectory,
fileName,
formatExportTypeError,
Expand All @@ -33,17 +32,16 @@ const generateStage2 = async ({
await deleteAsync(distDirectory, { force: true })
await fs.mkdir(distDirectory, { recursive: true })

const entryPoint = getLocalEntryPoint(functions, { formatExportTypeError, formatImportError })
const entryPoint = getLocalEntryPoint(functions, { bootstrapURL, formatExportTypeError, formatImportError })
const stage2Path = join(distDirectory, fileName)

await fs.writeFile(stage2Path, entryPoint)

return stage2Path
}

const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP ?? BOOTSTRAP_LATEST

interface GetLocalEntryPointOptions {
bootstrapURL: string
formatExportTypeError?: FormatFunction
formatImportError?: FormatFunction
}
Expand All @@ -54,11 +52,12 @@ interface GetLocalEntryPointOptions {
const getLocalEntryPoint = (
functions: EdgeFunction[],
{
bootstrapURL,
formatExportTypeError = defaultFormatExportTypeError,
formatImportError = defaultFormatImpoortError,
}: GetLocalEntryPointOptions,
) => {
const bootImport = `import { boot } from "${getBootstrapURL()}";`
const bootImport = `import { boot } from "${bootstrapURL}";`
const declaration = `const functions = {}; const metadata = { functions: {} };`
const imports = functions.map((func) => {
const url = pathToFileURL(func.path)
Expand Down Expand Up @@ -87,4 +86,4 @@ const getLocalEntryPoint = (
return [bootImport, declaration, ...imports, bootCall].join('\n\n')
}

export { generateStage2, getBootstrapURL, getLocalEntryPoint }
export { generateStage2, getLocalEntryPoint }
3 changes: 3 additions & 0 deletions node/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export { bundle } from './bundler.js'
export { DenoBridge } from './bridge.js'
export type { FunctionConfig } from './config.js'
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 { serve } from './server/server.js'
Expand Down
1 change: 1 addition & 0 deletions node/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface Route {
pattern: string
generator?: string
}

interface EdgeFunctionConfig {
excluded_patterns: string[]
on_error?: string
Expand Down
1 change: 1 addition & 0 deletions node/server/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test('Starts a server and serves requests for edge functions', async () => {
const port = await getPort()
const importMapPaths = [join(paths.internal, 'import_map.json'), join(paths.user, 'import-map.json')]
const server = await serve({
bootstrapURL: 'https://edge.netlify.com/bootstrap/index-combined.ts',
importMapPaths,
port,
})
Expand Down
6 changes: 6 additions & 0 deletions node/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { killProcess, waitForServer } from './util.js'
type FormatFunction = (name: string) => string

interface PrepareServerOptions {
bootstrapURL: string
deno: DenoBridge
distDirectory: string
entryPoint?: string
Expand All @@ -29,6 +30,7 @@ interface StartServerOptions {
}

const prepareServer = ({
bootstrapURL,
deno,
distDirectory,
flags: denoFlags,
Expand All @@ -51,6 +53,7 @@ const prepareServer = ({
let graph

const stage2Path = await generateStage2({
bootstrapURL,
distDirectory,
fileName: 'dev.js',
functions,
Expand Down Expand Up @@ -110,6 +113,7 @@ interface InspectSettings {
address?: string
}
interface ServeOptions {
bootstrapURL: string
certificatePath?: string
debug?: boolean
distImportMapPath?: string
Expand All @@ -124,6 +128,7 @@ interface ServeOptions {
}

const serve = async ({
bootstrapURL,
certificatePath,
debug,
distImportMapPath,
Expand Down Expand Up @@ -179,6 +184,7 @@ const serve = async ({
}

const server = prepareServer({
bootstrapURL,
deno,
distDirectory,
flags,
Expand Down

0 comments on commit a3b01c0

Please sign in to comment.