Skip to content

Commit

Permalink
feat: add bundler (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
eduardoboucas committed Jan 31, 2022
1 parent 1c86a79 commit 0e367b6
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 17 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"version": "1.0.0",
"description": "A Node module to install and interact with the Deno CLI",
"type": "module",
"main": "./src/main.js",
"exports": "./src/main.js",
"main": "./dist/index.js",
"exports": "./dist/index.js",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts"
Expand Down Expand Up @@ -60,6 +60,7 @@
"@commitlint/cli": "^16.0.0",
"@commitlint/config-conventional": "^16.0.0",
"@netlify/eslint-config-node": "^4.1.5",
"@types/glob-to-regexp": "^0.4.1",
"@types/node": "^17.0.10",
"@types/semver": "^7.3.9",
"@types/sinon": "^10.0.8",
Expand All @@ -77,6 +78,7 @@
"dependencies": {
"env-paths": "^3.0.0",
"execa": "^6.0.0",
"glob-to-regexp": "^0.4.1",
"node-fetch": "^3.1.1",
"node-stream-zip": "^1.15.0",
"semver": "^7.3.5"
Expand Down
40 changes: 28 additions & 12 deletions src/deno.ts → src/bridge.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import fs from 'fs'
import { promises as fs } from 'fs'
import path from 'path'

import { execa } from 'execa'
import semver from 'semver'

import { bundle } from './bundler.js'
import type { Declaration } from './declaration.js'
import { download } from './downloader.js'
import { getPathInHome } from './home_path.js'
import { generateManifest } from './manifest.js'
import { getBinaryExtension } from './platform.js'

const DENO_VERSION_FILE = 'version.txt'
Expand Down Expand Up @@ -51,13 +54,13 @@ class DenoBridge {
}
}

async getCachedBinary() {
private async getCachedBinary() {
const versionFilePath = path.join(this.cacheDirectory, DENO_VERSION_FILE)

let cachedVersion

try {
cachedVersion = await fs.promises.readFile(versionFilePath, 'utf8')
cachedVersion = await fs.readFile(versionFilePath, 'utf8')
} catch {
return
}
Expand All @@ -71,7 +74,7 @@ class DenoBridge {
return path.join(this.cacheDirectory, binaryName)
}

async getGlobalBinary() {
private async getGlobalBinary() {
if (!this.useGlobal) {
return
}
Expand All @@ -86,12 +89,12 @@ class DenoBridge {
return globalBinaryName
}

async getRemoteBinary() {
private async getRemoteBinary() {
if (this.onBeforeDownload) {
this.onBeforeDownload()
}

await fs.promises.mkdir(this.cacheDirectory, { recursive: true })
await fs.mkdir(this.cacheDirectory, { recursive: true })

const binaryPath = await download(this.cacheDirectory)
const version = await DenoBridge.getBinaryVersion(binaryPath)
Expand All @@ -109,6 +112,25 @@ class DenoBridge {
return binaryPath
}

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

await fs.writeFile(versionFilePath, version)
}

async bundle(sourceDirectories: string[], distDirectory: string, declarations: Declaration[]) {
await fs.rm(distDirectory, { force: true, recursive: true })

const { bundlePath, handlers, preBundlePath } = await bundle(sourceDirectories, distDirectory)
const relativeBundlePath = path.relative(distDirectory, bundlePath)
const manifestContents = generateManifest(relativeBundlePath, handlers, declarations)
const manifestPath = path.join(distDirectory, 'manifest.json')

await this.run(['bundle', preBundlePath, bundlePath])
await fs.writeFile(manifestPath, JSON.stringify(manifestContents))
await fs.unlink(preBundlePath)
}

async getBinaryPath(): Promise<string> {
const globalPath = await this.getGlobalBinary()

Expand All @@ -130,12 +152,6 @@ class DenoBridge {

return await execa(binaryPath, args)
}

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

await fs.promises.writeFile(versionFilePath, version)
}
}

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

import { findHandlers } from './finder.js'
import { Handler } from './handler.js'
import { getStringHash } from './utils/sha256.js'

interface HandlerLine {
exportLine: string
importLine: string
}

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

const handlers = await findHandlers(sourceDirectories)
const lines = handlers.map((handler, index) => generateHandlerReference(handler, index, distDirectory))
const bundleContents = generateBundle(lines)
const hash = await getStringHash(bundleContents)
const preBundlePath = join(distDirectory, `${hash}-pre.ts`)
const bundlePath = join(distDirectory, `${hash}.ts`)

await fs.writeFile(preBundlePath, bundleContents)

return { handlers, preBundlePath, bundlePath }
}

const generateBundle = (lines: HandlerLine[]) => {
const importLines = lines.map(({ importLine }) => importLine).join('\n')
const exportLines = lines.map(({ exportLine }) => exportLine).join(', ')
const exportDeclaration = `const handlers = {${exportLines}};`

return `${importLines}\n\n${exportDeclaration}\n\nexport default handlers;`
}

const generateHandlerReference = (handler: Handler, index: number, targetDirectory: string): HandlerLine => {
const importName = `handler${index}`
const exportLine = `"${handler.name}": ${importName}.handler`
const relativePath = relative(targetDirectory, handler.path)

return {
exportLine,
importLine: `import * as ${importName} from "${relativePath}";`,
}
}

export { bundle }
13 changes: 13 additions & 0 deletions src/declaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface DeclarationWithPath {
handler: string
path: string
}

interface DeclarationWithPattern {
handler: string
pattern: string
}

type Declaration = DeclarationWithPath | DeclarationWithPattern

export { Declaration, DeclarationWithPath, DeclarationWithPattern }
76 changes: 76 additions & 0 deletions src/finder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { promises as fs } from 'fs'
import { basename, extname, join } from 'path'

import { Handler } from './handler.js'
import { nonNullable } from './utils/non_nullable.js'

const ALLOWED_EXTENSIONS = new Set(['.js', '.ts'])

const findHandlerInDirectory = async (directory: string): Promise<Handler | undefined> => {
const name = basename(directory)
const candidatePaths = [`${name}.js`, `index.js`, `${name}.ts`, `index.ts`].map((filename) =>
join(directory, filename),
)

let handlerPath

for (const candidatePath of candidatePaths) {
try {
const stats = await fs.stat(candidatePath)

// eslint-disable-next-line max-depth
if (stats.isFile()) {
handlerPath = candidatePath

break
}
} catch {
// no-op
}
}

if (handlerPath === undefined) {
return
}

return {
name,
path: handlerPath,
}
}

const findHandlerInPath = async (path: string): Promise<Handler | undefined> => {
const stats = await fs.stat(path)

if (stats.isDirectory()) {
return findHandlerInDirectory(path)
}

const extension = extname(path)

if (ALLOWED_EXTENSIONS.has(extension)) {
return { name: basename(path, extension), path }
}
}

const findHandlersInDirectory = async (baseDirectory: string) => {
let items: string[] = []

try {
items = await fs.readdir(baseDirectory)
} catch {
// no-op
}

const handlers = await Promise.all(items.map((item) => findHandlerInPath(join(baseDirectory, item))))

return handlers.filter(nonNullable)
}

const findHandlers = async (directories: string[]) => {
const handlers = await Promise.all(directories.map(findHandlersInDirectory))

return handlers.flat()
}

export { findHandlers }
4 changes: 4 additions & 0 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Handler {
name: string
path: string
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { bundle } from './bundler.js'
export { DenoBridge } from './bridge.js'
30 changes: 30 additions & 0 deletions src/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import globToRegExp from 'glob-to-regexp'

import type { Declaration } from './declaration.js'
import { Handler } from './handler.js'
import { nonNullable } from './utils/non_nullable.js'

const generateManifest = (bundlePath: string, handlers: Handler[], declarations: Declaration[]) => {
const handlersWithRoutes = handlers.map((handler) => {
const declaration = declarations.find((candidate) => candidate.handler === handler.name)

if (declaration === undefined) {
return
}

const pattern = 'pattern' in declaration ? new RegExp(declaration.pattern) : globToRegExp(declaration.path)

return {
handler: handler.name,
pattern: pattern.toString(),
}
})
const manifest = {
bundle: bundlePath,
handlers: handlersWithRoutes.filter(nonNullable),
}

return manifest
}

export { generateManifest }
3 changes: 3 additions & 0 deletions src/utils/non_nullable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const nonNullable = <T>(value: T): value is NonNullable<T> => value !== null && value !== undefined

export { nonNullable }
13 changes: 13 additions & 0 deletions src/utils/sha256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import crypto from 'crypto'

const getStringHash = (input: string) => {
const shasum = crypto.createHash('sha256')

shasum.update(input)

const hash = shasum.digest('hex')

return hash
}

export { getStringHash }

0 comments on commit 0e367b6

Please sign in to comment.