@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
TypeScript
/** 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