UNPKG

@oazmi/esbuild-plugin-deno

Version:

A portable non-invasive suite of esbuild plugins for loading http, jsr, npm, and import-maps. Supports Deno, Node, Bun, Web. Alternate to @luca/esbuild-deno-loader , while compatible with other plugins and native esbuild resolvers (e.g. css loader).

272 lines 19 kB
/** an esbuild plugin that strips away `npm:` specifiers, * and indirectly resolves the npm-package resource path through the {@link resolverPlugin}. * * @module */ import { type DeepPartial } from "../../deps.js"; import type { EsbuildEntryPointsType, EsbuildPlugin, EsbuildPluginBuild, EsbuildPluginSetup, LoggerFunction } from "../typedefs.js"; import { DIRECTORY } from "../typedefs.js"; /** acceptable directory formats for specifying your "resolve directory" for scanning and traversing `"./node_modules/"` folders. */ export type NodeModuleDirFormat = (string | URL | DIRECTORY); /** these options let you precisely customize how and where your missing npm-packages should get installed. */ export interface NpmAutoInstallCliConfig { /** specify the working-directory where your `npm` {@link command} should be invoked, * so that your package will get installed to `${dir}/node_modules/`. * * note that a trailing slash is always added to `dir` if it's missing it, and you can also provide a non-normalized path, * or relative paths with respect to esbuild's `absWorkingDir` (which fallbacks to the runtime's current-working-directory if `undefined`). * * furthermore, you should **not** add this directory to the {@link NpmPluginSetupConfig.nodeModulesDirs} array, * because the plugin **will** add this `dir` to the beginning of that array internally. * * @defaultValue `DIRECTORY.ABS_WORKING_DIR` */ dir: NodeModuleDirFormat; /** a function which should accept the package name-and-version string (such as `"react@17 - 19"`), * and then return a cli command, that when executed, will install the npm-package to the `${dir}/node_modules/` folder. * * note that you _can_ technically change the directory where the command is executed using `cd`, * however if you down a subdirectory (for instance `cd ./temp/ && npm install "${package_name_and_version}" --no-save`), * then the installed package will not be discoverable by esbuild's node-resolution-algorithm, because it only traverses upwards. * thus, you may only navigate to ancestral directories (such as `cd ../temp/ && npm install "${package_name_and_version}" --no-save`), * for the new package to be discoverable by esbuild. * * @defaultValue ```(package_name_and_version: string) => (`npm install "${package_name_and_version}" --no-save`)``` */ command: (package_name_and_version: string) => string; /** enable logging of the npm-package installation command, when DEBUG.LOG is ennabled. */ log?: boolean | LoggerFunction; } /** configuration options for the {@link npmPluginSetup} and {@link npmPlugin} functions. */ export interface NpmPluginSetupConfig { /** provide a list of prefix specifiers used for npm packages. * * @defaultValue `["npm:"]` */ specifiers: string[]; /** specify the side-effects potential of all npm-packages. * - `true`: this would mark all packages as having side-effects, resulting in basically no tree-shaking (large bundle size). * - `false`: this would mark all packages being side-effects free, allowing for tree-shaking to take place and reducing bundle size. * - `"auto"`: this would let esbuild decide which packages are side-effect free, * by probing into the package's `package.json` file and searching for the `"sideEffects"` field. * however, since many unseasoned package authors do not know about this field (i.e. me), the lack of it makes esbuild default to `false`. * which is in effect results in a larger bundled code size. * * TODO: in the future, I would like to probe into the `package.json` file of the package myself (by deriving its path from the `resolved_path`), * and then determine weather or not the `"sideEffects"` field is actually present. * if it isn't then we will default to `false` if this config option is set to `"defaultFalse"`, * or default to `true` if this config option is set to `"defaultTrue"`. * (esbuild exhibits the `"defaultTrue"` behavior by default anyway, so this specific option selection will be kind of redundant). * TODO: since in effect the `"auto"` option is equivalent to `"defaultTrue"`, I'm uncertain whether I should even keep the `"auto"` option. * * @defaultValue `"auto"` */ sideEffects: boolean | "auto" | "defaultFalse" | "defaultTrue"; /** auto install missing npm-package (the executed action/technique will vary based on the js-runtime-environment). * * ### options * * - `false`: missing npm-packages are not installed. * this will result in esbuild's build process to terminate, since the missing package will not be resolvable. * * - `true`: equivalent to the `"auto-cli"` option. * * - `"auto-cli"`: a cli-command will be picked depend on your js-runtime: * - for Deno, the cli-command of the `"deno"` option will be executed. * - for Bun, the cli-command of the `"bun"` option will be executed. * - for Nodejs, the cli-command of the `"npm"` option will be executed. * * - `"auto"`: the technique picked will depend on your js-runtime: * - for Deno and Bun, the `"dynamic"` option will be chosen. * - for Nodejs, the `"npm"` option will be chosen. * * - `"dynamic"`: a dynamic "on-the-fly" import will be performed, * forcing your runtime to cache the npm-package in a local `"./node_modules/"` directory. * > [!warning] * > the underlying technique for the `"dynamic"` option used will only work for Deno and Bun, but not Nodejs. * > * > moreover, you will **need** to have a certain configurations for this option to work on Deno and Bun: * > - for Deno, your project's "deno.json" file's `"nodeModulesDir"` should be set to `"auto"`, * > so that a local `"./node_modules/"` folder will be created for installed packages. * > - for Bun, your project's directory, or one of its ancesteral directory, must contain a `"./node_modules/"` folder, * > so that bun will opt for node-package-resolution instead of its default bun-style-resolution. * > TODO: I haven't actually tried it on bun, and I'm only speculating based on the information here: * > [link](https://bun.sh/docs/runtime/autoimport) * * - `"npm"`: this will run the `npm install "${pkg}" --no-save` cli-command in your `absWorkingDir`. * > [!note] * > the `--no-save` flag warrants that your `package.json` file will not be modified (nor created if lacking) when a package is being installed. * * - `"deno"`: this will run the `deno cache --no-config --node-modules-dir="auto" --allow-scripts "npm:${pkg}"` cli-command in your `absWorkingDir`. * > [!note] * > - `deno cache` installs the package, but without modifying (or creating) the "deno.json" file. * > - the `--no-config` option prevents deno from reading the "deno.json" (and "deno.lock") config file that may exist in an ancesteral directory of the desired installation directory, * > thereby changing the location where the `"./node_modules/"` installation occurs. * > moreover, giving deno access to the config file makes it reload all dependency modules listed in the "deno.json", not just our requested module alone. * > this can be extremely annoying at times, and especially annoying when verifying the behavior of the plugins. * > - the `--node-modules-dir="auto"` flag ensures that the package is installed under the `absWorkingDir` directory's `"./node_modules/"` subdirectory, * > instead of the global cache directory (which uses a non-node-module compatible layout). * > - the `--allow-scripts` flag permits `preinstall` and `postinstall` [lifecycle scripts](https://docs.npmjs.com/cli/v10/using-npm/scripts#life-cycle-scripts) to run. * * - `"deno-noscript"`: this will run the `deno cache --no-config --node-modules-dir="auto" "${pkg}"` cli-command in your `absWorkingDir`. * > [!note] * > this is different from the `"deno"` option because it does not permit the execution of `preinstall` and `postinstall` lifecycle scripts. * > lifecycle scripts could pose a security thread, but some popular packages that bind to native binaries (such as `npm:sqlite3`) require it. * * - `"bun"`: this will run the `bun install "${pkg}" --no-save` cli-command in your `absWorkingDir`. * > [!note] * > the `--no-save` flag warrants that your `package.json` file will not be modified (nor created if lacking) when a package is being installed. * * - `"pnpm"`: this will run the `pnpm install "${pkg}"` cli-command in your `absWorkingDir`. * > [!caution] * > since the `--no-save` flag is not supported in pnpm (see [issue#1237](https://github.com/pnpm/pnpm/issues/1237)), * > your `package.json` file will either get modified, or a new one will get created if it does not exist. * * - for any other custom cli-command, or to use an alternate installation directory, * you may provide an object that adheres to the {@link NpmAutoInstallCliConfig} interface. * * @defaultValue `true` */ autoInstall: boolean | "auto-cli" | "auto" | "dynamic" | "npm" | "deno" | "deno-noscript" | "bun" | "pnpm" | Partial<NpmAutoInstallCliConfig>; /** specify implicit peer-dependencies that your project requires, or it can even serve as a dependency aliasing import-map. * * when {@link autoInstall} is enabled, these peer-dependencies will be the first thing to get installed in bulk (during esbuild's `build.onStart` phase). * * moreover, an `npm:` prefix will always be ensured in your collection of peer-dependencies. * this means you cannot use this field to route a package to anywhere other than an npm-package. * for instance `{ "react": "react@18" }` will be converted to `{ "react": "npm:react@18" }`. * * TODO: currently, I'm not inserting `peerDependencies` as an import-map for all resolved npm-packages. * this is because, if the user had intended to use an npm-package alias, they could've set an entry in the `globalImportMap`, * and it would have sufficed in most cases. * but I may implement it in the future, since the user may only wish to expose this alias to npm-packages captured by this plugin, * rather the global scope (which includes entities from http, jsr, etc...). * moreover, since dynamic-import-based installations cannot be aliased, * injecting the `peerDependencies` as an import-map inside the `pluginData` would be needed for the package to be discoverable. * * @defaultValue `{}` (no peer-dependencies) */ peerDependencies: EsbuildEntryPointsType; /** specify which `namespace`s should be intercepted by the npm-specifier-plugin. * all other `namespace`s will not be processed by this plugin. * * if you have a plugin with a custom loader that works under some `"custom-namespace"`, * you can include your `"custom-namespace"` here, so that if it performs an npm-specifier import, * that import path will be captured by this plugin, and then consequently fetched by the http-loader plugin. * but do note that the namespace of the loaded resource will switch to the http-plugin's loader {@link namespace} * (which defaults to {@link PLUGIN_NAMESPACE.LOADER_HTTP}), instead of your `"custom-namespace"`. * * @defaultValue `[undefined, "", "file"]` (also this plugin's {@link namespace} gets added later on) */ acceptNamespaces: Array<string | undefined>; /** specify which directories should be used for scanning for npm-packages inside of various `node_modules` folders. * * here, you may provide a collection of: * - absolute filesystem paths (`string`). * - paths relative to your current working directory (`string` beginning with "./" or "../"). * - file-uris which being with the `"file://"` protocol (`string` or `URL`). * - or one of the accepted special directory enums in {@link DIRECTORY}. * * each directory that you provide here will be used by esbuild's native resolver as a starting point for scanning for `node_modules` npm-packages, * and it will work upwards from there until the root of your drive is reached, * and until all `"./node_modules/"` folders up the directory tree have been scanned. * * > [!tip] * > to understand how the scanning works, and to defer for inefficient redundant scanning, * > refer to the underlying scanner function's documentation: {@link findResolveDirOfNpmPackageFactory}. * * @defaultValue `[DIRECTORY.ABS_WORKING_DIR]` (equivalent to `[DIRECTORY.ABS_WORKING_DIR, DIRECTORY.CWD]`) */ nodeModulesDirs: NodeModuleDirFormat[]; /** enable logging of resolved npm-package's path, when {@link DEBUG.LOG} is ennabled. * * when set to `true`, the logs will show up in your console via `console.log()`. * you may also provide your own custom logger function if you wish. * * @defaultValue `false` */ log: boolean | LoggerFunction; } /** this plugin lets you redirect resource-paths beginning with an `"npm:"` specifier to your local `node_modules` folder. * after that, the module resolution task is carried by esbuild (for which you must ensure that you've ran `npm install`). * check the interface {@link NpmPluginSetupConfig} to understand what configuration options are available to you. * * example: `"npm:@oazmi/kitchensink@^0.9.8"` will be redirected to `"@oazmi/kitchensink"`. * and yes, the version number does currently get lost as a result. * so you'll have to pray that esbuild ends up in the `node_modules` folder consisting of the correct version, otherwise, rip. */ export declare const npmPluginSetup: (config?: DeepPartial<NpmPluginSetupConfig>) => EsbuildPluginSetup; /** {@inheritDoc npmPluginSetup} */ export declare const npmPlugin: (config?: Partial<NpmPluginSetupConfig>) => EsbuildPlugin; /** the signature of the function returned by {@link findResolveDirOfNpmPackageFactory}. * * this function makes esbuild scan multiple directories in search for an npm-package inside of some `"./node_modules/"` folder. * the first valid directory, in which the npm-package was scanned for was successful, will be returned. * * @param package_name the name of the npm-package to search for in the list of directories to scan. * @param directories_to_scan the list of parent directories in which the `"./node_modules/"` scanning should be performed. * note to **not** include directories which are an ancestor to another directory in the list. * this is because esbuild traverses up the directory tree when searching for the npm-package in various `"./node_modules/"` folders. * thus it would be redundant to include ancesteral directories. * moreover, you may also provide `file://` urls (in either `string` or `URL` format), instead of a filesystem path. * @returns if the requested npm-package was found in one of the listed directories, * then that directory's absolute path will be returned, otherwise `undefined` will be returned. */ export type FindResolveDirOfNpmPackage_FunctionSignature = (package_name: string, directories_to_scan: (string | URL)[]) => Promise<string | undefined>; /** generate a function that makes esbuild scan multiple directories in search for an npm-package inside of some `"./node_modules/"` folder. * the first valid directory, in which the npm-package was scanned for was successful, will be returned. * * @param build the esbuild "build" object that's available inside of esbuild plugin setup functions. * for mock testing, you can also provide a stub like this: `const build = { esbuild }`. * follow the example below that makes use of a similar stub. * * @example * ```ts ignore * import * as esbuild from "npm:esbuild@0.25.0" * * const build = { esbuild } * const myPackageDirScanner = findResolveDirOfNpmPackageFactory(build) * const my_package_resolve_dir = await myPackageDirScanner("@oazmi/tsignal", [ * "D:/temp/node/", * "file:///d:/sdk/cache/", * ]) * * console.log(`the "@oazmi/tsignal" package can be located when resolveDir is set to:`, my_package_resolve_dir) * ``` */ export declare const findResolveDirOfNpmPackageFactory: (build: EsbuildPluginBuild) => FindResolveDirOfNpmPackage_FunctionSignature; /** see {@link NpmAutoInstallCliConfig} and {@link NpmPluginSetupConfig.autoInstall} for details on how to customize. */ export type InstallNpmPackageConfig = "dynamic" | NpmAutoInstallCliConfig & { dir: string | URL; }; /** this function installs an npm-package to your project's `"./node_modules/"` folder. * see {@link NpmAutoInstallCliConfig} and {@link NpmPluginSetupConfig.autoInstall} for details on how to customize. * * > [!caution] * > make sure that you run only a SINGLE instance of this function at a time. * > that's because running multiple installations in parallel on the same working-directory will corrupt shared files. * > * > this becomes very evident in larger projects when multiple `npm install` commands are run in parallel, * > resulting in only a few of them actually successfully installing, * > while the rest are either partially installed, or ignored altogether. * > * > to mitigate this issue, run your multiple `npm install` commands through a synchronous task queuer, * > like the one that can be generated from the {@link syncTaskQueueFactory} utility function in this library. */ export declare const installNpmPackage: (package_name: string, config: InstallNpmPackageConfig) => Promise<void>; /** this function executes a cli command to install an npm-package. * see {@link NpmAutoInstallCliConfig} and {@link NpmPluginSetupConfig.autoInstall} for details on how to customize. */ export declare const installNpmPackageCli: (package_name: string, config: Exclude<InstallNpmPackageConfig, string>) => Promise<void>; /** this function indirectly makes the deno or bun runtimes automatically install an npm-package. * doing so will hopefully make it available under your project's `"./node_modules/"` directory, * allowing esbuild to access it when bundling code. * * > [!important] * > for the npm-package to be installed to your project's directory, * > you **must** have the `"nodeModulesDir"` field set to `"auto"` in your project's "deno.json" configuration file. * > otherwise, the package will get cached in deno's cache directory which uses a different file structure from `node_modules`, * > making it impossible for esbuild to traverse through it to discover the package natively. */ export declare const installNpmPackageDynamic: (package_name: string) => Promise<void>; //# sourceMappingURL=npm.d.ts.map