Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 180 additions & 120 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,199 @@
import { join } from 'path';
import path from 'path';
import { Plugin } from 'rollup';
import {
CompilerOptions,
findConfigFile,
nodeModuleNameResolver,
parseConfigFileTextToJson,
sys,
CompilerOptions,
findConfigFile,
nodeModuleNameResolver,
parseConfigFileTextToJson,
sys,
} from 'typescript';

export const typescriptPaths = ({
absolute = true,
nonRelative = false,
preserveExtensions = false,
tsConfigPath = findConfigFile('./', sys.fileExists),
transform,
absolute = true,
nonRelative = false,
preserveExtensions = false,
tsConfigPath = findConfigFile('./', sys.fileExists),
transform,
}: Options = {}): Plugin => {
const { compilerOptions, outDir } = getTsConfig(tsConfigPath);

return {
name: 'resolve-typescript-paths',
resolveId: (importee: string, importer?: string) => {
const enabled = Boolean(
compilerOptions.paths || (compilerOptions.baseUrl && nonRelative),
);

if (
typeof importer === 'undefined' ||
importee.startsWith('\0') ||
!enabled
) {
return null;
}

const hasMatchingPath =
!!compilerOptions.paths &&
Object.keys(compilerOptions.paths).some((path) =>
new RegExp('^' + path.replace('*', '.+') + '$').test(importee),
);

if (!hasMatchingPath && !nonRelative) {
return null;
}

if (importee.startsWith('.')) {
return null; // never resolve relative modules, only non-relative
}

const { resolvedModule } = nodeModuleNameResolver(
importee,
importer,
compilerOptions,
sys,
);

if (!resolvedModule) {
return null;
}

const { resolvedFileName } = resolvedModule;

if (!resolvedFileName || resolvedFileName.endsWith('.d.ts')) {
return null;
}

const targetFileName = join(
outDir,
preserveExtensions
? resolvedFileName
: resolvedFileName.replace(/\.tsx?$/i, '.js'),
);

const resolved = absolute
? sys.resolvePath(targetFileName)
: targetFileName;

return transform ? transform(resolved) : resolved;
},
};
const { compilerOptions } = getTsConfig(tsConfigPath);
const outDir = compilerOptions.outDir ?? '.';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was pointed out here already: #14 (comment)

Copy link
Author

@bennobuilder bennobuilder Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right in my case, outDir was always the default (".") since I've defined it in my compilerOptions -> never was read in.

Since the resolvedFileName is already an absolute path, joining (with path.join()) it with the default outDir (".") resulted in the correct absolute path, as resolvedFileName seems already correct?

Example resolvedFileName: "C:/path/to/my/monorepo/packages/dtif-to-svg/src/d3.ts"

However, now that it reads in the outDir correctly due to your suggested getParsedCommandLineOfConfigFile() method that works like a charm, the outDir is something like "C:/path/to/my/monorepo/packages/dtif-to-svg/dist" which I defined in the compilerOptions of my tsconfig.json. However, now path.join() will join these both absolute paths that result in: "C:/path/to/my/monorepo/packages\dtif-to-svg\dist\C:/path/to/my/monorepo/packages\dtif-to-svg\src\d3.ts". And thats not correct.

-> For me it works best without any defined outDir as resolvedFileName already points to the correct location
-> Do we actually need to define the outDir and when might it be required, because for me it isn't?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the same issue comments I think further down there's a solution with using path.relative instead of path.join.

Copy link
Author

@bennobuilder bennobuilder Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I tested that but the resulting relative path was something like ../src/path/to/file and since the script was executed in the root of the package it navigated out of the package and tried to find the src/path/to/file.ts there..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but anyway if the MR doesn't fit the project's philosophy that's ok too :) I've implemented your very useful plugin in my scripts and updated it to my project's needs (represented in this MR).. but I've not tested it in other environments and everyone has different requirements and expectations, which is of course hard to cover in a single project.. cheers :)

image


return {
name: 'resolve-typescript-paths',
resolveId: (importee: string, importer?: string) => {
const enabled = Boolean(
compilerOptions.paths || (compilerOptions.baseUrl && nonRelative)
);

if (
typeof importer === 'undefined' ||
importee.startsWith('\0') ||
!enabled
) {
return null;
}

const hasMatchingPath =
!!compilerOptions.paths &&
Object.keys(compilerOptions.paths).some((path) =>
new RegExp('^' + path.replace('*', '.+') + '$').test(importee)
);

if (!hasMatchingPath && !nonRelative) {
return null;
}

if (importee.startsWith('.')) {
return null; // never resolve relative modules, only non-relative
}

const { resolvedModule } = nodeModuleNameResolver(
importee,
importer,
compilerOptions,
sys
);

if (!resolvedModule) {
return null;
}

const { resolvedFileName } = resolvedModule;

if (!resolvedFileName || resolvedFileName.endsWith('.d.ts')) {
return null;
}

const targetFileName = path.join(
outDir,
preserveExtensions
? resolvedFileName
: resolvedFileName.replace(/\.tsx?$/i, '.js')
);

const resolved = absolute
? sys.resolvePath(targetFileName)
: targetFileName;

return transform ? transform(resolved) : resolved;
},
};
};

