Skip to content

Commit

Permalink
Merge pull request #46 from vitonsky/26-add-option-to-provide-a-path-…
Browse files Browse the repository at this point in the history
…to-tsconfigjsconfig

feat: Add option to provide a path to tsconfig/jsconfig
vitonsky authored Apr 13, 2024
2 parents 0f948fe + 7cb0326 commit be66a6d
Showing 7 changed files with 159 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -37,7 +37,8 @@
"lang": "en_US",
"skipWords": [
"tsconfig",
"jsconfig"
"jsconfig",
"qux"
],
// Check if word contains numbers
"skipIfMatch": [
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -50,4 +50,12 @@ and this code will be invalid

import foo from './foo';
import barZ from './bar/x/y/z';
```
```

# Options

## configFilePath

Provide path to json file with a compiler config.

When not set, used `tsconfig.json` from root directory if exists or `jsconfig.json` if not.
43 changes: 43 additions & 0 deletions src/rules/alias.test.ts
Original file line number Diff line number Diff line change
@@ -73,3 +73,46 @@ tester.run('paths-alias', rule, {
},
],
});

[
'testFiles/custom-tsconfig-with-foo-and-qux.json',
'./testFiles/custom-tsconfig-with-foo-and-qux.json',
path.resolve('testFiles/custom-tsconfig-with-foo-and-qux.json'),
].forEach((configFilePath) => {
const options = [
{
configFilePath,
},
];

tester.run(`paths-alias rule with configFile option "${configFilePath}"`, rule, {
valid: [
{
name: 'relative import from bar are possible, because used another config',
filename: path.resolve('./src/index.ts'),
options,
code: `import bar from './bar/index';`,
},
{
name: 'import from @foo',
filename: path.resolve('./src/index.ts'),
code: `import foo from '@foo';`,
},
{
name: 'import from @qux',
filename: path.resolve('./src/index.ts'),
code: `import qux from '@qux';`,
},
],
invalid: [
{
name: 'relative import from alias must be fixed',
filename: path.resolve('./src/index.ts'),
options,
code: `import z from './foo/x/y/z';`,
output: `import z from '@foo/x/y/z';`,
errors: ['Update import to @foo/x/y/z'],
},
],
});
});
122 changes: 54 additions & 68 deletions src/rules/alias.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict';

import { parse as parseJsonWithComments } from 'comment-json';
import { Rule } from 'eslint';
import fs from 'fs';
import path from 'path';

import { CompilerOptions } from '../types';
import { getCompilerConfigFromFile } from '../utils/getCompilerConfigFromFile';

