diff --git a/src/bin.ts b/src/bin.ts index 5e6f208..12b11d2 100755 --- a/src/bin.ts +++ b/src/bin.ts @@ -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[] = []; @@ -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); @@ -31,14 +36,22 @@ 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, @@ -46,4 +59,4 @@ try { String(err.stack).replace(/^/gm, ' '), ); process.exit(1); -} +}); diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..9d270da --- /dev/null +++ b/src/helpers.ts @@ -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; +} diff --git a/src/index.ts b/src/index.ts index 9a30836..4566eba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 = {}; + const rewrittenImports: string[] = []; + const rewrittenDirectives: string[] = []; + + sourceFile.getPathReferenceDirectives().forEach((directive) => { + rewrittenDirectives.push(`/// `); + }); + sourceFile.getTypeReferenceDirectives().forEach((directive) => { + rewrittenDirectives.push(`/// `); + }); + sourceFile.getLibReferenceDirectives().forEach((directive) => { + rewrittenDirectives.push(`/// `); + }); + + /** 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) => { + 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)); @@ -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 = {}; - const rewrittenImports: string[] = []; - const rewrittenDirectives: string[] = []; - - sourceFile.getPathReferenceDirectives().forEach((directive) => { - rewrittenDirectives.push(`/// `); - }); - sourceFile.getTypeReferenceDirectives().forEach((directive) => { - rewrittenDirectives.push(`/// `); - }); - sourceFile.getLibReferenceDirectives().forEach((directive) => { - rewrittenDirectives.push(`/// `); - }); - - /** 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) => { - 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) { @@ -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 {