// Copyright 2017-2026 @pezkuwi/dev-ts authors & contributors // SPDX-License-Identifier: Apache-2.0 import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL, URL } from 'node:url'; import { CWD_URL, EXT_JS_REGEX, EXT_JSON_REGEX, EXT_TS_ARRAY, EXT_TS_REGEX } from './common.js'; import { tsAliases } from './tsconfig.js'; interface Resolved { format: 'commonjs' | 'json' | 'module'; shortCircuit?: boolean; url: string; } interface ResolverContext { parentURL?: string; } type Resolver = (specifier: string, context: ResolverContext) => Resolved | undefined; /** * @internal * * From a specified URL, extract the actual full path as well as the * directory that this path reflects (either equivalent to path or the * root of the file being referenced) */ function getParentPath (parentUrl: URL | string): { parentDir: string; parentPath: string; } { const parentPath = fileURLToPath(parentUrl); return { parentDir: fs.existsSync(parentPath) && fs.lstatSync(parentPath).isDirectory() ? parentPath : path.dirname(parentPath), parentPath }; } /** * @internal * * Resolve fully-specified imports with extensions. **/ export function resolveExtTs (specifier: string, parentUrl: URL | string): Resolved | void { // handle .ts extensions directly if (EXT_TS_REGEX.test(specifier)) { return { format: 'module', shortCircuit: true, url: new URL(specifier, parentUrl).href }; } } /** * @internal * * Resolve fully-specified imports with extensions. Here we cater for the TS * mapping of import foo from './bar.js' where only './bar.ts' exists **/ export function resolveExtJs (specifier: string, parentUrl: URL | string): Resolved | void { // handle ts imports where import *.js -> *.ts // (unlike the ts resolution, we only cater for relative paths) if (specifier.startsWith('.') && EXT_JS_REGEX.test(specifier)) { const full = fileURLToPath(new URL(specifier, parentUrl)); // when it doesn't exist, we try and see if a source replacement helps if (!fs.existsSync(full)) { const found = EXT_TS_ARRAY .map((e) => full.replace(EXT_JS_REGEX, e)) .find((f) => fs.existsSync(f) && fs.lstatSync(f).isFile()); if (found) { return { format: 'module', shortCircuit: true, url: pathToFileURL(found).href }; } } } } /** * @internal * * Resolution for Json files. Generally these would be via path aliasing. */ export function resolveExtJson (specifier: string, parentUrl: URL | string): Resolved | void { if (specifier.startsWith('.') && EXT_JSON_REGEX.test(specifier)) { const { parentDir } = getParentPath(parentUrl); const jsonPath = path.join(parentDir, specifier); if (fs.existsSync(jsonPath)) { return { // .json needs to be in 'json' format for the loader, for the // the rest (it should only be TS) we use the 'module' format format: 'json', shortCircuit: true, url: pathToFileURL(jsonPath).href }; } } } /** * @internal * * Resolve relative (extensionless) paths. * * At some point we probably might need to extend this to cater for the * ts (recommended) approach for using .js extensions inside the sources. * However, since we don't use this in the pezkuwi-js code, can kick this * down the line **/ export function resolveExtBare (specifier: string, parentUrl: URL | string): Resolved | void { if (specifier.startsWith('.')) { const { parentDir, parentPath } = getParentPath(parentUrl); const found = specifier === '.' ? ( // handle . imports for