Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@nodejs-loaders/alias

Package Overview
Dependencies
Maintainers
2
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nodejs-loaders/alias - npm Package Compare versions

Comparing version
2.0.1
to
2.1.0
+20
get-aliases-from-tsconfig.d.mts
/**
* @param {FileURL} parentURL Relative to where.
* @param {string} [filename] Filename or fully resolved location of the tsconfig.
*/
export function getAliases(parentURL: FileURL, filename?: string): AliasMap;
/**
* @param {FileURL} resolvedLocus The resolved location of the tsconfig file.
*/
export function readTSConfigFile(resolvedLocus: FileURL): AliasMap;
export namespace meta {
let filename: string;
}
export type TSConfig = import("type-fest").TsConfigJson;
export type AbsoluteFilePath = import("../types.d.ts").AbsoluteFilePath;
export type FileURL = import("../types.d.ts").FileURL;
/**
* A map of resolved aliases.
*/
export type AliasMap = Map<string, string>;
//# sourceMappingURL=get-aliases-from-tsconfig.d.mts.map
{"version":3,"file":"get-aliases-from-tsconfig.d.mts","sourceRoot":"","sources":["get-aliases-from-tsconfig.mjs"],"names":[],"mappings":"AAgCA;;;GAGG;AACH,sCAHW,OAAO,aACP,MAAM,YA+BhB;AAGD;;GAEG;AACH,gDAFW,OAAO,YAiBjB;;;;uBA7EY,OAAO,WAAW,EAAE,YAAY;+BAChC,OAAO,eAAe,EAAE,gBAAgB;sBACxC,OAAO,eAAe,EAAE,OAAO;;;;uBAa/B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC"}
import { readFileSync } from 'node:fs';
import { findPackageJSON } from 'node:module';
import path from 'node:path';
import { emitWarning, env } from 'node:process';
import { pathToFileURL, URL } from 'node:url';
import JSON5 from 'json5';
/**
* @typedef {import('type-fest').TsConfigJson} TSConfig
* @typedef {import('../types.d.ts').AbsoluteFilePath} AbsoluteFilePath
* @typedef {import('../types.d.ts').FileURL} FileURL
*/
export const meta = {
filename: env.TS_NODE_PROJECT
// oxlint-disable-next-line eslint/no-nested-ternary
? env.TS_NODE_PROJECT?.startsWith('file:')
? env.TS_NODE_PROJECT
: pathToFileURL(path.resolve(env.TS_NODE_PROJECT)).href
: 'tsconfig.json',
};
/**
* @typedef {Map<string, string>} AliasMap A map of resolved aliases.
*/
/**
* @type {Map<FileURL, AliasMap>}
*/
const aliasesMap = new Map();
/**
* @param {FileURL} parentURL Relative to where.
* @param {string} [filename] Filename or fully resolved location of the tsconfig.
*/
export function getAliases(
parentURL,
filename = meta.filename,
) {
if (!parentURL) return; // "resolving" the entry-point (it's already resolved)
const tsConfigLocus = /** @type {FileURL} */ (
filename.startsWith('file:')
? filename
: pathToFileURL(findPackageJSON('./', parentURL)?.replace(
PJSON_FNAME,
path.basename(filename),
))
);
if (aliasesMap.has(tsConfigLocus)) return aliasesMap.get(tsConfigLocus);
const aliases = readTSConfigFile(tsConfigLocus);
if (aliases == null) {
emitWarning([
`Alias loader was registered but no "paths" were found in "${filename}" for "${parentURL}".`,
'This loader will behave as a noop (but you should probably remove it if you aren’t using it).',
].join(''));
}
aliasesMap.set(tsConfigLocus, aliases);
return aliases;
}
const PJSON_FNAME = 'package.json';
/**
* @param {FileURL} resolvedLocus The resolved location of the tsconfig file.
*/
export function readTSConfigFile(resolvedLocus) {
const fileURL = new URL(resolvedLocus); // URL for cross-compatibility with Windows
let contents;
try {
contents = readFileSync(fileURL, 'utf8');
} catch (err) {
if (err.code !== 'ENOENT' && err.code !== 'MODULE_NOT_FOUND') throw err;
}
if (!contents) return;
const { compilerOptions } = /** @type {TSConfig} */ (JSON5.parse(contents));
return buildAliasMaps(compilerOptions, resolvedLocus);
}
/**
* @param {TSConfig['compilerOptions']|undefined} compilerOptions The value of "compilerOptions" if it exists.
* @param {FileURL} tsConfigLocus The location of the controlling tsconfig.
*/
// oxlint-disable-next-line eslint/default-param-last
function buildAliasMaps({ baseUrl: base = './', paths } = {}, tsConfigLocus) {
if (!paths) return;
// URL() drops/overwrites the final segment of the 2nd arg when it does not end in '/' 🤪
const basePath = base.at(-1) === '/' ? base : `${base}/`;
const baseURL = new URL(basePath, tsConfigLocus);
const aliases = /** @type {AliasMap} */ (new Map());
for (const rawKey of Object.keys(paths)) {
const alias = paths[rawKey][0];
const isPrefix = rawKey.endsWith('*');
const key = isPrefix ? rawKey.slice(0, -1) /* strip '*' */ : rawKey;
const baseDest = isPrefix ? alias.slice(0, -1) /* strip '*' */ : alias;
const dest = (baseDest[0] === '/' || URL.canParse(baseDest))
? baseDest
: new URL(baseDest, baseURL).href;
aliases.set(key, dest);
}
return aliases;
}
+14
-3
export function resolveAliases(specifier: string, context: import("module").ResolveHookContext, nextResolve: (specifier: string, context?: Partial<import("module").ResolveHookContext>) => import("module").ResolveFnOutput | Promise<import("module").ResolveFnOutput>): import("module").ResolveFnOutput | Promise<import("module").ResolveFnOutput>;
export function readConfigFile(filename: any): Promise<void | AliasMap>;
export { resolveAlias as resolve };
export type AliasMap = Map<string, string>;
export type TsConfigJson = import("type-fest").TsConfigJson;
export type FileURL = import("../types.d.ts").FileURL;
export type AliasInitConfig = {
/**
* The name or fully resolved location of the tsconfig.
*/
location: string | FileURL;
};
export type ResolveHook = import("node:module").ResolveHook;
export type ResolveParams = Parameters<ResolveHook>;
export type ResolveSpecifier = ResolveParams[0];
export type ResolveCtx = ResolveParams[1];
declare function initialiseAlias(data: any): void | Promise<void>;
declare function resolveAlias(specifier: string, context: import("module").ResolveHookContext, nextResolve: (specifier: string, context?: Partial<import("module").ResolveHookContext>) => import("module").ResolveFnOutput | Promise<import("module").ResolveFnOutput>): import("module").ResolveFnOutput | Promise<import("module").ResolveFnOutput>;
export { initialiseAlias as initialize, resolveAlias as resolve };
//# sourceMappingURL=alias.loader.d.mts.map
+1
-1

