Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Deno Bundle to ESbuild #71

Merged
merged 13 commits into from
Oct 26, 2023
4 changes: 2 additions & 2 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
}
},
"tasks": {
"test": "deno fmt --check && deno lint && deno test --allow-read --allow-net --allow-write --allow-run src",
"coverage": "deno test --allow-read --allow-net --allow-write --allow-run --coverage=.coverage src && deno coverage --exclude=fixtures --exclude=test --lcov --output=lcov.info .coverage && deno run --allow-read https://deno.land/x/[email protected]/cli.ts"
"test": "deno fmt --check && deno lint && deno test --allow-read --allow-net --allow-write --allow-run --allow-env src",
"coverage": "deno test --allow-read --allow-net --allow-write --allow-run --allow-env --coverage=.coverage src && deno coverage --exclude=fixtures --exclude=test --lcov --output=lcov.info .coverage && deno run --allow-read https://deno.land/x/[email protected]/cli.ts"
},
"lock": false
}
53 changes: 27 additions & 26 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import type { Protocol } from "./deps.ts";
import { cleanManifest, getManifest } from "./get_manifest.ts";
import { validateManifestFunctions } from "./utilities.ts";
import { EsbuildBundler } from "./bundler/mods.ts";

export const validateAndCreateFunctions = async (
workingDirectory: string,
Expand Down Expand Up @@ -37,6 +38,7 @@ export const validateAndCreateFunctions = async (
fnDef.source_file,
);
await createFunctionFile(
workingDirectory,
outputDirectory,
fnId,
fnFilePath,
Expand All @@ -45,42 +47,41 @@ export const validateAndCreateFunctions = async (
}
};

async function solveDenoConfigPath(
directory: string = Deno.cwd(),
): Promise<string> {
for (const name of ["deno.json", "deno.jsonc"]) {
const denoConfigPath = path.join(directory, name);
try {
await Deno.stat(denoConfigPath);
return denoConfigPath;
} catch (error) {
if (!(error instanceof Deno.errors.NotFound)) {
throw error;
}
}
}
throw new Error(
`Could not find a deno.json or deno.jsonc file in the current directory.`,
);
}

const createFunctionFile = async (
workingDirectory: string,
outputDirectory: string,
fnId: string,
fnFilePath: string,
protocol: Protocol,
) => {
const fnFileRelative = path.join("functions", `${fnId}.js`);
const fnBundledPath = path.join(outputDirectory, fnFileRelative);

// We'll default to just using whatever Deno executable is on the path
// Ideally we should be able to rely on Deno.execPath() so we make sure to bundle with the same version of Deno
// that called this script. This is perhaps a bit overly cautious, so we can look to remove the defaulting here in the future.
let denoExecutablePath = "deno";
try {
denoExecutablePath = Deno.execPath();
} catch (e) {
protocol.error("Error calling Deno.execPath()", e);
}

try {
// call out to deno to handle bundling
const p = Deno.run({
cmd: [
denoExecutablePath,
"bundle",
"--quiet",
fnFilePath,
fnBundledPath,
],
const bundler = new EsbuildBundler({
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
entrypoint: fnFilePath,
absWorkingDir: workingDirectory,
configPath: await solveDenoConfigPath(workingDirectory),
});

const status = await p.status();
p.close();
if (status.code !== 0 || !status.success) {
throw new Error(`Error bundling function file: ${fnId}`);
}
await Deno.writeFile(fnBundledPath, await bundler.bundle());
} catch (e) {
protocol.error(`Error bundling function file: ${fnId}`);
throw e;
Expand Down
40 changes: 40 additions & 0 deletions src/bundler/EsbuildBundler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { denoPlugins, esbuild } from "../deps.ts";
import { Bundler } from "./types.ts";

export type EsbuildBundlerOptions = {
/** The path to the file being bundled */
entrypoint: string;
/** The path to the deno.json / deno.jsonc config file. */
configPath: string;
/** specify the working directory to use for the build */
absWorkingDir: string;
};

export class EsbuildBundler implements Bundler {
constructor(private options: EsbuildBundlerOptions) {}

async bundle(): Promise<Uint8Array> {
try {
// esbuild configuration options https://esbuild.github.io/api/#overview
const result = await esbuild.build({
entryPoints: [this.options.entrypoint],
platform: "neutral",
target: "deno1",
format: "esm", // esm format stands for "ECMAScript module"
bundle: true, // inline any imported dependencies into the file itself
treeShaking: true, // dead code elimination, removes unreachable code
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
minify: true, // the generated code will be minified instead of pretty-printed
WilliamBergamin marked this conversation as resolved.
Show resolved Hide resolved
sourcemap: "inline", // source map is appended to the end of the .js output file
absWorkingDir: this.options.absWorkingDir,
write: false, // Favor returning the contents
outdir: "out", // Nothing is being written to file here
plugins: [
...denoPlugins({ configPath: this.options.configPath }),
],
});
return result.outputFiles[0].contents;
} finally {
esbuild.stop();
}
}
}
2 changes: 2 additions & 0 deletions src/bundler/mods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EsbuildBundler } from "./EsbuildBundler.ts";
export type { EsbuildBundlerOptions } from "./EsbuildBundler.ts";
9 changes: 9 additions & 0 deletions src/bundler/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type BundleContentType =
| string
| ReadableStream<string>
| Uint8Array
| ReadableStream<Uint8Array>;

export interface Bundler {
bundle(): Promise<BundleContentType>;
}
2 changes: 2 additions & 0 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export type { JSONValue } from "https://deno.land/[email protected]/encoding/jsonc.ts"
export { deepMerge } from "https://deno.land/[email protected]/collections/deep_merge.ts";
export { getProtocolInterface } from "https://deno.land/x/[email protected]/mod.ts";
export type { Protocol } from "https://deno.land/x/[email protected]/types.ts";
export * as esbuild from "https://deno.land/x/[email protected]/mod.js";
export { denoPlugins } from "https://deno.land/x/[email protected]/mod.ts";
59 changes: 32 additions & 27 deletions src/tests/build_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Deno.test("build hook tests", async (t) => {
});

await tt.step(
"should invoke `deno bundle` once per non-API function",
"should invoke `esbuild` once per non-API function",
async () => {
const protocol = MockProtocol();
const manifest = {
Expand Down Expand Up @@ -66,24 +66,24 @@ Deno.test("build hook tests", async (t) => {
},
};
const outputDir = await Deno.makeTempDir();
// Stub out call to `Deno.run` and fake return a success
const runResponse = {
close: () => {},
status: () => Promise.resolve({ code: 0, success: true }),
} as unknown as Deno.Process<Deno.RunOptions>;
const runStub = stub(
// Stub out call to `Deno.writeFile` and fake return a success
const writeFileResponse = Promise.resolve();
const writeFileStub = stub(
Deno,
"run",
returnsNext([runResponse, runResponse]),
"writeFile",
returnsNext([writeFileResponse, writeFileResponse]),
);
await validateAndCreateFunctions(
Deno.cwd(),
outputDir,
manifest,
protocol,
);
assertSpyCalls(runStub, 2);
runStub.restore();
try {
await validateAndCreateFunctions(
Deno.cwd(),
outputDir,
manifest,
protocol,
);
assertSpyCalls(writeFileStub, 2);
} finally {
writeFileStub.restore();
}
},
);

Expand Down Expand Up @@ -262,18 +262,23 @@ Deno.test("build hook tests", async (t) => {
protocol,
);
// Stub out call to `Deno.run` and fake return a success
const runResponse = {
close: () => {},
status: () => Promise.resolve({ code: 0, success: true }),
} as unknown as Deno.Process<Deno.RunOptions>;
const runStub = stub(
const writeFileResponse = Promise.resolve();
const writeFileStub = stub(
Deno,
"run",
returnsNext([runResponse, runResponse]),
"writeFile",
returnsNext([writeFileResponse, writeFileResponse]),
);
// Make sure we didn't shell out to Deno.run
assertSpyCalls(runStub, 0);
runStub.restore();
try {
await validateAndCreateFunctions(
Deno.cwd(),
outputDir,
manifest,
protocol,
);
assertSpyCalls(writeFileStub, 0);
} finally {
writeFileStub.restore();
}
});
});
});
Loading