@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).
224 lines • 13.7 kB
TypeScript
/** this submodule contains a namespaced esbuild plugin that resolves paths based on the three strategies:
* _runtime-package_ resolution, _import-map_ resolution, and _absolute-path_ resolution.
*
* > [!note]
* > the default namespace you should use in your plugins to call this pipeline of resolvers is {@link PLUGIN_NAMESPACE.RESOLVER_PIPELINE}.
* > otherwise, if you want, you can customize the namespace of this plugin by specifying it in the `config.namespace` option.
*
* below is a summary what the path resolution procedure of this plugin entails, given a certain esbuild input {@link OnResolveArgs | `args`}:
* 1. first, if {@link RuntimePackage | `args.pluginData.runtimePackage`} exists,
* then the plugin tries to resolve the input `args.path` through the use of the {@link RuntimePackage.resolveImport} method.
* - if it succeeds (i.e. `args.path` was an alias/import-map specified in the package file),
* then the resolved path, along with the original `args.pluginData`, is returned.
* - otherwise, if it fails, then the next path resolution takes place (import-map).
* 2. next, if either {@link ImportMap | `args.pluginData.importMap`} exists,
* or {@link ImportMapResolverConfig.globalImportMap | `config.importMap.globalImportMap`} exists,
* then the plugin will try to resolve the input `args.path` with respect to these import-maps,
* through the use of the {@link resolvePathFromImportMap} function.
* - if it succeeds (i.e. `args.path` was part of the import-map(s)),
* then the resolved path, along with the original `args.pluginData`, is returned.
* - otherwise, if it fails, then the next path resolution will take place (node-modules).
* 3. up ahead, we let esbuild's native resolver give a shot at resolving `args.path` as a node-module package,
* if and only if the path is neither a relative path (i.e. doesn't begin with `"./"` or `"../"`),
* nor is it an absolute path (such as "http://example.com/...", or "C:/users/...").
* this is because these scenarios can be handled much quicker by the next path-resolver that follows this one
* (since that one does not perform filesystem read/scan operations, unlike esbuild's native resolver).
* - if it succeeds (i.e. `args.path` was indeed some package inside some `"./node_modules/"` folder up the directory tree),
* then the resolved absolute filesystem path of the resource will be returned.
* - otherwise, if it fails, then the next path resolution will take place (path-joining).
* > [!tip]
* > under the hood, this function uses the {@link nodeModulesResolverFactory} function that initializes a mock build process,
* > which lets us indirectly query esbuild for the resolved path to the resource (if any).
* 4. finally, when all else has failed and we've made it to here, and `args.path` is a relative path:
* - then we'll simply compute the absolute path of the input `args.path` by combining it with `args.importer`, and `args.resolveDir`.
* - if neither of those two arguments are non-empty strings, then we will resort to using esbuild's `absWorkingDir` build option.
* - if `absWorkingDir` was itself not specified, then we will fallback to the runtime's current-working-directory as the base directory of `args.path`.
* - and again, the original `args.pluginData` is returned along with the result, so that the child dependnecies can inherit it.
*
* @module
*/
import { type DeepPartial } from "../deps.js";
import type { ImportMap } from "../importmap/typedefs.js";
import type { EsbuildPlugin, EsbuildPluginBuild, EsbuildPluginSetup, LoggerFunction, OnResolveResult } from "./typedefs.js";
import { type OnResolveArgs } from "./typedefs.js";
/** configuration for the runtime-package resolver in {@link resolverPluginSetup},
* which operates on the {@link CommonPluginData.runtimePackage} plugin-data property.
*/
export interface RuntimePackageResolverConfig {
/** enable or disable runtime-package (such as "deno.json") alias resolution.
*
* @defaultValue `true` (enabled)
*/
enabled: boolean;
}
/** configuration for the import-map resolver in {@link resolverPluginSetup},
* which operates on the {@link CommonPluginData.importMap} plugin-data property.
*/
export interface ImportMapResolverConfig {
/** enable or disable import-map resolution.
*
* @defaultValue `true` (enabled)
*/
enabled: boolean;
/** specify a global import-map for aliases to resources.
*
* the full import-map within the body of the resolver function will be a merger between _this_ global import-map,
* and the import-map acquired from the plugin data (i.e. {@link CommonPluginData.importMap}).
* the plugin data's import-map will take a higher priority in case of conflicting key (i.e. same alias key but different absolute path values).
*
* the resolve function will always look for a match for `args.path` inside of the import-map,
* before resolving it with respect to the current importer or resolve-directory (i.e. `args.importer` or `args.resolveDir`).
*
* @defaultValue `{}` (empty object/dictionary)
*/
globalImportMap: ImportMap;
}
/** configuration for esbuild's native `node_modules` resolver in {@link resolverPluginSetup}. */
export interface NodeModulesResolverConfig {
/** enable or disable `node_modules` resolution.
*
* @defaultValue `true` (enabled).
* however, the default `pluginData.resolverConfig.useNodeModules` option is set to `false`,
* meaning that it will only work when the user explicitly sets their `pluginData.resolverConfig.useNodeModules` to `true`.
*/
enabled: boolean;
}
/** configuration for the relative-path resolver in {@link resolverPluginSetup}. */
export interface RelativePathResolverConfig {
/** enable or disable relative path resolution.
*
* @defaultValue `true` (enabled)
*/
enabled: boolean;
/** a function that resolves a path segment to an absolute path (i.e. a path that the plugin itself can recognize as an absolute path).
*
* this function is utilized by the `relativePathResolver` function inside the plugin's body,
* and it will operate independent of what your {@link isAbsolutePath} function is.
* this means that you will have to check for absolute paths yourself,
* because every resource that will make its way to the `relativePathResolver` will be processed by this `resolvePath` function.
*
* in general, there are four checks that your path resolver function should perform in the provided order:
* 1. if `path` is `undefined` or an empty string, then the current working directory should be returned.
* 2. if the path begins with a leading slash ("/"), you should treat it as a root-path and join it with the `importer`'s domain
* (for instance, the domain of `https://esm.sh/hello/world` would be `https://esm.sh/`).
* but if no `importer` argument is present, then go to the next case (where you'll effectively resolve it as an absolute local unix file path).
* 3. if the `path` is an absolute path of some kind, you should simply return it as is, after ensuring posix separators (`"/"`) are being used.
* 4. finally, for all remaining cases (i.e. relative paths, which begin with either "./", "../", or a bare file subpath),
* you should join them with their `importer` if it is present, otherwise join them with your current working directory (or `absWorkingDir`).
*
* @param path the path of the resource that is being resolved.
* it could be a relative path (i.e. starts with `"./"` or `"../"`),
* or a root-path (i.e. starts with `"/"`), or a pre-existing absolute path (such as `jsr:@std/jsonc/parses`).
* and it may use windows backslashes instead of posix forward slashes, so make sure to handle all of these cases.
* @param importer when an importer file is present, this parameter will be provided,
* and you will be expected to join the `importer` with the `path` if the `path` is either relative or a root-path.
* do note that the importer is never an empty string.
* @returns the absolute path to the provided resource.
*/
resolvePath: (path?: string | undefined, importer?: string | undefined) => string;
/** a function that declares whether or not a given path segment is an absolute path (i.e. the plugin itself recognizes it as an absolute path). */
isAbsolutePath: (segment: string) => boolean;
}
/** configuration options for the {@link resolverPluginSetup} esbuild-setup factory function. */
export interface ResolverPluginSetupConfig {
/** {@inheritDoc RuntimePackageResolverConfig} */
runtimePackage: RuntimePackageResolverConfig;
/** {@inheritDoc ImportMapResolverConfig} */
importMap: ImportMapResolverConfig;
/** {@inheritDoc NodeModulesResolverConfig} */
nodeModules: NodeModulesResolverConfig;
/** {@inheritDoc RelativePathResolverConfig} */
relativePath: RelativePathResolverConfig;
/** specify the input-namespace on which the resolver-pipeline will operate on.
*
* > [!caution]
* > it is best if you don't modify it to something besides the default value {@link PLUGIN_NAMESPACE.RESOLVER_PIPELINE},
* > as the filter-category plugins rely on that specific namespace to have their intercepted entities resolved as absolute-paths.
*/
namespace: string;
/** enable logging of the input arguments and resolved paths, 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 is a 4-in-1 namespaced-plugin that assists in resolving esbuild paths,
* based on {@link CommonPluginData | `pluginData`}, esbuild's native-resolver, and relative path resolver.
*
* for details on what gets resolved an how, refer to the documentation comments of this submodule ({@link "plugins/resolvers"}).
*
* the way it is intended to be used is by being called indirectly via `build.resolve`, after specifying the correct namespace of this plugin.
*
* @example
* ```ts
* import type { EsbuildPlugin, EsbuildPluginBuild, OnLoadArgs, OnResolveArgs } from "./typedefs.ts"
* import { PLUGIN_NAMESPACE.RESOLVER_PIPELINE } from "./typedefs.ts"
*
* const THIS_plugins_namespace = PLUGIN_NAMESPACE.RESOLVER_PIPELINE // == "oazmi-resolver-pipeline"
*
* const myShinyPlugin: EsbuildPlugin = {
* name: "my-shiny-plugin",
* setup: (build: EsbuildPluginBuild) => {
* const everything_shiny_filter = /\.shiny$/
*
* build.onResolve({ filter: everything_shiny_filter }, async (args: OnResolveArgs) => {
* const { path, ...rest_args } = args
* const result = await build.resolve(path, { ...rest_args, namespace: THIS_plugins_namespace })
* if (result) {
* const {
* path: absolute_path,
* pluginData: useful_plugindata,
* // there will be a temporary namespace that you MUST drop.
* // the reason why it exists is because esbuild forbids the use of the default namespace,
* // unless the path is explicitly an absolute filesystem path.
* // so paths that begin with "http://", "file://", "jsr:", etc... are all invalidated by esbuild if `namespace = ""`.
* namespace: _drop_the_temp_namespace,
* ...rest_result
* } = result
* return {
* path: absolute_path,
* pluginData: useful_plugindata,
* namespace: "my-shiny-namespace",
* ...rest_result,
* }
* }
* })
*
* build.onLoad({ filter: RegExp(".*"), namespace: "my-shiny-namespace" }, async (args: OnLoadArgs) => {
* const { path, pluginData } = args
* const contents = "" // load your shiny stuff here
* return { contents, loader: "ts", pluginData }
* })
* }
* }
* ```
*/
export declare const resolverPluginSetup: (config?: DeepPartial<ResolverPluginSetupConfig>) => EsbuildPluginSetup;
/** {@inheritDoc resolverPluginSetup} */
export declare const resolverPlugin: (config?: DeepPartial<ResolverPluginSetupConfig>) => EsbuildPlugin;
/** an interface to configure the {@link nodeModulesResolverFactory} function. */
export interface NodeModulesResolverFactoryConfig {
/** the fallback resolve-dir path which should be used in place of `args.resolveDir` and `args.importer`, if neither are available.
* the `absWorkingDir` path will generally correspond to your project's directory (i.e. current working directory).
*/
absWorkingDir?: string;
}
/** this factory function creates a path-resolver function that mimics esbuild's native-resolver's behavior.
*
* the slyish way how it works is that we create a new build process to query esbuild what it would hypothetically do if so and so input arguments were given,
* then through a custom inner plugin, we capture esbuild's response (the resolved path), and return it back to the user.
*
* > [!note]
* > the internal resolver function accepts `"file://"` uris for the following list of {@link OnResolveArgs} fields,
* > and converts them to local paths for esbuild to understand:
* > - `path`
* > - `resolveDir`
* > - `importer`
* >
* > with this feature, you won't have to worry about file-uris at all.
*/
export declare const nodeModulesResolverFactory: (config: NodeModulesResolverFactoryConfig, build: EsbuildPluginBuild) => ((args: Pick<OnResolveArgs, "path" | "resolveDir" | "importer">) => Promise<OnResolveResult>);
//# sourceMappingURL=resolvers.d.ts.map