Files
pezkuwi-dev/packages/dev-ts/src/resolver.ts
T
Kurdistan Tech Ministry cb6801a3cc Fix lint errors: Update copyright years to 2026 and ignore build outputs
- Updated all copyright headers from 2025 to 2026 (system date shows 2026)
- Added build output files to eslint ignore list in packages/dev/config/eslint.js
- Added build output patterns to .gitignore
- Ignored: packages/*/*.{d.ts,js,mjs,cjs}, packages/*/cjs/**, packages/*/env/**,
  packages/*/rootJs/**, packages/*/rootStatic/**
- Successfully resolved 521 lint errors by properly ignoring generated files
- Build outputs should not be linted (source files are linted instead)
- Lint and build now pass successfully
2026-01-17 21:02:45 +03:00

225 lines
6.5 KiB
TypeScript

// 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 <dir>/index.ts
EXT_TS_ARRAY
.map((e) => path.join(parentDir, `index${e}`))
.find((f) => fs.existsSync(f)) ||
// handle the case where parentUrl needs an extension (generally via alias)
EXT_TS_ARRAY
.map((e) => `${parentPath}${e}`)
.find((f) => fs.existsSync(f))
)
: (
// tests to see if this is a file (without extension)
EXT_TS_ARRAY
.map((e) => path.join(parentDir, `${specifier}${e}`))
.find((f) => fs.existsSync(f)) ||
// test to see if this is a directory
EXT_TS_ARRAY
.map((e) => path.join(parentDir, `${specifier}/index${e}`))
.find((f) => fs.existsSync(f))
);
if (found) {
return {
format: 'module',
shortCircuit: true,
url: pathToFileURL(found).href
};
}
}
}
/**
* @internal
*
* Resolve anything that is not an alias
**/
export function resolveNonAlias (specifier: string, parentUrl: URL | string): Resolved | void {
return (
resolveExtTs(specifier, parentUrl) ||
resolveExtJs(specifier, parentUrl) ||
resolveExtJson(specifier, parentUrl) ||
resolveExtBare(specifier, parentUrl)
);
}
/**
* @internal
*
* Resolve TS alias mappings as defined in the tsconfig.json file
**/
export function resolveAlias (specifier: string, _parentUrl: URL | string, aliases = tsAliases): Resolved | void {
const parts = specifier.split(/[\\/]/);
const found = aliases
// return a [filter, [...partIndex]] mapping
.map((alias) => ({
alias,
indexes: parts
.map((_, i) => i)
.filter((start) =>
(
alias.isWildcard
// parts should have more entries than the wildcard
? parts.length > alias.filter.length
// or the same amount in case of a non-wildcard match
: parts.length === alias.filter.length
) &&
// match all parts of the alias
alias.filter.every((f, i) =>
parts[start + i] &&
parts[start + i] === f
)
)
}))
// we only return the first
.find(({ indexes }) => indexes.length);
if (found) {
// do the actual un-aliased resolution
return resolveNonAlias(
`./${found.alias.path.replace('*', path.join(...parts.slice(found.alias.filter.length)))}`,
found.alias.url
);
}
}
/**
* Resolves a path using our logic.
*
* 1. First we attempt to directly resolve if .ts/.tsx extension is found
* 2. Then we do relative resolves (this is for extension-less .ts files)
* 3. The we try to do resolution via TS aliases
*
* ... finally, try the next loader in the chain
*/
export function resolve (specifier: string, context: ResolverContext, nextResolve: Resolver) {
const parentUrl = context.parentURL || CWD_URL;
return (
resolveNonAlias(specifier, parentUrl) ||
resolveAlias(specifier, parentUrl) ||
nextResolve(specifier, context)
);
}