Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 17 additions & 4 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import chalk = require('chalk');
import program = require('commander');
import { resolve } from 'path';
import { tsImportTypes } from '.';
import { readStdin } from './helpers';
import { tsImportTypes, tsImportTypesStdio } from '.';

const sourcePatterns: string[] = [];

Expand All @@ -16,10 +17,14 @@ program
.option('-d, --dry-run', 'write output to stdout instead of overwriting files')
.option('-p, --project [path]', 'path to tsconfig.json')
.option('-O, --no-organise-imports', "disable use of VS Code's organise imports refactoring")
.option('--stdio', 'read from stdin and write to stdout')
.option('--file-path [path]', 'file location to use for --stdio source code')
.parse(process.argv);

const dryRun = program.dryRun === true;
const organiseImports = program.organiseImports !== false;
const stdio = program.stdio === true;
const filePath = program.filePath;
const project = program.project || './tsconfig.json';
const tsConfigFilePath = resolve(process.cwd(), project);

Expand All @@ -31,19 +36,27 @@ try {
process.exit(1);
}

try {
async function main() {
if (stdio) {
const source = await readStdin();
const result = tsImportTypesStdio({ filePath, source, tsConfigFilePath });
process.stdout.write(result);
return;
}
tsImportTypes({
dryRun,
organiseImports,
sourcePatterns,
tsConfigFilePath,
});
} catch (err) {
}

main().catch(err => {
console.error(
chalk.red('! %s\n\n! Please raise an issue at %s\n\n%s'),
err.message,
chalk.underline('https://github.com/JamieMason/ts-import-types-cli/issues'),
String(err.stack).replace(/^/gm, ' '),
);
process.exit(1);
}
});
8 changes: 8 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Reads input from stdin */
export async function readStdin() {
let data = ''
for await (const chunk of process.stdin) {
data += chunk;
}
return data;
}
191 changes: 107 additions & 84 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,112 @@ const getSourceFiles = (sourcePatterns: string[], project: Project): SourceFile[
return sourcePatterns.length ? project.getSourceFiles(sourcePatterns) : project.getSourceFiles();
};

const getFakeSourceFile = (source: string, path: string, project: Project): SourceFile => {
return project.createSourceFile(path, source, { overwrite: true });
};

function fixSourceFile(sourceFile: SourceFile, options: { organiseImports: boolean }) {
const { organiseImports} = options;
let hasChanged = false;

const importDeclarations = sourceFile.getImportDeclarations();
const imports: Record<string, ModuleImports> = {};
const rewrittenImports: string[] = [];
const rewrittenDirectives: string[] = [];

sourceFile.getPathReferenceDirectives().forEach((directive) => {
rewrittenDirectives.push(`/// <reference path="${directive.getText()}" />`);
});
sourceFile.getTypeReferenceDirectives().forEach((directive) => {
rewrittenDirectives.push(`/// <reference type="${directive.getText()}" />`);
});
sourceFile.getLibReferenceDirectives().forEach((directive) => {
rewrittenDirectives.push(`/// <reference lib="${directive.getText()}" />`);
});

/** import Default, { named1, named2 as alias } from './file' */
importDeclarations.forEach((importDeclaration: ImportDeclaration) => {
/** Default */
const defaultImport = importDeclaration.getDefaultImport();
/** { named1, named2 as alias } */
const namedImports = importDeclaration.getNamedImports();
/** eg './file' or 'some-dependency' */
const modulePath = importDeclaration.getModuleSpecifierValue();

imports[modulePath] = imports[modulePath] || {
codeImports: [],
defaultImport: '',
typeImports: [],
};

if (defaultImport) {
imports[modulePath].defaultImport = defaultImport.getText();
hasChanged = true;
}

namedImports.forEach((namedImport: ImportSpecifier) => {
/** import { named2 as alias } */
const alias = namedImport.getAliasNode()?.getText();
const definitions = namedImport.getNameNode().getDefinitions();
/** determine whether this import is a type or an implementation */
definitions.forEach((definition: DefinitionInfo<ts.DefinitionInfo>) => {
const definitionName = definition.getName();
const finalName = alias ? `${definitionName} as ${alias}` : definitionName;
const definitionKind = definition.getKind();
if (['type', 'interface'].includes(definitionKind)) {
hasChanged = true;
imports[modulePath].typeImports.push(finalName);
} else {
hasChanged = true;
imports[modulePath].codeImports.push(finalName);
}
});
});

if (hasChanged) {
importDeclaration.remove();
}
});

// write new imports for those we've collected and removed
Object.entries(imports).forEach(
([identifier, { codeImports, defaultImport, typeImports }]: [string, ModuleImports]) => {
if (defaultImport && codeImports.length) {
rewrittenImports.push(`import ${defaultImport}, { ${codeImports.join(', ')} } from '${identifier}'`);
}
if (defaultImport && !codeImports.length) {
rewrittenImports.push(`import ${defaultImport} from '${identifier}'`);
}
if (!defaultImport && codeImports.length) {
rewrittenImports.push(`import { ${codeImports.join(', ')} } from '${identifier}'`);
}
if (typeImports.length) {
rewrittenImports.push(`import type { ${typeImports.join(', ')} } from '${identifier}'`);
}
},
);

sourceFile.insertText(0, rewrittenImports.join(EOL) + EOL + EOL);

if (organiseImports !== false) {
sourceFile.organizeImports();
}

return {
rewrittenImports,
rewrittenDirectives,
}
}

