Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b0bab6e
feat: deploy edge functions as tarballs
eduardoboucas Dec 13, 2024
d959b54
chore: cleanup
eduardoboucas Dec 13, 2024
780b95a
refactor: abstract TS transpilation logic
eduardoboucas Dec 13, 2024
d3e77d0
chore: remove comment
eduardoboucas Dec 13, 2024
eb616ac
chore: fix test
eduardoboucas Dec 13, 2024
828c05d
chore: update CI versions
eduardoboucas Dec 17, 2024
d22e87d
chore: remove node prefix
eduardoboucas Dec 17, 2024
2ece335
Merge branch 'main' into feat/ef-tarball
eduardoboucas Dec 19, 2024
840776a
fix: add type check
eduardoboucas Dec 19, 2024
ebcf3de
chore: support Node 14
eduardoboucas Dec 19, 2024
6757215
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 22, 2025
ccb5bcc
chore: update test
eduardoboucas Jul 22, 2025
107b5bc
fix: fix test
eduardoboucas Jul 22, 2025
aba6353
fix: fix lint issue
eduardoboucas Jul 23, 2025
7b99fd8
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 23, 2025
1bd36cf
refactor: use deno bundle
eduardoboucas Jul 24, 2025
06e5536
chore: add comment
eduardoboucas Jul 24, 2025
17c7d4e
chore: update Deno version
eduardoboucas Jul 24, 2025
5091196
refactor: remove unused files
eduardoboucas Jul 25, 2025
56eb725
chore: remove lock file
eduardoboucas Jul 25, 2025
19f8e02
refactor: revert lock flag
eduardoboucas Jul 25, 2025
e6c83ff
fix: fix test
eduardoboucas Jul 25, 2025
d8b704b
chore: update Deno
eduardoboucas Jul 25, 2025
10b288c
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 25, 2025
db6a971
refactor: revert lock flag
eduardoboucas Jul 25, 2025
b89cb5e
chore: add debug log
eduardoboucas Jul 25, 2025
638ae47
chore: add more debug
eduardoboucas Jul 25, 2025
a6b2333
refactor: fix path generation
eduardoboucas Jul 28, 2025
7700a45
chore: clean up test util
eduardoboucas Jul 28, 2025
bc9e874
refactor: revert version bump
eduardoboucas Jul 28, 2025
9d818e2
chore: simplify test
eduardoboucas Jul 28, 2025
261772c
chore: do not use lock file
eduardoboucas Jul 28, 2025
5d62045
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 28, 2025
4ceec14
fix: read feature flag
eduardoboucas Jul 28, 2025
2204a2c
refactor: revert fixture change
eduardoboucas Jul 28, 2025
070c47f
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 28, 2025
492a475
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 29, 2025
92603bb
fix: use stable hash
eduardoboucas Jul 29, 2025
b1a719b
fix: fix file type check
eduardoboucas Jul 29, 2025
719b9c1
fix: improve sorting
eduardoboucas Jul 29, 2025
514e77e
Merge branch 'main' into feat/ef-tarball
eduardoboucas Jul 30, 2025
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
4 changes: 3 additions & 1 deletion .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ jobs:
os: [ubuntu-24.04, macos-14, windows-2025]
node-version: ['22']
# Must include the minimum deno version from the `DENO_VERSION_RANGE` constant in `node/bridge.ts`.
deno-version: ['v1.39.0', 'v2.2.4']
# We're adding v2.4.2 here because it's needed for the upcoming nimble release, so we can test
# those workflows ahead of time before we can update the base version across the board.
deno-version: ['v1.39.0', 'v2.2.4', 'v2.4.2']
include:
- os: ubuntu-24.04
# Earliest supported version
Expand Down
8 changes: 1 addition & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/edge-bundler/deno/lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const loadWithRetry = (specifier: string, delay = 1000, maxTry = 3) => {
maxTry,
});
} catch (error) {
if (isTooManyTries(error as Error)) {
if (error instanceof Error && isTooManyTries(error)) {
console.error(`Loading ${specifier} failed after ${maxTry} tries.`);
}
throw error;
Expand Down
28 changes: 16 additions & 12 deletions packages/edge-bundler/node/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import pathKey from 'path-key'
import semver from 'semver'

import { download } from './downloader.js'
import { FeatureFlags } from './feature_flags.js'
import { getPathInHome } from './home_path.js'
import { getLogger, Logger } from './logger.js'
import { getBinaryExtension } from './platform.js'
Expand All @@ -16,27 +17,31 @@ const DENO_VERSION_FILE = 'version.txt'
// When updating DENO_VERSION_RANGE, ensure that the deno version
// on the netlify/buildbot build image satisfies this range!
// https://github.com/netlify/buildbot/blob/f9c03c9dcb091d6570e9d0778381560d469e78ad/build-image/noble/Dockerfile#L410
const DENO_VERSION_RANGE = '1.39.0 - 2.2.4'
export const DENO_VERSION_RANGE = '1.39.0 - 2.2.4'

type OnBeforeDownloadHook = () => void | Promise<void>
type OnAfterDownloadHook = (error?: Error) => void | Promise<void>
const NEXT_DENO_VERSION_RANGE = '^2.4.2'

interface DenoOptions {
export type OnBeforeDownloadHook = () => void | Promise<void>
export type OnAfterDownloadHook = (error?: Error) => void | Promise<void>

export interface DenoOptions {
cacheDirectory?: string
debug?: boolean
denoDir?: string
featureFlags?: FeatureFlags
logger?: Logger
onAfterDownload?: OnAfterDownloadHook
onBeforeDownload?: OnBeforeDownloadHook
useGlobal?: boolean
versionRange?: string
}

interface ProcessRef {
export interface ProcessRef {
ps?: ExecaChildProcess<string>
}

interface RunOptions {
cwd?: string
env?: NodeJS.ProcessEnv
extendEnv?: boolean
pipeOutput?: boolean
Expand All @@ -45,7 +50,7 @@ interface RunOptions {
rejectOnExitCode?: boolean
}

class DenoBridge {
export class DenoBridge {
cacheDirectory: string
currentDownload?: ReturnType<DenoBridge['downloadBinary']>
debug: boolean
Expand All @@ -64,7 +69,9 @@ class DenoBridge {
this.onAfterDownload = options.onAfterDownload
this.onBeforeDownload = options.onBeforeDownload
this.useGlobal = options.useGlobal ?? true
this.versionRange = options.versionRange ?? DENO_VERSION_RANGE
this.versionRange =
options.versionRange ??
(options.featureFlags?.edge_bundler_generate_tarball ? NEXT_DENO_VERSION_RANGE : DENO_VERSION_RANGE)
}

private async downloadBinary() {
Expand Down Expand Up @@ -245,11 +252,11 @@ class DenoBridge {
// process, awaiting its execution.
async run(
args: string[],
{ env: inputEnv, extendEnv = true, rejectOnExitCode = true, stderr, stdout }: RunOptions = {},
{ cwd, env: inputEnv, extendEnv = true, rejectOnExitCode = true, stderr, stdout }: RunOptions = {},
) {
const { path: binaryPath } = await this.getBinaryPath()
const env = this.getEnvironmentVariables(inputEnv)
const options: Options = { env, extendEnv, reject: rejectOnExitCode }
const options: Options = { cwd, env, extendEnv, reject: rejectOnExitCode }

return DenoBridge.runWithBinary(binaryPath, args, { options, stderr, stdout })
}
Expand All @@ -271,6 +278,3 @@ class DenoBridge {
}
}
}

export { DENO_VERSION_RANGE, DenoBridge }
export type { DenoOptions, OnAfterDownloadHook, OnBeforeDownloadHook, ProcessRef }
1 change: 1 addition & 0 deletions packages/edge-bundler/node/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum BundleFormat {
ESZIP2 = 'eszip2',
JS = 'js',
TARBALL = 'tar',
}

export interface Bundle {
Expand Down
104 changes: 100 additions & 4 deletions packages/edge-bundler/node/bundler.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Buffer } from 'buffer'
import { execSync } from 'node:child_process'
import { access, readdir, readFile, rm, writeFile } from 'fs/promises'
import { join, resolve } from 'path'
import process from 'process'
import { pathToFileURL } from 'url'

import { lt } from 'semver'
import tmp from 'tmp-promise'
import { test, expect, vi } from 'vitest'
import { test, expect, vi, describe } from 'vitest'

import { importMapSpecifier } from '../shared/consts.js'
import { runESZIP, useFixture } from '../test/util.js'
import { runESZIP, runTarball, useFixture } from '../test/util.js'

import { BundleError } from './bundle_error.js'
import { bundle, BundleOptions } from './bundler.js'
Expand Down Expand Up @@ -48,7 +50,6 @@ test('Produces an ESZIP bundle', async () => {
expect(importMapURL).toBe(importMapSpecifier)

const bundlePath = join(distPath, bundles[0].asset)

const { func1, func2, func3 } = await runESZIP(bundlePath)

expect(func1).toBe('HELLO, JANE DOE!')
Expand Down Expand Up @@ -499,7 +500,7 @@ test('Loads npm modules from bare specifiers', async () => {
const { func1 } = await runESZIP(bundlePath, vendorDirectory.path)

expect(func1).toBe(
`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>`,
`<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>, TmV0bGlmeQ==`,
)

await cleanup()
Expand Down Expand Up @@ -692,3 +693,98 @@ test('Loads edge functions from the Frameworks API', async () => {

await cleanup()
})

const denoVersion = execSync('deno eval --no-lock "console.log(Deno.version.deno)"').toString()

describe.skipIf(lt(denoVersion, '2.4.2'))(
'Produces a tarball bundle',
() => {
test('With only local imports', async () => {
const systemLogger = vi.fn()
const { basePath, cleanup, distPath } = await useFixture('imports_node_builtin', { copyDirectory: true })
const declarations: Declaration[] = [
{
function: 'func1',
path: '/func1',
},
]
const vendorDirectory = await tmp.dir()

await bundle([join(basePath, 'netlify/edge-functions')], distPath, declarations, {
basePath,
configPath: join(basePath, '.netlify/edge-functions/config.json'),
featureFlags: {
edge_bundler_generate_tarball: true,
},
systemLogger,
})

expect(
systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:'),
).toBeUndefined()

const expectedOutput = {
func1: 'ok',
}

const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8')
const manifest = JSON.parse(manifestFile)

const tarballPath = join(distPath, manifest.bundles[0].asset)
const tarballResult = await runTarball(tarballPath)
expect(tarballResult).toStrictEqual(expectedOutput)

const eszipPath = join(distPath, manifest.bundles[1].asset)
const eszipResult = await runESZIP(eszipPath)
expect(eszipResult).toStrictEqual(expectedOutput)

await cleanup()
await rm(vendorDirectory.path, { force: true, recursive: true })
})

// TODO: https://github.com/denoland/deno/issues/30187
test.todo('Using npm modules', async () => {
const systemLogger = vi.fn()
const { basePath, cleanup, distPath } = await useFixture('imports_npm_module', { copyDirectory: true })
const sourceDirectory = join(basePath, 'functions')
const declarations: Declaration[] = [
{
function: 'func1',
path: '/func1',
},
]
const vendorDirectory = await tmp.dir()

await bundle([sourceDirectory], distPath, declarations, {
basePath,
featureFlags: {
edge_bundler_generate_tarball: true,
},
importMapPaths: [join(basePath, 'import_map.json')],
vendorDirectory: vendorDirectory.path,
systemLogger,
})

expect(
systemLogger.mock.calls.find((call) => call[0] === 'Could not track dependencies in edge function:'),
).toBeUndefined()

const expectedOutput = `<parent-1><child-1>JavaScript</child-1></parent-1>, <parent-2><child-2><grandchild-1>APIs<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-2>, <parent-3><child-2><grandchild-1>Markup<cwd>${process.cwd()}</cwd></grandchild-1></child-2></parent-3>, TmV0bGlmeQ==`

const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8')
const manifest = JSON.parse(manifestFile)

const tarballPath = join(distPath, manifest.bundles[0].asset)
const tarballResult = await runTarball(tarballPath)
expect(tarballResult.func1).toBe(expectedOutput)

const eszipPath = join(distPath, manifest.bundles[1].asset)
const eszipResult = await runESZIP(eszipPath, vendorDirectory.path)
expect(eszipResult.func1).toBe(expectedOutput)

await cleanup()
await rm(vendorDirectory.path, { force: true, recursive: true })
})
},
10_000,
)
50 changes: 36 additions & 14 deletions packages/edge-bundler/node/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EdgeFunction } from './edge_function.js'
import { FeatureFlags, getFlags } from './feature_flags.js'
import { findFunctions } from './finder.js'
import { bundle as bundleESZIP } from './formats/eszip.js'
import { bundle as bundleTarball } from './formats/tarball.js'
import { ImportMap } from './import_map.js'
import { getLogger, LogFunction, Logger } from './logger.js'
import { writeManifest } from './manifest.js'
Expand Down Expand Up @@ -66,6 +67,7 @@ export const bundle = async (
const options: DenoOptions = {
debug,
cacheDirectory,
featureFlags,
logger,
onAfterDownload,
onBeforeDownload,
Expand Down Expand Up @@ -114,27 +116,47 @@ export const bundle = async (
vendorDirectory,
})

const bundles: Bundle[] = []

if (featureFlags.edge_bundler_generate_tarball) {
bundles.push(
await bundleTarball({
basePath,
buildID,
debug,
deno,
distDirectory,
functions,
featureFlags,
importMap: importMap.clone(),
vendorDirectory: vendor?.directory,
}),
)
}

if (vendor) {
importMap.add(vendor.importMap)
}

const functionBundle = await bundleESZIP({
basePath,
buildID,
debug,
deno,
distDirectory,
externals,
functions,
featureFlags,
importMap,
vendorDirectory: vendor?.directory,
})
bundles.push(
await bundleESZIP({
basePath,
buildID,
debug,
deno,
distDirectory,
externals,
functions,
featureFlags,
importMap,
vendorDirectory: vendor?.directory,
}),
)

// The final file name of the bundles contains a SHA256 hash of the contents,
// which we can only compute now that the files have been generated. So let's
// rename the bundles to their permanent names.
await createFinalBundles([functionBundle], distDirectory, buildID)
await createFinalBundles(bundles, distDirectory, buildID)

// Retrieving a configuration object for each function.
// Run `getFunctionConfig` in parallel as it is a non-trivial operation and spins up deno
Expand Down Expand Up @@ -165,7 +187,7 @@ export const bundle = async (
})

const manifest = await writeManifest({
bundles: [functionBundle],
bundles,
declarations,
distDirectory,
featureFlags,
Expand Down
Loading
Loading