Skip to content

Commit

Permalink
feat: add support for multi-stage ESZIPs (#19)
Browse files Browse the repository at this point in the history
* feat: add support for multi-stage ESZIPs

* chore: format using Prettier

* fix: add extension to import
  • Loading branch information
eduardoboucas committed Apr 11, 2022
1 parent 2e122c4 commit 2d78f5b
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 140 deletions.
33 changes: 4 additions & 29 deletions deno/bundle.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,6 @@
import { load } from 'https://deno.land/x/eszip@v0.16.0/loader.ts'
import { build, LoadResponse } from 'https://deno.land/x/eszip@v0.16.0/mod.ts'
import { writeStage2 } from 'https://dinosaurs:are-the-future!@deploy-preview-20--edge-bootstrap.netlify.app/bundler/mod.ts'

const IDENTIFIER_BUNDLE_COMBINED = 'netlify:bundle-combined'
const [payload] = Deno.args
const { basePath, destPath, functions } = JSON.parse(payload)

const createLoader = (srcPath: string): ((specifier: string) => Promise<LoadResponse | undefined>) => {
return async (specifier: string): Promise<LoadResponse | undefined> => {
// If we're loading the combined bundle identifier, we override the loading
// to read the file from disk and return the contents.
if (specifier === IDENTIFIER_BUNDLE_COMBINED) {
const content = await Deno.readTextFile(new URL(srcPath))

return {
content,
headers: {
'content-type': 'application/typescript',
},
kind: 'module',
specifier,
}
}

// Falling back to the default loading logic.
return load(specifier)
}
}

const [srcPath, destPath] = Deno.args
const bytes = await build([IDENTIFIER_BUNDLE_COMBINED], createLoader(srcPath))

await Deno.writeFile(destPath, bytes)
await writeStage2({ basePath, functions, destPath })
7 changes: 3 additions & 4 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"node": "^12.20.0 || ^14.14.0 || >=16.0.0"
},
"dependencies": {
"common-path-prefix": "^3.0.0",
"del": "^6.0.0",
"env-paths": "^3.0.0",
"execa": "^6.0.0",
Expand Down
126 changes: 34 additions & 92 deletions src/bundler.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { env } from 'process'
import { pathToFileURL } from 'url'

import del from 'del'
import commonPathPrefix from 'common-path-prefix'
import { v4 as uuidv4 } from 'uuid'

import { DenoBridge, LifecycleHook } from './bridge.js'
import type { Bundle } from './bundle.js'
import type { Declaration } from './declaration.js'
import { EdgeFunction } from './edge_function.js'
import { generateEntryPoint } from './entry_point.js'
import { getESZIPBundler } from './eszip.js'
import { findFunctions } from './finder.js'
import { bundleESZIP } from './formats/eszip.js'
import { bundleJS } from './formats/javascript.js'
import { ImportMap, ImportMapFile } from './import_map.js'
import { generateManifest } from './manifest.js'
import { getFileHash } from './utils/sha256.js'
import { writeManifest } from './manifest.js'

interface BundleOptions {
cacheDirectory?: string
debug?: boolean
distImportMapPath?: string
importMaps?: ImportMapFile[]
onAfterDownload?: LifecycleHook
onBeforeDownload?: LifecycleHook
}

interface BundleWithFormatOptions {
buildID: string
debug?: boolean
deno: DenoBridge
distDirectory: string
importMap: ImportMap
preBundlePath: string
}

const bundle = async (
sourceDirectories: string[],
distDirectory: string,
declarations: Declaration[] = [],
{ debug, distImportMapPath, importMaps, onAfterDownload, onBeforeDownload }: BundleOptions = {},
{ cacheDirectory, debug, distImportMapPath, importMaps, onAfterDownload, onBeforeDownload }: BundleOptions = {},
) => {
const deno = new DenoBridge({
cacheDirectory,
onAfterDownload,
onBeforeDownload,
})
const basePath = commonPathPrefix(sourceDirectories)

// The name of the bundle will be the hash of its contents, which we can't
// compute until we run the bundle process. For now, we'll use a random ID
Expand All @@ -54,72 +45,49 @@ const bundle = async (
// if any.
const importMap = new ImportMap(importMaps)
const functions = await findFunctions(sourceDirectories)
const preBundlePath = await preBundle(functions, distDirectory, `${buildID}-pre.js`)
const bundleOps = [bundleJS({ debug, buildID, deno, distDirectory, importMap, preBundlePath })]
const bundleOps = [
bundleJS({
buildID,
debug,
deno,
distDirectory,
functions,
importMap,
}),
]

if (env.BUNDLE_ESZIP) {
bundleOps.push(bundleESZIP({ buildID, deno, distDirectory, importMap, preBundlePath }))
bundleOps.push(
bundleESZIP({
basePath,
buildID,
debug,
deno,
distDirectory,
functions,
}),
)
}

const bundles = await Promise.all(bundleOps)

// 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(bundles, distDirectory, buildID)

const manifest = await writeManifest({
await writeManifest({
bundles,
declarations,
distDirectory,
functions,
})

await fs.unlink(preBundlePath)

if (distImportMapPath) {
await importMap.writeToFile(distImportMapPath)
}

return { functions, manifest, preBundlePath }
}

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])

const hash = await getFileHash(eszipBundlePath)

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

const bundleJS = async ({
buildID,
debug,
deno,
distDirectory,
importMap,
preBundlePath,
}: BundleWithFormatOptions): Promise<Bundle> => {
const extension = '.js'
const jsBundlePath = join(distDirectory, `${buildID}${extension}`)
const flags = [`--import-map=${importMap.toDataURL()}`]

if (!debug) {
flags.push('--quiet')
}

await deno.run(['bundle', ...flags, preBundlePath, jsBundlePath])

const hash = await getFileHash(jsBundlePath)

return { extension, format: 'js', hash }
return { functions }
}

const createFinalBundles = async (bundles: Bundle[], distDirectory: string, buildID: string) => {
Expand All @@ -133,30 +101,4 @@ const createFinalBundles = async (bundles: Bundle[], distDirectory: string, buil
await Promise.all(renamingOps)
}

const preBundle = async (functions: EdgeFunction[], distDirectory: string, preBundleName: string) => {
await del(distDirectory, { force: true })
await fs.mkdir(distDirectory, { recursive: true })

const entrypoint = generateEntryPoint(functions)
const preBundlePath = join(distDirectory, preBundleName)

await fs.writeFile(preBundlePath, entrypoint)

return preBundlePath
}

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

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))
}

