Oxc Resolver

repository·main·Indexed 42 days ago

https://github.com/oxc-project/oxc-resolver

A high-performance Rust port of module resolution tools like enhanced-resolve and tsconfck. It provides fast, accurate resolution for ESM and CommonJS, featuring built-in tsconfig.json discovery, alias support, and project references. Available as a Rust library and an npm package for Node.js and WASM environments.

Tokens
15.5K
Snippets
44
Records
74
Agent score
53%

What's inside Oxc Resolver

  1. Understand directory and specifier terminology

    main

    To use the resolver correctly, understand these two core concepts:

    • directory: An absolute path to a directory where the specifier is resolved against.
      • For CommonJS, this is typically the __dirname of the current module.
      • For ESM, this is the value of import.meta.url.
      • Note: Providing a path to a file instead of a directory results in undefined behavior.
    • specifier: The string passed to a module loading function, such as the argument in require("specifier") or import "specifier".
  2. Use the oxc-resolver npm package

    main

    The oxc-resolver npm package provides high-performance module resolution for both Node.js and WASM environments. It implements ESM and CommonJS resolution algorithms and includes built-in support for tsconfig.json features like paths aliases, project references, and tsconfig.extends.

    There are two primary ways to use the API:

    1. Direct resolve function: A simple utility for resolving specifiers against a directory.
    2. ResolverFactory: A more robust way to manage resolution state and configuration, allowing for cloning with different options (e.g., switching between ESM and CJS conditions).
    import resolve, { ResolverFactory } from "./index.js";
    
    // Using the direct resolve function
    const result = resolve.sync(process.cwd(), "./index.js");
    
    // Using ResolverFactory
    const resolver = new ResolverFactory();
    const result2 = resolver.sync(process.cwd(), "./index.js");
  3. Understand Package Target Resolution Logic

    main

    The resolver implements the Node.js algorithm for resolving package targets within exports and imports fields. When a target is encountered, the resolver follows these rules:

    1. String Targets: If the target is a string, it is normalized. If the resulting path contains invalid segments (like .. or node_modules in certain contexts), it throws an InvalidPackageTarget error.
    2. Object Targets: If the target is an object, the resolver iterates through its keys in insertion order. It checks for matches against the "default" key or any active conditions (e.g., import, require, node). The first matching condition returns the resolved path.
    3. Array Targets: If the target is an array, the resolver iterates through the items. It returns the first successful resolution. If an item fails with an error other than NotFound, the resolution fails immediately. This allows for fallback mechanisms where a NotFound error in one array entry allows the resolver to try the next entry.
    4. Null Targets: If the target is null, the resolution returns None (undefined).
  4. How module resolution works in Oxc Resolver

    main

    Oxc Resolver implements Node.js [CommonJS] and [ECMAScript] module resolution algorithms.

    Key Concepts

    • Specifier: The string used to request a module. In require("id"), "id" is the specifier. In import { x } from "specifier", "specifier" is the specifier. This is also sometimes called a "request".
    • Resolution: The process of mapping a specifier to a concrete file path on the file system.
    • Resolution Result: A successful resolution returns a Resolution object containing:
      • path: The absolute path to the resolved file.
      • query: Any query parameters extracted from the specifier.
      • fragment: Any URL fragment extracted from the specifier.
      • package_json: The PackageJson associated with the resolved module (if applicable).
      • module_type: The detected module type (ESM or CJS).

    Resolution Logic Flow

    1. Parsing: The specifier is parsed into a Specifier object.
    2. Alias/Fallback: The resolver checks for configured alias or fallback rules.
    3. Core Modules: If the specifier matches a Node.js built-in (e.g., node:fs), it handles it as a core module.
    4. Path Types:
      • Absolute: Starts with / or a drive prefix.
      • Relative: Starts with ./, ../, or ..
      • Hash: Starts with # (handles package#imports).
      • Bare: A standard package name (e.g., lodash), which triggers node_modules lookup.
    5. File/Directory Loading: The resolver attempts to find the file by checking extensions (e.g., .js, .json) or by looking for a package.json with a main field if the specifier points to a directory.
  5. Understand the Builtin object

    main

    When a resolution results in a Node.js builtin module, the builtin field in ResolveResult is populated with a Builtin object.

    Fields

    • resolved (string): The resolved module name, always prefixed with node: (e.g., node:fs).
    • is_runtime_module (boolean): Indicates if the original request was prefixed with node:. For example, a request for fs returns false, while node:fs returns true.
  6. How package exports and imports are resolved

    main

    Oxc Resolver implements the Node.js resolution algorithm for exports and imports fields in package.json.

    Exports Resolution

    When a specifier matches a key in the exports field, the resolver uses PACKAGE_EXPORTS_RESOLVE to find the target. It supports:

    • Direct matches: Exact keys in the exports object.
    • Subpath patterns: Keys containing * (e.g., ./utils/*) which are matched against the subpath. The resolver uses PATTERN_KEY_COMPARE to ensure the most specific pattern wins.
    • Folder mapping: Supports mapping directories (e.g., ./dist/ to ./dist/*), though it notes this is deprecated in modern Node.js.

    Imports Resolution

    Specifiers starting with # are treated as package imports. The resolver looks up the imports field in the nearest package.json and performs matching similar to the exports logic.

    Key Constraints

    • Invalid Configuration: An error ResolveError::InvalidPackageConfig is thrown if an exports object contains both keys starting with . or # and keys that do not.
    • Invalid Targets: For imports, targets must start with ./ unless they are absolute or relative to the package root in specific ways. Otherwise, ResolveError::InvalidPackageTarget is returned.
  7. Understand TsConfig ownership and file inclusion

    main

    The TsConfig implementation includes logic to determine if a specific file is 'owned' or 'included' by a configuration, which is critical for auto-discovery of the correct config for a given file.

    Inclusion Logic

    A file is considered included if:

    1. It is explicitly listed in the files array (highest priority).
    2. It matches one of the patterns in the include array, provided it is not also matched by an exclude pattern.
    3. If files is not present, it matches the default **/* pattern (scoped to the config directory and restricted to allowed TS/JS extensions).

    Ownership (claims_ownership_of)

    A TsConfig claims ownership of a path if:

    • The file is included in the config via files or include/exclude patterns.
    • The file is part of a resolved project reference.

    Note on Solution-style configs: Configs that use references but have explicit empty files: [] and include: [] arrays are considered 'solution-style' and do not claim ownership of files themselves; they exist only to orchestrate sub-projects.

  8. Configure TypeScript configuration discovery

    main

    The tsconfig option in ResolveOptions allows you to control how TypeScript configuration files are discovered. It uses the TsconfigDiscovery enum.

    • TsconfigDiscovery::Auto: Automatically discover the tsconfig.json.
    • TsconfigDiscovery::Manual(TsconfigOptions): Provide specific configuration via TsconfigOptions.

    TsconfigOptions allows you to specify:

    • config_file: A PathBuf to the configuration file (can be relative to cwd).
    • references: A TsconfigReferences enum to enable or disable support for TypeScript Project References (Auto or Disabled).
    use oxc_resolver::{ResolveOptions, TsconfigDiscovery, TsconfigOptions, TsconfigReferences};
    use std::path::PathBuf;
    
    let options = ResolveOptions::default().tsconfig(Some(TsconfigDiscovery::Manual(
        TsconfigOptions {
            config_file: PathBuf::from("tsconfig.json"),
            references: TsconfigReferences::Auto,
        }
    )));
  9. Resolve compiler options like `paths`, `baseUrl`, and `rootDirs`

    main

    The resolver can resolve TypeScript compiler options against a tsconfig. This includes:

    • compilerOptions.paths: Path aliases.
    • compilerOptions.baseUrl: Base directory for non-relative specifiers.
    • compilerOptions.rootDirs: Used when the specifier starts with ..

    Note on rootDirs: If a specifier starts with ., the resolver attempts to match the candidate path against the root_dirs defined in the tsconfig. It selects the longest matching prefix and then attempts to resolve the file within that root directory.

  10. Manage file extension enforcement

    main

    The enforce_extension option controls whether file extensions are mandatory for resolution, aligning with ESM requirements. It uses the EnforceExtension enum.

    • EnforceExtension::Auto: The default behavior. It automatically switches to Enabled if extensions contains an empty string, otherwise it sets to Disabled.
    • EnforceExtension::Enabled: Resolution will not allow extension-less files. For example, require('./foo.js') will resolve, but require('./foo') will not.
    • EnforceExtension::Disabled: Explicitly disables automatic behavior.
  11. Configure aliasFields and mainFields

    main

    You can customize how the resolver handles specific fields in package.json using the following options:

    • aliasFields: An array of field names to treat as aliases. For example, to support the non-standard browser field for client-side bundling, use ["browser"].
    • mainFields: An array of fields to prioritize when looking for entry points. Common values include ["module", "main"] to prioritize ESM-style modules over standard CommonJS main entries.
    const options = {
      aliasFields: ["browser"],
      mainFields: ["module", "main"]
    };
  12. How `tsconfig` discovery modes work

    main

    The resolver supports different strategies for finding tsconfig files via TsconfigDiscovery:

    1. TsconfigDiscovery::Auto: The resolver automatically traverses parent directories to find the nearest tsconfig.json that 'claims ownership' of the file (via include/exclude/files or project references).
    2. TsconfigDiscovery::Manual(TsconfigOptions): The resolver uses a specific configuration file provided in the options. In this mode, Auto discovery is skipped.
    3. Disabled: No tsconfig resolution is performed.