@@ -1,1 +0,1 @@

{"version":3,"file":"alias.loader.d.mts","sourceRoot":"","sources":["alias.loader.mjs"],"names":[],"mappings":"wIAmFs5c,CAAC;AA1Cv5c,wEAcC;;uBAGY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;uIAyBs3c,CAAC"}
{"version":3,"file":"alias.loader.d.mts","sourceRoot":"","sources":["alias.loader.mjs"],"names":[],"mappings":"wIAgFg7c,CAAC;2BA5En6c,OAAO,WAAW,EAAE,YAAY;sBAChC,OAAO,eAAe,EAAE,OAAO;;;;;cAInC,MAAM,GAAG,OAAO;;0BAcb,OAAO,aAAa,EAAE,WAAW;4BACjC,UAAU,CAAC,WAAW,CAAC;+BACvB,aAAa,CAAC,CAAC,CAAC;yBAChB,aAAa,CAAC,CAAC,CAAC;;uIAsDm5c,CAAC"}

@@ -1,22 +0,45 @@

import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { pathToFileURL, URL } from 'node:url';
// oxlint-disable eslint/max-depth
import JSON5 from 'json5';
import { getAliases, meta } from './get-aliases-from-tsconfig.mjs';
const projectRoot = pathToFileURL(`${process.cwd()}/`);
/** @typedef {import('type-fest').TsConfigJson} TsConfigJson */
/** @typedef {import('../types.d.ts').FileURL} FileURL */
const aliases = await readConfigFile('tsconfig.json');
/**
* @typedef {object} AliasInitConfig
* @prop {string | FileURL} AliasInitConfig.location The name or fully resolved location of the tsconfig.
*/
/**
* @type {import('node:module').InitializeHook}
* @param {AliasInitConfig} [config] Configuration object to customise Alias loader.
*/
function initialiseAlias(config) {
if (config == null) return;
if (!aliases)
console.warn(
'Alias loader was registered but no "paths" were found in tsconfig.json',
'This loader will behave as a noop (but you should probably remove it if you aren’t using it).',
);
if (config.location) meta.filename = config.location;
}
export { initialiseAlias as initialize };
/**
* @typedef {import('node:module').ResolveHook} ResolveHook
* @typedef {Parameters<ResolveHook>} ResolveParams
* @typedef {ResolveParams[0]} ResolveSpecifier
* @typedef {ResolveParams[1]} ResolveCtx
*/
/**
* @type {import('node:module').ResolveHook}
* @param {ResolveSpecifier} specifier The unresolved module specifier.
* @param {ResolveCtx & { parentURL?: FileURL }} ctx The ResolveHookContext.
*/
function resolveAlias(specifier, ctx, next) {
return (aliases ? resolveAliases : next)(specifier, ctx, next);
const aliases = getAliases(ctx.parentURL);
if (!aliases) return next(specifier, ctx);
return resolveAliases(specifier, {
...ctx,
// @ts-expect-error not sure why it isn't picking up the type union
aliases,
}, next);
}