export { bundle, preBundle }
export { bundle }
10 changes: 0 additions & 10 deletions src/eszip.ts

This file was deleted.

53 changes: 53 additions & 0 deletions src/formats/eszip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { join, resolve } from 'path'

import { DenoBridge } from '../bridge.js'
import type { Bundle } from '../bundle.js'
import { EdgeFunction } from '../edge_function.js'
import { getFileHash } from '../utils/sha256.js'

interface BundleESZIPOptions {
basePath: string
buildID: string
debug?: boolean
deno: DenoBridge
distDirectory: string
functions: EdgeFunction[]
}

const bundleESZIP = async ({
basePath,
buildID,
debug,
deno,
distDirectory,
functions,
}: BundleESZIPOptions): Promise<Bundle> => {
const extension = '.eszip'
const destPath = join(distDirectory, `${buildID}${extension}`)
const bundler = getESZIPBundler()
const payload = {
basePath,
destPath,
functions,
}
const flags = ['--allow-all']

if (!debug) {
flags.push('--quiet')
}

await deno.run(['run', ...flags, bundler, JSON.stringify(payload)])

const hash = await getFileHash(destPath)

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

const getESZIPBundler = () => {
const { pathname } = new URL(import.meta.url)
const bundlerPath = resolve(pathname, '../../../deno/bundle.ts')

return bundlerPath
}

export { bundleESZIP }
59 changes: 59 additions & 0 deletions src/formats/javascript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { promises as fs } from 'fs'
import { join } from 'path'

import del from 'del'

import { DenoBridge } from '../bridge.js'
import type { Bundle } from '../bundle.js'
import { EdgeFunction } from '../edge_function.js'
import { generateEntryPoint } from '../entry_point.js'
import { ImportMap } from '../import_map.js'
import { getFileHash } from '../utils/sha256.js'

interface BundleJSOptions {
buildID: string
debug?: boolean
deno: DenoBridge
distDirectory: string
functions: EdgeFunction[]
importMap: ImportMap
}

const bundleJS = async ({
buildID,
debug,
deno,
distDirectory,
functions,
importMap,
}: BundleJSOptions): Promise<Bundle> => {
const stage2Path = await generateStage2(functions, distDirectory, `${buildID}-pre.js`)
const extension = '.js'
const jsBundlePath = join(distDirectory, `${buildID}${extension}`)
const flags = [`--import-map=${importMap.toDataURL()}`]

if (!debug) {
flags.push('--quiet')
}

await deno.run(['bundle', ...flags, stage2Path, jsBundlePath])
await fs.unlink(stage2Path)

const hash = await getFileHash(jsBundlePath)

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

const generateStage2 = async (functions: EdgeFunction[], distDirectory: string, fileName: string) => {
await del(distDirectory, { force: true })
await fs.mkdir(distDirectory, { recursive: true })

const entrypoint = generateEntryPoint(functions)
const stage2Path = join(distDirectory, fileName)

await fs.writeFile(stage2Path, entrypoint)

return stage2Path
}

export { bundleJS, generateStage2 }

0 comments on commit 2d78f5b

Please sign in to comment.