export function tsImportTypesStdio({ source, filePath, tsConfigFilePath }: { source: string, filePath: string, tsConfigFilePath: string }) {
const project = new Project({ tsConfigFilePath });
const sourceFile = getFakeSourceFile(source, filePath, project)

fixSourceFile(sourceFile, { organiseImports: true })

return sourceFile.getFullText()
}

export function tsImportTypes({ dryRun, organiseImports, sourcePatterns, tsConfigFilePath }: Options) {
info('Analysing', relative(process.cwd(), tsConfigFilePath));

Expand All @@ -46,84 +152,7 @@ export function tsImportTypes({ dryRun, organiseImports, sourcePatterns, tsConfi

sourceFiles.forEach((sourceFile: SourceFile, i) => {
try {
let hasChanged = false;

const importDeclarations = sourceFile.getImportDeclarations();
const imports: Record<string, ModuleImports> = {};
const rewrittenImports: string[] = [];
const rewrittenDirectives: string[] = [];

sourceFile.getPathReferenceDirectives().forEach((directive) => {
rewrittenDirectives.push(`/// <reference path="${directive.getText()}" />`);
});
sourceFile.getTypeReferenceDirectives().forEach((directive) => {
rewrittenDirectives.push(`/// <reference type="${directive.getText()}" />`);
});
sourceFile.getLibReferenceDirectives().forEach((directive) => {
rewrittenDirectives.push(`/// <reference lib="${directive.getText()}" />`);
});

/** import Default, { named1, named2 as alias } from './file' */
importDeclarations.forEach((importDeclaration: ImportDeclaration) => {
/** Default */
const defaultImport = importDeclaration.getDefaultImport();
/** { named1, named2 as alias } */
const namedImports = importDeclaration.getNamedImports();
/** eg './file' or 'some-dependency' */
const modulePath = importDeclaration.getModuleSpecifierValue();

imports[modulePath] = imports[modulePath] || {
codeImports: [],
defaultImport: '',
typeImports: [],
};

if (defaultImport) {
imports[modulePath].defaultImport = defaultImport.getText();
hasChanged = true;
}

namedImports.forEach((namedImport: ImportSpecifier) => {
/** import { named2 as alias } */
const alias = namedImport.getAliasNode()?.getText();
const definitions = namedImport.getNameNode().getDefinitions();
/** determine whether this import is a type or an implementation */
definitions.forEach((definition: DefinitionInfo<ts.DefinitionInfo>) => {
const definitionName = definition.getName();
const finalName = alias ? `${definitionName} as ${alias}` : definitionName;
const definitionKind = definition.getKind();
if (['type', 'interface'].includes(definitionKind)) {
hasChanged = true;
imports[modulePath].typeImports.push(finalName);
} else {
hasChanged = true;
imports[modulePath].codeImports.push(finalName);
}
});
});

if (hasChanged) {
importDeclaration.remove();
}
});

// write new imports for those we've collected and removed
Object.entries(imports).forEach(
([identifier, { codeImports, defaultImport, typeImports }]: [string, ModuleImports]) => {
if (defaultImport && codeImports.length) {
rewrittenImports.push(`import ${defaultImport}, { ${codeImports.join(', ')} } from '${identifier}'`);
}
if (defaultImport && !codeImports.length) {
rewrittenImports.push(`import ${defaultImport} from '${identifier}'`);
}
if (!defaultImport && codeImports.length) {
rewrittenImports.push(`import { ${codeImports.join(', ')} } from '${identifier}'`);
}
if (typeImports.length) {
rewrittenImports.push(`import type { ${typeImports.join(', ')} } from '${identifier}'`);
}
},
);
const { rewrittenDirectives, rewrittenImports } = fixSourceFile(sourceFile, { organiseImports })

// nothing to do
if (rewrittenImports.length === 0) {
Expand All @@ -138,12 +167,6 @@ export function tsImportTypes({ dryRun, organiseImports, sourcePatterns, tsConfi
console.log(chalk.yellow('! contains triple-slash directives'));
}

sourceFile.insertText(0, rewrittenImports.join(EOL) + EOL + EOL);

if (organiseImports !== false) {
sourceFile.organizeImports();
}

if (dryRun === true) {
console.log(sourceFile.getText());
} else {
Expand Down