const getTsConfig = (configPath?: string): TsConfig => {
const defaults: TsConfig = { compilerOptions: {}, outDir: '.' };

if (!configPath) {
return defaults;
}

const configJson = sys.readFile(configPath);

if (!configJson) {
return defaults;
}

const { config } = parseConfigFileTextToJson(configPath, configJson);
const defaults: TsConfig = { compilerOptions: { outDir: '.' } };
if (typeof configPath !== 'string') {
return defaults;
}

// Read in tsconfig.json
const configJson = sys.readFile(configPath);
if (configJson == null) {
return defaults;
}

const { config: rootConfig } = parseConfigFileTextToJson(
configPath,
configJson
);
const rootConfigWithDefaults = {
...rootConfig,
...defaults,
compilerOptions: {
...defaults.compilerOptions,
...(rootConfig.compilerOptions ?? {}),
},
};
const resolvedConfig = handleTsConfigExtends(
rootConfigWithDefaults,
configPath
);

return resolvedConfig;
};

return { ...defaults, ...config };
const handleTsConfigExtends = (
config: TsConfig,
rootConfigPath: string
): TsConfig => {
if (!('extends' in config) || typeof config.extends !== 'string') {
return config;
}

let extendedConfigPath;
try {
// Try to resolve as a module (npm)
extendedConfigPath = require.resolve(config.extends);
} catch (e) {
// Try to resolve as a file relative to the current config
extendedConfigPath = path.join(
path.dirname(rootConfigPath),
config.extends
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extends can actually be an array since TS 5.0. I'm pretty sure there's probably a better method from the TypeScript API to retrieve the tsconfig.

I wrote this a long time ago when I was still completely unfamiliar with the API 😅

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://stackoverflow.com/a/70013087/2897426 maybe this is the correct solution

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that actually worked quite good :)

);
}

// Read in extended tsconfig.json
const extendedConfig = getTsConfig(extendedConfigPath);

// Merge base config and current config.
// This does not handle array concatenation or nested objects,
// besides 'compilerOptions' paths as the other options are not relevant
config = {
...extendedConfig,
...config,
compilerOptions: {
...extendedConfig.compilerOptions,
...config.compilerOptions,
paths: {
...(extendedConfig.compilerOptions.paths ?? {}),
...(config.compilerOptions.paths ?? {}),
},
},
};

// Remove the "extends" field
delete config.extends;

return config;
};

export interface Options {
/**
* Whether to resolve to absolute paths; defaults to `true`.
*/
absolute?: boolean;

/**
* Whether to resolve non-relative paths based on tsconfig's `baseUrl`, even
* if none of the `paths` are matched; defaults to `false`.
*
* @see https://www.typescriptlang.org/docs/handbook/module-resolution.html#relative-vs-non-relative-module-imports
* @see https://www.typescriptlang.org/docs/handbook/module-resolution.html#base-url
*/
nonRelative?: boolean;

/**
* Whether to preserve `.ts` and `.tsx` file extensions instead of having them
* changed to `.js`; defaults to `false`.
*/
preserveExtensions?: boolean;

/**
* Custom path to your `tsconfig.json`. Use this if the plugin can't seem to
* find the correct one by itself.
*/
tsConfigPath?: string;

/**
* If the plugin successfully resolves a path, this function allows you to
* hook into the process and transform that path before it is returned.
*/
transform?(path: string): string;
/**
* Whether to resolve to absolute paths; defaults to `true`.
*/
absolute?: boolean;

/**
* Whether to resolve non-relative paths based on tsconfig's `baseUrl`, even
* if none of the `paths` are matched; defaults to `false`.
*
* @see https://www.typescriptlang.org/docs/handbook/module-resolution.html#relative-vs-non-relative-module-imports
* @see https://www.typescriptlang.org/docs/handbook/module-resolution.html#base-url
*/
nonRelative?: boolean;

/**
* Whether to preserve `.ts` and `.tsx` file extensions instead of having them
* changed to `.js`; defaults to `false`.
*/
preserveExtensions?: boolean;

/**
* Custom path to your `tsconfig.json`. Use this if the plugin can't seem to
* find the correct one by itself.
*/
tsConfigPath?: string;

/**
* If the plugin successfully resolves a path, this function allows you to
* hook into the process and transform that path before it is returned.
*/
transform?(path: string): string;
}

interface TsConfig {
compilerOptions: CompilerOptions;
outDir: string;
compilerOptions: CompilerOptions;
extends?: string;
}

/**
Expand Down