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 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 ?? 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);
        },
    };
}
//# sourceMappingURL=plugin_deno_loader.js.map