From 2a82485790469dd1ba629bc727b35266e1a1c1d2 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Tue, 3 Dec 2024 03:04:34 -0500 Subject: [PATCH] feat(bundle): add an imports replacement plugin --- bundle/ts/bundle.ts | 34 +++++++++++++++++-- bundle/ts/bundle_test.ts | 13 +++++++ bundle/ts/testing/test_overrides_imports.ts | 4 +++ .../test_overrides_imports_func_failure.ts | 4 +++ .../test_overrides_imports_func_success.ts | 4 +++ 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 bundle/ts/testing/test_overrides_imports.ts create mode 100644 bundle/ts/testing/test_overrides_imports_func_failure.ts create mode 100644 bundle/ts/testing/test_overrides_imports_func_success.ts diff --git a/bundle/ts/bundle.ts b/bundle/ts/bundle.ts index 337dbb8b..0620c27d 100644 --- a/bundle/ts/bundle.ts +++ b/bundle/ts/bundle.ts @@ -5,7 +5,7 @@ // Imports import * as esbuild from "esbuild" -import { denoPlugins as plugins } from "@luca/esbuild-deno-loader" +import { denoLoaderPlugin, denoResolverPlugin } from "@luca/esbuild-deno-loader" import { encodeBase64 } from "@std/encoding/base64" import { minify as terser } from "terser" import { fromFileUrl } from "@std/path/from-file-url" @@ -38,12 +38,16 @@ import { delay } from "@std/async/delay" * console.log(await bundle(`console.log("Hello world")`)) * ``` */ -export async function bundle(input: URL | string, { minify = "terser", format = "esm", debug = false, banner = "", shadow = true, config, exports, raw } = {} as options): Promise { +export async function bundle(input: URL | string, { minify = "terser", format = "esm", debug = false, banner = "", shadow = true, config, exports, raw, overrides } = {} as options): Promise { const url = input instanceof URL ? input : new URL(`data:application/typescript;base64,${encodeBase64(input)}`) let code = "" try { const { outputFiles: [{ text: output }] } = await esbuild.build({ - plugins: [...plugins({ configPath: config ? fromFileUrl(config) : undefined })], + plugins: [ + overrides?.imports ? overridesImports({ imports: overrides.imports }) : null, + denoResolverPlugin({ configPath: config ? fromFileUrl(config) : undefined }), + denoLoaderPlugin({ configPath: config ? fromFileUrl(config) : undefined }), + ].filter((plugin): plugin is esbuild.Plugin => Boolean(plugin)), entryPoints: [url.href], format, globalName: exports, @@ -95,4 +99,28 @@ export type options = { banner?: string shadow?: boolean raw?: Record + overrides?: { + imports?: Record + } +} + +/** Override imports. */ +function overridesImports(options: { imports: NonNullable["imports"]> }): esbuild.Plugin { + return ({ + name: "libs-bundler-overrides-imports", + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => { + if (!(args.path in options.imports)) { + return null + } + const url = new URL(options.imports[args.path]) + if (url.protocol === "file:") { + return { path: fromFileUrl(url), namespace: "file" } + } + const namespace = url.protocol.slice(0, -1) + const path = url.href.slice(namespace.length + 1) + return { path, namespace } + }) + }, + }) as esbuild.Plugin } diff --git a/bundle/ts/bundle_test.ts b/bundle/ts/bundle_test.ts index a9ec0aec..ee3f60b2 100644 --- a/bundle/ts/bundle_test.ts +++ b/bundle/ts/bundle_test.ts @@ -1,5 +1,6 @@ import { bundle } from "./bundle.ts" import { expect, test } from "@libs/testing" +import { fromFileUrl } from "@std/path" const base = new URL("testing/", import.meta.url) const config = new URL("deno.jsonc", base) @@ -53,3 +54,15 @@ test("`bundle()` handles specifiers", async () => { const url = new URL("test_specifiers.ts", base) await expect(bundle(url)).resolves.toContain("success") }, { permissions: { read: true, net: ["jsr.io"], env: true, write: true, run: true } }) + +test("`bundle()` handles imports replacements (file scheme)", async () => { + const url = new URL("test_overrides_imports.ts", base) + const replaced = import.meta.resolve("./testing/test_overrides_imports_func_success.ts") + await expect(bundle(url, { overrides: { imports: { "./test_overrides_imports_func_failure.ts": replaced } } })).resolves.toContain("success") +}, { permissions: { read: true, net: ["jsr.io"], env: true, write: true, run: true } }) + +test("`bundle()` handles imports replacements (non-file scheme)", async () => { + const url = new URL("test_overrides_imports.ts", base) + const replaced = `data:text/typescript;base64,${btoa(await Deno.readTextFile(fromFileUrl(import.meta.resolve("./testing/test_overrides_imports_func_success.ts"))))}` + await expect(bundle(url, { overrides: { imports: { "./test_overrides_imports_func_failure.ts": replaced } } })).resolves.toContain("success") +}, { permissions: { read: true, net: ["jsr.io"], env: true, write: true, run: true } }) diff --git a/bundle/ts/testing/test_overrides_imports.ts b/bundle/ts/testing/test_overrides_imports.ts new file mode 100644 index 00000000..b48c88c9 --- /dev/null +++ b/bundle/ts/testing/test_overrides_imports.ts @@ -0,0 +1,4 @@ +// deno-lint-ignore-file no-console +// Example module with overrides imports +import { ok } from "./test_overrides_imports_func_failure.ts" +console.log(ok) diff --git a/bundle/ts/testing/test_overrides_imports_func_failure.ts b/bundle/ts/testing/test_overrides_imports_func_failure.ts new file mode 100644 index 00000000..53317207 --- /dev/null +++ b/bundle/ts/testing/test_overrides_imports_func_failure.ts @@ -0,0 +1,4 @@ +// Example module with overrides imports +export function ok() { + return "failure" +} diff --git a/bundle/ts/testing/test_overrides_imports_func_success.ts b/bundle/ts/testing/test_overrides_imports_func_success.ts new file mode 100644 index 00000000..4f10ffc2 --- /dev/null +++ b/bundle/ts/testing/test_overrides_imports_func_success.ts @@ -0,0 +1,4 @@ +// Example module with overrides imports +export function ok() { + return "success" +}