-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
27ffcd0
commit c6c7a01
Showing
1 changed file
with
160 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,168 +2,216 @@ | |
|
||
Native Vite support for Deno imports (jsr:, npm:, https://). | ||
|
||
Use the Deno for the frontend and enjoy development without the hassle of `node_modules`! | ||
Use Deno for the frontend and enjoy development without the hassle of `node_modules`! | ||
|
||
## Overview | ||
|
||
1. Enable Deno for the whole workspace or at least the frontend | ||
1. Enable Deno for the whole workspace | ||
|
||
2. Adjust your Vite config to use this plugin | ||
|
||
3. Enjoy development without `node_modules` | ||
3. Enjoy development without `node_modules` and package managers | ||
|
||
## How does this work? | ||
|
||
This plugin consists of a custom rollup loader that is injected at the earliest stage possible (even before the builtin | ||
fs loader). It catches every import that: | ||
This plugin injects a custom rollup resolver at the earliest stage possible (even before the builtin fs loader) which | ||
catches nearly every import. | ||
|
||
- starts with `jsr:` | ||
- starts with `npm:` | ||
- starts with `node:` (will be replaced by a polyfill package) | ||
- starts with `remote:` or `https://`[^1] | ||
- originates from an import fulfilling the above criteria (e.g. `import {foo} from "./foo.ts"` inside | ||
`jsr:@foo/[email protected]/mod.ts`) | ||
- is included in the deno.json import map | ||
- is not explicitly excluded | ||
Instead of letting vite resolve the imports the plugin consults the Deno CLI (`deno info --json`). This ensures that all | ||
imports link to exactly the same files Deno would use (including import mapping). | ||
|
||
All files are cached in a global cache file (one big SQLite DB). The cache location can be configured via | ||
`VITE_PLUGIN_DENO_CACHE`. All information about file integrity, resolved versions and HTTP redirects is stored in a | ||
separate lockfile (`vite.deno.lock` in the project dir). The plugin does not share lockfiles or cache with Deno. | ||
Additionally the plugin contains a Node.js/NPM compatible resolver (including probing and package.json exports), because | ||
`deno info` only outputs the resolved npm package versions, not the exact files. | ||
|
||
[^1]: Although `remote:` does not work with Deno, vite-plugin-deno rewrites all `https://` imports to `remote:https://` | ||
in order to simplify internal handling. | ||
A custom loader is able to locate the source code for all those new specifier schemes (for example in the global deno | ||
cache). `file:` URLs are mapped back to ordinary paths to make HMR work (Vite has no clue about `file:` URLs). The | ||
source code of `npm:` imports is transformed from CJS to ESM if necessary. | ||
|
||
## Package resolution | ||
## Supported imports | ||
|
||
The package resolution and even the download is handled on-the-fly: If the plugin encounters a semver range that has not | ||
already been resolved (entry in the `resolve` section of `vite.deno.lock`) it consults the respective registry for a | ||
list of all versions and chooses the newest matching version. | ||
### Provided by Deno | ||
|
||
Vite-plugin-deno will always respect semver, but the exact version might differ from the one Deno (or NPM) chooses, | ||
because the lockfiles are not synced. Do not be confused if this happens, if this causes problems you should adjust the | ||
respective semver range (that is exactly what they were introduced for). | ||
- `./foo.ts`: relative imports | ||
- `https://deno.land/x/[email protected]/mod.ts`: HTTPS imports | ||
- `jsr:@scope/foo`: JSR | ||
- `npm:foo@^1.0.0`: NPM with version range | ||
- `foo`: Mapped imports (`deno.json`>`imports`) | ||
|
||
In order to minimize problems with version resolution it is recommended to use `deno add ...` (equivalent to | ||
`npm install ...`) | ||
... and maybe imports that are not even invented yet, none of the imports above is explicitly handled by the plugin, | ||
`deno info` tells us everything that is needed to get the module source code. | ||
|
||
To manually control package resolution, edit the `resolve` section of `vite.deno.lock`. Proceed with _**EXTREME | ||
CAUTION**_, because it is possible to resolve a version range with a completely unrelated or not existing one. (e.g. | ||
`foo@^2.0.0` might be resolved as `[email protected]` or `foo@=1.2.3` as `[email protected]`). | ||
### Provided by the Plugin | ||
|
||
Sometimes a package does not specify all of its dependencies (which is considered bad practice), in this case the plugin | ||
will refuse to resolve the import because it has no information about the required version. Sometimes HMR support | ||
injects additional code with imports to HMR frontend code which replicates the same behavior. In this case the | ||
`allowed_undeclared_dependencies` option should be used, which treats those as a dependency of the _project_. See _Usage | ||
with Preact_ below. | ||
- `npm:[email protected]`: resolved NPM imports (including subpath imports) | ||
|
||
In general the package resolution is pretty robust and has been shown to handle even very complex dependency trees. The | ||
main reason why I have built this was because I had problems with a JSR package which depends on a NPM package which | ||
requires preact as as peer dependency. The JSR compatibility layer was not able to handle this situation correctly (it | ||
duplicated preact, but in this case preact usually throws Errors like: | ||
`cannot read properties of undefined, reading '__h' on 'C', 'C' is undefined`) In case you encounter any problems (not | ||
only regarding to package resolution) please file an issue | ||
The following imports are only used internally, but you may get in contact with them in the DevTools. You cannot use | ||
them in your source code because Deno wont be able to resolve them, but you may specify them in | ||
[`extra_import_map`](https://jsr.io/@deno-plc/vite-plugin-deno/doc/~/PluginDenoOptions.extra_import_map) | ||
|
||
## Usage | ||
- `npm-data:[email protected]/bar.js`: This references an internal file of the package tarball | ||
- `npm-probe:[email protected]/bar`: Same as above, but with probing. Will be resolved to a `npm-data:` URL | ||
|
||
## But I need this one package to be in `node_modules` | ||
|
||
There are various reasons why a single dependency has to reside in `node_modules`. The most popular reasons are the | ||
Babel and PostCSS plugin loaders (they depend on `node_modules` to find the plugins) and dependency pre-bundling. | ||
|
||
There are good news: Deno supports `node_modules`! | ||
|
||
Read more about [package.json compatibility](https://deno.com/blog/v1.31#packagejson-support) and | ||
[node_modules compatibility](https://docs.deno.com/runtime/manual/tools/unstable_flags/#--unstable-byonm) | ||
|
||
[> Configuration options documentation](https://jsr.io/@deno-plc/vite-plugin-deno/doc/~/PluginDenoOptions) | ||
If you have installed a dependency locally you can | ||
[`exclude`](https://jsr.io/@deno-plc/vite-plugin-deno/doc/~/PluginDenoOptions.exclude) it and reenable the Vite module | ||
resolution. This might be required for dependencies with **many** files. This plugin currently does no pre-bundling, so | ||
every file is loaded individually, in case of `lodash-es` this results in ~650 HTTP requests. | ||
|
||
## Usage | ||
|
||
Currently only build script configurations are supported because `vite.config.ts` will always be run with Node :-(. It | ||
is not that complicated as it sounds, you just have to use the JS API of Vite | ||
is not that complicated as it sounds, you just have to use the JS API of Vite. | ||
|
||
### `build.config.ts` | ||
### `scripts/vite.ts` | ||
|
||
```ts | ||
import { pluginDeno } from "vite-plugin-deno"; | ||
import { type InlineConfig } from "vite"; | ||
|
||
export const config: InlineConfig = { | ||
configFile: false, | ||
configFile: false, // configuration is inlined here | ||
server: { | ||
port: 80, | ||
}, | ||
plugins: [ | ||
await pluginDeno({ | ||
// options | ||
pluginDeno({ | ||
// see configuration docs | ||
}), | ||
], | ||
}; | ||
``` | ||
|
||
### `build.dev.ts`: | ||
### `scripts/dev.ts`: | ||
|
||
```ts | ||
import { createServer } from "vite"; | ||
import { config } from "./build.config.ts"; | ||
import { config } from "./vite.ts"; | ||
|
||
const server = await createServer(config); | ||
await server.listen(); | ||
server.printUrls(); | ||
``` | ||
|
||
### `build.release.ts` | ||
### `scripts/release.ts` | ||
|
||
```ts | ||
import { build } from "vite"; | ||
import { config } from "./build.config.ts"; | ||
import { config } from "./vite.ts"; | ||
|
||
await build(config); | ||
``` | ||
|
||
### `Deno.json` | ||
|
||
Include the DOM in the TS compiler options | ||
Include the DOM in the TS compiler options and define the build tasks | ||
|
||
```json | ||
"compilerOptions": { | ||
"lib": [ | ||
"deno.window", | ||
"deno.ns", | ||
"ESNext", | ||
"DOM", | ||
"DOM.Iterable", | ||
"webworker" | ||
] | ||
{ | ||
"tasks": { | ||
"dev": "deno run -A scripts/dev.ts", | ||
"release": "deno run -A scripts/release.ts" | ||
}, | ||
"compilerOptions": { | ||
"lib": [ | ||
"deno.window", | ||
"deno.ns", | ||
"ESNext", | ||
"DOM", | ||
"DOM.Iterable", | ||
"DOM.AsyncIterable", | ||
"webworker" | ||
] | ||
} | ||
} | ||
``` | ||
|
||
## Usage with React | ||
## Configuration options | ||
|
||
[> auto-generated docs](https://jsr.io/@deno-plc/vite-plugin-deno/doc/~/PluginDenoOptions) | ||
|
||
### `deno_json` and `deno_lock` | ||
|
||
Override the locations of `deno.json` and `deno.lock`. Passed to `deno --config ...` and `deno --lock ...` | ||
|
||
### `env`: `"deno"` or `"browser"`(default) | ||
|
||
Set to `"deno"` if you are bundling for Deno. This enables `node:` imports. | ||
|
||
Set to `"browser"` (default) if you are bundling for Browsers. This prefers NPM `browser` exports. | ||
|
||
### `undeclared_npm_imports`: `string[]` | ||
|
||
Those packages can always be imported, even when they don't show up in the deno module graph. This can be used to | ||
resolve packages that are imported by injected code like HMR. | ||
|
||
// coming soon | ||
### `extra_import_map`: `string` => `string` | ||
|
||
This import map can be used to polyfill `node:` or do the same as `undeclared_npm_imports` on a more granular level. | ||
|
||
### `exclude`: `(string | RegExp)[]` | ||
|
||
Those imports wont be touched. | ||
|
||
## Usage with React (coming soon) | ||
|
||
I personally only use Preact, so this is not top priority. | ||
|
||
Until this is supported out of the box you should be able to use the Preact configuration, it will use the React | ||
compatibility layer. Read more: https://preactjs.com/guide/v10/switching-to-preact | ||
|
||
If you have experience with Vite plugins and import resolving you should be able to get React working natively. | ||
|
||
## Usage with Preact | ||
|
||
Although `@preact/preset-vite` works when the respective Babel plugins are installed via NPM/Yarn, I do recommend | ||
against using it. You can achieve the same results with a few lines configuration without having to use `node_modules`. | ||
By the way, this will speed up your build process a lot because is uses ESBuild instead of Babel. | ||
**against** using it. | ||
|
||
With a few lines of configuration you can set up prefresh (the Preact HMR Engine) and use ESBuild for JSX | ||
transformation. | ||
|
||
By the way: ESBuild is many times faster than Babel and used by Vite to pre-process all files anyway (even if they are | ||
handled by Babel) | ||
|
||
Add this to your Vite config: | ||
Just update your Vite config: | ||
|
||
```typescript | ||
import { pluginDeno } from "@deno-plc/vite-plugin-deno"; | ||
import prefresh from "@prefresh/vite"; // HMR | ||
import type { InlineConfig, Plugin } from "vite"; | ||
|
||
export const config: InlineConfig = { | ||
// React aliasing | ||
resolve: { | ||
alias: [ | ||
{ | ||
find: "react", | ||
replacement: "@preact/compat", | ||
}, | ||
{ | ||
find: "react-dom", | ||
replacement: "@preact/compat", | ||
}, | ||
], | ||
}, | ||
plugins: [ | ||
await pluginDeno({ | ||
allowed_undeclared_dependencies: ["@prefresh/core", "@prefresh/utils"], // injected HMR code | ||
pluginDeno({ | ||
env: "browser", | ||
undeclared_npm_imports: [ | ||
// injected by JSX transform | ||
"preact/jsx-runtime", | ||
"preact/jsx-dev-runtime", | ||
// injected by HMR | ||
"@prefresh/core", | ||
"@prefresh/utils", | ||
// injected by react compat | ||
"@preact/compat", | ||
], | ||
extra_import_map: new Map([ | ||
// react compat | ||
["react", "@preact/compat"], | ||
["react-dom", "@preact/compat"], | ||
]), | ||
}), | ||
// HMR Plugin | ||
prefresh({ | ||
exclude: [/^npm:/, /registry.npmjs.org/, /^jsr:/], // see below | ||
// `node_modules` is included internally, lets do the same | ||
exclude: [/^npm/, /registry.npmjs.org/, /^jsr/, /^https?/], | ||
}) as Plugin, | ||
], | ||
// JSX transform | ||
|
@@ -174,11 +222,11 @@ export const config: InlineConfig = { | |
}; | ||
``` | ||
|
||
And this to your `deno.json`: | ||
And your `deno.json`: | ||
|
||
```json | ||
"compilerOptions": { | ||
"jsx": "react-jsx", | ||
"jsx": "automatic", | ||
"jsxImportSource": "preact", | ||
} | ||
``` | ||
|
@@ -187,7 +235,7 @@ If you want to use the Preact DevTools, follow the instructions there: https://p | |
one import) | ||
|
||
We need the prefresh exclude rule to replicate the internal exclude of all paths containing `node_modules`. Otherwise | ||
prefresh would inject HMR helpers into the code that powers HMR, which causes strange and hard to debug ReferenceErrors. | ||
prefresh would inject HMR helpers into libraries and the code that powers HMR, which causes very strange errors. | ||
|
||
## Known limitations | ||
|
||
|
@@ -197,21 +245,24 @@ The classic `vite.config.ts` file would be executed using Node.js instead of Den | |
|
||
### Dependency optimization | ||
|
||
Unsupported because dependency optimization depends on `node_modules`. If you really need it (lodash), exclude the | ||
dependency (`exclude: [/lodash-es/]`) and install it via NPM/Yarn. | ||
Unsupported because dependency optimization relies on `node_modules`. If you really need it (lodash), see | ||
[But I need this one package to be in `node_modules`](#but-i-need-this-one-package-to-be-in-node_modules) | ||
|
||
### Babel | ||
|
||
Some other plugins require Babel and Babel plugins. The Babel plugin loader depends on `node_modules`, so you have to | ||
install these using NPM/Yarn. In order to get the best DX possible, you should avoid Babel based plugins (for most | ||
setups Babel isn't really needed, see Usage wit Preact). | ||
Some other plugins require Babel and Babel plugins. The Babel plugin loader depends on `node_modules`, see | ||
[But I need this one package to be in `node_modules`](#but-i-need-this-one-package-to-be-in-node_modules). In order to | ||
get the best DX possible, you should avoid Babel based plugins (for most setups Babel isn't really needed, see Usage wit | ||
Preact. Using builtin esbuild is usually way faster). | ||
|
||
### PostCSS/TailwindCSS | ||
|
||
`tailwindcss` currently needs to be installed via NPM/Yarn, otherwise the PostCSS plugin loader is unable to find it. | ||
You could also use the Tailwind Play CDN during development. | ||
`tailwindcss` currently needs to be installed in `node_modules`, see | ||
[But I need this one package to be in `node_modules`](#but-i-need-this-one-package-to-be-in-node_modules) | ||
|
||
The recommended way is to use Tailwind Play CDN during development and Tailwind CLI for release build. | ||
|
||
### `Deno.stat` workaround needed | ||
### `Deno.stat` workaround needed (`Windows only`) | ||
|
||
Until https://github.com/denoland/deno/issues/24899 has been resolved, you need to include the following snippet in | ||
order to achieve the correct behavior when `node:fs.stat()` is called with an invalid file path. Otherwise you get | ||
|
@@ -220,17 +271,23 @@ errors like `[vite] Pre-transform error: EINVAL: invalid argument, stat`. | |
```typescript | ||
const deno_stat = Deno.stat; | ||
|
||
Deno.stat = (...args) => { | ||
const path = args[0].toString().replaceAll("\\", "/"); | ||
|
||
if (path.includes("/jsr:@")) { | ||
return deno_stat("./not-existing"); | ||
} else { | ||
return deno_stat(...args); | ||
} | ||
}; | ||
Deno.stat = (...args) => | ||
deno_stat(...args).catch((err) => { | ||
if (String(err.message).startsWith(`The filename, directory name, or volume label syntax is incorrect.`)) { | ||
return Deno.stat("./not-existing"); | ||
} else { | ||
throw err; | ||
} | ||
}); | ||
``` | ||
|
||
## Acknowledgements | ||
|
||
[esbuild_deno_loader](https://github.com/lucacasonato/esbuild_deno_loader) does essentially the same for esbuild. The | ||
basic principle of operation is the same. | ||
|
||
[resolve.exports](https://github.com/lukeed/resolve.exports) helped a lot, it handles all the `package.json` fields. | ||
|
||
## License (LGPL-2.1-or-later) | ||
|
||
Copyright (C) 2024 Hans Schallmoser | ||
|