├── tests ├── fixtures │ ├── virtual.ts │ ├── external.ts │ ├── mapped.ts │ ├── data.json │ ├── simple.ts │ ├── jsx.tsx │ ├── mime-db.ts │ ├── node.ts │ ├── npm.ts │ ├── platform.ts │ ├── preact.ts │ ├── https.ts │ ├── jsr.ts │ ├── with-json.ts │ ├── with-text.ts │ ├── with-bytes.ts │ └── env-vars.ts ├── external.test.ts └── bundle.test.ts ├── src ├── mod.ts └── plugin.ts ├── .github └── workflows │ ├── publish.yml │ └── ci.yml ├── deno.json ├── README.md ├── LICENSE └── deno.lock /tests/fixtures/virtual.ts: -------------------------------------------------------------------------------- 1 | import "foo"; 2 | -------------------------------------------------------------------------------- /tests/fixtures/external.ts: -------------------------------------------------------------------------------- 1 | import "mapped"; 2 | -------------------------------------------------------------------------------- /tests/fixtures/mapped.ts: -------------------------------------------------------------------------------- 1 | import "mapped"; 2 | -------------------------------------------------------------------------------- /tests/fixtures/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "it works" 3 | } 4 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | export { denoPlugin, type DenoPluginOptions } from "./plugin.ts"; 2 | -------------------------------------------------------------------------------- /tests/fixtures/simple.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore no-console 2 | console.log("hey"); 3 | -------------------------------------------------------------------------------- /tests/fixtures/jsx.tsx: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore no-console 2 | console.log(
it works
); 3 | -------------------------------------------------------------------------------- /tests/fixtures/mime-db.ts: -------------------------------------------------------------------------------- 1 | import * as mime from "mime-db"; 2 | // deno-lint-ignore no-console 3 | console.log(mime); 4 | -------------------------------------------------------------------------------- /tests/fixtures/node.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | // deno-lint-ignore no-console 3 | console.log(path); 4 | -------------------------------------------------------------------------------- /tests/fixtures/npm.ts: -------------------------------------------------------------------------------- 1 | import { h } from "npm:preact"; 2 | // deno-lint-ignore no-console 3 | console.log(h("div", null)); 4 | -------------------------------------------------------------------------------- /tests/fixtures/platform.ts: -------------------------------------------------------------------------------- 1 | import * as fflate from "fflate"; 2 | // deno-lint-ignore no-console 3 | console.log(fflate); 4 | -------------------------------------------------------------------------------- /tests/fixtures/preact.ts: -------------------------------------------------------------------------------- 1 | import { h } from "preact"; 2 | // deno-lint-ignore no-console 3 | console.log(h("div", null)); 4 | -------------------------------------------------------------------------------- /tests/fixtures/https.ts: -------------------------------------------------------------------------------- 1 | import { h } from "https://esm.sh/preact"; 2 | // deno-lint-ignore no-console 3 | console.log(h("div", null)); 4 | -------------------------------------------------------------------------------- /tests/fixtures/jsr.ts: -------------------------------------------------------------------------------- 1 | import * as foo from "jsr:@marvinh-test/fresh-island"; 2 | // deno-lint-ignore no-console 3 | console.log(foo); 4 | -------------------------------------------------------------------------------- /tests/fixtures/with-json.ts: -------------------------------------------------------------------------------- 1 | import json from "./data.json" with { type: "json" }; 2 | // deno-lint-ignore no-console 3 | console.log(json); 4 | -------------------------------------------------------------------------------- /tests/fixtures/with-text.ts: -------------------------------------------------------------------------------- 1 | import json from "./data.json" with { type: "text" }; 2 | // deno-lint-ignore no-console 3 | console.log(json); 4 | -------------------------------------------------------------------------------- /tests/fixtures/with-bytes.ts: -------------------------------------------------------------------------------- 1 | import buffer from "./data.json" with { type: "bytes" }; 2 | // deno-lint-ignore no-console 3 | console.log(new TextDecoder().decode(buffer)); 4 | -------------------------------------------------------------------------------- /tests/fixtures/env-vars.ts: -------------------------------------------------------------------------------- 1 | export const deno = Deno.env.get("TEST_PUBLIC_FOO"); 2 | export const deno2 = Deno.env.get("TEST_PUBLIC_FOO"); 3 | // deno-lint-ignore no-process-global 4 | export const node = process.env.TEST_PUBLIC_FOO; 5 | -------------------------------------------------------------------------------- /tests/external.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { externalToRegex } from "../src/plugin.ts"; 3 | 4 | Deno.test("externalToRegex", () => { 5 | expect(externalToRegex("foo")).toEqual(/^foo$/); 6 | expect(externalToRegex("/foo*")).toEqual(/^\/foo.*$/); 7 | expect(externalToRegex("*.png")).toEqual(/^.*\.png$/); 8 | }); 9 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write # The OIDC ID token is used for authentication with JSR. 14 | steps: 15 | - uses: actions/checkout@v4 16 | - run: npx jsr publish 17 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno/esbuild-plugin", 3 | "unstable": ["raw-imports"], 4 | "version": "1.2.1", 5 | "license": "MIT", 6 | "exports": "./src/mod.ts", 7 | "compilerOptions": { 8 | "jsx": "react-jsx", 9 | "jsxImportSource": "preact" 10 | }, 11 | "imports": { 12 | "@deno/loader": "jsr:@deno/loader@^0.3.10", 13 | "@std/expect": "jsr:@std/expect@^1.0.16", 14 | "@std/path": "jsr:@std/path@^1.1.1", 15 | "esbuild": "npm:esbuild@^0.25.5", 16 | "fflate": "npm:fflate@^0.8.2", 17 | "mime-db": "npm:mime-db@^1.54.0", 18 | "preact": "npm:preact@^10.26.8", 19 | "mapped": "./tests/fixtures/simple.ts" 20 | }, 21 | "lint": { 22 | "rules": { 23 | "include": ["no-console"], 24 | "exclude": ["no-import-prefix", "no-unversioned-import"] 25 | } 26 | }, 27 | "publish": { 28 | "exclude": [".github/", "tests/"] 29 | }, 30 | "exclude": ["./tests/fixtures/virtual.ts"] 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno esbuild plugin 2 | 3 | This package adds support for Deno-style resolution and loading to 4 | [`esbuild`](https://esbuild.github.io/). It's based on 5 | [`@deno/loader`](https://jsr.io/@deno/loader). 6 | 7 | The key difference to 8 | [`@luca/esbuild-deno-loader`](https://jsr.io/@luca/esbuild-deno-loader) is that 9 | leverages a WASM build of the same Rust crates that Deno itself uses for 10 | resolving and loading modules. 11 | 12 | It supports the following specifiers: 13 | 14 | - `file:` 15 | - `data:` 16 | - `npm:` 17 | - `jsr:` 18 | - `http:` + `https:` 19 | 20 | ## Usage 21 | 22 | 1. Install this package 23 | 2. Import it and add it to the `esbuild` config. 24 | 25 | ```ts 26 | import * as esbuild from "esbuild"; 27 | import { denoPlugin } from "@deno/esbuild-plugin"; 28 | 29 | await esbuild.build({ 30 | entryPoints: ["app.js"], 31 | bundle: true, 32 | outfile: "out.js", 33 | plugins: [denoPlugin()], 34 | }); 35 | ``` 36 | 37 | ## License 38 | 39 | MIT, see the [LICENSE file](./LICENSE). 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [macOS-latest, windows-latest, ubuntu-latest] 17 | include: 18 | - os: ubuntu-latest 19 | cache_path: ~/.cache/deno/ 20 | - os: macos-latest 21 | cache_path: ~/Library/Caches/deno/ 22 | - os: windows-latest 23 | cache_path: ~\AppData\Local\deno\ 24 | 25 | steps: 26 | - name: Checkout repo 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Deno 30 | uses: denoland/setup-deno@v2 31 | with: 32 | cache: true 33 | deno-version: 2.x 34 | 35 | - name: Verify formatting 36 | if: startsWith(matrix.os, 'ubuntu') 37 | run: deno fmt --check 38 | 39 | - name: Run linter 40 | if: startsWith(matrix.os, 'ubuntu') 41 | run: deno lint 42 | 43 | - name: Type check 44 | run: deno check 45 | 46 | - name: Spell-check 47 | if: startsWith(matrix.os, 'ubuntu') 48 | uses: crate-ci/typos@master 49 | 50 | - name: Run tests 51 | run: deno test -A 52 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MediaType, 3 | RequestedModuleType, 4 | ResolutionMode, 5 | Workspace, 6 | type WorkspaceOptions, 7 | } from "@deno/loader"; 8 | import type { 9 | Loader, 10 | OnLoadArgs, 11 | OnLoadResult, 12 | OnResolveArgs, 13 | OnResolveResult, 14 | Platform, 15 | Plugin, 16 | } from "esbuild"; 17 | import * as path from "@std/path"; 18 | import { isBuiltin } from "node:module"; 19 | 20 | export interface DenoPluginOptions { 21 | /** Show debugging logs */ 22 | debug?: boolean; 23 | /** Use this path to a `deno.json` instead of auto-discovering it. */ 24 | configPath?: string; 25 | /** Don't transpile files when loading them */ 26 | noTranspile?: boolean; 27 | /** Keep JSX as is, instead of transpiling it according to compilerOptions. */ 28 | preserveJsx?: boolean; 29 | /** 30 | * Prefix for public environment variables that should be inlined during 31 | * bundling. 32 | * @example `FRESH_PUBLIC_` 33 | */ 34 | publicEnvVarPrefix?: string; 35 | } 36 | 37 | /** 38 | * Create an instance of the Deno plugin for esbuild 39 | */ 40 | export function denoPlugin(options: DenoPluginOptions = {}): Plugin { 41 | return { 42 | name: "deno", 43 | async setup(ctx) { 44 | const workspace = new Workspace({ 45 | debug: options.debug, 46 | configPath: options.configPath, 47 | nodeConditions: ctx.initialOptions.conditions, 48 | noTranspile: options.noTranspile, 49 | preserveJsx: options.preserveJsx, 50 | platform: getPlatform(ctx.initialOptions.platform), 51 | }); 52 | 53 | const loader = await workspace.createLoader(); 54 | 55 | ctx.onDispose(() => { 56 | loader[Symbol.dispose]?.(); 57 | }); 58 | 59 | const externals = (ctx.initialOptions.external ?? []).map((item) => 60 | externalToRegex(item) 61 | ); 62 | 63 | const onResolve = async ( 64 | args: OnResolveArgs, 65 | ): Promise => { 66 | if ( 67 | isBuiltin(args.path) || externals.some((reg) => reg.test(args.path)) 68 | ) { 69 | return { 70 | path: args.path, 71 | external: true, 72 | }; 73 | } 74 | const kind = 75 | args.kind === "require-call" || args.kind === "require-resolve" 76 | ? ResolutionMode.Require 77 | : ResolutionMode.Import; 78 | 79 | try { 80 | const res = await loader.resolve(args.path, args.importer, kind); 81 | 82 | let namespace: string | undefined; 83 | if (res.startsWith("file:")) { 84 | namespace = "file"; 85 | } else if (res.startsWith("http:")) { 86 | namespace = "http"; 87 | } else if (res.startsWith("https:")) { 88 | namespace = "https"; 89 | } else if (res.startsWith("npm:")) { 90 | namespace = "npm"; 91 | } else if (res.startsWith("jsr:")) { 92 | namespace = "jsr"; 93 | } 94 | 95 | const resolved = res.startsWith("file:") 96 | ? path.fromFileUrl(res) 97 | : res; 98 | 99 | return { 100 | path: resolved, 101 | namespace, 102 | }; 103 | } catch (err) { 104 | const couldNotResolveReg = 105 | /not a dependency and not in import map|Relative import path ".*?" not prefixed with/; 106 | 107 | if (err instanceof Error && couldNotResolveReg.test(err.message)) { 108 | return null; 109 | } 110 | 111 | throw err; 112 | } 113 | }; 114 | 115 | // Esbuild doesn't detect namespaces in entrypoints. We need 116 | // a catchall resolver for that. 117 | ctx.onResolve({ filter: /.*/ }, onResolve); 118 | ctx.onResolve({ filter: /.*/, namespace: "file" }, onResolve); 119 | ctx.onResolve({ filter: /.*/, namespace: "http" }, onResolve); 120 | ctx.onResolve({ filter: /.*/, namespace: "https" }, onResolve); 121 | ctx.onResolve({ filter: /.*/, namespace: "data" }, onResolve); 122 | ctx.onResolve({ filter: /.*/, namespace: "npm" }, onResolve); 123 | ctx.onResolve({ filter: /.*/, namespace: "jsr" }, onResolve); 124 | 125 | const onLoad = async ( 126 | args: OnLoadArgs, 127 | ): Promise => { 128 | const url = 129 | args.path.startsWith("http:") || args.path.startsWith("https:") || 130 | args.path.startsWith("npm:") || args.path.startsWith("jsr:") 131 | ? args.path 132 | : path.toFileUrl(args.path).toString(); 133 | 134 | const moduleType = getModuleType(args.path, args.with); 135 | const res = await loader.load(url, moduleType); 136 | 137 | if (res.kind === "external") { 138 | return null; 139 | } 140 | 141 | const esbuildLoader = mediaToLoader(res.mediaType); 142 | 143 | const envPrefix = options.publicEnvVarPrefix; 144 | if ( 145 | envPrefix && 146 | moduleType === RequestedModuleType.Default 147 | ) { 148 | let code = new TextDecoder().decode(res.code); 149 | 150 | code = code.replaceAll( 151 | /Deno\.env\.get\(["']([^)]+)['"]\)|process\.env\.([\w_-]+)/g, 152 | (m, name, processName) => { 153 | if (name !== undefined && name.startsWith(envPrefix)) { 154 | return JSON.stringify(Deno.env.get(name)); 155 | } 156 | if ( 157 | processName !== undefined && processName.startsWith(envPrefix) 158 | ) { 159 | return JSON.stringify(Deno.env.get(processName)); 160 | } 161 | return m; 162 | }, 163 | ); 164 | 165 | return { 166 | contents: code, 167 | loader: esbuildLoader, 168 | }; 169 | } 170 | 171 | return { 172 | contents: res.code, 173 | loader: esbuildLoader, 174 | }; 175 | }; 176 | ctx.onLoad({ filter: /.*/, namespace: "file" }, onLoad); 177 | ctx.onLoad({ filter: /.*/, namespace: "jsr" }, onLoad); 178 | ctx.onLoad({ filter: /.*/, namespace: "npm" }, onLoad); 179 | ctx.onLoad({ filter: /.*/, namespace: "http" }, onLoad); 180 | ctx.onLoad({ filter: /.*/, namespace: "https" }, onLoad); 181 | ctx.onLoad({ filter: /.*/, namespace: "data" }, onLoad); 182 | }, 183 | }; 184 | } 185 | 186 | function mediaToLoader(type: MediaType): Loader { 187 | switch (type) { 188 | case MediaType.Jsx: 189 | return "jsx"; 190 | case MediaType.JavaScript: 191 | case MediaType.Mjs: 192 | case MediaType.Cjs: 193 | return "js"; 194 | case MediaType.TypeScript: 195 | case MediaType.Mts: 196 | case MediaType.Dmts: 197 | case MediaType.Dcts: 198 | return "ts"; 199 | case MediaType.Tsx: 200 | return "tsx"; 201 | case MediaType.Css: 202 | return "css"; 203 | case MediaType.Json: 204 | return "json"; 205 | case MediaType.Html: 206 | return "default"; 207 | case MediaType.Sql: 208 | return "default"; 209 | case MediaType.Wasm: 210 | return "binary"; 211 | case MediaType.SourceMap: 212 | return "json"; 213 | case MediaType.Unknown: 214 | return "default"; 215 | default: 216 | return "default"; 217 | } 218 | } 219 | 220 | function getPlatform( 221 | platform: Platform | undefined, 222 | ): WorkspaceOptions["platform"] { 223 | switch (platform) { 224 | case "browser": 225 | return "browser"; 226 | case "node": 227 | return "node"; 228 | case "neutral": 229 | default: 230 | return undefined; 231 | } 232 | } 233 | 234 | function getModuleType( 235 | file: string, 236 | withArgs: Record, 237 | ): RequestedModuleType { 238 | switch (withArgs.type) { 239 | case "text": 240 | return RequestedModuleType.Text; 241 | case "bytes": 242 | return RequestedModuleType.Bytes; 243 | case "json": 244 | return RequestedModuleType.Json; 245 | default: 246 | if (file.endsWith(".json")) { 247 | return RequestedModuleType.Json; 248 | } 249 | return RequestedModuleType.Default; 250 | } 251 | } 252 | 253 | // For some reason esbuild passes external specifiers to plugins. 254 | // See: https://esbuild.github.io/api/#external 255 | export function externalToRegex(external: string): RegExp { 256 | return new RegExp( 257 | "^" + external.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&").replace( 258 | /\*/g, 259 | ".*", 260 | ) + "$", 261 | ); 262 | } 263 | -------------------------------------------------------------------------------- /tests/bundle.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@std/expect"; 2 | import { build, type BuildOptions } from "esbuild"; 3 | import { denoPlugin } from "@deno/esbuild-plugin"; 4 | import * as path from "@std/path"; 5 | 6 | async function testEsbuild(options: { 7 | jsx?: BuildOptions["jsx"]; 8 | jsxDev?: BuildOptions["jsxDev"]; 9 | jsxImportSource?: BuildOptions["jsxImportSource"]; 10 | entryPoints: BuildOptions["entryPoints"]; 11 | plugins?: BuildOptions["plugins"]; 12 | external?: BuildOptions["external"]; 13 | platform?: BuildOptions["platform"]; 14 | }) { 15 | const res = await build({ 16 | entryPoints: options.entryPoints, 17 | write: false, 18 | format: "esm", 19 | bundle: true, 20 | jsx: options.jsx ?? "automatic", 21 | jsxDev: options.jsxDev ?? undefined, 22 | jsxImportSource: options.jsxImportSource ?? "preact", 23 | plugins: [denoPlugin(), ...(options.plugins ?? [])], 24 | external: options.external, 25 | platform: options.platform, 26 | }); 27 | 28 | expect(res.errors).toEqual([]); 29 | expect(res.warnings).toEqual([]); 30 | expect(res.outputFiles.length).toEqual(1); 31 | 32 | return res; 33 | } 34 | 35 | function getFixture(name: string) { 36 | return path.join(import.meta.dirname!, "fixtures", name); 37 | } 38 | 39 | Deno.test({ 40 | name: "entrypoints - string array", 41 | fn: async () => { 42 | await testEsbuild({ 43 | entryPoints: [getFixture("simple.ts")], 44 | }); 45 | }, 46 | sanitizeResources: false, 47 | sanitizeOps: false, 48 | }); 49 | 50 | Deno.test({ 51 | name: "entrypoints - object array", 52 | fn: async () => { 53 | await testEsbuild({ 54 | entryPoints: [ 55 | { 56 | in: getFixture("simple.ts"), 57 | out: "foo", 58 | }, 59 | ], 60 | }); 61 | }, 62 | sanitizeResources: false, 63 | sanitizeOps: false, 64 | }); 65 | 66 | Deno.test({ 67 | name: "entrypoints - record", 68 | fn: async () => { 69 | await testEsbuild({ 70 | entryPoints: { 71 | foo: getFixture("simple.ts"), 72 | }, 73 | }); 74 | }, 75 | sanitizeResources: false, 76 | sanitizeOps: false, 77 | }); 78 | 79 | Deno.test({ 80 | name: "resolves/loads - mapped", 81 | fn: async () => { 82 | await testEsbuild({ 83 | entryPoints: [getFixture("preact.ts")], 84 | }); 85 | }, 86 | sanitizeResources: false, 87 | sanitizeOps: false, 88 | }); 89 | 90 | Deno.test({ 91 | name: "resolves/loads - https:", 92 | fn: async () => { 93 | await testEsbuild({ 94 | entryPoints: [getFixture("https.ts")], 95 | }); 96 | }, 97 | sanitizeResources: false, 98 | sanitizeOps: false, 99 | }); 100 | 101 | Deno.test({ 102 | name: "resolves/loads - npm:", 103 | fn: async () => { 104 | await testEsbuild({ 105 | entryPoints: [getFixture("npm.ts")], 106 | }); 107 | }, 108 | sanitizeResources: false, 109 | sanitizeOps: false, 110 | }); 111 | 112 | Deno.test({ 113 | name: "resolves/loads - jsr:", 114 | fn: async () => { 115 | await testEsbuild({ 116 | entryPoints: [getFixture("jsr.ts")], 117 | }); 118 | }, 119 | sanitizeResources: false, 120 | sanitizeOps: false, 121 | }); 122 | 123 | Deno.test({ 124 | name: "resolves/loads - node:", 125 | fn: async () => { 126 | await testEsbuild({ 127 | entryPoints: [getFixture("node.ts")], 128 | }); 129 | }, 130 | sanitizeResources: false, 131 | sanitizeOps: false, 132 | }); 133 | 134 | Deno.test({ 135 | name: "resolves/loads - file:", 136 | fn: async () => { 137 | await testEsbuild({ 138 | entryPoints: [path.toFileUrl(getFixture("simple.ts")).href], 139 | }); 140 | }, 141 | sanitizeResources: false, 142 | sanitizeOps: false, 143 | }); 144 | 145 | Deno.test({ 146 | name: "resolves/loads - tsx", 147 | fn: async () => { 148 | const res = await testEsbuild({ 149 | entryPoints: [path.toFileUrl(getFixture("jsx.tsx")).href], 150 | }); 151 | 152 | expect(res.outputFiles[0].text).toContain('"div"'); 153 | }, 154 | sanitizeResources: false, 155 | sanitizeOps: false, 156 | }); 157 | 158 | Deno.test({ 159 | name: "plugins can participate in resolution", 160 | fn: async () => { 161 | const res = await testEsbuild({ 162 | entryPoints: [getFixture("mapped.ts")], 163 | plugins: [ 164 | { 165 | name: "test", 166 | setup(ctx) { 167 | ctx.onResolve({ filter: /mapped$/ }, () => { 168 | return { 169 | path: getFixture("simple.ts"), 170 | namespace: "test-internal", 171 | }; 172 | }); 173 | 174 | ctx.onLoad({ filter: /.*/, namespace: "test-internal" }, () => { 175 | return { 176 | contents: "hey", 177 | }; 178 | }); 179 | }, 180 | }, 181 | ], 182 | }); 183 | 184 | expect(res.outputFiles[0].text).toContain("hey"); 185 | }, 186 | sanitizeResources: false, 187 | sanitizeOps: false, 188 | }); 189 | 190 | Deno.test({ 191 | name: "jsx import source", 192 | fn: async () => { 193 | const res = await testEsbuild({ 194 | jsx: "automatic", 195 | jsxDev: true, 196 | jsxImportSource: "preact", 197 | entryPoints: [getFixture("jsx.tsx")], 198 | }); 199 | 200 | expect(res.outputFiles[0].text).toContain("it works"); 201 | }, 202 | sanitizeResources: false, 203 | sanitizeOps: false, 204 | }); 205 | 206 | Deno.test({ 207 | name: "with json", 208 | fn: async () => { 209 | const res = await testEsbuild({ 210 | entryPoints: [getFixture("with-json.ts")], 211 | }); 212 | 213 | expect(res.outputFiles[0].text).toContain("it works"); 214 | }, 215 | sanitizeResources: false, 216 | sanitizeOps: false, 217 | }); 218 | 219 | Deno.test({ 220 | name: "with text", 221 | fn: async () => { 222 | const res = await testEsbuild({ 223 | entryPoints: [getFixture("with-text.ts")], 224 | }); 225 | 226 | expect(res.outputFiles[0].text).toContain("it works"); 227 | }, 228 | sanitizeResources: false, 229 | sanitizeOps: false, 230 | }); 231 | 232 | Deno.test({ 233 | name: "with bytes", 234 | fn: async () => { 235 | const res = await testEsbuild({ 236 | entryPoints: [getFixture("with-bytes.ts")], 237 | }); 238 | 239 | expect(res.outputFiles[0].text).toContain("it works"); 240 | }, 241 | sanitizeResources: false, 242 | sanitizeOps: false, 243 | }); 244 | 245 | Deno.test({ 246 | name: "keep external specifiers", 247 | fn: async () => { 248 | const res = await testEsbuild({ 249 | entryPoints: [getFixture("external.ts")], 250 | external: ["mapped"], 251 | }); 252 | 253 | expect(res.outputFiles[0].text).toContain(`import "mapped"`); 254 | }, 255 | sanitizeResources: false, 256 | sanitizeOps: false, 257 | }); 258 | 259 | Deno.test({ 260 | name: "ignore modules it cannot resolve", 261 | fn: async () => { 262 | const res = await testEsbuild({ 263 | entryPoints: [":::"], 264 | plugins: [ 265 | { 266 | name: ":::", 267 | setup(build) { 268 | build.onResolve({ filter: /:::/ }, () => { 269 | return { 270 | path: getFixture("simple.ts"), 271 | }; 272 | }); 273 | }, 274 | }, 275 | ], 276 | }); 277 | 278 | expect(res.outputFiles[0].text).toContain("hey"); 279 | }, 280 | sanitizeResources: false, 281 | sanitizeOps: false, 282 | }); 283 | 284 | Deno.test({ 285 | name: "respects 'platform: browser'", 286 | fn: async () => { 287 | const res = await testEsbuild({ 288 | entryPoints: [getFixture("platform.ts")], 289 | platform: "browser", 290 | }); 291 | 292 | expect(res.outputFiles[0].text).not.toContain("worker_threads"); 293 | }, 294 | sanitizeResources: false, 295 | sanitizeOps: false, 296 | }); 297 | 298 | Deno.test({ 299 | name: "bundle json require in npm module", 300 | fn: async () => { 301 | // This threw previously. 302 | await testEsbuild({ 303 | entryPoints: [getFixture("mime-db.ts")], 304 | }); 305 | }, 306 | sanitizeResources: false, 307 | sanitizeOps: false, 308 | }); 309 | 310 | Deno.test({ 311 | name: "support public env vars", 312 | fn: async () => { 313 | Deno.env.set("TEST_PUBLIC_FOO", "foo-public-var"); 314 | const res = await build({ 315 | entryPoints: [getFixture("env-vars.ts")], 316 | write: false, 317 | format: "esm", 318 | bundle: true, 319 | plugins: [denoPlugin({ publicEnvVarPrefix: "TEST_PUBLIC_" })], 320 | }); 321 | 322 | const code = res.outputFiles[0].text; 323 | 324 | const binString = String.fromCodePoint(...res.outputFiles[0].contents); 325 | const data = btoa(binString); 326 | 327 | expect(code).not.toContain("Deno.env"); 328 | expect(code).not.toContain("process.env"); 329 | 330 | const dataURL = `data:text/javascript;base64,${data}`; 331 | 332 | // then import that as new JS module, which will fail. 333 | const mod = await import(dataURL); 334 | 335 | expect(mod.deno).toEqual("foo-public-var"); 336 | expect(mod.deno2).toEqual("foo-public-var"); 337 | expect(mod.node).toEqual("foo-public-var"); 338 | }, 339 | sanitizeResources: false, 340 | sanitizeOps: false, 341 | }); 342 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "specifiers": { 4 | "jsr:@deno/loader@~0.3.10": "0.3.10", 5 | "jsr:@marvinh-test/fresh-island@*": "0.0.1", 6 | "jsr:@std/assert@1": "1.0.13", 7 | "jsr:@std/assert@^1.0.13": "1.0.13", 8 | "jsr:@std/expect@^1.0.16": "1.0.16", 9 | "jsr:@std/internal@^1.0.6": "1.0.10", 10 | "jsr:@std/internal@^1.0.7": "1.0.10", 11 | "jsr:@std/internal@^1.0.9": "1.0.10", 12 | "jsr:@std/path@^1.1.1": "1.1.1", 13 | "npm:@preact/signals@^1.2.3": "1.3.2_preact@10.26.8", 14 | "npm:@types/node@*": "22.15.15", 15 | "npm:esbuild@~0.25.5": "0.25.5", 16 | "npm:fflate@~0.8.2": "0.8.2", 17 | "npm:mime-db@^1.54.0": "1.54.0", 18 | "npm:preact@*": "10.26.8", 19 | "npm:preact@^10.22.0": "10.26.8", 20 | "npm:preact@^10.26.8": "10.26.8" 21 | }, 22 | "jsr": { 23 | "@deno/loader@0.3.10": { 24 | "integrity": "a9c0aa44a0499e7fecef52c29fbc206c1c8f8946388f25d9d0789a23313bfd43" 25 | }, 26 | "@marvinh-test/fresh-island@0.0.1": { 27 | "integrity": "890f2595e60b1aaeaa8d73c6ad2c1247d4c5b895387df230f7f3b2a4da29b585", 28 | "dependencies": [ 29 | "npm:@preact/signals", 30 | "npm:preact@^10.22.0", 31 | "npm:preact@^10.26.8" 32 | ] 33 | }, 34 | "@std/assert@1.0.13": { 35 | "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 36 | "dependencies": [ 37 | "jsr:@std/internal@^1.0.6" 38 | ] 39 | }, 40 | "@std/expect@1.0.16": { 41 | "integrity": "ceeef6dda21f256a5f0f083fcc0eaca175428b523359a9b1d9b3a1df11cc7391", 42 | "dependencies": [ 43 | "jsr:@std/assert@^1.0.13", 44 | "jsr:@std/internal@^1.0.7" 45 | ] 46 | }, 47 | "@std/internal@1.0.8": { 48 | "integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5" 49 | }, 50 | "@std/internal@1.0.9": { 51 | "integrity": "bdfb97f83e4db7a13e8faab26fb1958d1b80cc64366501af78a0aee151696eb8" 52 | }, 53 | "@std/internal@1.0.10": { 54 | "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 55 | }, 56 | "@std/path@1.1.1": { 57 | "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", 58 | "dependencies": [ 59 | "jsr:@std/internal@^1.0.9" 60 | ] 61 | } 62 | }, 63 | "npm": { 64 | "@esbuild/aix-ppc64@0.25.5": { 65 | "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", 66 | "os": ["aix"], 67 | "cpu": ["ppc64"] 68 | }, 69 | "@esbuild/android-arm64@0.25.5": { 70 | "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", 71 | "os": ["android"], 72 | "cpu": ["arm64"] 73 | }, 74 | "@esbuild/android-arm@0.25.5": { 75 | "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", 76 | "os": ["android"], 77 | "cpu": ["arm"] 78 | }, 79 | "@esbuild/android-x64@0.25.5": { 80 | "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", 81 | "os": ["android"], 82 | "cpu": ["x64"] 83 | }, 84 | "@esbuild/darwin-arm64@0.25.5": { 85 | "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", 86 | "os": ["darwin"], 87 | "cpu": ["arm64"] 88 | }, 89 | "@esbuild/darwin-x64@0.25.5": { 90 | "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", 91 | "os": ["darwin"], 92 | "cpu": ["x64"] 93 | }, 94 | "@esbuild/freebsd-arm64@0.25.5": { 95 | "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", 96 | "os": ["freebsd"], 97 | "cpu": ["arm64"] 98 | }, 99 | "@esbuild/freebsd-x64@0.25.5": { 100 | "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", 101 | "os": ["freebsd"], 102 | "cpu": ["x64"] 103 | }, 104 | "@esbuild/linux-arm64@0.25.5": { 105 | "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", 106 | "os": ["linux"], 107 | "cpu": ["arm64"] 108 | }, 109 | "@esbuild/linux-arm@0.25.5": { 110 | "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", 111 | "os": ["linux"], 112 | "cpu": ["arm"] 113 | }, 114 | "@esbuild/linux-ia32@0.25.5": { 115 | "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", 116 | "os": ["linux"], 117 | "cpu": ["ia32"] 118 | }, 119 | "@esbuild/linux-loong64@0.25.5": { 120 | "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", 121 | "os": ["linux"], 122 | "cpu": ["loong64"] 123 | }, 124 | "@esbuild/linux-mips64el@0.25.5": { 125 | "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", 126 | "os": ["linux"], 127 | "cpu": ["mips64el"] 128 | }, 129 | "@esbuild/linux-ppc64@0.25.5": { 130 | "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", 131 | "os": ["linux"], 132 | "cpu": ["ppc64"] 133 | }, 134 | "@esbuild/linux-riscv64@0.25.5": { 135 | "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", 136 | "os": ["linux"], 137 | "cpu": ["riscv64"] 138 | }, 139 | "@esbuild/linux-s390x@0.25.5": { 140 | "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", 141 | "os": ["linux"], 142 | "cpu": ["s390x"] 143 | }, 144 | "@esbuild/linux-x64@0.25.5": { 145 | "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", 146 | "os": ["linux"], 147 | "cpu": ["x64"] 148 | }, 149 | "@esbuild/netbsd-arm64@0.25.5": { 150 | "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", 151 | "os": ["netbsd"], 152 | "cpu": ["arm64"] 153 | }, 154 | "@esbuild/netbsd-x64@0.25.5": { 155 | "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", 156 | "os": ["netbsd"], 157 | "cpu": ["x64"] 158 | }, 159 | "@esbuild/openbsd-arm64@0.25.5": { 160 | "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", 161 | "os": ["openbsd"], 162 | "cpu": ["arm64"] 163 | }, 164 | "@esbuild/openbsd-x64@0.25.5": { 165 | "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", 166 | "os": ["openbsd"], 167 | "cpu": ["x64"] 168 | }, 169 | "@esbuild/sunos-x64@0.25.5": { 170 | "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", 171 | "os": ["sunos"], 172 | "cpu": ["x64"] 173 | }, 174 | "@esbuild/win32-arm64@0.25.5": { 175 | "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", 176 | "os": ["win32"], 177 | "cpu": ["arm64"] 178 | }, 179 | "@esbuild/win32-ia32@0.25.5": { 180 | "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", 181 | "os": ["win32"], 182 | "cpu": ["ia32"] 183 | }, 184 | "@esbuild/win32-x64@0.25.5": { 185 | "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", 186 | "os": ["win32"], 187 | "cpu": ["x64"] 188 | }, 189 | "@preact/signals-core@1.9.0": { 190 | "integrity": "sha512-uUgFHJLWxb33rfCtb1g+1e3Rg7Jl5EALhGTHlQ5Y0w37OF+fdidYdYEE6crbpUOYDOjlmelIWf0ulXr1ggfUkg==" 191 | }, 192 | "@preact/signals@1.3.2_preact@10.26.8": { 193 | "integrity": "sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==", 194 | "dependencies": [ 195 | "@preact/signals-core", 196 | "preact" 197 | ] 198 | }, 199 | "@types/node@22.15.15": { 200 | "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", 201 | "dependencies": [ 202 | "undici-types" 203 | ] 204 | }, 205 | "esbuild@0.25.5": { 206 | "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", 207 | "optionalDependencies": [ 208 | "@esbuild/aix-ppc64", 209 | "@esbuild/android-arm", 210 | "@esbuild/android-arm64", 211 | "@esbuild/android-x64", 212 | "@esbuild/darwin-arm64", 213 | "@esbuild/darwin-x64", 214 | "@esbuild/freebsd-arm64", 215 | "@esbuild/freebsd-x64", 216 | "@esbuild/linux-arm", 217 | "@esbuild/linux-arm64", 218 | "@esbuild/linux-ia32", 219 | "@esbuild/linux-loong64", 220 | "@esbuild/linux-mips64el", 221 | "@esbuild/linux-ppc64", 222 | "@esbuild/linux-riscv64", 223 | "@esbuild/linux-s390x", 224 | "@esbuild/linux-x64", 225 | "@esbuild/netbsd-arm64", 226 | "@esbuild/netbsd-x64", 227 | "@esbuild/openbsd-arm64", 228 | "@esbuild/openbsd-x64", 229 | "@esbuild/sunos-x64", 230 | "@esbuild/win32-arm64", 231 | "@esbuild/win32-ia32", 232 | "@esbuild/win32-x64" 233 | ], 234 | "scripts": true, 235 | "bin": true 236 | }, 237 | "fflate@0.8.2": { 238 | "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" 239 | }, 240 | "mime-db@1.54.0": { 241 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" 242 | }, 243 | "preact@10.26.8": { 244 | "integrity": "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ==" 245 | }, 246 | "undici-types@6.21.0": { 247 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" 248 | } 249 | }, 250 | "redirects": { 251 | "https://esm.sh/preact": "https://esm.sh/preact@10.26.8" 252 | }, 253 | "remote": { 254 | "https://esm.sh/preact@10.26.8": "f6c6195e67293b6df707b53dd3075ca149604e1ba989c7832179b3c1b7e8a302", 255 | "https://esm.sh/preact@10.26.8/denonext/preact.mjs": "15cb76ed3f64c60ee55efae71db9561133cbb37af609921ddef445c4a543fad2" 256 | }, 257 | "workspace": { 258 | "dependencies": [ 259 | "jsr:@deno/loader@~0.3.10", 260 | "jsr:@std/expect@^1.0.16", 261 | "jsr:@std/path@^1.1.1", 262 | "npm:esbuild@~0.25.5", 263 | "npm:fflate@~0.8.2", 264 | "npm:mime-db@^1.54.0", 265 | "npm:preact@^10.26.8" 266 | ] 267 | } 268 | } 269 | --------------------------------------------------------------------------------