├── deno.json ├── src ├── mod.test.ts └── mod.ts ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md └── deno.lock /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno/rolldown-plugin", 3 | "exports": "./src/mod.ts", 4 | "lint": { 5 | "rules": { 6 | "exclude": ["no-explicit-any"] 7 | } 8 | }, 9 | "exclude": [ 10 | "./target" 11 | ], 12 | "imports": { 13 | "@deno/loader": "jsr:@deno/loader@^0.3.0", 14 | "@std/assert": "jsr:@std/assert@^1.0.13", 15 | "@std/path": "jsr:@std/path@^1.1.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/mod.test.ts: -------------------------------------------------------------------------------- 1 | import denoPlugin from "./mod.ts"; 2 | import { assertEquals } from "@std/assert"; 3 | import { fromFileUrl } from "@std/path"; 4 | 5 | Deno.test("should load and resolve", async () => { 6 | const plugin = denoPlugin({ 7 | noTranspile: true, 8 | }); 9 | await plugin.buildStart({ 10 | input: import.meta.url, 11 | }); 12 | { 13 | const value = (await plugin.resolveId("./mod.ts", import.meta.url, { 14 | kind: "import-statement", 15 | })) as string; 16 | assertEquals(value, fromFileUrl(import.meta.resolve("./mod.ts"))); 17 | const text = await plugin.load(value); 18 | assertEquals(text, Deno.readTextFileSync(value)); 19 | } 20 | // node specifier 21 | { 22 | const value = await plugin.resolveId("node:events", import.meta.url, { 23 | kind: "import-statement", 24 | }); 25 | if (typeof value === "string") { 26 | throw new Error("Fail."); 27 | } 28 | assertEquals(value.external, true); 29 | assertEquals(value.id, "node:events"); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-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: [push, pull_request] 4 | 5 | concurrency: 6 | group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | deno: 11 | if: | 12 | github.event_name == 'push' || 13 | !startsWith(github.event.pull_request.head.label, 'denoland:') 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 30 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install deno 21 | uses: denoland/setup-deno@v2 22 | with: 23 | deno-version: canary 24 | 25 | - name: fmt 26 | run: deno fmt --check 27 | 28 | - name: lint 29 | run: deno lint 30 | 31 | - name: test 32 | run: deno test -A 33 | 34 | jsr: 35 | runs-on: ubuntu-latest 36 | permissions: 37 | contents: read 38 | id-token: write 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Install deno 42 | uses: denoland/setup-deno@v2 43 | with: 44 | deno-version: canary 45 | - name: Publish to JSR on tag 46 | run: deno run -A jsr:@david/publish-on-tag@0.2.0 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@deno/rolldown-plugin` 2 | 3 | A rolldown and rollup plugin for bundling Deno code. 4 | 5 | Still early days and it will probably not work well for npm packages atm (ex. 6 | ESM/CJS interop is not implemented). 7 | 8 | 1. Discovers your _deno.json_ and _deno.lock_ file. 9 | 1. Uses the same code as is used in the Deno CLI, but compiled to Wasm. 10 | 11 | ## Usage 12 | 13 | You must run rolldown via `Deno` or this won't work (running it via Node.js 14 | would require [this issue](https://github.com/dsherret/sys_traits/issues/4) to 15 | be resolved). 16 | 17 | 1. `deno install npm:rolldown jsr:@deno/rolldown-plugin` 18 | 1. Add a `bundle` task to your deno.json file: 19 | ```jsonc 20 | { 21 | "tasks": { 22 | "bundle": "rolldown -c" 23 | } 24 | // ...etc... 25 | } 26 | ``` 27 | 1. Add a `rolldown.config.js` file and specify the Deno plugin. Configure the 28 | input and output as desired. For example: 29 | ```js 30 | import denoPlugin from "@deno/rolldown-plugin"; 31 | import { defineConfig } from "rolldown"; 32 | 33 | export default defineConfig({ 34 | input: "./main.js", 35 | output: { 36 | file: "bundle.js", 37 | }, 38 | plugins: denoPlugin(), 39 | }); 40 | ``` 41 | 1. Run `deno task bundle`. 42 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "specifiers": { 4 | "jsr:@deno/loader@0.3": "0.3.0", 5 | "jsr:@std/assert@^1.0.13": "1.0.13", 6 | "jsr:@std/internal@^1.0.6": "1.0.8", 7 | "jsr:@std/internal@^1.0.9": "1.0.10", 8 | "jsr:@std/path@^1.1.1": "1.1.1" 9 | }, 10 | "jsr": { 11 | "@deno/loader@0.3.0": { 12 | "integrity": "f6a293012922c3c560ae0fb24db2065566f8307f48fceac2d4ddfcce5fd3a9d7" 13 | }, 14 | "@std/assert@1.0.13": { 15 | "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", 16 | "dependencies": [ 17 | "jsr:@std/internal@^1.0.6" 18 | ] 19 | }, 20 | "@std/internal@1.0.8": { 21 | "integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5" 22 | }, 23 | "@std/internal@1.0.10": { 24 | "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 25 | }, 26 | "@std/path@1.1.1": { 27 | "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", 28 | "dependencies": [ 29 | "jsr:@std/internal@^1.0.9" 30 | ] 31 | } 32 | }, 33 | "workspace": { 34 | "dependencies": [ 35 | "jsr:@deno/loader@0.3", 36 | "jsr:@std/assert@^1.0.13", 37 | "jsr:@std/path@^1.1.1" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Loader, 3 | type LoadResponse, 4 | MediaType, 5 | RequestedModuleType, 6 | ResolutionMode, 7 | Workspace, 8 | type WorkspaceOptions, 9 | } from "@deno/loader"; 10 | import { fromFileUrl } from "@std/path/from-file-url"; 11 | 12 | interface Module { 13 | specifier: string; 14 | code: string; 15 | } 16 | 17 | /** Options for creating the Deno plugin. */ 18 | export interface DenoPluginOptions extends WorkspaceOptions { 19 | } 20 | 21 | export interface BuildStartOptions { 22 | input: string | string[] | Record; 23 | } 24 | 25 | export interface ResolveIdOptions { 26 | kind: "import-statement" | "dynamic-import" | "require-call"; 27 | } 28 | 29 | export interface DenoPlugin extends Disposable { 30 | name: string; 31 | buildStart(options: BuildStartOptions): Promise; 32 | resolveId( 33 | source: string, 34 | importer: string | undefined, 35 | options: ResolveIdOptions, 36 | ): Promise; 37 | load(id: string): string | undefined; 38 | } 39 | 40 | /** 41 | * Creates a deno plugin for use with rolldown or rollup. 42 | * @returns The plugin. 43 | */ 44 | export default function denoPlugin( 45 | pluginOptions: DenoPluginOptions = {}, 46 | ): DenoPlugin { 47 | let loader: Loader; 48 | const loads = new Map>(); 49 | const modules = new Map(); 50 | 51 | return { 52 | name: "deno-plugin", 53 | [Symbol.dispose]() { 54 | loader?.[Symbol.dispose](); 55 | }, 56 | async buildStart(options: BuildStartOptions) { 57 | const inputs = Array.isArray(options.input) 58 | ? options.input 59 | : typeof options.input === "object" 60 | ? Object.values(options.input) 61 | : [options.input]; 62 | 63 | const workspace = new Workspace({ 64 | ...pluginOptions, 65 | }); 66 | loader = await workspace.createLoader(); 67 | await loader.addEntrypoints(inputs); 68 | }, 69 | async resolveId( 70 | source: string, 71 | importer: string | undefined, 72 | options: ResolveIdOptions, 73 | ) { 74 | const resolutionMode = resolveKindToResolutionMode(options.kind); 75 | importer = importer == null 76 | ? undefined 77 | : (modules.get(importer)?.specifier ?? importer); 78 | const resolvedSpecifier = await loader.resolve( 79 | source, 80 | importer, 81 | resolutionMode, 82 | ); 83 | 84 | // now load 85 | let loadPromise = loads.get(resolvedSpecifier); 86 | if (loadPromise == null) { 87 | loadPromise = loader.load( 88 | resolvedSpecifier, 89 | RequestedModuleType.Default, 90 | ); 91 | } 92 | const result = await loadPromise; 93 | if (result == null) { 94 | modules.set(resolvedSpecifier, undefined); 95 | return resolvedSpecifier; 96 | } 97 | if (result.kind === "external") { 98 | return { 99 | id: result.specifier, 100 | external: true, 101 | }; 102 | } 103 | const ext = mediaTypeToExtension(result.mediaType); 104 | let specifier = result.specifier; 105 | if (!specifier.endsWith(ext)) { 106 | specifier += ".rolldown" + ext; 107 | } 108 | if (specifier.startsWith("file:///")) { 109 | // use a path for files so the base gets stripped 110 | specifier = fromFileUrl(specifier); 111 | } 112 | if (pluginOptions.debug && result.specifier !== specifier) { 113 | console.error("Remapped", result.specifier, "to", specifier); 114 | } 115 | modules.set(specifier, { 116 | specifier: result.specifier, 117 | code: new TextDecoder().decode(result.code), 118 | }); 119 | return specifier; 120 | }, 121 | load(id: string) { 122 | return modules.get(id)?.code; 123 | }, 124 | }; 125 | } 126 | 127 | function mediaTypeToExtension(mediaType: MediaType) { 128 | switch (mediaType) { 129 | case MediaType.JavaScript: 130 | return ".js"; 131 | case MediaType.Mjs: 132 | return ".mjs"; 133 | case MediaType.Cjs: 134 | return ".cjs"; 135 | case MediaType.Jsx: 136 | return ".jsx"; 137 | case MediaType.TypeScript: 138 | case MediaType.Mts: 139 | return ".ts"; 140 | case MediaType.Cts: 141 | return ".cts"; 142 | case MediaType.Dts: 143 | return ".d.ts"; 144 | case MediaType.Dmts: 145 | return ".d.mts"; 146 | case MediaType.Dcts: 147 | return ".d.cts"; 148 | case MediaType.Tsx: 149 | return ".tsx"; 150 | case MediaType.Css: 151 | return ".css"; 152 | case MediaType.Json: 153 | return ".json"; 154 | case MediaType.Html: 155 | return ".html"; 156 | case MediaType.Sql: 157 | return ".sql"; 158 | case MediaType.Wasm: 159 | return ".wasm"; 160 | case MediaType.SourceMap: 161 | return ".map"; 162 | case MediaType.Unknown: 163 | default: 164 | return ""; 165 | } 166 | } 167 | 168 | function resolveKindToResolutionMode(kind: string): ResolutionMode { 169 | switch (kind) { 170 | case "import-statement": 171 | case "dynamic-import": 172 | return ResolutionMode.Import; 173 | case "require-call": 174 | return ResolutionMode.Require; 175 | default: 176 | throw new Error("not implemented: " + kind); 177 | } 178 | } 179 | --------------------------------------------------------------------------------