Skip to content

Commit

Permalink
feat: add ability to restart isolate (netlify/edge-bundler#20)
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

* feat: add logic for restarting the isolate

* refactor: move default formatting functions to variables

* chore: add comment

* chore: add comment

* chore: 😎
  • Loading branch information
eduardoboucas authored Apr 11, 2022
1 parent f7567b2 commit e4864bd
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 111 deletions.
27 changes: 24 additions & 3 deletions packages/edge-bundler/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 packages/edge-bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"glob-to-regexp": "^0.4.1",
"node-fetch": "^3.1.1",
"node-stream-zip": "^1.15.0",
"p-wait-for": "^4.1.0",
"semver": "^7.3.5",
"tmp-promise": "^3.0.3",
"uuid": "^8.3.2"
Expand Down
45 changes: 35 additions & 10 deletions packages/edge-bundler/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { promises as fs } from 'fs'
import path from 'path'
import process from 'process'

import { execa } from 'execa'
import { execa, ExecaChildProcess } from 'execa'
import semver from 'semver'

import { download } from './downloader.js'
Expand All @@ -22,6 +22,14 @@ interface DenoOptions {
versionRange?: string
}

interface ProcessRef {
ps?: ExecaChildProcess<string>
}

interface RunOptions {
pipeOutput?: boolean
}

class DenoBridge {
cacheDirectory: string
onAfterDownload?: LifecycleHook
Expand Down Expand Up @@ -113,6 +121,17 @@ class DenoBridge {
return binaryPath
}

private static runWithBinary(binaryPath: string, args: string[], pipeOutput?: boolean) {
const runDeno = execa(binaryPath, args)

if (pipeOutput) {
runDeno.stdout?.pipe(process.stdout)
runDeno.stderr?.pipe(process.stderr)
}

return runDeno
}

private async writeVersionFile(version: string) {
const versionFilePath = path.join(this.cacheDirectory, DENO_VERSION_FILE)

Expand All @@ -137,20 +156,26 @@ class DenoBridge {
return { global: false, path: downloadedPath }
}

async run(args: string[], { wait = true }: { wait?: boolean } = {}) {
// Runs the Deno CLI in the background and returns a reference to the child
// process, awaiting its execution.
async run(args: string[], { pipeOutput }: RunOptions = {}) {
const { path: binaryPath } = await this.getBinaryPath()
const runDeno = execa(binaryPath, args)

runDeno.stdout?.pipe(process.stdout)
runDeno.stderr?.pipe(process.stderr)
return DenoBridge.runWithBinary(binaryPath, args, pipeOutput)
}

if (!wait) {
return runDeno
}
// Runs the Deno CLI in the background, assigning a reference of the child
// process to a `ps` property in the `ref` argument, if one is supplied.
async runInBackground(args: string[], pipeOutput?: boolean, ref?: ProcessRef) {
const { path: binaryPath } = await this.getBinaryPath()
const ps = DenoBridge.runWithBinary(binaryPath, args, pipeOutput)

return await runDeno
if (ref !== undefined) {
// eslint-disable-next-line no-param-reassign
ref.ps = ps
}
}
}

export { DenoBridge }
export type { LifecycleHook }
export type { LifecycleHook, ProcessRef }
33 changes: 0 additions & 33 deletions packages/edge-bundler/src/entry_point.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/edge-bundler/src/formats/eszip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const bundleESZIP = async ({
flags.push('--quiet')
}

await deno.run(['run', ...flags, bundler, JSON.stringify(payload)])
await deno.run(['run', ...flags, bundler, JSON.stringify(payload)], { pipeOutput: true })

const hash = await getFileHash(destPath)

Expand Down
101 changes: 95 additions & 6 deletions packages/edge-bundler/src/formats/javascript.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { promises as fs } from 'fs'
import { join } from 'path'
import { env } from 'process'
import { pathToFileURL } from 'url'

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 type { FormatFunction } from '../server/server.js'
import { getFileHash } from '../utils/sha256.js'

const BOOTSTRAP_LATEST =
'https://dinosaurs:are-the-future!@624accb72787800009364b4f--edge-bootstrap.netlify.app/index.ts'

interface BundleJSOptions {
buildID: string
debug?: boolean
Expand All @@ -27,7 +32,7 @@ const bundleJS = async ({
functions,
importMap,
}: BundleJSOptions): Promise<Bundle> => {
const stage2Path = await generateStage2(functions, distDirectory, `${buildID}-pre.js`)
const stage2Path = await generateStage2({ distDirectory, functions, fileName: `${buildID}-pre.js` })
const extension = '.js'
const jsBundlePath = join(distDirectory, `${buildID}${extension}`)
const flags = [`--import-map=${importMap.toDataURL()}`]
Expand All @@ -36,24 +41,108 @@ const bundleJS = async ({
flags.push('--quiet')
}

await deno.run(['bundle', ...flags, stage2Path, jsBundlePath])
await deno.run(['bundle', ...flags, stage2Path, jsBundlePath], { pipeOutput: true })
await fs.unlink(stage2Path)

const hash = await getFileHash(jsBundlePath)

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

const generateStage2 = async (functions: EdgeFunction[], distDirectory: string, fileName: string) => {
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 {
distDirectory: string
fileName: string
formatExportTypeError?: FormatFunction
formatImportError?: FormatFunction
functions: EdgeFunction[]
type?: 'local' | 'production'
}

const generateStage2 = async ({
distDirectory,
fileName,
formatExportTypeError,
formatImportError,
functions,
type = 'production',
}: GenerateStage2Options) => {
await del(distDirectory, { force: true })
await fs.mkdir(distDirectory, { recursive: true })

const entrypoint = generateEntryPoint(functions)
const entryPoint =
type === 'local'
? getLocalEntryPoint(functions, { formatExportTypeError, formatImportError })
: getProductionEntryPoint(functions)
const stage2Path = join(distDirectory, fileName)

await fs.writeFile(stage2Path, entrypoint)
await fs.writeFile(stage2Path, entryPoint)

return stage2Path
}

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

interface GetLocalEntryPointOptions {
formatExportTypeError?: FormatFunction
formatImportError?: FormatFunction
}

// For the local development environment, we import the user functions with
// dynamic imports to gracefully handle the case where the file doesn't have
// a valid default export.
const getLocalEntryPoint = (
functions: EdgeFunction[],
{
formatExportTypeError = defaultFormatExportTypeError,
formatImportError = defaultFormatImpoortError,
}: GetLocalEntryPointOptions,
) => {
const bootImport = `import { boot } from "${getBootstrapURL()}";`
const declaration = `const functions = {};`
const imports = functions.map(
(func) => `
try {
const { default: func } = await import("${pathToFileURL(func.path)}");
if (typeof func === "function") {
functions["${func.name}"] = func;
} else {
console.log(${JSON.stringify(formatExportTypeError(func.name))});
}
} catch (error) {
console.log(${JSON.stringify(formatImportError(func.name))});
console.error(error);
}
`,
)
const bootCall = `boot(functions);`

return [bootImport, declaration, ...imports, bootCall].join('\n\n')
}

const getProductionEntryPoint = (functions: EdgeFunction[]) => {
const bootImport = `import { boot } from "${getBootstrapURL()}";`
const lines = functions.map((func, index) => {
const importName = `func${index}`
const exportLine = `"${func.name}": ${importName}`
const url = pathToFileURL(func.path)

return {
exportLine,
importLine: `import ${importName} from "${url}";`,
}
})
const importLines = lines.map(({ importLine }) => importLine).join('\n')
const exportLines = lines.map(({ exportLine }) => exportLine).join(', ')
const exportDeclaration = `const functions = {${exportLines}};`
const defaultExport = 'boot(functions);'

return [bootImport, importLines, exportDeclaration, defaultExport].join('\n\n')
}

export { bundleJS, generateStage2 }
3 changes: 2 additions & 1 deletion packages/edge-bundler/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { bundle } from './bundler.js'
export { DenoBridge } from './bridge.js'
export { findFunctions as find } from './finder.js'
export { serve } from './server.js'
export { generateManifest } from './manifest.js'
export { serve } from './server/server.js'
4 changes: 2 additions & 2 deletions packages/edge-bundler/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getPackageVersion } from './package_json.js'
import { nonNullable } from './utils/non_nullable.js'

interface GenerateManifestOptions {
bundles: Bundle[]
bundles?: Bundle[]
functions: EdgeFunction[]
declarations?: Declaration[]
}
Expand All @@ -22,7 +22,7 @@ interface Manifest {
routes: { function: string; pattern: string }[]
}

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

Expand Down
Loading

0 comments on commit e4864bd

Please sign in to comment.