@@ -27,58 +50,32 @@ export { resolveAlias as resolve };

* @type {import('node:module').ResolveHook}
* @param {ResolveSpecifier} specifier The unresolved module specifier.
* @param {ResolveCtx & { aliases: import('./get-aliases-from-tsconfig.mjs').AliasMap }} ctx The ResolveHookContext.
*/
export function resolveAliases(specifier, ctx, next) {
// biome-ignore format: https://github.com/biomejs/biome/issues/4799
for (const [key, dest] of /** @type {AliasMap} */ (aliases)) {
export function resolveAliases(specifier, { aliases }, next) {
for (const [key, dest] of aliases) {
if (specifier === key) {
return next(dest, ctx);
return next(dest);
}
if (specifier.startsWith(key)) {
return next(specifier.replace(key, dest), ctx);
}
}
let resolved;
// Need try/catch for the sync path (module.registerHooks)
try { resolved = next(specifier.replace(key, dest)) }
catch (err) { if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err }
return next(specifier, ctx);
}
// Need the promise path for the async path (module.register)
if ('catch' in resolved && typeof resolved?.catch === 'function') {
return resolved.catch((err) => {
if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err;
export function readConfigFile(filename) {
const filepath = path.join(projectRoot.pathname, filename);
return next(specifier);
});
}
return (
readFile(filepath)
.then((contents) => contents.toString())
.then((contents) => JSON5.parse(contents))
// Get the `compilerOptions.paths` object from the parsed JSON
.then((contents) => contents?.compilerOptions?.paths)
.then(buildAliasMaps)
.catch((err) => {
if (err.code !== 'ENOENT') throw err;
})
);
}
if (resolved) return resolved;
/**
* @typedef {Map<string, string>} AliasMap
*/
function buildAliasMaps(config) {
if (!config) return;
// biome-ignore format: https://github.com/biomejs/biome/issues/4799
const aliases = /** @type {AliasMap} */ (new Map());
for (const rawKey of Object.keys(config)) {
const alias = config[rawKey][0];
const isPrefix = rawKey.endsWith('*');
const key = isPrefix ? rawKey.slice(0, -1) /* strip '*' */ : rawKey;
const baseDest = isPrefix ? alias.slice(0, -1) /* strip '*' */ : alias;
const dest =
baseDest[0] === '/' || URL.canParse(baseDest)
? baseDest
: new URL(baseDest, projectRoot).href;
aliases.set(key, dest);
return next(specifier);
}
}
return aliases;
return next(specifier);
}
{
"version": "2.0.1",
"version": "2.1.0",
"name": "@nodejs-loaders/alias",

@@ -4,0 +4,0 @@ "description": "Extend node to support TypeScript 'paths' via customization hooks.",

@@ -5,5 +5,18 @@ # Nodejs Loaders: Alias

[![npm version](https://img.shields.io/npm/v/@nodejs-loaders/alias.svg)](https://www.npmjs.com/package/nodejs-loaders/alias)
[![npm version](https://img.shields.io/npm/v/@nodejs-loaders/alias.svg)](https://www.npmjs.com/package/@nodejs-loaders/alias)
![unpacked size](https://img.shields.io/npm/unpacked-size/@nodejs-loaders/alias)
[![compatible node version(s)](https://img.shields.io/node/v/@nodejs-loaders/alias.svg)](https://nodejs.org/download)
## Usage
```console
$ npm i -D @nodejs-loaders/alias
```
```console
$ node --import @nodejs-loaders/alias main.js
```
See `README.md` in the repository's root for more details.
**Environments**: dev, test

@@ -13,3 +26,3 @@

This loader facilitates TypeScript's [`paths`](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths), handling the (important) half of work TypeScript ignores. It looks for a `tsconfig.json` in the project root (the current working directory) and builds aliases from `compilerOptions.paths` if it exists. If your tsconfig lives in a different location, create a symlink to it from your project root.
This loader facilitates TypeScript's [`paths`](https://www.typescriptlang.org/docs/handbook/modules/reference.html#paths), handling the (important) half of work TypeScript ignores. It looks for a `tsconfig.json` in the project root (the current working directory) and builds aliases from `compilerOptions.paths` if it exists. If your tsconfig lives in a different location, see [Configuration](#configuration) below.

@@ -19,2 +32,18 @@ > [!CAUTION]

## `compilerOptions.baseUrl`
In order for Alias loader to leverage `baseUrl`, there must be at least 1 path in `compilerOptions.paths`. If, for example, you wish to only facilitate absolute specifiers (relative to some base folder, like `./src`, such as is common in Next.js projects), include the following "dummy" `"paths"`:
```json5
{
"compilerOptions": {
"baseUrl": "./src",
"paths": { "*": ["./*"] }, // ⚠️ Effectively prepends ./src
},
}
```
> [!WARN]
> If an aliased specifier successfully resolves to a "local" module, you will not be able to reach one in `node_modules`. This behaviour is consistent with Node.js and tsc, but it can still be a gotcha.
## A simple prefix

@@ -33,1 +62,10 @@

This is a static specifier similar to a bare module specifier: `foo` → `${project_root}/src/app/foo.mts`. This may be useful when you have a commonly referenced file like config (which may conditionally not even live on the same filesystem): `import CONF from 'conf';` → `${project_root}/config.json`.
## Configuration
The are 2 ways to configure the tsconfig alias loader uses:
* Environment variable: `TS_NODE_PROJECT`
* `node:module.register`'s options.data argument: `register(…, …, { data: import.meta.resolve(…) })`.
For both options, the value can be either a simple filename like `'tsconfig.whatever.json'` or a fully resolved location `'file:///path/to/someplace/tsconfig.whatever.json'` (or its absolute file path).