Skip to content

Commit

Permalink
feat: use new manifest format (#11)
Browse files Browse the repository at this point in the history
* feat: use new manifest format

* chore: fix test

* chore: add test

* refactor: add Manifest type

* feat: update bootstrap layer
  • Loading branch information
eduardoboucas committed Mar 24, 2022
1 parent a3906d4 commit 7bee912
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 53 deletions.
2 changes: 1 addition & 1 deletion src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { env } from 'process'

const BOOTSTRAP_LATEST =
'https://dinosaurs:are-the-future!@62337d29c8edd9000870ec20--edge-bootstrap.netlify.app/index.ts'
'https://dinosaurs:are-the-future!@623c91447947b7000844416d--edge-bootstrap.netlify.app/index.ts'

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

Expand Down
7 changes: 7 additions & 0 deletions src/bundle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface Bundle {
extension: string
format: string
hash: string
}

export type { Bundle }
1 change: 0 additions & 1 deletion src/bundle_alternate.ts

This file was deleted.

68 changes: 34 additions & 34 deletions src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { v4 as uuidv4 } from 'uuid'

import { getBootstrapImport } from './bootstrap.js'
import { DenoBridge, LifecycleHook } from './bridge.js'
import type { BundleAlternate } from './bundle_alternate.js'
import type { Bundle } from './bundle.js'
import type { Declaration } from './declaration.js'
import { EdgeFunction } from './edge_function.js'
import { getESZIPBundler } from './eszip.js'
Expand All @@ -29,7 +29,7 @@ interface BundleOptions {
onBeforeDownload?: LifecycleHook
}

interface BundleAlternateOptions {
interface BundleWithFormatOptions {
buildID: string
deno: DenoBridge
distDirectory: string
Expand Down Expand Up @@ -57,18 +57,18 @@ const bundle = async (
// if any.
const importMap = new ImportMap(importMaps)
const { functions, preBundlePath } = await preBundle(sourceDirectories, distDirectory, `${buildID}-pre.js`)
const bundleAlternates: BundleAlternate[] = ['js']
const bundleOps = [bundleJS({ buildID, deno, distDirectory, importMap, preBundlePath })]

if (env.BUNDLE_ESZIP) {
bundleAlternates.push('eszip2')
bundleOps.push(bundleESZIP({ buildID, deno, distDirectory, importMap, preBundlePath }))
}

const bundleHash = await createFinalBundles(bundleOps, distDirectory, buildID)
const bundles = await Promise.all(bundleOps)

await createFinalBundles(bundles, distDirectory, buildID)

const manifest = await writeManifest({
bundleAlternates,
bundleHash,
bundles,
declarations,
distDirectory,
functions,
Expand All @@ -83,43 +83,50 @@ const bundle = async (
return { functions, manifest, preBundlePath }
}

const bundleESZIP = async ({ buildID, deno, distDirectory, preBundlePath }: BundleAlternateOptions) => {
const bundleESZIP = async ({
buildID,
deno,
distDirectory,
preBundlePath,
}: BundleWithFormatOptions): Promise<Bundle> => {
const extension = '.eszip2'
const preBundleFileURL = pathToFileURL(preBundlePath).toString()
const eszipBundlePath = join(distDirectory, `${buildID}${extension}`)
const bundler = getESZIPBundler()

await deno.run(['run', '-A', bundler, preBundleFileURL, eszipBundlePath])

return extension
const hash = await getFileHash(eszipBundlePath)

return { extension, format: 'eszip2', hash }
}

const bundleJS = async ({ buildID, deno, distDirectory, importMap, preBundlePath }: BundleAlternateOptions) => {
const bundleJS = async ({
buildID,
deno,
distDirectory,
importMap,
preBundlePath,
}: BundleWithFormatOptions): Promise<Bundle> => {
const extension = '.js'
const jsBundlePath = join(distDirectory, `${buildID}${extension}`)

await deno.run(['bundle', `--import-map=${importMap.toDataURL()}`, preBundlePath, jsBundlePath])

return extension
}
const hash = await getFileHash(jsBundlePath)

const createFinalBundles = async (bundleOps: Promise<string>[], distDirectory: string, buildID: string) => {
const bundleExtensions = await Promise.all(bundleOps)
return { extension, format: 'js', hash }
}

// We want to generate a fingerprint of the functions and their dependencies,
// so let's compute a SHA256 hash of the bundle. That hash will be different
// for the various artifacts we produce, so we can just take the first one.
const bundleHash = await getFileHash(join(distDirectory, `${buildID}${bundleExtensions[0]}`))
const renameOps = bundleExtensions.map((extension) => {
const createFinalBundles = async (bundles: Bundle[], distDirectory: string, buildID: string) => {
const renamingOps = bundles.map(async ({ extension, hash }) => {
const tempBundlePath = join(distDirectory, `${buildID}${extension}`)
const finalBundlePath = join(distDirectory, `${bundleHash}${extension}`)
const finalBundlePath = join(distDirectory, `${hash}${extension}`)

return fs.rename(tempBundlePath, finalBundlePath)
await fs.rename(tempBundlePath, finalBundlePath)
})

await Promise.all(renameOps)

return bundleHash
await Promise.all(renamingOps)
}

const generateEntrypoint = (functions: EdgeFunction[], distDirectory: string) => {
Expand Down Expand Up @@ -161,21 +168,14 @@ const preBundle = async (sourceDirectories: string[], distDirectory: string, pre
}

interface WriteManifestOptions {
bundleAlternates: BundleAlternate[]
bundleHash: string
bundles: Bundle[]
declarations: Declaration[]
distDirectory: string
functions: EdgeFunction[]
}

const writeManifest = ({
bundleAlternates,
bundleHash,
declarations = [],
distDirectory,
functions,
}: WriteManifestOptions) => {
const manifest = generateManifest({ bundleAlternates, bundleHash, declarations, functions })
const writeManifest = ({ bundles, declarations = [], distDirectory, functions }: WriteManifestOptions) => {
const manifest = generateManifest({ bundles, declarations, functions })
const manifestPath = join(distDirectory, 'manifest.json')

return fs.writeFile(manifestPath, JSON.stringify(manifest))
Expand Down
36 changes: 21 additions & 15 deletions src/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import globToRegExp from 'glob-to-regexp'

import type { BundleAlternate } from './bundle_alternate.js'
import type { Bundle } from './bundle.js'
import type { Declaration } from './declaration.js'
import { EdgeFunction } from './edge_function.js'
import { getPackageVersion } from './package_json.js'
import { nonNullable } from './utils/non_nullable.js'

interface GenerateManifestOptions {
bundleAlternates?: BundleAlternate[]
bundleHash: string
bundles: Bundle[]
functions: EdgeFunction[]
declarations?: Declaration[]
}

const generateManifest = ({
bundleAlternates = [],
bundleHash,
declarations = [],
functions,
}: GenerateManifestOptions) => {
const functionsWithRoutes = declarations.map((declaration) => {
interface Manifest {
// eslint-disable-next-line camelcase
bundler_version: string
bundles: { asset: string; format: string }[]
routes: { function: string; pattern: string }[]
}

const generateManifest = ({ bundles, declarations = [], functions }: GenerateManifestOptions) => {
const routes = declarations.map((declaration) => {
const func = functions.find(({ name }) => declaration.function === name)

if (func === undefined) {
Expand All @@ -33,13 +35,17 @@ const generateManifest = ({
pattern: serializablePattern,
}
})
const manifest = {
bundle: bundleHash,
bundle_alternates: bundleAlternates,
functions: functionsWithRoutes.filter(nonNullable),
const manifestBundles = bundles.map(({ extension, format, hash }) => ({
asset: hash + extension,
format,
}))
const manifest: Manifest = {
bundles: manifestBundles,
routes: routes.filter(nonNullable),
bundler_version: getPackageVersion(),
}

return manifest
}

export { generateManifest }
export { generateManifest, Manifest }
8 changes: 8 additions & 0 deletions src/package_json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createRequire } from 'module'

const require = createRequire(import.meta.url)
const pkgJson = require('../package.json')

const getPackageVersion = (): string => pkgJson.version

export { getPackageVersion }
3 changes: 1 addition & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ const serve = async (
})
const distDirectory = await tmpName()
const { functions, preBundlePath } = await preBundle(sourceDirectories, distDirectory, 'dev.js')
const getManifest = (declarations: Declaration[]) =>
generateManifest({ bundleHash: preBundlePath, declarations, functions })
const getManifest = (declarations: Declaration[]) => generateManifest({ bundles: [], declarations, functions })

// Wait for the binary to be downloaded if needed.
await deno.getBinaryPath()
Expand Down
79 changes: 79 additions & 0 deletions test/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { env } from 'process'

import test from 'ava'

import { generateManifest } from '../src/manifest.js'

test('Generates a manifest with different bundles', (t) => {
const bundle1 = {
extension: '.ext1',
format: 'format1',
hash: '123456',
}
const bundle2 = {
extension: '.ext2',
format: 'format2',
hash: '654321',
}
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations = [{ function: 'func-1', path: '/f1' }]
const manifest = generateManifest({ bundles: [bundle1, bundle2], declarations, functions })

const expectedBundles = [
{ asset: bundle1.hash + bundle1.extension, format: bundle1.format },
{ asset: bundle2.hash + bundle2.extension, format: bundle2.format },
]
const expectedRoutes = [{ function: 'func-1', pattern: '^/f1$' }]

t.deepEqual(manifest.bundles, expectedBundles)
t.deepEqual(manifest.routes, expectedRoutes)
t.is(manifest.bundler_version, env.npm_package_version as string)
})

test('Excludes functions for which there are function files but no matching config declarations', (t) => {
const bundle1 = {
extension: '.ext2',
format: 'format1',
hash: '123456',
}
const functions = [
{ name: 'func-1', path: '/path/to/func-1.ts' },
{ name: 'func-2', path: '/path/to/func-2.ts' },
]
const declarations = [{ function: 'func-1', path: '/f1' }]
const manifest = generateManifest({ bundles: [bundle1], declarations, functions })

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

t.deepEqual(manifest.routes, expectedRoutes)
})

test('Excludes functions for which there are config declarations but no matching function files', (t) => {
const bundle1 = {
extension: '.ext2',
format: 'format1',
hash: '123456',
}
const functions = [{ name: 'func-2', path: '/path/to/func-2.ts' }]
const declarations = [
{ function: 'func-1', path: '/f1' },
{ function: 'func-2', path: '/f2' },
]
const manifest = generateManifest({ bundles: [bundle1], declarations, functions })

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

t.deepEqual(manifest.routes, expectedRoutes)
})

test('Generates a manifest without bundles', (t) => {
const functions = [{ name: 'func-1', path: '/path/to/func-1.ts' }]
const declarations = [{ function: 'func-1', path: '/f1' }]
const manifest = generateManifest({ bundles: [], declarations, functions })

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

t.deepEqual(manifest.bundles, [])
t.deepEqual(manifest.routes, expectedRoutes)
t.is(manifest.bundler_version, env.npm_package_version as string)
})

0 comments on commit 7bee912

Please sign in to comment.