Skip to content

Commit

Permalink
docs: update to v2
Browse files Browse the repository at this point in the history
  • Loading branch information
hansSchall committed Aug 31, 2024
1 parent 27ffcd0 commit c6c7a01
Showing 1 changed file with 160 additions and 103 deletions.
263 changes: 160 additions & 103 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
}
```
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit c6c7a01

Please sign in to comment.