import-sort-style-custom
Version:
Sort your imports using import-sort with aliases and custom configurations
319 lines (272 loc) • 8.62 kB
text/typescript
import { dirname } from 'path';
import escapeStringRegexp from 'escape-string-regexp';
import { IImport } from 'import-sort-parser';
import { IStyle } from 'import-sort-style';
import {
CacheStrategy,
CacheStrategyType,
TsConfigResult,
tsconfigResolver,
} from 'tsconfig-resolver';
const defaultSettings: CustomSettings = {
ignoreTsConfig: false,
spaceAfterAliases: true,
tsconfigName: 'tsconfig.json',
tsconfigFilePath: undefined,
cacheStrategy: CacheStrategy.Directory,
wildcardAtStart: false,
ignoredAliases: [],
extraAliases: [],
bottomAliases: [],
};
/**
* The aliased module configuration object.
*/
export interface CustomSettings {
/**
* When `true` will not search for any tsconfig.json. This might provide a slight performance boost.
*
* This options takes precedence over the other tsconfig options.
*/
ignoreTsConfig: boolean;
/**
* The name to use when searching for a TsConfig.
*
* @default 'tsconfig.json'
*/
tsconfigName: string;
/**
* A direct path to the tsconfig file relative to the `cwd`.
*/
tsconfigFilePath: string | undefined;
/**
* Determines how often to check for a new parent tsconfig file. By default it
* will check every time the directory changes. If you only have one
* tsconfig.json file for the whole project with consistent it makes sense to
* update this to 'never'.
*
* @default 'directory'
*/
cacheStrategy: CacheStrategyType;
/**
* When true will allow patterns which start with a `*` character.
*
* @default false
*/
wildcardAtStart: boolean;
/**
* Extra patterns that should be recognised as internal aliases.
*
* The pattern is the same as `tsconfig` files support supporting `*` as the
* wildcard character.
*/
extraAliases: string[];
/**
* When true this will insert a space after the alias section causing the
* relative imports to appear as a separate block.
*
* @default true
*/
spaceAfterAliases: boolean;
/**
* Ignore all paths that match this pattern. This takes preference over any
* matching aliases. If a module path matches the alias but doesn't
*
* The pattern is the same as `tsconfig` files support supporting `*` as the
* wildcard character.
*/
ignoredAliases: string[];
/**
* Files matching this pattern will be moved to a special group at the end of
* the imports.
*
* The pattern is the same as `tsconfig` files support supporting `*` as the
* wildcard character.
*/
bottomAliases: string[];
}
const getSettings = (rawSettings: object = {}): CustomSettings => {
return { ...defaultSettings, ...rawSettings };
};
/**
* Converts a tsconfig pattern to a RegExp.
*/
const patternToRegExp = (pattern: string) =>
new RegExp(escapeStringRegexp(pattern).replace(/\\\*/g, '.+'));
/**
* Creates an isAliasedModule method which is used to check when the import is
* an alias.
*/
const isAliasedModuleCreator = (
{ extraAliases, wildcardAtStart, ignoredAliases }: CustomSettings,
config: TsConfigResult['config'],
) => {
const cache = new Map<IImport, boolean>();
const regExps: RegExp[] = [];
const ignoredRegExps: RegExp[] = [];
const paths = config?.compilerOptions?.paths ?? {};
// Populate the array of aliases
for (const alias of [...Object.keys(paths), ...extraAliases]) {
if (!wildcardAtStart && alias.startsWith('*')) {
continue;
}
regExps.push(patternToRegExp(alias));
}
// Populate the array of ignored patterns.
for (const alias of ignoredAliases) {
ignoredRegExps.push(patternToRegExp(alias));
}
const calculateReturn = (imported: IImport) => {
// No alias patterns to check. Return false for a negative match.
if (!regExps.length) {
return false;
}
// Check to see if the pattern matches the ignored patterns. If this is the
// case return a negative match.
for (const regExp of ignoredRegExps) {
if (regExp.test(imported.moduleName)) {
return false;
}
}
// Check if any alias patterns match the module name then return a positive
// match.
for (const regExp of regExps) {
if (regExp.test(imported.moduleName)) {
return true;
}
}
// Nothing matched so return a negative match.
return false;
};
return (imported: IImport) => {
const cachedValue = cache.get(imported);
if (cachedValue !== undefined) {
return cachedValue;
}
const value = calculateReturn(imported);
cache.set(imported, value);
return value;
};
};
/**
* Creates a function that checks if the imported module should be placed at the
* bottom.
*/
const isBottomModuleCreator = (bottomModules: string[]) => {
const cache = new Map<IImport, boolean>();
const regExps: RegExp[] = [];
// Populate the array of pattern matchers for bottom modules
for (const alias of bottomModules) {
regExps.push(patternToRegExp(alias));
}
const calculateReturn = (imported: IImport) => {
// No patterns to check. Return false to show this is import should not be
// placed at the bottom.
if (!regExps.length) {
return false;
}
// Check if any alias patterns match the module name then return a positive
// match.
for (const regExp of regExps) {
if (regExp.test(imported.moduleName)) {
return true;
}
}
// Nothing matched so return a negative match.
return false;
};
return (imported: IImport) => {
const cachedValue = cache.get(imported);
if (cachedValue !== undefined) {
return cachedValue;
}
const value = calculateReturn(imported);
cache.set(imported, value);
return value;
};
};
/**
* Sort the modules in the following order.
*
* - Imports with no members are left unsorted at the top of the file. These
* tend to have side effects and their order is important. `import 'tolu';`
* - Node module imports. `import { join } from 'path';`
* - Absolute module imports (but not aliased). `import main from 'main';`
* - Aliased imports taken from the `tsconfig.json`
* - Relative module imports
* - Bottom imports, which are set in the settings object as `bottomAliases`
*/
const sortStyleCustom: IStyle = (styleApi, fileName, rawSettings) => {
// console.log({ fileName, settings });
const settings = getSettings(rawSettings);
const config = settings.ignoreTsConfig
? undefined
: tsconfigResolver({
cacheStrategy: settings.cacheStrategy,
cwd: fileName ? dirname(fileName) : undefined,
searchName: settings.tsconfigName,
filePath: settings.tsconfigFilePath,
}).config;
const isAliasedModule = isAliasedModuleCreator(settings, config);
const isBottomModule = isBottomModuleCreator(settings.bottomAliases);
const {
alias,
and,
dotSegmentCount,
hasNoMember,
isAbsoluteModule,
isNodeModule,
isRelativeModule,
moduleName,
naturally,
unicode,
not,
} = styleApi;
return [
// No member - `import "foo"; import "./foo"; import "$aliased";`
{ match: and(hasNoMember, not(isBottomModule)) }, // No sorting here since these can have side effects.
{ separator: true },
// Builtin - `import … from "fs";`
{
match: and(isNodeModule, not(isBottomModule)),
sort: moduleName(naturally),
sortNamedMembers: alias(unicode),
},
{ separator: true },
// Absolute - `import … from "foo";`
{
match: and(isAbsoluteModule, not(isBottomModule), not(isAliasedModule)),
sort: moduleName(naturally),
sortNamedMembers: alias(unicode),
},
{ separator: true },
// Aliased - `import … from "$aliased";`
{
match: and(not(isRelativeModule), not(isBottomModule), isAliasedModule),
sort: moduleName(naturally),
sortNamedMembers: alias(unicode),
},
...(settings.spaceAfterAliases ? [{ separator: true }] : []),
// Relative - `import … from "./foo"; import … from "../foo";`
{
match: and(isRelativeModule, not(isBottomModule)),
sort: [dotSegmentCount, moduleName(naturally)],
sortNamedMembers: alias(unicode),
},
{ separator: true },
// Bottom
{
match: and(isBottomModule, not(isRelativeModule)),
sort: [dotSegmentCount, moduleName(naturally)],
sortNamedMembers: alias(unicode),
},
// Relative bottom
{
match: isBottomModule,
sort: [dotSegmentCount, moduleName(naturally)],
sortNamedMembers: alias(unicode),
},
{ separator: true },
];
};
export default sortStyleCustom;