From 4e36629ca87cf28364e8cac963bc66705c7514b8 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 10 Feb 2025 23:44:04 +0100 Subject: [PATCH] refactor!: cleanup `defineEnv` and docs (#434) --- README.md | 113 +++++++++------------- lib/index.d.mts | 43 ++++----- package.json | 1 + src/env.ts | 202 ++++++++++++++++++++++++---------------- src/preset.ts | 87 +++++++++++++++++ src/presets/node.ts | 21 ----- src/presets/nodeless.ts | 106 --------------------- src/presets/utils.ts | 3 - 8 files changed, 272 insertions(+), 304 deletions(-) create mode 100644 src/preset.ts delete mode 100644 src/presets/node.ts delete mode 100644 src/presets/nodeless.ts delete mode 100644 src/presets/utils.ts diff --git a/README.md b/README.md index 5a52a5c6..9f1bbe1d 100644 --- a/README.md +++ b/README.md @@ -7,90 +7,71 @@ -unenv provides a collection of Node.js and Web polyfills and mocking utilities with configurable presets for converting JavaScript code and libraries to be platform and runtime agnostic, working in any environment including Browsers, Workers, Node.js, Cloudflare Workers, Deno. - -Unenv is used by [Nitro](https://nitro.unjs.io/) and [Nuxt](https://nuxt.com/) today. - > [!NOTE] > You are on the development (v2) branch. Check out [v1](https://github.com/unjs/unenv/tree/v1) for the current release. -## Install - - - -```sh -# ✨ Auto-detect -npx nypm install -D unenv - -# npm -npm install -D unenv - -# yarn -yarn add -D unenv - -# pnpm -pnpm install -D unenv +unenv, provides (build-time) polyfills to add [Node.js](https://nodejs.org/) compatibility for any JavaScript runtime, including browsers and edge workers. -# bun -bun install -D unenv +## 🌟 Used by -# deno -deno install --dev unenv -``` - - +- [Nitro](https://nitro.build/) +- [Nuxt](https://nuxt.com/) +- [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) +- [ESM.sh](https://esm.sh/) ## Usage -Using `env` utility and built-in presets, `unenv` will provide an abstract configuration that can be used in bundlers ([rollup.js](https://rollupjs.org), [webpack](https://webpack.js.org), etc.). +The `defineEnv` utility can generate a target environment configuration. ```js import { defineEnv } from "unenv"; const { env } = defineEnv({ - nodeCompat: true, - resolve: true, - presets: [], - overrides: {}, + /* options */ }); -const { alias, inject, polyfill, external } = env; +const { alias, inject, external, polyfill } = env; ``` -**Note:** You can provide as many presets as you want. unenv will merge them internally and the right-most preset has a higher priority. - -## Presets - -### `node` - -[(view source)](./src/presets/node.ts) +You can then integrate the env object with your build tool: -Suitable to convert universal libraries working in Node.js. +| Bundler | `alias` | `inject` | `external` | +| -------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| rollup | [`@rollup/plugin-alias`](https://www.npmjs.com/package/@rollup/plugin-alias) | [`@rollup/plugin-inject`](https://www.npmjs.com/package/@rollup/plugin-inject) | [`external`](https://rollupjs.org/configuration-options/#external) | +| rolldown | [`resolve.alias`](https://rolldown.rs/reference/config-options#resolve-alias) | [`inject`](https://rolldown.rs/reference/config-options#inject) | [`external`](https://rolldown.rs/reference/config-options#external) | +| vite | [`resolve.alias`](https://vite.dev/config/shared-options#resolve-alias) | [`@rollup/plugin-inject`](https://www.npmjs.com/package/@rollup/plugin-inject) | [`ssr.external`](https://vite.dev/config/ssr-options#ssr-external) | +| esbuild | [`alias`](https://esbuild.github.io/api/#alias) | [`inject`](https://esbuild.github.io/api/#inject) | [`external`](https://esbuild.github.io/api/#external) | +| rspack | [`resolve.alias`](https://rspack.dev/config/resolve#resolvealias) | - | [`externals`](https://rspack.dev/config/externals#externals-1) | +| webpack | [`resolve.alias`](https://webpack.js.org/configuration/resolve/#resolvealias) | [`webpack-plugin-inject`](https://www.npmjs.com/package/webpack-inject-plugin) | [`externals`](https://webpack.js.org/configuration/externals/#externals) | -- Add supports for global [`fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) -- Set Node.js built-ins as externals +**Note:** You can provide as many presets as you want. unenv will merge them internally and the right-most preset has a higher priority. -```js -import { env, node } from "unenv"; +### Options -const envConfig = env(node, {}); -``` +- `nodeCompat`: Add `alias` entries for node builtins both as `id` and `node:id` + `inject` entries for Node.js globals + such as `global`, `Buffer`, and `process` (default: `true`). +- `npmShims`: Add `alias` entries to replace common NPM packages such as `node-fetch` with native Web APIs (default: `true`). +- `presets`: Additional presets (for example [`@cloudflare/unenv-preset`](https://npmjs.com/@cloudflare/unenv-preset/). +- `overrides`: Additional overrides for env config. +- `resolve`: Resolve config values to absolute paths (default: `false`). -### `nodeless` +### Using direct imports -[(view source)](./src/presets/nodeless.ts) +You can also directly import `unenv/` polyfills: -Suitable to transform libraries made for Node.js to run in other JavaScript runtimes. +| Polyfills | Description | Source | +| ---------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- | +| `unenv/mock/*` | Mocking utils | [`src/runtime/mock`](https://github.com/unjs/unenv/tree/main/src/runtime/mock) | +| `unenv/node/*` | APIs compatible with `Node.js` API | [`src/runtime/node`](https://github.com/unjs/unenv/tree/main/src/runtime/node) | +| `unenv/npm` | NPM package shims for lighter replacements | [`src/runtime/npm`](https://github.com/unjs/unenv/tree/main/src/runtime/mock) | +| `unenv/polyfill` | Global polyfills | [`src/runtime/polyfill`](https://github.com/unjs/unenv/tree/main/src/runtime/polyfill) | +| `unenv/web` | Subset of Web APIs | [`src/runtime/web`](https://github.com/unjs/unenv/tree/main/src/runtime/web) | -```js -import { env, nodeless } from "unenv"; +## Node.js compatibility -const envConfig = env(nodeless, {}); -``` - -## Built-in Node.js modules +`unenv` replaces Node.js built-in modules compatible with any runtime [(view source)](./src/runtime/node). -`unenv` provides a replacement for Node.js built-in modules compatible with any runtime. +
@@ -151,19 +132,13 @@ const envConfig = env(nodeless, {}); -[(view source)](./src/runtime/node) - -## Package replacements - -`unenv` provides a replacement for common npm packages for cross-platform compatibility. - -[(view source)](./src/runtime/npm) +
## Manual mocking utils ```js // Magic proxy to replace any unknown API -import MockProxy from "unenv/runtime/mock/proxy"; +import MockProxy from "unenv/mock/proxy"; // You can also create named mocks const lib = MockProxy.__createMock__("lib", { @@ -173,14 +148,12 @@ const lib = MockProxy.__createMock__("lib", { [(view source)](./src/runtime/mock) -## Other polyfills - -To discover other polyfills, please check [./src/runtime](./src/runtime). - ## Nightly release channel You can use the nightly release channel to try the latest changes in the `main` branch via [`unenv-nightly`](https://www.npmjs.com/package/unenv-nightly). +
+ If directly using `unenv` in your project: ```json @@ -201,6 +174,8 @@ If using `unenv` via another tool (Nuxt or Nitro) in your project: } ``` +
+ ## License diff --git a/lib/index.d.mts b/lib/index.d.mts index 32343100..0afd7e57 100644 --- a/lib/index.d.mts +++ b/lib/index.d.mts @@ -1,14 +1,5 @@ /** * Configure a target environment. - * - * @example - * ```ts - * const { env } = defineEnv({ - * nodeCompat: true, - * resolve: true, - * presets: [myPreset], - * overrides: {} - * }); */ export declare function defineEnv(opts?: CreateEnvOptions): { env: Environment; @@ -17,19 +8,29 @@ export declare function defineEnv(opts?: CreateEnvOptions): { export interface CreateEnvOptions { /** - * Enable Node.js compatibility (nodeless) preset. + * Node.js compatibility aliases. * - * Default: `false` + * Default: `true` */ nodeCompat?: boolean; + + /** + * NPM compatibility aliases. + * + * Default: `true` + */ + npmShims?: boolean; + /** * Additional presets. */ presets?: Preset[]; + /** * Additional overrides. */ overrides?: Partial; + /** * Resolve paths in the environment to absolute paths. * @@ -48,17 +49,13 @@ export interface EnvResolveOptions { } export interface Environment { - alias: { - [key: string]: string; - }; - inject: { - [key: string]: string | string[]; - }; - polyfill: string[]; - external: string[]; + alias: Readonly>; + inject: Readonly>; + polyfill: readonly string[]; + external: readonly string[]; } -export interface Preset { +export interface Preset extends Partial { meta?: { /** * Preset name. @@ -73,10 +70,4 @@ export interface Preset { */ readonly url?: string | URL; }; - alias?: Environment["alias"]; - inject?: { - [key: string]: string | string[] | false; - }; - polyfill?: Environment["polyfill"]; - external?: Environment["external"]; } diff --git a/package.json b/package.json index 031e62b3..388cb416 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "types": "./lib/index.d.mts", "default": "./dist/index.mjs" }, + "./package.json": "./package.json", "./mock/proxy-cjs": { "types": "./lib/mock.d.cts", "default": "./lib/mock.cjs" diff --git a/src/env.ts b/src/env.ts index e9c15bdc..cfa813fc 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,103 +1,142 @@ import { resolvePathSync, type ResolveOptions } from "mlly"; import type { Preset, Environment, CreateEnvOptions } from "../lib/index.d.mts"; -import nodeCompatPreset from "./presets/nodeless.ts"; - -/** - * Configure a target environment. - * - * @example - * ```ts - * const { env } = defineEnv({ - * nodeCompat: true, - * resolve: true, - * presets: [myPreset], - * overrides: {} - * }); - */ +import { version } from "../package.json" with { type: "json" }; +import { nodeCompatAliases, nodeCompatInjects, npmShims } from "./preset"; + export function defineEnv(opts: CreateEnvOptions = {}): { env: Environment; presets: Preset[]; } { const presets: Preset[] = []; - if (opts.nodeCompat) { - presets.push(nodeCompatPreset); - } + // Dynamically create unenv preset + presets.push(unenvPreset(opts)); + // Additional presets if (opts.presets) { presets.push(...opts.presets); } + // Additional overrides if (opts.overrides) { presets.push(opts.overrides); } - const resolvedEnv = env(...presets); + // Merge all presets + const env = mergePresets(...presets); + // Optionally resolve paths if (opts.resolve) { - const resolvePaths: (string | URL)[] = [ - ...(opts.resolve === true ? [] : opts.resolve.paths || []), - ...presets - .map((preset) => preset.meta?.url) - .filter((v) => v !== undefined), - import.meta.url, - ]; - const resolveOpts: ResolveOptions = { - url: resolvePaths, - }; - - const _tryResolve = (id: string) => { - try { - return resolvePathSync(id, resolveOpts); - } catch {} - }; - - const _resolve = (id: string) => { - if (!id) { - return id; - } - let resolved = _tryResolve(id); - if (!resolved && id.startsWith("unenv/")) { - resolved = _tryResolve(id.replace("unenv/", "unenv-nightly/")); - } - return resolved || id; - }; - - // Resolve aliases - for (const alias in resolvedEnv.alias) { - resolvedEnv.alias[alias] = _resolve(resolvedEnv.alias[alias]); - } - // Resolve polyfills - for (let i = 0; i < resolvedEnv.polyfill.length; i++) { - resolvedEnv.polyfill[i] = _resolve(resolvedEnv.polyfill[i]); - } - // Resolve injects - for (const global in resolvedEnv.inject) { - const inject = resolvedEnv.inject[global]; - if (Array.isArray(inject)) { - const [id, ...path] = inject; - resolvedEnv.inject[global] = [_resolve(id), ...path]; - } else { - resolvedEnv.inject[global] = _resolve(inject); - } - } + resolveEnvPaths(env, presets, opts); } - return { env: resolvedEnv, presets }; + return { env, presets }; } -/** - * Merge presets into a final environment. - * Later presets take precedence over earlier ones. - */ -export function env(...presets: Preset[]): Environment { - const _env: Environment = { +function unenvPreset(opts: CreateEnvOptions) { + const preset = { + meta: { + name: "unenv", + version: version, + url: import.meta.url, + }, alias: {}, inject: {}, - polyfill: [], external: [], + polyfill: [], + } satisfies Preset; + + if (opts.nodeCompat !== false) { + Object.assign(preset.inject, nodeCompatInjects); + Object.assign(preset.alias, { + ...Object.fromEntries( + Object.entries(nodeCompatAliases).flatMap(([from, to]) => { + const aliases = [ + [from, to], // => unenv/node/id + [`node:${from}`, to], // node: => unenv/node/id + ]; + return aliases; + }), + ), + }); + } + + if (opts.npmShims !== false) { + Object.assign(preset.alias, npmShims); + } + + return preset; +} + +function resolveEnvPaths( + env: Environment, + presets: Preset[], + opts: CreateEnvOptions = {}, +) { + if (!opts.resolve) { + return; + } + + const resolvePaths: (string | URL)[] = [ + ...(opts.resolve === true ? [] : opts.resolve.paths || []), + ...presets.map((preset) => preset.meta?.url).filter((v) => v !== undefined), + import.meta.url, + ]; + const resolveOpts: ResolveOptions = { + url: resolvePaths, + }; + + const _tryResolve = (id: string) => { + try { + return resolvePathSync(id, resolveOpts); + } catch {} }; + const _resolve = (id: string) => { + if (!id) { + return id; + } + let resolved = _tryResolve(id); + if (!resolved && id.startsWith("unenv/")) { + resolved = _tryResolve(id.replace("unenv/", "unenv-nightly/")); + } + return resolved || id; + }; + + // Resolve aliases + for (const alias in env.alias) { + // @ts-expect-error readonly + env.alias[alias] = _resolve(env.alias[alias]); + } + // Resolve polyfills + for (let i = 0; i < env.polyfill.length; i++) { + // @ts-expect-error readonly + env.polyfill[i] = _resolve(env.polyfill[i]); + } + // Resolve injects + for (const global in env.inject) { + const inject = env.inject[global]; + if (Array.isArray(inject)) { + const [id, ...path] = inject; + // @ts-expect-error readonly + env.inject[global] = [_resolve(id), ...path]; + } else { + // @ts-expect-error readonly + env.inject[global] = _resolve(inject); + } + } + + return env; +} + +function mergePresets(...presets: Preset[]): Environment { + const env = { + alias: {} as Record, + inject: {} as Record, + polyfill: [] as string[], + external: [] as string[], + } satisfies Environment; + for (const preset of presets) { // Alias if (preset.alias) { @@ -107,7 +146,7 @@ export function env(...presets: Preset[]): Environment { b.split("/").length - a.split("/").length || b.length - a.length, ); for (const from of aliases) { - _env.alias[from] = preset.alias[from]; + env.alias[from] = preset.alias[from]; } } @@ -117,25 +156,30 @@ export function env(...presets: Preset[]): Environment { const globalValue = preset.inject[global]; if (Array.isArray(globalValue)) { const [id, ...path] = globalValue; - _env.inject[global] = [id, ...path]; + env.inject[global] = [id, ...path]; } else if (globalValue === false) { - delete _env.inject[global]; + delete env.inject[global]; } else { - _env.inject[global] = globalValue; + env.inject[global] = globalValue as string; } } } // Polyfill if (preset.polyfill) { - _env.polyfill.push(...(preset.polyfill.filter(Boolean) as string[])); + env.polyfill.push(...(preset.polyfill.filter(Boolean) as string[])); } // External if (preset.external) { - _env.external.push(...preset.external); + env.external.push(...preset.external); } } - return _env; + env.polyfill = [...new Set(env.polyfill)]; + env.external = [...new Set(env.external)]; + + return env; } + +console.log(defineEnv()); diff --git a/src/preset.ts b/src/preset.ts new file mode 100644 index 00000000..a333a661 --- /dev/null +++ b/src/preset.ts @@ -0,0 +1,87 @@ +export const nodeCompatAliases = { + _http_agent: "unenv/mock/proxy-cjs", + _http_client: "unenv/mock/proxy-cjs", + _http_common: "unenv/mock/proxy-cjs", + _http_incoming: "unenv/mock/proxy-cjs", + _http_outgoing: "unenv/mock/proxy-cjs", + _http_server: "unenv/mock/proxy-cjs", + _stream_duplex: "unenv/mock/proxy-cjs", + _stream_passthrough: "unenv/mock/proxy-cjs", + _stream_readable: "unenv/mock/proxy-cjs", + _stream_transform: "unenv/mock/proxy-cjs", + _stream_wrap: "unenv/mock/proxy-cjs", + _stream_writable: "unenv/mock/proxy-cjs", + _tls_common: "unenv/mock/proxy-cjs", + _tls_wrap: "unenv/mock/proxy-cjs", + assert: "unenv/node/assert", + "assert/strict": "unenv/node/assert/strict", + async_hooks: "unenv/node/async_hooks", + buffer: "unenv/node/buffer", + child_process: "unenv/node/child_process", + cluster: "unenv/node/cluster", + console: "unenv/node/console", + constants: "unenv/node/constants", + crypto: "unenv/node/crypto", + dgram: "unenv/node/dgram", + diagnostics_channel: "unenv/node/diagnostics_channel", + dns: "unenv/node/dns", + "dns/promises": "unenv/node/dns/promises", + domain: "unenv/node/domain", + events: "unenv/node/events", + fs: "unenv/node/fs", + "fs/promises": "unenv/node/fs/promises", + http: "unenv/node/http", + http2: "unenv/node/http2", + https: "unenv/node/https", + inspector: "unenv/node/inspector", + "inspector/promises": "unenv/node/inspector/promises", + module: "unenv/node/module", + net: "unenv/node/net", + os: "unenv/node/os", + path: "unenv/node/path", + "path/posix": "unenv/node/path", + "path/win32": "unenv/node/path", + perf_hooks: "unenv/node/perf_hooks", + process: "unenv/node/process", + punycode: "unenv/node/punycode", + querystring: "unenv/node/querystring", + readline: "unenv/node/readline", + "readline/promises": "unenv/node/readline/promises", + repl: "unenv/node/repl", + stream: "unenv/node/stream", + "stream/consumers": "unenv/node/stream/consumers", + "stream/promises": "unenv/node/stream/promises", + "stream/web": "unenv/node/stream/web", + string_decoder: "unenv/node/string_decoder", + sys: "unenv/node/util", + timers: "unenv/node/timers", + "timers/promises": "unenv/node/timers/promises", + tls: "unenv/node/tls", + trace_events: "unenv/node/trace_events", + tty: "unenv/node/tty", + url: "unenv/node/url", + util: "unenv/node/util", + "util/types": "unenv/node/util/types", + v8: "unenv/node/v8", + vm: "unenv/node/vm", + wasi: "unenv/node/wasi", + worker_threads: "unenv/node/worker_threads", + zlib: "unenv/node/zlib", +} as const; + +export const npmShims = { + fsevents: "unenv/npm/fsevents", + "node-fetch": "unenv/npm/node-fetch", + "node-fetch-native": "unenv/npm/node-fetch", + "node-fetch-native/polyfill": "unenv/mock/empty", + "cross-fetch": "unenv/npm/cross-fetch", + "cross-fetch/polyfill": "unenv/mock/empty", + "isomorphic-fetch": "unenv/mock/empty", + inherits: "unenv/npm/inherits", +} as const; + +export const nodeCompatInjects = { + global: "unenv/polyfill/globalthis", + process: "unenv/node/process", + Buffer: ["unenv/node/buffer", "Buffer"], +} as const; diff --git a/src/presets/node.ts b/src/presets/node.ts deleted file mode 100644 index ed7f2d42..00000000 --- a/src/presets/node.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { builtinModules } from "../runtime/node/module.ts"; -import type { Preset } from "../../lib/index.d.mts"; -import { version } from "../../package.json"; - -export default { - meta: { - name: "unenv:node", - version, - }, - - alias: { - "node-fetch": "unenv/npm/node-fetch", - "cross-fetch": "unenv/npm/cross-fetch", - "cross-fetch/polyfill": "unenv/mock/empty", - "isomorphic-fetch": "unenv/mock/empty", - }, - - polyfill: [], - - external: [...builtinModules], -} as Preset; diff --git a/src/presets/nodeless.ts b/src/presets/nodeless.ts deleted file mode 100644 index 56980bc8..00000000 --- a/src/presets/nodeless.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { mapArrToVal } from "./utils.ts"; -import { builtinModules } from "../runtime/node/module.ts"; -import type { Preset } from "../../lib/index.d.mts"; -import { version } from "../../package.json"; - -const nodeless: Preset & { alias: Map } = { - meta: { - name: "unenv:nodeless", - version, - }, - - alias: { - // Generic mock for built-ins - ...mapArrToVal("unenv/mock/proxy-cjs", builtinModules), - - // Built-ins implemented by unenv - "buffer/index.js": "buffer", - ...Object.fromEntries( - [ - "assert", - "assert/strict", - "async_hooks", - "buffer", - "console", - "child_process", - "constants", - "cluster", - "crypto", - "dgram", - "diagnostics_channel", - "dns", - "dns/promises", - "domain", - "events", - "fs", - "fs/promises", - "http", - "https", - "http2", - "inspector", - "inspector/promises", - "module", - "net", - "os", - "path", - "punycode", - "perf_hooks", - "process", - "querystring", - "readline", - "readline/promises", - "repl", - "stream", - "stream/promises", - "stream/consumers", - "stream/web", - "string_decoder", - "trace_events", - "timers", - "timers/promises", - "tls", - "tty", - "url", - "util", - "util/types", - "v8", - "vm", - "wasi", - "worker_threads", - "zlib", - ].map((m) => [m, `unenv/node/${m}`]), - ), - - "path/posix": "unenv/node/path", - "path/win32": "unenv/node/path", - - // The sys module is deprecated and has been renamed util - // https://github.com/nodejs/node/blob/main/lib/sys.js#L27 - sys: "unenv/node/util", - - // npm - fsevents: "unenv/npm/fsevents", - "node-fetch": "unenv/npm/node-fetch", - "node-fetch-native": "unenv/npm/node-fetch", - "node-fetch-native/polyfill": "unenv/mock/empty", - "cross-fetch": "unenv/npm/cross-fetch", - "cross-fetch/polyfill": "unenv/mock/empty", - "isomorphic-fetch": "unenv/mock/empty", - inherits: "unenv/npm/inherits", - }, - - inject: { - global: "unenv/polyfill/globalthis", // no side effects - process: "unenv/node/process", - Buffer: ["unenv/node/buffer", "Buffer"], - }, - - polyfill: [], -}; - -// Add node: aliases -for (const m of builtinModules) { - nodeless.alias[`node:${m}`] = nodeless.alias[m]; -} - -export default nodeless; diff --git a/src/presets/utils.ts b/src/presets/utils.ts deleted file mode 100644 index e93d8acf..00000000 --- a/src/presets/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function mapArrToVal(val: any, arr: readonly any[]) { - return Object.fromEntries(arr.map((c) => [c, val])); -}