Skip to content

Commit

Permalink
fix: generate hash of final bundle file (#4)
Browse files Browse the repository at this point in the history
* fix: generate hash of final bundle file

* refactor: use -pre suffix on pre-bundle

* refactor: rename variables
  • Loading branch information
eduardoboucas committed Mar 3, 2022
1 parent 3556f60 commit d27184a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 43 deletions.
49 changes: 38 additions & 11 deletions package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@types/node": "^17.0.10",
"@types/semver": "^7.3.9",
"@types/sinon": "^10.0.8",
"@types/uuid": "^8.3.4",
"ava": "^4.0.1",
"husky": "^7.0.4",
"nyc": "^15.0.0",
Expand All @@ -83,6 +84,7 @@
"node-fetch": "^3.1.1",
"node-stream-zip": "^1.15.0",
"semver": "^7.3.5",
"tmp-promise": "^3.0.3"
"tmp-promise": "^3.0.3",
"uuid": "^8.3.2"
}
}
68 changes: 48 additions & 20 deletions src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import { join, relative } from 'path'
import { env } from 'process'
import { pathToFileURL } from 'url'

import { v4 as uuidv4 } from 'uuid'

import { DenoBridge, LifecycleHook } from './bridge.js'
import type { BundleAlternate } from './bundle_alternate.js'
import type { Declaration } from './declaration.js'
import { getESZIPBundler } from './eszip.js'
import { findHandlers } from './finder.js'
import { Handler } from './handler.js'
import { generateManifest } from './manifest.js'
import { getStringHash } from './utils/sha256.js'
import { getFileHash } from './utils/sha256.js'

interface HandlerLine {
exportLine: string
Expand All @@ -33,42 +35,70 @@ const bundle = async (
onBeforeDownload,
})

const { entrypointHash, handlers, preBundlePath } = await preBundle(sourceDirectories, distDirectory)
const bundlePath = join(distDirectory, entrypointHash)
// 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
// to create the bundle artifacts and rename them later.
const buildID = uuidv4()
const { handlers, preBundlePath } = await preBundle(sourceDirectories, distDirectory, `${buildID}-pre.js`)
const bundleAlternates: BundleAlternate[] = ['js']
const bundleOps = [bundleJS(deno, preBundlePath, bundlePath)]
const bundleOps = [bundleJS(deno, preBundlePath, distDirectory, buildID)]

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

const bundleHash = await createFinalBundles(bundleOps, distDirectory, buildID)
const manifest = await writeManifest({
bundleAlternates,
bundlePath: entrypointHash,
bundleHash,
declarations,
distDirectory,
handlers,
})

await Promise.all(bundleOps)
await fs.unlink(preBundlePath)

return { handlers, manifest, preBundlePath }
}

const bundleESZIP = (deno: DenoBridge, preBundlePath: string, bundlePath: string) => {
const bundleESZIP = async (deno: DenoBridge, preBundlePath: string, distDirectory: string, buildID: string) => {
const extension = '.eszip2'
const preBundleFileURL = pathToFileURL(preBundlePath).toString()
const eszipBundlePath = `${bundlePath}.eszip2`
const eszipBundlePath = join(distDirectory, `${buildID}${extension}`)
const bundler = getESZIPBundler()

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

return extension
}

const bundleJS = async (deno: DenoBridge, preBundlePath: string, distDirectory: string, buildID: string) => {
const extension = '.js'
const jsBundlePath = join(distDirectory, `${buildID}${extension}`)

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

return extension
}

const bundleJS = (deno: DenoBridge, preBundlePath: string, bundlePath: string) => {
const jsBundlePath = `${bundlePath}.js`
const createFinalBundles = async (bundleOps: Promise<string>[], distDirectory: string, buildID: string) => {
const bundleExtensions = await Promise.all(bundleOps)

// We want to generate a fingerprint of the handlers 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 tempBundlePath = join(distDirectory, `${buildID}${extension}`)
const finalBundlePath = join(distDirectory, `${bundleHash}${extension}`)

return fs.rename(tempBundlePath, finalBundlePath)
})

await Promise.all(renameOps)

return deno.run(['bundle', preBundlePath, jsBundlePath])
return bundleHash
}

const generateEntrypoint = (handlers: Handler[], distDirectory: string) => {
Expand All @@ -93,40 +123,38 @@ const generateHandlerReference = (handler: Handler, index: number, targetDirecto
}
}

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

const handlers = await findHandlers(sourceDirectories)
const entrypoint = generateEntrypoint(handlers, distDirectory)
const entrypointHash = getStringHash(entrypoint)
const preBundlePath = join(distDirectory, `${entrypointHash}-pre.js`)
const preBundlePath = join(distDirectory, preBundleName)

await fs.writeFile(preBundlePath, entrypoint)

return {
entrypointHash,
handlers,
preBundlePath,
}
}

interface WriteManifestOptions {
bundleAlternates: BundleAlternate[]
bundlePath: string
bundleHash: string
declarations: Declaration[]
distDirectory: string
handlers: Handler[]
}

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

return fs.writeFile(manifestPath, JSON.stringify(manifest))
Expand Down
6 changes: 3 additions & 3 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { nonNullable } from './utils/non_nullable.js'

interface GenerateManifestOptions {
bundleAlternates?: BundleAlternate[]
bundlePath: string
bundleHash: string
handlers: Handler[]
declarations?: Declaration[]
}

const generateManifest = ({
bundleAlternates = [],
bundlePath,
bundleHash,
declarations = [],
handlers,
}: GenerateManifestOptions) => {
Expand All @@ -34,7 +34,7 @@ const generateManifest = ({
}
})
const manifest = {
bundle: bundlePath,
bundle: bundleHash,
bundle_alternates: bundleAlternates,
handlers: handlersWithRoutes.filter(nonNullable),
}
Expand Down
4 changes: 2 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ const serve = async (
onBeforeDownload,
})
const distDirectory = await tmpName()
const { handlers, preBundlePath } = await preBundle(sourceDirectories, distDirectory)
const { handlers, preBundlePath } = await preBundle(sourceDirectories, distDirectory, 'dev.js')
const getManifest = (declarations: Declaration[]) =>
generateManifest({ bundlePath: preBundlePath, declarations, handlers })
generateManifest({ bundleHash: preBundlePath, declarations, handlers })

// Wait for the binary to be downloaded if needed.
await deno.getBinaryPath()
Expand Down
22 changes: 16 additions & 6 deletions src/utils/sha256.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import crypto from 'crypto'
import fs from 'fs'

const getStringHash = (input: string) => {
const shasum = crypto.createHash('sha256')
const getFileHash = (path: string): Promise<string> => {
const hash = crypto.createHash('sha256')

shasum.update(input)
hash.setEncoding('hex')

const hash = shasum.digest('hex')
return new Promise((resolve, reject) => {
const file = fs.createReadStream(path)

return hash
file.on('end', () => {
hash.end()

resolve(hash.read())
})
file.on('error', reject)

file.pipe(hash)
})
}

export { getStringHash }
export { getFileHash }

0 comments on commit d27184a

Please sign in to comment.