UNPKG

@oazmi/build-tools

Version:

general deno build tool scripts which I practically use in all of my typescript repos

283 lines (282 loc) 12 kB
import * as dntShim from "../../../../../../_dnt.shims.js"; import { dirname, join } from "../../../../@std/path/1.0.8/mod.js"; import { NativeLoader } from "./loader_native.js"; import { PortableLoader } from "./loader_portable.js"; import { findWorkspace, isInNodeModules } from "./shared.js"; import { esbuildResolutionToURL, isNodeModulesResolution, urlToEsbuildResolution, } from "./shared.js"; const LOADERS = ["native", "portable"]; /** The default loader to use. */ export const DEFAULT_LOADER = await dntShim.Deno.permissions.query({ name: "run" }) .then((res) => res.state !== "granted") ? "portable" : "native"; const BUILTIN_NODE_MODULES = new Set([ "assert", "assert/strict", "async_hooks", "buffer", "child_process", "cluster", "console", "constants", "crypto", "dgram", "diagnostics_channel", "dns", "dns/promises", "domain", "events", "fs", "fs/promises", "http", "http2", "https", "module", "net", "os", "path", "path/posix", "path/win32", "perf_hooks", "process", "punycode", "querystring", "repl", "readline", "stream", "stream/consumers", "stream/promises", "stream/web", "string_decoder", "sys", "test", "timers", "timers/promises", "tls", "tty", "url", "util", "util/types", "v8", "vm", "worker_threads", "zlib", ]); /** * The Deno loader plugin for esbuild. This plugin will load fully qualified * `file`, `http`, `https`, and `data` URLs. * * **Note** that this plugin does not do relative->absolute specifier * resolution, or import map resolution. You must use the `denoResolverPlugin` * _before_ the `denoLoaderPlugin` to do that. * * This plugin can be backed by two different loaders, the `native` loader and * the `portable` loader. * * ### Native Loader * * The native loader shells out to the Deno executable under the hood to load * files. Requires `--allow-read` and `--allow-run`. In this mode the download * cache is shared with the Deno executable. This mode respects deno.lock, * DENO_DIR, DENO_AUTH_TOKENS, and all similar loading configuration. Files are * cached on disk in the same Deno cache as the Deno executable, and will not be * re-downloaded on subsequent builds. * * NPM specifiers can be used in the native loader without requiring a local * `node_modules` directory. NPM packages are resolved, downloaded, cached, and * loaded in the same way as the Deno executable does. * * JSR specifiers can be used without restrictions in the native loader. To * ensure dependencies are de-duplicated correctly, it is recommended to use a * lockfile. * * ### Portable Loader * * The portable loader does module downloading and caching with only Web APIs. * Requires `--allow-read` and/or `--allow-net`. This mode does not respect * deno.lock, DENO_DIR, DENO_AUTH_TOKENS, or any other loading configuration. It * does not cache downloaded files. It will re-download files on every build. * * NPM specifiers can be used in the portable loader, but require a local * `node_modules` directory. The `node_modules` directory must be created prior * using Deno's `--node-modules-dir` flag. * * JSR specifiers require a lockfile to be present to resolve. */ export function denoLoaderPlugin(options = {}) { const loader = options.loader ?? DEFAULT_LOADER; if (LOADERS.indexOf(loader) === -1) { throw new Error(`Invalid loader: ${loader}`); } return { name: "deno-loader", setup(build) { const cwd = build.initialOptions.absWorkingDir ?? dntShim.Deno.cwd(); let nodeModulesDir = null; let loaderImpl; const packageIdByNodeModules = new Map(); build.onStart(function onStart() { loaderImpl?.[Symbol.dispose]?.(); loaderImpl = undefined; packageIdByNodeModules.clear(); let nodeModulesDirOpt = options.nodeModulesDir; let lockPath = options.lockPath; if ((nodeModulesDirOpt === undefined || (loader === "portable" && lockPath === undefined))) { const workspace = findWorkspace(cwd, build.initialOptions.entryPoints, options.configPath); try { if (nodeModulesDirOpt === undefined) { nodeModulesDirOpt = workspace.node_modules_dir(); } if (loader === "portable" && lockPath === undefined) { lockPath = workspace.lock_path(); } } finally { workspace.free(); } } if (nodeModulesDirOpt === "auto" || nodeModulesDirOpt === "manual") { nodeModulesDir = join(cwd, "node_modules"); } switch (loader) { case "native": loaderImpl = new NativeLoader({ infoOptions: { cwd, config: options.configPath, importMap: options.importMapURL, lock: options.lockPath, nodeModulesDir: nodeModulesDirOpt, }, }); break; case "portable": { loaderImpl = new PortableLoader({ lock: lockPath, }); } } }); async function onResolve(args) { if (isNodeModulesResolution(args)) { if (BUILTIN_NODE_MODULES.has(args.path) || BUILTIN_NODE_MODULES.has("node:" + args.path)) { return { path: args.path, external: true, }; } if (nodeModulesDir !== null) { return undefined; } else if (loaderImpl.nodeModulesDirForPackage && loaderImpl.packageIdFromNameInPackage) { let parentPackageId; let path = args.importer; while (true) { const packageId = packageIdByNodeModules.get(path); if (packageId) { parentPackageId = packageId; break; } const pathBefore = path; path = dirname(path); if (path === pathBefore) break; } if (!parentPackageId) { throw new Error(`Could not find package ID for importer: ${args.importer}`); } if (args.path.startsWith(".")) { return undefined; } else { let packageName; let pathParts; if (args.path.startsWith("@")) { const [scope, name, ...rest] = args.path.split("/"); packageName = `${scope}/${name}`; pathParts = rest; } else { const [name, ...rest] = args.path.split("/"); packageName = name; pathParts = rest; } const packageId = loaderImpl.packageIdFromNameInPackage(packageName, parentPackageId); const id = packageId ?? parentPackageId; const resolveDir = await loaderImpl.nodeModulesDirForPackage(id); packageIdByNodeModules.set(resolveDir, id); const path = [packageName, ...pathParts].join("/"); return await build.resolve(path, { kind: args.kind, resolveDir, importer: args.importer, }); } } else { throw new Error(`To use "npm:" specifiers, you must specify 'nodeModulesDir: "manual"', or use 'loader: "native"'.`); } } const specifier = esbuildResolutionToURL(args); // Once we have an absolute path, let the loader resolver figure out // what to do with it. const res = await loaderImpl.resolve(specifier); switch (res.kind) { case "esm": { const { specifier } = res; return urlToEsbuildResolution(specifier); } case "npm": { let resolveDir; if (nodeModulesDir !== null) { resolveDir = nodeModulesDir; } else if (loaderImpl.nodeModulesDirForPackage) { resolveDir = await loaderImpl.nodeModulesDirForPackage(res.packageId); packageIdByNodeModules.set(resolveDir, res.packageId); } else { throw new Error(`To use "npm:" specifiers, you must specify 'nodeModulesDir: "manual"', or use 'loader: "native"'.`); } const path = `${res.packageName}${res.path ?? ""}`; return await build.resolve(path, { kind: args.kind, resolveDir, importer: args.importer, }); } case "node": { return { path: res.path, external: true, }; } } } build.onResolve({ filter: /.*/, namespace: "file" }, onResolve); build.onResolve({ filter: /.*/, namespace: "http" }, onResolve); build.onResolve({ filter: /.*/, namespace: "https" }, onResolve); build.onResolve({ filter: /.*/, namespace: "data" }, onResolve); build.onResolve({ filter: /.*/, namespace: "npm" }, onResolve); build.onResolve({ filter: /.*/, namespace: "jsr" }, onResolve); build.onResolve({ filter: /.*/, namespace: "node" }, onResolve); function onLoad(args) { if (args.namespace === "file" && isInNodeModules(args.path)) { // inside node_modules, just let esbuild do it's thing return undefined; } const specifier = esbuildResolutionToURL(args); return loaderImpl.loadEsm(specifier); } // TODO(lucacasonato): once https://github.com/evanw/esbuild/pull/2968 is fixed, remove the catch all "file" handler build.onLoad({ filter: /.*/, namespace: "file" }, onLoad); build.onLoad({ filter: /.*/, namespace: "http" }, onLoad); build.onLoad({ filter: /.*/, namespace: "https" }, onLoad); build.onLoad({ filter: /.*/, namespace: "data" }, onLoad); }, }; }