├── tests ├── linked │ ├── main.ts │ ├── deno.json │ └── sub.ts ├── fixture │ ├── mapped │ │ ├── dep.ts │ │ ├── main.ts │ │ └── foo.ts │ ├── alias-target.ts │ ├── alias-mapped.ts │ ├── alias.ts │ ├── resolveInRootDir.ts │ ├── alias-hash-prefix.ts │ ├── jsr.ts │ ├── npm.ts │ ├── http.ts │ ├── inlineJsr.ts │ ├── inlineNpm.ts │ ├── inlineExternal.ts │ ├── inlineHttp.ts │ ├── linking.ts │ ├── deno.json │ ├── deno.lock │ └── vite.config.ts └── plugin.test.ts ├── deno.json ├── tsconfig.json ├── src ├── index.ts ├── utils.ts ├── prefixPlugin.ts ├── resolvePlugin.ts └── resolver.ts ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── package.json ├── README.md ├── LICENSE └── .gitignore /tests/linked/main.ts: -------------------------------------------------------------------------------- 1 | export * from "./sub.ts"; 2 | -------------------------------------------------------------------------------- /tests/fixture/mapped/dep.ts: -------------------------------------------------------------------------------- 1 | export const dep = "it works"; 2 | -------------------------------------------------------------------------------- /tests/fixture/alias-target.ts: -------------------------------------------------------------------------------- 1 | export const result = "it works"; 2 | -------------------------------------------------------------------------------- /tests/fixture/alias-mapped.ts: -------------------------------------------------------------------------------- 1 | import { value } from "mapped/main.ts"; 2 | 3 | console.log(value); 4 | -------------------------------------------------------------------------------- /tests/fixture/alias.ts: -------------------------------------------------------------------------------- 1 | import { result } from "import-map-alias"; 2 | 3 | console.log(result); 4 | -------------------------------------------------------------------------------- /tests/fixture/mapped/main.ts: -------------------------------------------------------------------------------- 1 | import { dep } from "./dep.ts"; 2 | 3 | export const value = dep; 4 | -------------------------------------------------------------------------------- /tests/fixture/resolveInRootDir.ts: -------------------------------------------------------------------------------- 1 | import { value } from "mapped/foo.ts"; 2 | 3 | console.log(value); 4 | -------------------------------------------------------------------------------- /tests/fixture/alias-hash-prefix.ts: -------------------------------------------------------------------------------- 1 | import { value } from "#hash-prefix/main.ts"; 2 | 3 | console.log(value); 4 | -------------------------------------------------------------------------------- /tests/fixture/mapped/foo.ts: -------------------------------------------------------------------------------- 1 | // will be transformed by a plugin 2 | export const value = "it doesn't work"; 3 | -------------------------------------------------------------------------------- /tests/linked/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linked", 3 | "exports": { 4 | ".": "./main.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/linked/sub.ts: -------------------------------------------------------------------------------- 1 | // This is a dummy file to test the linked module functionality. 2 | 3 | export function linkedFunction() {} 4 | -------------------------------------------------------------------------------- /tests/fixture/jsr.ts: -------------------------------------------------------------------------------- 1 | import { join } from "@std/path"; 2 | 3 | if (typeof join === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/npm.ts: -------------------------------------------------------------------------------- 1 | import { render } from "preact"; 2 | 3 | if (typeof render === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/http.ts: -------------------------------------------------------------------------------- 1 | import { render } from "preact-http"; 2 | 3 | if (typeof render === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/inlineJsr.ts: -------------------------------------------------------------------------------- 1 | import { join } from "jsr:@std/path"; 2 | 3 | if (typeof join === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/inlineNpm.ts: -------------------------------------------------------------------------------- 1 | import { render } from "npm:preact"; 2 | 3 | if (typeof render === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/inlineExternal.ts: -------------------------------------------------------------------------------- 1 | import * as helper from "\0vite/preload-helper.js"; 2 | 3 | if (helper !== undefined) { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/inlineHttp.ts: -------------------------------------------------------------------------------- 1 | import { render } from "https://esm.sh/preact"; 2 | 3 | if (typeof render === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixture/linking.ts: -------------------------------------------------------------------------------- 1 | import { linkedFunction } from "linked"; 2 | 3 | if (typeof linkedFunction === "function") { 4 | console.log("it works"); 5 | } 6 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "unstable": ["sloppy-imports"], 3 | "lint": { 4 | "rules": { 5 | "exclude": ["no-sloppy-imports"] 6 | }, 7 | "exclude": ["tests/fixture/dist/", "dist/"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "declaration": true, 5 | "target": "ES2020", 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext" 8 | }, 9 | "include": ["src"], 10 | "exclude": ["vendor/"] 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "vite"; 2 | import prefixPlugin from "./prefixPlugin.js"; 3 | import mainPlugin from "./resolvePlugin.js"; 4 | import type { DenoResolveResult } from "./resolver.js"; 5 | 6 | export default function deno(): Plugin[] { 7 | const cache = new Map(); 8 | 9 | return [prefixPlugin(cache), mainPlugin(cache)]; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import child_process from "node:child_process"; 2 | 3 | export async function execAsync( 4 | cmd: string, 5 | options: child_process.ExecOptions, 6 | ): Promise<{ stderr: string; stdout: string }> { 7 | return await new Promise((resolve, reject) => 8 | child_process.exec(cmd, options, (error, stdout, stderr) => { 9 | if (error) reject(error); 10 | else resolve({ stdout, stderr }); 11 | }) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixture/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "unstable": ["sloppy-imports"], 3 | "lint": { 4 | "rules": { 5 | "exclude": ["no-sloppy-imports"] 6 | } 7 | }, 8 | "nodeModulesDir": "auto", 9 | "links": ["../linked"], 10 | "imports": { 11 | "@std/path": "jsr:@std/path@^1.0.6", 12 | "import-map-alias": "./alias-target.ts", 13 | "preact": "npm:preact@^10.24.0", 14 | "preact-http": "https://esm.sh/preact", 15 | "mapped/": "./mapped/", 16 | "#hash-prefix/": "./mapped/" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | id-token: write 11 | steps: 12 | - uses: actions/checkout@v4 13 | # Setup .npmrc file to publish to npm 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: "22.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - run: npm ci 19 | - run: npm publish --access public 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /tests/fixture/deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@std/path@^1.0.6": "1.0.8", 5 | "npm:preact@^10.24.0": "10.25.4" 6 | }, 7 | "jsr": { 8 | "@std/path@1.0.8": { 9 | "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" 10 | } 11 | }, 12 | "npm": { 13 | "preact@10.25.4": { 14 | "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==" 15 | } 16 | }, 17 | "remote": { 18 | "https://esm.sh/preact@10.25.4/denonext/preact.mjs": "5b19c5a3daab9e9f4f4bed775afbb84dd4de2b8cc670ee332f5458c53a6a2d45" 19 | }, 20 | "workspace": { 21 | "dependencies": [ 22 | "jsr:@std/path@^1.0.6", 23 | "npm:preact@^10.24.0" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deno/vite-plugin", 3 | "version": "1.0.5", 4 | "type": "module", 5 | "license": "MIT", 6 | "exports": { 7 | ".": { 8 | "import": "./dist/index.js" 9 | } 10 | }, 11 | "files": [ 12 | "dist/" 13 | ], 14 | "keywords": [ 15 | "deno", 16 | "vite", 17 | "plugin" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/denoland/deno-vite-plugin.git" 22 | }, 23 | "scripts": { 24 | "test": "vitest", 25 | "build": "rimraf dist/ && tsc", 26 | "build:fixture": "cd tests/fixture && vite build", 27 | "prepublishOnly": "npm run build" 28 | }, 29 | "peerDependencies": { 30 | "vite": "5.x || 6.x || 7.x" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^22.5.5", 34 | "esbuild": "^0.23.1", 35 | "rimraf": "^6.0.1", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.6.2", 38 | "vitest": "^2.1.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno vite plugin 2 | 3 | Plugin to enable Deno resolution inside [vite](https://github.com/vitejs/vite). 4 | It supports: 5 | 6 | - Alias mappings in `deno.json` 7 | - `npm:` specifier 8 | - `jsr:` specifier 9 | - `http:` and `https:` specifiers 10 | 11 | ## Limitations 12 | 13 | Deno specific resolution cannot be used in `vite.config.ts` because it's not 14 | possible to intercept the bundling process of the config file in vite. 15 | 16 | ## Usage 17 | 18 | Install this package: 19 | 20 | ```sh 21 | # npm 22 | npm install @deno/vite-plugin 23 | # pnpm 24 | pnpm install @deno/vite-plugin 25 | # deno 26 | deno install npm:@deno/vite-plugin 27 | ``` 28 | 29 | Add the plugin to your vite configuration file `vite.config.ts`: 30 | 31 | ```diff 32 | import { defineConfig } from "vite"; 33 | + import deno from "@deno/vite-plugin"; 34 | 35 | export default defineConfig({ 36 | + plugins: [deno()], 37 | }); 38 | ``` 39 | 40 | ## License 41 | 42 | MIT, see [the license file](./LICENSE). 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 the Deno authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/fixture/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import deno from "../../src/index"; 3 | import path from "node:path"; 4 | 5 | export default defineConfig({ 6 | plugins: [deno(), { 7 | name: "mapped-transform", 8 | // @ts-ignore not sure 9 | transform(code, id) { 10 | if (id.startsWith("\0")) return; 11 | if (!id.includes("mapped") || path.basename(id) !== "foo.ts") return; 12 | 13 | return code.replace("it doesn't work", "it works"); 14 | }, 15 | }], 16 | build: { 17 | lib: { 18 | formats: ["es"], 19 | entry: { 20 | importMapAlias: "alias.ts", 21 | importMapAliasMapped: "alias-mapped.ts", 22 | importMapAliasHashPrefix: "alias-hash-prefix.ts", 23 | importMapNpm: "npm.ts", 24 | importMapJsr: "jsr.ts", 25 | importMapHttp: "http.ts", 26 | inlineExternal: "inlineExternal.ts", 27 | inlineNpm: "inlineNpm.ts", 28 | inlineJsr: "inlineJsr.ts", 29 | inlineHttp: "inlineHttp.ts", 30 | resolveInRootDir: "resolveInRootDir.ts", 31 | linking: "linking.ts", 32 | }, 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /.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 | lint-and-format: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: denoland/setup-deno@v2 15 | with: 16 | deno-version: v2.x 17 | - run: deno fmt --check 18 | - run: deno lint 19 | 20 | test: 21 | strategy: 22 | matrix: 23 | platform: [ubuntu-latest, macos-latest, windows-latest] 24 | node-version: ["22.x"] 25 | 26 | runs-on: ${{ matrix.platform }} 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: denoland/setup-deno@v1 31 | with: 32 | deno-version: canary 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | 38 | - run: npm i 39 | - run: npm run build --if-present 40 | - name: Build fixture 41 | working-directory: ./tests/fixture 42 | run: npx vite build --debug 43 | 44 | - run: npm test 45 | -------------------------------------------------------------------------------- /src/prefixPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "vite"; 2 | import { 3 | type DenoResolveResult, 4 | resolveDeno, 5 | resolveViteSpecifier, 6 | } from "./resolver.js"; 7 | import process from "node:process"; 8 | import path from "node:path"; 9 | 10 | export default function denoPrefixPlugin( 11 | cache: Map, 12 | ): Plugin { 13 | let root = process.cwd(); 14 | 15 | return { 16 | name: "deno:prefix", 17 | enforce: "pre", 18 | configResolved(config) { 19 | // Root path given by Vite always uses posix separators. 20 | root = path.normalize(config.root); 21 | }, 22 | async resolveId(id, importer) { 23 | if (id.startsWith("npm:")) { 24 | const resolved = await resolveDeno(id, root); 25 | if (resolved === null) return; 26 | 27 | // TODO: Resolving custom versions is not supported at the moment 28 | const actual = resolved.id.slice(0, resolved.id.indexOf("@")); 29 | const result = await this.resolve(actual); 30 | return result ?? actual; 31 | } else if (id.startsWith("http:") || id.startsWith("https:")) { 32 | return await resolveViteSpecifier(id, cache, root, importer); 33 | } 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /tests/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { beforeAll, describe, expect, it } from "vitest"; 3 | import { execAsync } from "../src/utils.ts"; 4 | 5 | const fixtureDir = path.join(import.meta.dirname!, "fixture"); 6 | 7 | async function runTest(file: string) { 8 | const res = await execAsync(`node dist/${file}`, { 9 | cwd: fixtureDir, 10 | }); 11 | expect(res.stdout.trim()).toEqual("it works"); 12 | } 13 | 14 | describe("Deno plugin", () => { 15 | beforeAll(async () => { 16 | await execAsync(`npx vite build`, { 17 | cwd: fixtureDir, 18 | }); 19 | }); 20 | 21 | describe("import map", () => { 22 | it("resolves alias", async () => { 23 | await runTest(`importMapAlias.js`); 24 | }); 25 | 26 | it("resolves alias mapped", async () => { 27 | await runTest(`importMapAliasMapped.js`); 28 | }); 29 | 30 | it("resolves alias mapped with hash prefix", async () => { 31 | await runTest(`importMapAliasHashPrefix.js`); 32 | }); 33 | 34 | it("resolves npm:", async () => { 35 | await runTest(`importMapNpm.js`); 36 | }); 37 | 38 | it("resolves jsr:", async () => { 39 | await runTest(`importMapJsr.js`); 40 | }); 41 | 42 | it("resolves http:", async () => { 43 | await runTest(`importMapHttp.js`); 44 | }); 45 | }); 46 | 47 | describe("inline", () => { 48 | it("resolves external:", async () => { 49 | await runTest(`inlineExternal.js`); 50 | }); 51 | 52 | it("resolves npm:", async () => { 53 | await runTest(`inlineNpm.js`); 54 | }); 55 | 56 | it("resolves jsr:", async () => { 57 | await runTest(`inlineJsr.js`); 58 | }); 59 | 60 | it("resolves http:", async () => { 61 | await runTest(`inlineHttp.js`); 62 | }); 63 | }); 64 | 65 | // https://github.com/denoland/deno-vite-plugin/issues/42 66 | it("resolve to file in root dir", async () => { 67 | await runTest(`resolveInRootDir.js`); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/resolvePlugin.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "vite"; 2 | import { 3 | type DenoMediaType, 4 | type DenoResolveResult, 5 | isDenoSpecifier, 6 | parseDenoSpecifier, 7 | resolveViteSpecifier, 8 | } from "./resolver.js"; 9 | import { type Loader, transform } from "esbuild"; 10 | import * as fsp from "node:fs/promises"; 11 | import process from "node:process"; 12 | import path from "node:path"; 13 | 14 | export default function denoPlugin( 15 | cache: Map, 16 | ): Plugin { 17 | let root = process.cwd(); 18 | 19 | return { 20 | name: "deno", 21 | configResolved(config) { 22 | // Root path given by Vite always uses posix separators. 23 | root = path.normalize(config.root); 24 | }, 25 | async resolveId(id, importer) { 26 | // The "pre"-resolve plugin already resolved it 27 | if (isDenoSpecifier(id)) return; 28 | 29 | return await resolveViteSpecifier(id, cache, root, importer); 30 | }, 31 | async load(id) { 32 | if (!isDenoSpecifier(id)) return; 33 | 34 | const { loader, resolved } = parseDenoSpecifier(id); 35 | 36 | const content = await fsp.readFile(resolved, "utf-8"); 37 | if (loader === "JavaScript") return content; 38 | if (loader === "Json") { 39 | return `export default ${content}`; 40 | } 41 | 42 | const result = await transform(content, { 43 | format: "esm", 44 | loader: mediaTypeToLoader(loader), 45 | logLevel: "debug", 46 | }); 47 | 48 | // Issue: https://github.com/denoland/deno-vite-plugin/issues/38 49 | // Esbuild uses an empty string as empty value and vite expects 50 | // `null` to be the empty value. This seems to be only the case in 51 | // `dev` mode 52 | const map = result.map === "" ? null : result.map; 53 | 54 | return { 55 | code: result.code, 56 | map, 57 | }; 58 | }, 59 | }; 60 | } 61 | 62 | function mediaTypeToLoader(media: DenoMediaType): Loader { 63 | switch (media) { 64 | case "JSX": 65 | return "jsx"; 66 | case "JavaScript": 67 | return "js"; 68 | case "Json": 69 | return "json"; 70 | case "TSX": 71 | return "tsx"; 72 | case "TypeScript": 73 | return "ts"; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | .vite/ 133 | 134 | # deno vendor 135 | vendor/ 136 | dist/ 137 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | import { execFile } from "node:child_process"; 2 | import process from "node:process"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import { execAsync } from "./utils.js"; 6 | 7 | export type DenoMediaType = 8 | | "TypeScript" 9 | | "TSX" 10 | | "JavaScript" 11 | | "JSX" 12 | | "Json"; 13 | 14 | interface ResolvedInfo { 15 | kind: "esm"; 16 | local: string; 17 | size: number; 18 | mediaType: DenoMediaType; 19 | specifier: string; 20 | dependencies: Array<{ 21 | specifier: string; 22 | code: { 23 | specifier: string; 24 | span: { start: unknown; end: unknown }; 25 | }; 26 | }>; 27 | } 28 | 29 | interface NpmResolvedInfo { 30 | kind: "npm"; 31 | specifier: string; 32 | npmPackage: string; 33 | } 34 | 35 | interface ExternalResolvedInfo { 36 | kind: "external"; 37 | specifier: string; 38 | } 39 | 40 | interface ResolveError { 41 | specifier: string; 42 | error: string; 43 | } 44 | 45 | interface DenoInfoJsonV1 { 46 | version: 1; 47 | redirects: Record; 48 | roots: string[]; 49 | modules: Array< 50 | NpmResolvedInfo | ResolvedInfo | ExternalResolvedInfo | ResolveError 51 | >; 52 | } 53 | 54 | export interface DenoResolveResult { 55 | id: string; 56 | kind: "esm" | "npm"; 57 | loader: DenoMediaType | null; 58 | dependencies: ResolvedInfo["dependencies"]; 59 | } 60 | 61 | function isResolveError( 62 | info: NpmResolvedInfo | ResolvedInfo | ExternalResolvedInfo | ResolveError, 63 | ): info is ResolveError { 64 | return "error" in info && typeof info.error === "string"; 65 | } 66 | 67 | let checkedDenoInstall = false; 68 | const DENO_BINARY = process.platform === "win32" ? "deno.exe" : "deno"; 69 | 70 | export async function resolveDeno( 71 | id: string, 72 | cwd: string, 73 | ): Promise { 74 | if (!checkedDenoInstall) { 75 | try { 76 | await execAsync(`${DENO_BINARY} --version`, { cwd }); 77 | checkedDenoInstall = true; 78 | } catch { 79 | throw new Error( 80 | `Deno binary could not be found. Install Deno to resolve this error.`, 81 | ); 82 | } 83 | } 84 | 85 | // There is no JS-API in Deno to get the final file path in Deno's 86 | // cache directory. The `deno info` command reveals that information 87 | // though, so we can use that. 88 | const output = await new Promise((resolve, reject) => { 89 | execFile(DENO_BINARY, ["info", "--json", id], { cwd }, (error, stdout) => { 90 | if (error) { 91 | if (String(error).includes("Integrity check failed")) { 92 | reject(error); 93 | } else { 94 | resolve(null); 95 | } 96 | } else resolve(stdout); 97 | }); 98 | }); 99 | 100 | if (output === null) return null; 101 | 102 | const json = JSON.parse(output) as DenoInfoJsonV1; 103 | const actualId = json.roots[0]; 104 | 105 | // Find the final resolved cache path. First, we need to check 106 | // if the redirected specifier, which represents the final specifier. 107 | // This is often used for `http://` imports where a server can do 108 | // redirects. 109 | const redirected = json.redirects[actualId] ?? actualId; 110 | 111 | // Find the module information based on the redirected speciffier 112 | const mod = json.modules.find((info) => info.specifier === redirected); 113 | if (mod === undefined) return null; 114 | 115 | // Specifier not found by deno 116 | if (isResolveError(mod)) { 117 | return null; 118 | } 119 | 120 | if (mod.kind === "esm") { 121 | return { 122 | id: mod.local, 123 | kind: mod.kind, 124 | loader: mod.mediaType, 125 | dependencies: mod.dependencies, 126 | }; 127 | } else if (mod.kind === "npm") { 128 | return { 129 | id: mod.npmPackage, 130 | kind: mod.kind, 131 | loader: null, 132 | dependencies: [], 133 | }; 134 | } else if (mod.kind === "external") { 135 | // Let vite handle this 136 | return null; 137 | } 138 | 139 | throw new Error(`Unsupported: ${JSON.stringify(mod, null, 2)}`); 140 | } 141 | 142 | export async function resolveViteSpecifier( 143 | id: string, 144 | cache: Map, 145 | posixRoot: string, 146 | importer?: string, 147 | ) { 148 | const root = path.normalize(posixRoot); 149 | 150 | // Resolve import map 151 | if (!id.startsWith(".") && !id.startsWith("/")) { 152 | try { 153 | id = import.meta.resolve(id); 154 | } catch { 155 | // Ignore: not resolvable 156 | } 157 | } 158 | 159 | if (importer && isDenoSpecifier(importer)) { 160 | const { resolved: parent } = parseDenoSpecifier(importer); 161 | 162 | const cached = cache.get(parent); 163 | if (cached === undefined) return; 164 | 165 | const found = cached.dependencies.find((dep) => dep.specifier === id); 166 | 167 | if (found === undefined) return; 168 | 169 | // Check if we need to continue resolution 170 | id = found.code.specifier; 171 | if (id.startsWith("file://")) { 172 | return fileURLToPath(id); 173 | } 174 | } 175 | 176 | const resolved = cache.get(id) ?? await resolveDeno(id, root); 177 | 178 | // Deno cannot resolve this 179 | if (resolved === null) return; 180 | 181 | if (resolved.kind === "npm") { 182 | return null; 183 | } 184 | 185 | cache.set(resolved.id, resolved); 186 | 187 | // Vite can load this 188 | if ( 189 | resolved.loader === null || 190 | resolved.id.startsWith(path.resolve(root)) && 191 | !path.relative(root, resolved.id).startsWith(".") 192 | ) { 193 | return resolved.id; 194 | } 195 | 196 | // We must load it 197 | return toDenoSpecifier(resolved.loader, id, resolved.id); 198 | } 199 | 200 | export type DenoSpecifierName = string & { __brand: "deno" }; 201 | 202 | export function isDenoSpecifier(str: string): str is DenoSpecifierName { 203 | return str.startsWith("\0deno"); 204 | } 205 | 206 | export function toDenoSpecifier( 207 | loader: DenoMediaType, 208 | id: string, 209 | resolved: string, 210 | ): DenoSpecifierName { 211 | return `\0deno::${loader}::${id}::${resolved}` as DenoSpecifierName; 212 | } 213 | 214 | export function parseDenoSpecifier(spec: DenoSpecifierName): { 215 | loader: DenoMediaType; 216 | id: string; 217 | resolved: string; 218 | } { 219 | const [_, loader, id, posixPath] = spec.split("::") as [ 220 | string, 221 | string, 222 | DenoMediaType, 223 | string, 224 | ]; 225 | const resolved = path.normalize(posixPath); 226 | return { loader: loader as DenoMediaType, id, resolved }; 227 | } 228 | --------------------------------------------------------------------------------