function findDirWithFile(filename: string) {
let dir = path.resolve(filename);

@@ -20,68 +20,48 @@ function findDirWithFile(filename: string) {
}

function findAlias(
compilerOptions: CompilerOptions,
baseDir: string,
importPath: string,
filePath: string,
ignoredPaths: string[] = [],
) {
const isTsconfigExists = fs.existsSync(path.join(baseDir, 'tsconfig.json'));
const isJsconfigExists = fs.existsSync(path.join(baseDir, 'jsconfig.json'));

const configFile = isTsconfigExists
? 'tsconfig.json'
: isJsconfigExists
? 'jsconfig.json'
: null;

if (configFile) {
const tsconfig = parseJsonWithComments(
fs.readFileSync(path.join(baseDir, configFile)).toString('utf8'),
);

const paths: Record<string, string[]> =
(tsconfig as any)?.compilerOptions?.paths ?? {};
for (const [alias, aliasPaths] of Object.entries(paths)) {
// TODO: support full featured glob patterns instead of trivial cases like `@utils/*` and `src/utils/*`
const matchedPath = aliasPaths.find((dirPath) => {
// Remove last asterisk
const dirPathBase = path
.join(baseDir, dirPath)
.split('/')
.slice(0, -1)
.join('/');

if (filePath.startsWith(dirPathBase)) return false;
if (
ignoredPaths.some((ignoredPath) =>
ignoredPath.startsWith(dirPathBase),
)
)
return false;

return importPath.startsWith(dirPathBase);
});

if (!matchedPath) continue;

// Split import path
// Remove basedir and slash in start
const slicedImportPath = importPath
.slice(baseDir.length + 1)
.slice(path.dirname(matchedPath).length + 1);

// Remove asterisk from end of alias
const replacedPathSegments = path
.join(path.dirname(alias), slicedImportPath)
.split('/');

// Add index in path
return (
replacedPathSegments.length === 1
? [...replacedPathSegments, 'index']
: replacedPathSegments
).join('/');
}
for (const [alias, aliasPaths] of Object.entries(compilerOptions.paths)) {
// TODO: support full featured glob patterns instead of trivial cases like `@utils/*` and `src/utils/*`
const matchedPath = aliasPaths.find((dirPath) => {
// Remove last asterisk
const dirPathBase = path
.join(baseDir, dirPath)
.split('/')
.slice(0, -1)
.join('/');

if (filePath.startsWith(dirPathBase)) return false;
if (ignoredPaths.some((ignoredPath) => ignoredPath.startsWith(dirPathBase)))
return false;

return importPath.startsWith(dirPathBase);
});

if (!matchedPath) continue;

// Split import path
// Remove basedir and slash in start
const slicedImportPath = importPath
.slice(baseDir.length + 1)
.slice(path.dirname(matchedPath).length + 1);

// Remove asterisk from end of alias
const replacedPathSegments = path
.join(path.dirname(alias), slicedImportPath)
.split('/');

// Add index in path
return (
replacedPathSegments.length === 1
? [...replacedPathSegments, 'index']
: replacedPathSegments
).join('/');
}

return null;
@@ -95,28 +75,34 @@ const rule: Rule.RuleModule = {
},
create(context) {
const baseDir = findDirWithFile('package.json');

if (!baseDir) throw new Error("Can't find base dir");

const [{ ignoredPaths = [], configFilePath = null } = {}] = context.options as [
{ ignoredPaths: string[]; configFilePath?: string },
];

const compilerOptions = getCompilerConfigFromFile(
baseDir,
configFilePath ?? undefined,
);
if (!compilerOptions) throw new Error('Compiler options did not found');

return {
ImportDeclaration(node) {
const [{ ignoredPaths = [] } = {}] = context.options as [
{ ignoredPaths: string[] },
];

const source = node.source.value;
if (typeof source === 'string' && source.startsWith('.')) {
const importPath = node.source.value;
if (typeof importPath === 'string' && importPath.startsWith('.')) {
const filename = context.getFilename();

const resolvedIgnoredPaths = ignoredPaths.map((ignoredPath) =>
path.normalize(path.join(path.dirname(filename), ignoredPath)),
);

const absolutePath = path.normalize(
path.join(path.dirname(filename), source),
path.join(path.dirname(filename), importPath),
);

const replacement = findAlias(
compilerOptions,
baseDir,
absolutePath,
filename,
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type CompilerOptions = {
baseUrl?: string;
paths: Record<string, string[]>;
};
36 changes: 36 additions & 0 deletions src/utils/getCompilerConfigFromFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { parse as parseJsonWithComments } from 'comment-json';
import fs from 'fs';
import path from 'path';

import { CompilerOptions } from '../types';

export function getCompilerConfigFromFile(
baseDir: string,
configFilePath?: string,
): CompilerOptions | null {
if (!configFilePath) {
// Looking for a config file
for (const filename of ['tsconfig.json', 'jsconfig.json']) {
const resolvedPath = path.resolve(path.join(baseDir, filename));
const isFileExists = fs.existsSync(resolvedPath);
if (isFileExists) {
configFilePath = resolvedPath;
break;
}
}

if (!configFilePath) return null;
}

const tsconfig = parseJsonWithComments(
fs.readFileSync(path.resolve(configFilePath)).toString('utf8'),
);

// TODO: validate options
const { baseUrl, paths = {} } = (tsconfig as any)?.compilerOptions ?? {};

return {
baseUrl,
paths,
};
}
11 changes: 11 additions & 0 deletions testFiles/custom-tsconfig-with-foo-and-qux.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
// Test comment, that must not break a parsing of config file
// See more info in https://github.com/vitonsky/eslint-plugin-paths/issues/37#issuecomment-2052542343
"baseUrl": ".",
"paths": {
"@foo/*": ["src/foo/*"],
"@qux/*": ["src/qux/*"]
}
}
}

0 comments on commit be66a6d

Please sign in to comment.