Repository: gregjacobs/js-to-ts-converter Branch: master Commit: 7444ece835eb Files: 87 Total size: 102.8 KB Directory structure: gitextract_7dpmj4fc/ ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── cli.ts │ ├── converter/ │ │ ├── add-class-property-declarations/ │ │ │ ├── add-class-property-declarations.ts │ │ │ ├── correct-js-properties.ts │ │ │ ├── js-class.ts │ │ │ ├── parse-js-classes.ts │ │ │ └── parse-superclass-name-and-path.ts │ │ ├── add-optionals-to-function-params.ts │ │ ├── convert.ts │ │ └── filter-out-node-modules.ts │ ├── create-ts-morph-project.ts │ ├── index.ts │ ├── js-to-ts-converter.ts │ ├── logger/ │ │ ├── index.ts │ │ ├── log-level.ts │ │ └── logger.ts │ └── util/ │ ├── find-import-for-identifier.ts │ ├── is-element-access-with-obj.ts │ ├── is-property-access-with-obj.ts │ ├── is-property-or-elemement-access-with-obj.ts │ ├── is-this-referencing-var.ts │ ├── is-valid-identifier.ts │ ├── parse-destructured-props.ts │ └── set-utils.ts ├── test/ │ ├── convert.spec.ts │ └── fixture/ │ ├── class-with-this-constructor-reference/ │ │ ├── expected/ │ │ │ └── my-class.ts │ │ └── input/ │ │ └── my-class.js │ ├── expression-extends/ │ │ ├── expected/ │ │ │ └── expression-extends.ts │ │ └── input/ │ │ └── expression-extends.js │ ├── function-calls-with-fewer-args-than-params/ │ │ ├── expected/ │ │ │ ├── call-to-exported-function.ts │ │ │ ├── call-to-local-function-with-default-value.ts │ │ │ ├── call-to-local-function.ts │ │ │ ├── call-to-sub-class-method.ts │ │ │ ├── call-to-super-class-method.ts │ │ │ ├── constructor-with-rest-param.ts │ │ │ ├── exported-function.ts │ │ │ ├── sub-class.ts │ │ │ └── super-class.ts │ │ └── input/ │ │ ├── call-to-exported-function.js │ │ ├── call-to-local-function-with-default-value.js │ │ ├── call-to-local-function.js │ │ ├── call-to-sub-class-method.js │ │ ├── call-to-super-class-method.js │ │ ├── constructor-with-rest-param.js │ │ ├── exported-function.js │ │ ├── sub-class.js │ │ └── super-class.js │ ├── function-expressions-and-declarations/ │ │ ├── expected/ │ │ │ └── class-with-function-expressions.ts │ │ └── input/ │ │ └── class-with-function-expressions.js │ ├── include-exclude-patterns/ │ │ ├── expected/ │ │ │ └── included/ │ │ │ └── included-file.ts │ │ └── input/ │ │ ├── included/ │ │ │ ├── excluded/ │ │ │ │ └── excluded-file.js │ │ │ └── included-file.js │ │ └── other-file-that-should-not-be-included.js │ ├── react-class-js/ │ │ ├── expected/ │ │ │ └── react-class.tsx │ │ └── input/ │ │ └── react-class.js │ ├── react-class-jsx/ │ │ ├── expected/ │ │ │ └── react-class.tsx │ │ └── input/ │ │ └── react-class.jsx │ ├── react-jsx-self-closing-element/ │ │ ├── expected/ │ │ │ └── react-self-closing-element.tsx │ │ └── input/ │ │ └── react-self-closing-element.js │ ├── superclass-subclass/ │ │ ├── expected/ │ │ │ ├── another-sub-class.ts │ │ │ ├── default-export-class.ts │ │ │ ├── my-class.ts │ │ │ ├── my-sub-class.ts │ │ │ ├── my-super-class.ts │ │ │ ├── superclass-in-node-modules.ts │ │ │ └── two-classes.ts │ │ └── input/ │ │ ├── another-sub-class.js │ │ ├── default-export-class.js │ │ ├── my-class.js │ │ ├── my-sub-class.js │ │ ├── my-super-class.js │ │ ├── package.json │ │ ├── superclass-in-node-modules.js │ │ └── two-classes.js │ ├── superclass-subclass-node-modules-not-installed/ │ │ ├── expected/ │ │ │ └── superclass-in-node-modules.ts │ │ └── input/ │ │ └── superclass-in-node-modules.js │ └── typescript-class/ │ ├── expected/ │ │ ├── declarations-in-superclass.ts │ │ ├── typescript-class.ts │ │ └── typescript-sub-class.ts │ └── input/ │ ├── declarations-in-superclass.ts │ ├── typescript-class.ts │ └── typescript-sub-class.ts ├── test.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.idea /dist /node_modules /yarn-error.log ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Gregory Jacobs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # js-to-ts-converter Small utility that I wrote to script converting a JS codebase to TypeScript, while trying to solve some of the common TypeScript errors that will be received upon such a conversion. The utility performs the following transformations: 1. Renames `.js` files to `.ts` 2. Adds property declarations to ES6 classes so that they are compilable by the TypeScript compiler (see below). 3. Any function calls that provide fewer arguments than the declared parameters in the function will cause the remaining parameters to be marked as optional for that function. This solves TS errors like "Expected 3 arguments, but got 2" Note: because this utility utilizes the TypeScript Language Service to perform the look-ups for #3, it may take a long time to run. For a small project, expect a few minutes. For a larger project, it could take tens of minutes. Still much better than the days/weeks it could take to fix an entire codebase by hand :) For #2 above, the utility basically looks at any `this` property accessed by a JS class, and fills in the appropriate TypeScript property declarations. Take this `.js` input source file as an example: ```js class Super { someMethod() { this.superProp = 1; } } class Sub extends Super { someMethod() { this.superProp = 2; this.subProp = 2; } } ``` The above JS classes are replaced with the following TS classes: ```ts class Super { public superProp: any; // <-- added someMethod() { this.superProp = 1; } } class Sub extends Super { public subProp: any; // <-- added someMethod() { this.superProp = 2; this.subProp = 2; } } ``` Note: properties used when `this` is assigned to another variable are also found for purposes of creating property declarations. Example: ```js class MyClass { myMethod() { var that = this; that.something; // <-- 'something' parsed as a class property } } ``` For #3 above, parameters are marked as 'optional' when there are callers that don't provide all of them. For example, the following JavaScript: ```js function myFunction( arg1, arg2 ) { // ... } myFunction( 1 ); // only provide arg1 ``` Will be transformed to the following TypeScript: ```ts function myFunction( arg1, arg2? ) { // <-- arg2 marked as optional // ... } myFunction( 1 ); ``` ## Goal The goal of this utility is to simply make the `.js` code compilable under the TypeScript compiler, so simply adding the property declarations typed as `any` was the quickest option there. The utility may look at property initializers in the future to determine a better type. If you have other types of compiler errors that you think might be able to be transformed by this utility, please feel free to raise an issue (or pull request!) Hopefully you only need to use this utility once, but if it saved you time, please star it so that I know it helped you out :) ## Fair Warning This utility makes modifications to the directory that you pass it. Make sure you are in a clean git (or other VCS) state before running it in case you need to revert! ## Running the Utility from the CLI ``` npx js-to-ts-converter ./path/to/js/files ``` If you would prefer to install the CLI globally, do this: ``` npm install --global js-to-ts-converter js-to-ts-converter ./path/to/js/files ``` ## Running the Utility from Node TypeScript: ```ts import { convertJsToTs, convertJsToTsSync } from 'js-to-ts-converter'; // Async convertJsToTs( 'path/to/js/files' ).then( () => console.log( 'Done!' ), ( err ) => console.log( 'Error: ', err ); ); // Sync convertJsToTsSync( 'path/to/js/files' ); console.log( 'Done!' ); ``` JavaScript: ```js const { convertJsToTs, convertJsToTsSync } = require( 'js-to-ts-converter' ); // Async convertJsToTs( 'path/to/js/files' ).then( () => console.log( 'Done!' ), ( err ) => console.log( 'Error: ', err ); ); // Sync convertJsToTsSync( 'path/to/js/files' ); console.log( 'Done!' ); ``` ## Developing Make sure you have [Node.js](https://nodejs.org) installed. Clone the git repo: ``` git clone https://github.com/gregjacobs/js-to-ts-converter.git cd js-to-ts-converter ``` Install dependencies: ``` npm install ``` Run Tests: ``` npm test ``` ================================================ FILE: package.json ================================================ { "name": "js-to-ts-converter", "version": "0.18.2", "description": "Small utility to rename .js->.ts, and convert ES6 classes to TypeScript classes by filling in property declarations. See readme for more details.", "scripts": { "build": "tsc", "cli": "npm run build && node dist/cli.js", "test": "mocha --require ts-node/register --timeout 60000 --watch-extensions ts \"**/*.spec.ts\"", "prepublishOnly": "npm test && npm run build" }, "keywords": [ "typescript", "conversion" ], "homepage": "https://github.com/gregjacobs/js-to-ts-converter", "repository": { "type": "git", "url": "https://github.com/gregjacobs/js-to-ts-converter.git" }, "bugs": { "url": "https://github.com/gregjacobs/js-to-ts-converter/issues" }, "author": "Gregory Jacobs ", "license": "MIT", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": "dist/cli.js", "files": [ "dist/" ], "dependencies": { "argparse": "^1.0.10", "fast-glob": "^3.2.4", "graphlib": "^2.1.5", "lodash": "^4.17.20", "resolve": "^1.8.1", "trace-error": "^0.0.7", "ts-morph": "^14.0.0", "typescript": "^4.6.2", "winston": "^3.0.0" }, "devDependencies": { "@types/chai": "^4.1.4", "@types/graphlib": "^2.1.4", "@types/lodash": "^4.14.112", "@types/mocha": "^5.2.4", "@types/node": "^10.5.2", "@types/winston": "^2.3.9", "chai": "^4.1.2", "mocha": "^5.2.0", "ts-node": "^7.0.0" } } ================================================ FILE: src/cli.ts ================================================ #!/usr/bin/env node import * as path from "path"; import * as fs from 'fs'; import { IndentationText } from "ts-morph"; import logger, { LogLevel, logLevels } from './logger'; import { convertJsToTsSync } from "./js-to-ts-converter"; const ArgumentParser = require('argparse').ArgumentParser; const parser = new ArgumentParser( { version: require( '../package.json' ).version, addHelp: true, description: 'JS -> TS Converter' } ); parser.addArgument( 'directory', { help: 'The directory of .js files to convert' } ); parser.addArgument( '--indentation-text', { help: 'How you would like new code to be indented', choices: [ 'tab', 'twospaces', 'fourspaces', 'eightspaces' ], defaultValue: 'tab' } ); parser.addArgument( '--include', { help: 'Glob patterns to include in the conversion. Separate multiple patterns ' + 'with a comma. The patterns must be valid for the "glob-all" npm ' + 'package (https://www.npmjs.com/package/glob-all), and are relative to ' + 'the input directory.\n' + 'Example: --include="**/myFolder/**,**/*.js"' } ); parser.addArgument( '--exclude', { help: 'Glob patterns to exclude from being converted. Separate multiple patterns ' + 'with a comma. The patterns must be valid for the "glob-all" npm ' + 'package (https://www.npmjs.com/package/glob-all), and are relative to ' + 'the input directory.\n' + 'Example: --exclude="**/myFolder/**,**/*.jsx"' } ); parser.addArgument( '--log-level', { help: ` The level of logs to print to the console. From highest amount of \ logging to lowest amount of logging: '${logLevels.join("', '")}' Defaults to verbose to tell you what's going on, as the script may take a long time to complete when looking up usages of functions. Use 'debug' to enable even more logging. `.trim().replace( /^\t*/gm, '' ), choices: logLevels, defaultValue: 'verbose' } ); const args = parser.parseArgs(); const absolutePath = path.resolve( args.directory.replace( /\/$/, '' ) ); // remove any trailing slash if( !fs.lstatSync( absolutePath ).isDirectory() ) { logger.error( `${absolutePath} is not a directory. Please provide a directory` ); process.exit( 1 ); } else { logger.info( `Processing directory: '${absolutePath}'` ); } convertJsToTsSync( absolutePath, { indentationText: resolveIndentationText( args.indentation_text ), logLevel: resolveLogLevel( args.log_level ), includePatterns: parseIncludePatterns( args.include ), excludePatterns: parseExcludePatterns( args.exclude ) } ); /** * Private helper to resolve the correct IndentationText enum from the CLI * 'indentation' argument. */ function resolveIndentationText( indentationText: 'tab' | 'twospaces' | 'fourspaces' | 'eightspaces' ) { switch( indentationText ) { case 'tab' : return IndentationText.Tab; case 'twospaces' : return IndentationText.TwoSpaces; case 'fourspaces' : return IndentationText.FourSpaces; case 'eightspaces' : return IndentationText.EightSpaces; default : return IndentationText.Tab; } } function resolveLogLevel( logLevelStr: string ): LogLevel { if( !logLevels.includes( logLevelStr ) ) { throw new Error( ` Unknown --log-level argument '${logLevelStr}' Must be one of: '${logLevels.join( "', '" )}' `.trim().replace( /\t*/gm, '' ) ); } return logLevelStr as LogLevel; } function parseIncludePatterns( includePatterns: string | undefined ): string[] | undefined { if( !includePatterns ) { return undefined; } // return undefined to use the default return includePatterns.split( ',' ); } function parseExcludePatterns( excludePatterns: string | undefined ): string[] { if( !excludePatterns ) { return []; } return excludePatterns.split( ',' ); } ================================================ FILE: src/converter/add-class-property-declarations/add-class-property-declarations.ts ================================================ import { Project, ClassInstancePropertyTypes, PropertyDeclarationStructure, Scope } from "ts-morph"; import { parseJsClasses } from "./parse-js-classes"; import { correctJsProperties } from "./correct-js-properties"; import logger from "../../logger/logger"; /** * Parses all source files looking for ES6 classes, and takes any `this` * property access to create a PropertyDeclaration for the class. * * For example: * * class Something { * constructor() { * this.someProp = 1; * } * } * * Is changed to: * * class Something { * someProp: any; * * constructor() { * this.someProp = 1; * } * } */ export function addClassPropertyDeclarations( tsAstProject: Project ): Project { // Parse the JS classes for all of the this.xyz properties that they use const jsClasses = parseJsClasses( tsAstProject ); // Correct the JS classes' properties for superclass/subclass relationships // (essentially remove properties from subclasses that are defined by their // superclasses) const propertiesCorrectedJsClasses = correctJsProperties( jsClasses ); // Fill in field definitions for each of the classes propertiesCorrectedJsClasses.forEach( jsClass => { const sourceFile = tsAstProject.getSourceFileOrThrow( jsClass.path ); logger.verbose( ` Updating class '${jsClass.name}' in '${sourceFile.getFilePath()}'` ); const classDeclaration = sourceFile.getClassOrThrow( jsClass.name! ); const jsClassProperties = jsClass.properties; // If the utility was run against a TypeScript codebase, we should not // fill in property declarations for properties that are already // declared in the class. However, we *should* fill in any missing // declarations. Removing any already-declared declarations from the // jsClassProperties. const currentPropertyDeclarations = classDeclaration.getInstanceProperties() .reduce( ( props: Set, prop: ClassInstancePropertyTypes ) => { const propName = prop.getName(); return propName ? props.add( propName ) : props; }, new Set() ); let undeclaredProperties = [ ...jsClassProperties ] .filter( ( propName: string ) => !currentPropertyDeclarations.has( propName ) ); // If the utility found a reference to this.constructor, we don't want to // add a property called 'constructor'. Filter that out now. // https://github.com/gregjacobs/js-to-ts-converter/issues/9 undeclaredProperties = undeclaredProperties .filter( ( propName: string ) => propName !== 'constructor' ); // Add all currently-undeclared properties const propertyDeclarations = undeclaredProperties.map( propertyName => { return { name: propertyName, type: 'any', scope: Scope.Public } as PropertyDeclarationStructure; } ); logger.verbose( ` Adding property declarations for properties: '${undeclaredProperties.join( "', '" )}'` ); classDeclaration.insertProperties( 0, propertyDeclarations ); } ); return tsAstProject; } ================================================ FILE: src/converter/add-class-property-declarations/correct-js-properties.ts ================================================ import { JsClass } from "./js-class"; import { Graph, alg } from "graphlib"; import logger from "../../logger/logger"; /** * After the graph of original {@link JsClass}es and their properties have been * created, we need to remove properties from subclasses that are defined in * their superclasses. * * This function takes the original graph of classes with all properties in each * class and returns a new list of JsClasses with the properties properly * filtered so that subclasses do not define the same properties that are * already present in their superclasses. * * ## Algorithm * * 1. Build graph of subclasses -> superclasses * 2. Take topological sort of graph * 3. Starting at the superclasses in the sort, fill in the * propertySets for each JsClass. For every subclass encountered, * filter out its superclass properties to create the subclass's property * set * 4. Use the propertySets to create a new list of JsClasses */ export function correctJsProperties( jsClasses: JsClass[] ): JsClass[] { logger.debug( 'Building graph of class hierarchy to determine which class properties belong to superclasses/subclasses' ); const jsClassHierarchyGraph = new Graph(); // First, add all nodes to the graph jsClasses.forEach( jsClass => { jsClassHierarchyGraph.setNode( jsClass.id, jsClass ); } ); // Second, connect the subclasses to superclasses in the graph jsClasses.forEach( jsClass => { if( jsClass.superclassId ) { // If we come across a JsClass whose superclass is in the node_modules // directory (i.e. imported from another package), do not try to // go into that package. We're not going to try to understand an ES5 // module if( jsClass.isSuperclassInNodeModules() ) { return; } // As a bit of error checking, make sure that we're not going to // accidentally create a graph node by adding an edge to // jsClass.superclassId. This would happen if we didn't figure out // the correct path to the superclass in the parse phase, or we // didn't have the superclass's source file added to the project. if( !jsClassHierarchyGraph.hasNode( jsClass.superclassId ) ) { throw new Error( ` An error occurred while adding property declarations to class '${jsClass.name}' in file: '${jsClass.path}' Did not parse this class's superclass ('${jsClass.superclassName}') from file: '${jsClass.superclassPath}' during the parse phase. Make sure that this class's superclass's .js file is within the directory passed to this conversion utility, or otherwise there is a bug in this utility. Please report at: https://github.com/gregjacobs/js-to-ts-converter/issues Debugging info: This class's graph ID: ${jsClass.id} It's superclass's graph ID: ${jsClass.superclassId} Current IDs in the graph: ${jsClassHierarchyGraph.nodes().join( '\n ' )} `.replace( /^\t*/gm, '' ) ); } jsClassHierarchyGraph.setEdge( jsClass.id, jsClass.superclassId ); } } ); // the topological sort is going to put superclasses later in the returned // array, so reverse it logger.debug( 'Topologically sorting the graph in superclass->subclass order' ); const superclassToSubclassOrder = alg.topsort( jsClassHierarchyGraph ).reverse(); // Starting from superclass JsClass instances and walking down to subclass // JsClass instances, fill in the property sets. When a subclass is // encountered, take all of the properties that were used in that subclass, // minus the properties in its superclass, in order to determine the // subclass-specific properties superclassToSubclassOrder.forEach( jsClassId => { const jsClass = jsClassHierarchyGraph.node( jsClassId ) as JsClass; const subclassOnlyProperties = new Set( jsClass.properties ); const superclasses = getSuperclasses( jsClass ); superclasses.forEach( ( superclass: JsClass ) => { // Filter out both properties and methods from each superclass superclass.members.forEach( ( superclassProp: string ) => { subclassOnlyProperties.delete( superclassProp ); } ); } ); const newJsClass = new JsClass( { path: jsClass.path, name: jsClass.name, superclassName: jsClass.superclassName, superclassPath: jsClass.superclassPath, methods: jsClass.methods, properties: subclassOnlyProperties } ); // Re-assign the new JsClass with the correct subclass properties back // to the graph for the next iteration, in case there is a subclass of // the current class which needs to read those properties jsClassHierarchyGraph.setNode( jsClassId, newJsClass ); } ); // Return all of the new JsClass instances with properties corrected for // superclass/subclasses return jsClassHierarchyGraph.nodes() .map( jsClassId => jsClassHierarchyGraph.node( jsClassId ) as JsClass ); function getSuperclasses( jsClass: JsClass ) { const superclasses: JsClass[] = []; while( jsClass.superclassId ) { const superclass = jsClassHierarchyGraph.node( jsClass.superclassId ) as JsClass; superclasses.push( superclass ); jsClass = superclass; } return superclasses; } } ================================================ FILE: src/converter/add-class-property-declarations/js-class.ts ================================================ import { union } from "../../util/set-utils"; /** * Represents a JavaScript class found in a source file. */ export class JsClass { /** * The name of the class. * * Will be undefined for a default export class. */ public readonly name: string | undefined; /** * The absolute path of the file that the class was found in. */ public readonly path: string; /** * The name of this class's superclass. Will be `undefined` if the class * does not have a superclass. */ public readonly superclassName: string | undefined; /** * The path to the file which holds this class's superclass. If the same * file that holds this class also holds its superclass, this will be the * same value as the {@link #path}. * * Will be `undefined` if the superclass was found in the node_modules * folder. We don't try to resolve the path of a module that exists in the * node_modules folder as they're not relevant to this conversion utility, * and we want to allow conversions of codebases that don't have * node_modules installed (which can really improve performance as * ts-morph doesn't try to resolve them when it finds imports in .ts * files) */ public readonly superclassPath: string | undefined; /** * The set of methods found in the class. */ public readonly methods: Set; /** * The set of properties found to be used in the class. These are inferred * from usages. For example: console.log(this.something) would tell us that * the class has a property `something` */ public readonly properties: Set; /** * A union of the {@link #methods} and {@link #properties} sets */ public readonly members: Set; constructor( cfg: { name: string | undefined; path: string; superclassName: string | undefined, superclassPath: string | undefined, methods?: Set; properties?: Set; } ) { this.name = cfg.name; this.path = cfg.path; this.superclassName = cfg.superclassName; this.superclassPath = cfg.superclassPath; this.methods = cfg.methods || new Set(); this.properties = cfg.properties || new Set(); this.members = union( this.methods, this.properties ); } /** * String identifier for the JsClass which is a combination of its file path * and class name. Used to store JsClass nodes on a graphlib Graph. */ public get id(): string { return `${this.path}_${this.name}`; } /** * Retrieves the ID of the superclass JsClass instance, if the JsClass has * one. If not, returns undefined. * * Also returns `undefined` if the class is found to be in the node_modules * folder, as we don't want to attempt to parse ES5 modules. */ public get superclassId(): string | undefined { if( this.isSuperclassInNodeModules() ) { // If the superclass is in the node_modules folder, we'll // essentially treat this JsClass as if it didn't have a superclass. // See `isSuperclassInNodeModules()` jsdoc for details. return undefined; } else { return this.superclassName && `${this.superclassPath}_${this.superclassName}`; } } /** * Determines if the JsClass's superclass was found in the node_modules * directory (i.e. it extends from another package). * * If so, we're not going to try to understand a possibly ES5 module for * its properties, so we'll just stop processing at that point. */ public isSuperclassInNodeModules(): boolean { return this.superclassPath === undefined; } } ================================================ FILE: src/converter/add-class-property-declarations/parse-js-classes.ts ================================================ import { Project, ts, ClassDeclaration, ClassInstancePropertyTypes, MethodDeclaration, PropertyAccessExpression, SourceFile, SyntaxKind, VariableDeclaration } from "ts-morph"; import { JsClass } from "./js-class"; import { difference, union } from "../../util/set-utils"; import { parseDestructuredProps } from "../../util/parse-destructured-props"; import { parseSuperclassNameAndPath } from "./parse-superclass-name-and-path"; import { isThisReferencingVar } from "../../util/is-this-referencing-var"; import { propertyAccessWithObjFilter } from "../../util/is-property-access-with-obj"; import logger from "../../logger/logger"; /** * Parses the classes out of each .js file in the SourceFilesCollection, and * forms a tree representing their hierarchy. * * ## Description of algorithm: * * Each source file is parsed to find all file-level classes. Their superclasses * and import paths for those superclasses are also recorded to form an * adjacency list graph of classes keyed by their file path. * * Each class is also processed to find and record any property accesses of the * `this` object. For instance, in the following class, there are 3 * PropertyAccessExpressions that pull from the `this` object ('something1', * 'something2', and 'something3'): * * class Something { * constructor() { * this.something1 = 1; * this.something2 = 2; * } * * someMethod() { * console.log( this.something3 ); * * console.log( window.location ); // <-- not a `this` PropertyAccessExpression * } * } * * The returned graph will be used later to determine which TS class property * definitions should be placed in superclasses vs. subclasses. Properties used * by a superclass and a subclass should only be defined in the superclass. */ export function parseJsClasses( tsAstProject: Project ): JsClass[] { logger.verbose( "Parsing JS classes in the codebase..." ); const files = tsAstProject.getSourceFiles(); const jsClasses = files.reduce( ( classes: JsClass[], file: SourceFile ) => { logger.debug( `Parsing classes in file: ${file.getFilePath()}` ); const fileClasses = parseFileClasses( file ); return classes.concat( fileClasses ); }, [] ); return jsClasses; } /** * Parses the file-level classes out of the given `sourceFile`. */ function parseFileClasses( sourceFile: SourceFile ): JsClass[] { return sourceFile.getClasses().map( fileClass => { const className = fileClass.getName(); logger.debug( ` Parsing class: ${className}` ); const { superclassName, superclassPath } = parseSuperclassNameAndPath( sourceFile, fileClass ); const methodNames = getMethodNames( fileClass ); const propertyNames = getPropertyNames( fileClass ); const propertiesMinusMethods = difference( propertyNames, methodNames ); // remove any method names from this Set return new JsClass( { path: sourceFile.getFilePath(), name: className, superclassName, superclassPath, methods: methodNames, properties: propertiesMinusMethods } ); } ); } /** * Parses the method names from the class into a Set of strings. */ function getMethodNames( fileClass: ClassDeclaration ): Set { return fileClass.getMethods() .reduce( ( methods: Set, method: MethodDeclaration ) => { return methods.add( method.getName() ); }, new Set() ); } /** * Retrieves the list of propertyNames used in the class. This may also include * method names (which are technically properties), which we'll filter out later. */ function getPropertyNames( fileClass: ClassDeclaration ) { const existingPropertyDeclarations = parsePropertyDeclarations( fileClass ); // in case we are actually parsing a TypeScript class with existing declarations const propertyAccesses = parsePropertyAccesses( fileClass ); const destructuringUsesOfProperties = parseDestructuringThisAssignments( fileClass ); const propertyAccessesOfThisAssignedVars = parsePropertyAccessesOfThisAssignedVars( fileClass ); return union( existingPropertyDeclarations, propertyAccesses, destructuringUsesOfProperties, propertyAccessesOfThisAssignedVars ); } /** * In the case that the utility is actually parsing TypeScript classes with * existing property declarations, we want to know about these so we don't * accidentally write in new ones of the same name. */ function parsePropertyDeclarations( fileClass: ClassDeclaration ): Set { return fileClass.getInstanceProperties() .reduce( ( props: Set, prop: ClassInstancePropertyTypes ) => { const propName = prop.getName(); return propName ? props.add( propName ) : props; // don't add unnamed properties (not sure how we would have one of those, but seems its possible according to the TsSimpleAst types) }, new Set() ); } /** * Parses the property names of `this` PropertyAccessExpressions. * * Examples: * * this.something = 42; * console.log( this.something2 ); * * const { destructured1, destructured2 } = this; * * Method returns: * * Set( [ 'something', 'something2', 'destructured1', 'destructured2' ] ) */ function parsePropertyAccesses( fileClass: ClassDeclaration ): Set { // First, find all of the `this.something` properties const thisProps = fileClass .getDescendantsOfKind( SyntaxKind.PropertyAccessExpression ) .filter( ( prop: PropertyAccessExpression ) => prop.getExpression().getKind() === SyntaxKind.ThisKeyword ); const propNamesSet = thisProps .reduce( ( props: Set, prop: PropertyAccessExpression ) => { return props.add( prop.getName() ); }, new Set() ); return propNamesSet; } /** * Parses any object destructuring statements of the form: * * var { a, b } = this; * * And returns Set( [ 'a', 'b' ] ) in this case. */ function parseDestructuringThisAssignments( fileClass: ClassDeclaration ): Set { // Second, find any `var { a, b } = this` statements const destructuredProps = fileClass .getDescendantsOfKind( SyntaxKind.VariableDeclaration ) .filter( ( varDec: VariableDeclaration ) => { return varDec.compilerNode.name.kind === SyntaxKind.ObjectBindingPattern; } ); return destructuredProps .reduce( ( propNames: Set, varDec: VariableDeclaration ) => { const destructuredPropNames = parseDestructuredProps( varDec.compilerNode.name as ts.ObjectBindingPattern ); destructuredPropNames.forEach( propName => propNames.add( propName ) ); return propNames; }, new Set() ); } /** * Parses property accesses of variables that are assigned to the `this` * keyword. * * For example: * * var that = this; * * that.someProp1 = 1; * that.someProp2 = 2; * * In the above code, the Set( [ 'someProp1', 'someProp2' ] ) is returned */ function parsePropertyAccessesOfThisAssignedVars( fileClass: ClassDeclaration ): Set { const methods = fileClass.getMethods(); return methods.reduce( ( propNames: Set, method: MethodDeclaration ) => { const thisVarDeclarations = method .getDescendantsOfKind( SyntaxKind.VariableDeclaration ) .filter( isThisReferencingVar ); // Get the array of identifiers assigned to `this`. Ex: [ 'that', 'self' ] const thisVarIdentifiers = thisVarDeclarations .map( ( thisVarDec: VariableDeclaration ) => thisVarDec.getName() ); thisVarIdentifiers.forEach( ( thisVarIdentifier: string ) => { // Get the properties accessed from the `this` identifiers (i.e. from // 'that', 'self', etc.) const propNamesAccessedFromIdentifier = method .getDescendantsOfKind( SyntaxKind.PropertyAccessExpression ) .filter( propertyAccessWithObjFilter( thisVarIdentifier ) ) .map( ( p: PropertyAccessExpression ) => p.getName() ); propNamesAccessedFromIdentifier .forEach( ( propName: string ) => propNames.add( propName ) ); } ); return propNames; }, new Set() ); } ================================================ FILE: src/converter/add-class-property-declarations/parse-superclass-name-and-path.ts ================================================ import { isValidIdentifier } from "../../util/is-valid-identifier"; import { ClassDeclaration, SourceFile } from "ts-morph"; import { findImportForIdentifier } from "../../util/find-import-for-identifier"; const resolve = require( 'resolve' ); const TraceError = require( 'trace-error' ); /** * Given a file and ClassDeclaration, finds the name of the superclass and the * full path to the module (file) that hosts the superclass. * * `superclass` and `superclassPath` in the return object will be `null` if * there is no superclass. */ export function parseSuperclassNameAndPath( file: SourceFile, fileClass: ClassDeclaration ): { superclassName: string | undefined; superclassPath: string | undefined; } { let superclassName: string | undefined; let superclassPath: string | undefined; const heritage = fileClass.getExtends(); if( heritage ) { superclassName = heritage.getExpression().getText(); // Confirm that the superclass is an identifier rather than an // expression. It would be a bit much to try to understand expressions // as a class's 'extends', so just ignore these for now. // Example of ignored class extends: // // class MyClass extends Mixin.mix( MixinClass1, MixinClass2 ) // if( !isValidIdentifier( superclassName ) ) { superclassName = undefined; // superclass was not a valid identifier } else if( !!file.getClass( superclassName ) ) { superclassPath = file.getFilePath(); } else { superclassPath = findImportPathForIdentifier( file, superclassName ); } } return { superclassName, superclassPath: superclassPath && superclassPath.replace( /\\/g, '/' ) // normalize backslashes on Windows to forward slashes so we can compare directories with the paths that ts-morph produces }; } /** * Finds the absolute path for the import with the given `identifier`. * * For example, if we were looking for the identifier 'MyClass' in the following * list of imports: * * import { Something } from './somewhere'; * import { MyClass } from './my-class'; * * Then the method would return '/absolute/path/to/my-class.js'; * * If there is no import for `identifier`, the method returns `undefined`. */ function findImportPathForIdentifier( sourceFile: SourceFile, identifier: string ): string | undefined { const importWithIdentifier = findImportForIdentifier( sourceFile, identifier ); if( importWithIdentifier ) { const moduleSpecifier = importWithIdentifier.getModuleSpecifier().getLiteralValue(); if( !moduleSpecifier.startsWith( '.' ) ) { // if the import path isn't relative (i.e. doesn't start with './' // or '../'), then it must be in node_modules. Return `undefined` to // represent that. We don't want to parse node_modules, and we // should be able to migrate the codebase without node_modules even // being installed. return undefined; } // If it's a relative import, return the absolute path to the module, // based on the source file that the import was found const basedir = sourceFile.getDirectoryPath(); try { return resolve.sync( moduleSpecifier, { basedir, extensions: [ '.ts', '.js' ] } ); } catch( error ) { throw new TraceError( ` An error occurred while trying to resolve the absolute path to the import of identifier '${identifier}' in source file: '${sourceFile.getFilePath()}' Was looking at the import with text: ${importWithIdentifier.getText()} `.trim().replace( /^\t*/gm, '' ), error ); } } // Nothing found, return undefined return undefined; } ================================================ FILE: src/converter/add-optionals-to-function-params.ts ================================================ import { Project, CallExpression, ClassDeclaration, ConstructorDeclaration, FunctionDeclaration, MethodDeclaration, NewExpression, Node, SourceFile, SyntaxKind } from "ts-morph"; import logger from "../logger/logger"; type NameableFunction = FunctionDeclaration | MethodDeclaration; type FunctionTransformTarget = NameableFunction | ConstructorDeclaration; /** * Adds the question token to function/method/constructor parameters that are * deemed to be optional based on the calls to that function/method/constructor * in the codebase. * * For example, if we have: * * function myFn( arg1, arg2, arg3 ) { * // ... * } * * myFn( 1, 2, 3 ); // all 3 args provided * myFn( 1, 2 ); // <-- a call site only provides two arguments * * Then the resulting TypeScript function will be: * * function myFn( arg1, arg2, arg3? ) { // <-- arg3 marked as optional * // ... * } * * Note: Just calling the language service to look up references takes a lot of * time. Might have to optimize this somehow in the future. */ export function addOptionalsToFunctionParams( tsAstProject: Project ): Project { logger.verbose( 'Beginning routine to mark function parameters as optional when calls exist that supply fewer args than parameters...' ); const sourceFiles = tsAstProject.getSourceFiles(); logger.verbose( 'Parsing function/method/constructor calls from codebase.' ); const constructorMinArgsMap = parseClassConstructorCalls( sourceFiles ); const functionsMinArgsMap = parseFunctionAndMethodCalls( sourceFiles ); logger.verbose( 'Marking parameters as optional' ); addOptionals( constructorMinArgsMap ); addOptionals( functionsMinArgsMap ); return tsAstProject; } /** * Finds the call sites of each ClassDeclaration's constructor in order to * determine if any of its parameters should be marked as optional. * * Returns a Map keyed by ClassDeclaration which contains the minimum number of * arguments passed to that class's constructor. * * Actually marking the parameters as optional is done in a separate phase. */ function parseClassConstructorCalls( sourceFiles: SourceFile[] ): Map { logger.verbose( 'Finding all calls to class constructors...' ); const constructorMinArgsMap = new Map(); sourceFiles.forEach( ( sourceFile: SourceFile ) => { logger.verbose( ` Processing classes in source file: ${sourceFile.getFilePath()}` ); const classes = sourceFile.getDescendantsOfKind( SyntaxKind.ClassDeclaration ); classes.forEach( ( classDeclaration: ClassDeclaration ) => { const constructorFns = classDeclaration.getConstructors() || []; const constructorFn = constructorFns.length > 0 ? constructorFns[ 0 ] : undefined; // only grab the first since we're converting JavaScript // If there is no constructor function for this class, then nothing to do if( !constructorFn ) { return; } logger.verbose( ` Looking for calls to the constructor of class: '${classDeclaration.getName()}'` ); const constructorFnParams = constructorFn.getParameters(); const numParams = constructorFnParams.length; const referencedNodes = classDeclaration.findReferencesAsNodes(); const callsToConstructor = referencedNodes .map( ( node: Node ) => node.getFirstAncestorByKind( SyntaxKind.NewExpression ) ) .filter( ( node ): node is NewExpression => !!node ); logger.debug( ` Found ${callsToConstructor.length} call(s) to the constructor` ); const minNumberOfCallArgs = callsToConstructor .reduce( ( minCallArgs: number, call: NewExpression ) => { return Math.min( minCallArgs, call.getArguments().length ); }, numParams ); if( callsToConstructor.length > 0 ) { logger.debug( ` Constructor currently expects ${numParams} params. Call(s) to the constructor supply a minimum of ${minNumberOfCallArgs} args.` ); } constructorMinArgsMap.set( constructorFn, minNumberOfCallArgs ); } ); } ); return constructorMinArgsMap; } /** * Finds the call sites of each FunctionDeclaration or MethodDeclaration in * order to determine if any of its parameters should be marked as optional. * * Returns a Map keyed by FunctionDeclaration or MethodDeclaration which contains * the minimum number of arguments passed to that function/method. * * Actually marking the parameters as optional is done in a separate phase. */ function parseFunctionAndMethodCalls( sourceFiles: SourceFile[] ): Map { logger.verbose( 'Finding all calls to functions/methods...' ); const functionsMinArgsMap = new Map(); sourceFiles.forEach( ( sourceFile: SourceFile ) => { logger.verbose( ` Processing functions/methods in source file: ${sourceFile.getFilePath()}` ); const fns = getFunctionsAndMethods( sourceFile ); fns.forEach( ( fn: NameableFunction ) => { logger.verbose( ` Looking for calls to the function: '${fn.getName()}'` ); const fnParams = fn.getParameters(); const numParams = fnParams.length; const referencedNodes = fn.findReferencesAsNodes(); const callsToFunction = referencedNodes .map( ( node: Node ) => node.getFirstAncestorByKind( SyntaxKind.CallExpression ) ) .filter( ( node ): node is CallExpression => !!node ); logger.debug( ` Found ${callsToFunction.length} call(s) to the function '${fn.getName()}'` ); const minNumberOfCallArgs = callsToFunction .reduce( ( minCallArgs: number, call: CallExpression ) => { return Math.min( minCallArgs, call.getArguments().length ); }, numParams ); if( callsToFunction.length > 0 ) { logger.debug( ` Function currently expects ${numParams} params. Call(s) to the function/method supply a minimum of ${minNumberOfCallArgs} args.` ); } functionsMinArgsMap.set( fn, minNumberOfCallArgs ); } ); } ); return functionsMinArgsMap; } /** * Retrieves all FunctionDeclarations and MethodDeclarations from the given * source file. */ function getFunctionsAndMethods( sourceFile: SourceFile ): NameableFunction[] { return ( [] as NameableFunction[] ).concat( sourceFile.getDescendantsOfKind( SyntaxKind.FunctionDeclaration ), sourceFile.getDescendantsOfKind( SyntaxKind.MethodDeclaration ) ); } /** * Marks parameters of class constructors / methods / functions as optional * based on the minimum number of arguments passed in at its call sites. * * Ex: * * class SomeClass { * constructor( arg1, arg2 ) {} * } * new SomeClass( 1 ); // no arg2 * * function myFn( arg1, arg2 ) {} * myFn(); // no args * * * Output class and function: * * class SomeClass { * constructor( arg1, arg2? ) {} // <-- arg2 marked as optional * } * * function myFn( arg1?, arg2? ) {} // <-- arg1 and arg2 marked as optional */ function addOptionals( minArgsMap: Map ) { const fns = minArgsMap.keys(); for( const fn of fns ) { const fnParams = fn.getParameters(); const numParams = fnParams.length; const minNumberOfCallArgs = minArgsMap.get( fn )!; // Mark all parameters greater than the minNumberOfCallArgs as // optional (if it's not a rest parameter or already has a default value) for( let i = minNumberOfCallArgs; i < numParams; i++ ) { const param = fnParams[ i ]; if( !param.isRestParameter() && !param.hasInitializer() ) { param.setHasQuestionToken( true ); } } } } ================================================ FILE: src/converter/convert.ts ================================================ import { Project, SyntaxKind } from "ts-morph"; import { addClassPropertyDeclarations } from "./add-class-property-declarations/add-class-property-declarations"; import { addOptionalsToFunctionParams } from "./add-optionals-to-function-params"; import { filterOutNodeModules } from "./filter-out-node-modules"; import logger from "../logger/logger"; /** * Converts the source .js code to .ts */ export function convert( tsAstProject: Project ): Project { if( tsAstProject.getSourceFiles().length === 0 ) { logger.info( 'Found no source files to process. Exiting.' ); return tsAstProject; } // Print input files logger.info( 'Processing the following source files:' ); printSourceFilesList( tsAstProject, ' ' ); logger.info( ` Converting source files... This may take anywhere from a few minutes to tens of minutes or longer depending on how many files are being converted. `.replace( /\t*/gm, '' ) ); // Fill in PropertyDeclarations for properties used by ES6 classes logger.info( 'Adding property declarations to JS Classes...' ); tsAstProject = addClassPropertyDeclarations( tsAstProject ); // Rename .js files to .ts files logger.info( 'Renaming .js files to .ts' ); tsAstProject.getSourceFiles().forEach( sourceFile => { const ext = sourceFile.getExtension(); if( ext === '.js' || ext === '.jsx' ) { const dir = sourceFile.getDirectoryPath(); const basename = sourceFile.getBaseNameWithoutExtension(); // in case there's a '.js' file which has JSX in it const fileHasJsx = sourceFile.getFirstDescendantByKind( SyntaxKind.JsxElement ) || sourceFile.getFirstDescendantByKind( SyntaxKind.JsxSelfClosingElement ); const extension = ( fileHasJsx || ext === '.jsx' ) ? 'tsx' : 'ts'; const outputFilePath = `${dir}/${basename}.${extension}`; logger.debug( ` Renaming ${sourceFile.getFilePath()} to ${outputFilePath}` ); sourceFile.move( outputFilePath ); } } ); // Filter out any node_modules files that accidentally got included by an import. // We don't want to modify these when we save the project tsAstProject = filterOutNodeModules( tsAstProject ); // Make function parameters optional for calls that supply fewer arguments // than there are function parameters. // NOTE: Must happen after .js -> .ts rename for the TypeScript Language // Service to work. logger.info( 'Making parameters optional for calls that supply fewer args than function parameters...' ); tsAstProject = addOptionalsToFunctionParams( tsAstProject ); // Filter out any node_modules files as we don't want to modify these when // we save the project. Also, some .d.ts files get included for some reason // like tslib.d.ts, so we don't want to output that as well. tsAstProject = filterOutNodeModules( tsAstProject ); // Print output files logger.info( 'Outputting .ts files:' ); printSourceFilesList( tsAstProject, ' ' ); // Even though the `tsAstProject` has been mutated (it is not an immutable // data structure), return it anyway to avoid the confusion of an output // parameter. return tsAstProject; } /** * Private helper to print out the source files list in the given `astProject` * to the console. */ function printSourceFilesList( astProject: Project, indent = '' ) { astProject.getSourceFiles().forEach( sf => { logger.info( `${indent}${sf.getFilePath()}` ); } ); } ================================================ FILE: src/converter/filter-out-node-modules.ts ================================================ import { Project } from "ts-morph"; /** * Given a Project, removes all files that are under the node_modules folder. * * It seems the language service can pull in some .d.ts files from node_modules * that we don't want to be output after we save. */ export function filterOutNodeModules( tsAstProject: Project ): Project { tsAstProject.getSourceFiles().forEach( sourceFile => { if( sourceFile.getFilePath().includes( 'node_modules' ) ) { tsAstProject.removeSourceFile( sourceFile ); } } ); return tsAstProject; } ================================================ FILE: src/create-ts-morph-project.ts ================================================ import { Project, IndentationText } from "ts-morph"; import fastGlob from 'fast-glob'; /** * Creates a ts-morph Project by including the source files under the given * `directory`. * * @param directory The absolute path to the directory of .js files to * include. * @param options * @param options.indentationText The text used to indent new class property * declarations. * @param options.excludePatterns Glob patterns to exclude files. */ export function createTsMorphProject( directory: string, options: { indentationText?: IndentationText, includePatterns?: string[], excludePatterns?: string[] } = {} ) { const tsMorphProject = new Project( { manipulationSettings: { indentationText: options.indentationText || IndentationText.Tab } } ); // Read files using fast-glob. fast-glob does a much better job over node-glob // at ignoring directories like node_modules without reading all of the files // in them first let files = fastGlob.sync( options.includePatterns || `**/*.+(js|ts|jsx|tsx)`, { cwd: directory, absolute: true, followSymbolicLinks: true, // filter out any path which includes node_modules. We don't want to // attempt to parse those as they may be ES5, and we also don't accidentally // want to write out into the node_modules folder ignore: ['**/node_modules/**'].concat(options.excludePatterns || []) } ); files.forEach( ( filePath: string ) => { tsMorphProject.addSourceFileAtPath( filePath ) } ); return tsMorphProject; } ================================================ FILE: src/index.ts ================================================ export * from './js-to-ts-converter'; export * from './logger/log-level'; ================================================ FILE: src/js-to-ts-converter.ts ================================================ import * as path from 'path'; import { createTsMorphProject } from "./create-ts-morph-project"; import { convert } from "./converter/convert"; import { Project, IndentationText } from "ts-morph"; import { LogLevel } from "./logger"; import logger from "./logger/logger"; export interface JsToTsConverterOptions { indentationText?: IndentationText, logLevel?: LogLevel, includePatterns?: string[], excludePatterns?: string[] } /** * Asynchronously converts the JavaScript files under the given `sourceFilesPath` * to TypeScript files. * * @param sourceFilesPath The path to the source files to convert * @param [options] * @param [options.indentationText] The text used to indent new class property * declarations. * @param [options.logLevel] The level of logging to show on the console. * One of: 'debug', 'verbose', 'info', 'warn', 'error' * @param [options.includePatterns] Glob patterns to include files. * @param [options.excludePatterns] Glob patterns to exclude files. */ export async function convertJsToTs( sourceFilesPath: string, options: JsToTsConverterOptions = {} ): Promise { const convertedTsAstProject = doConvert( sourceFilesPath, options ); // Save output files return convertedTsAstProject.save(); } /** * Synchronously converts the JavaScript files under the given `sourceFilesPath` * to TypeScript files. * * @param sourceFilesPath The path to the source files to convert * @param [options] * @param [options.indentationText] The text used to indent new class property * declarations. * @param [options.logLevel] The level of logging to show on the console. * One of: 'debug', 'verbose', 'info', 'warn', 'error' * @param [options.includePatterns] Glob patterns to include files. * @param [options.excludePatterns] Glob patterns to exclude files. */ export function convertJsToTsSync( sourceFilesPath: string, options: JsToTsConverterOptions = {} ) { const convertedTsAstProject = doConvert( sourceFilesPath, options ); // Save output files convertedTsAstProject.saveSync(); } /** * Performs the actual conversion given a `sourceFilesPath`, and returning a * `ts-morph` Project with the converted source files. * * @param sourceFilesPath The path to the source files to convert * @param [options] * @param [options.indentationText] The text used to indent new class property * declarations. * @param [options.logLevel] The level of logging to show on the console. * One of: 'debug', 'verbose', 'info', 'warn', 'error' * @param [options.includePatterns] Glob patterns to include files. * @param [options.excludePatterns] Glob patterns to exclude files. */ function doConvert( sourceFilesPath: string, options: JsToTsConverterOptions = {} ): Project { logger.setLogLevel( options.logLevel || 'verbose' ); const absolutePath = path.resolve( sourceFilesPath ); const tsAstProject = createTsMorphProject( absolutePath, options ); return convert( tsAstProject ); } ================================================ FILE: src/logger/index.ts ================================================ export * from './logger'; export * from './log-level'; import logger from './logger'; export default logger; ================================================ FILE: src/logger/log-level.ts ================================================ export type LogLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error'; export const logLevels = [ 'debug', 'verbose', 'info', 'warn', 'error' ]; ================================================ FILE: src/logger/logger.ts ================================================ import * as winston from 'winston'; import { LogLevel } from "./log-level"; const winstonLogger = winston.createLogger( { level: 'verbose', // may be changed by Logger.setLogLevel() transports: [ new winston.transports.Console( { format: winston.format.combine( winston.format.colorize(), winston.format.align(), winston.format.printf(info => `${info.level}: ${info.message}`) ) } ) ] } ); /** * Abstraction layer for the Winston logger. The methods are in order from * highest level of logging to lowest. */ class Logger { setLogLevel( logLevel: LogLevel ) { winstonLogger.level = logLevel; } debug( message: string ) { winstonLogger.log( 'debug', message ); } verbose( message: string ) { winstonLogger.log( 'verbose', message ); } info( message: string ) { winstonLogger.log( 'info', message ); } log( message: string ) { winstonLogger.log( 'info', message ); } warn( message: string ) { winstonLogger.log( 'warn', message ); } error( message: string ) { winstonLogger.log( 'error', message ); } } const logger = new Logger(); export default logger; ================================================ FILE: src/util/find-import-for-identifier.ts ================================================ import { ImportDeclaration, ImportSpecifier, SourceFile } from "ts-morph"; /** * Finds an ImportDeclaration for a given identifier (name). * * For instance, given this source file: * * import { SomeClass1, SomeClass2 } from './somewhere'; * import { SomeClass3 } from './somewhere-else'; * * // ... * * And a call such as: * * findImportForIdentifier( sourceFile, 'SomeClass3' ); * * Then the second ImportDeclaration will be returned. */ export function findImportForIdentifier( sourceFile: SourceFile, identifier: string ): ImportDeclaration | undefined { return sourceFile .getImportDeclarations() .find( ( importDeclaration: ImportDeclaration ) => { const hasNamedImport = importDeclaration.getNamedImports() .map( ( namedImport: ImportSpecifier ) => namedImport.getName() ) .includes( identifier ); const defaultImport = importDeclaration.getDefaultImport(); const hasDefaultImport = !!defaultImport && defaultImport.getText() === identifier; return hasNamedImport || hasDefaultImport; } ); } ================================================ FILE: src/util/is-element-access-with-obj.ts ================================================ import { ElementAccessExpression, Identifier, Node } from "ts-morph"; /** * Determines if the given `node` is a ElementAccessExpression whose object is * `obj`. * * Example, in the following expression: * * obj['a'] * * This function will return true if called as: * * isElementAccessWithObj( expr, 'obj' ); */ export function isElementAccessWithObj( node: Node, objIdentifier: string ): node is ElementAccessExpression { if( !Node.isElementAccessExpression( node ) ) { return false; } const expr = node.getExpression(); if( objIdentifier === 'this' ) { return Node.isThisExpression( expr ); } else if( Node.isIdentifier( expr ) ) { const identifier = expr as Identifier; return identifier.getText() === objIdentifier; } else { return false; } } /** * Function intended to be used with Array.prototype.filter() to return any * ElementAccessExpression that uses the object `obj`. * * For example, in this source code: * * const obj = { a: 1, b: 2 }; * obj['a'] = 3; * * const obj2 = { a: 3, b: 4 }; * obj2['b'] = 5; * * We can use the following to find the 'obj2' element access: * * const propAccesses = sourceFile * .getDescendantsOfKind( SyntaxKind.ElementAccessExpression ); * * const obj2PropAccesses = propAccesses * .filter( elementAccessWithObjFilter( 'obj2' ) ); */ export function elementAccessWithObjFilter( objIdentifier: string ): ( node: Node ) => node is ElementAccessExpression { return ( node: Node ): node is ElementAccessExpression => { return isElementAccessWithObj( node, objIdentifier ); }; } ================================================ FILE: src/util/is-property-access-with-obj.ts ================================================ import { Identifier, Node, PropertyAccessExpression } from "ts-morph"; /** * Determines if the given `node` is a PropertyAccessExpression or * ElementAccessExpression whose object is `obj`. * * Example, in the following expression: * * obj.a * * This function will return true if called as: * * isPropertyOrElemementAccessWithObj( expr, 'obj' ); */ export function isPropertyAccessWithObj( node: Node, objIdentifier: string ): node is PropertyAccessExpression { if( !Node.isPropertyAccessExpression( node ) ) { return false; } const expr = node.getExpression(); if( objIdentifier === 'this' ) { return Node.isThisExpression( expr ); } else if( Node.isIdentifier( expr ) ) { const identifier = expr as Identifier; return identifier.getText() === objIdentifier; } else { return false; } } /** * Function intended to be used with Array.prototype.filter() to return any * PropertyAccessExpression that uses the object `obj`. * * For example, in this source code: * * const obj = { a: 1, b: 2 }; * obj.a = 3; * * const obj2 = { a: 3, b: 4 }; * obj2.b = 5; * * We can use the following to find the 'obj2' property access: * * const propAccesses = sourceFile * .getDescendantsOfKind( SyntaxKind.PropertyAccessExpression ); * * const obj2PropAccesses = propAccesses * .filter( propAccessWithObjFilter( 'obj2' ) ); */ export function propertyAccessWithObjFilter( objIdentifier: string ): ( node: Node ) => node is PropertyAccessExpression { return ( node: Node ): node is PropertyAccessExpression => { return isPropertyAccessWithObj( node, objIdentifier ); }; } ================================================ FILE: src/util/is-property-or-elemement-access-with-obj.ts ================================================ import { ElementAccessExpression, Node, PropertyAccessExpression } from "ts-morph"; import { isPropertyAccessWithObj } from "./is-property-access-with-obj"; import { isElementAccessWithObj } from "./is-element-access-with-obj"; /** * Determines if the given `node` is a PropertyAccessExpression or * ElementAccessExpression whose object is `obj`. * * Example, in the following expression: * * obj.a * * This function will return true if called as: * * isPropertyOrElemementAccessWithObj( expr, 'obj' ); */ export function isPropertyOrElemementAccessWithObj( node: Node, objIdentifier: string ): node is PropertyAccessExpression | ElementAccessExpression { return isPropertyAccessWithObj( node, objIdentifier ) || isElementAccessWithObj( node, objIdentifier ); } /** * Function intended to be used with Array.prototype.filter() to return any * PropertyAccessExpression or ElementAccessExpression that uses the object * `obj`. * * For example, in this source code: * * const obj = { a: 1, b: 2 }; * obj.a = 3; * obj['b'] = 4; * * const obj2 = { a: 3, b: 4 }; * obj2.a = 5; * obj2['b'] = 6; * * We can use the following to find the two 'obj2' property accesses: * * const propOrElementAccesses = sourceFile * .getDescendantsOfKind( SyntaxKind.PropertyAccessExpression ) * .concat( sourceFile * .getDescendantsOfKind( SyntaxKind.ElementAccessExpression ) * ); * * const obj2PropOrElemAccesses = propOrElementAccesses * .filter( propertyOrElementAccessWithObjFilter( 'obj2' ) ); */ export function propertyOrElementAccessWithObjFilter( objIdentifier: string ): ( node: Node ) => node is PropertyAccessExpression | ElementAccessExpression { return ( node: Node ): node is PropertyAccessExpression | ElementAccessExpression => { return isPropertyOrElemementAccessWithObj( node, objIdentifier ); }; } ================================================ FILE: src/util/is-this-referencing-var.ts ================================================ import { Node, SyntaxKind, VariableDeclaration } from "ts-morph"; /** * Determines if the given AST Node is a VariableDeclaration of the form: * * var self = this; * * * Will return false for the following, however, since this is a destructuring * of the `this` object's properties. * * var { prop1, prop2 } = this; */ export function isThisReferencingVar( node: Node ): node is VariableDeclaration { if( !Node.isVariableDeclaration( node ) ) { return false; } const varDec = node as VariableDeclaration; const initializerIsThisKeyword = !!varDec.getInitializerIfKind( SyntaxKind.ThisKeyword ); const assignedToSingleIdentifier = varDec.compilerNode.name.kind === SyntaxKind.Identifier; return initializerIsThisKeyword && assignedToSingleIdentifier; } ================================================ FILE: src/util/is-valid-identifier.ts ================================================ /** * Helper to determine if a string of text is a valid JavaScript identifier. */ export function isValidIdentifier( text: string ) { return /^[\w$]+$/.test( text ); } ================================================ FILE: src/util/parse-destructured-props.ts ================================================ import { ts, SyntaxKind } from "ts-morph"; /** * Given a ts.ObjectBindingPattern node, returns an array of the names that * are bound to it. * * These names are essentially the property names pulled out of the object. * * Example: * * var { a, b } = this; * * Returns: * * [ 'a', 'b' ] */ export function parseDestructuredProps( node: ts.ObjectBindingPattern ): string[] { const elements = node.elements; return elements .filter( ( element: ts.BindingElement ) => { return element.name.kind === SyntaxKind.Identifier; } ) .map( ( element: ts.BindingElement ) => { return ( element.name as ts.Identifier ).text; } ); } ================================================ FILE: src/util/set-utils.ts ================================================ /** * Unions two or more sets to create a combined set. Does not mutate the input * sets. */ export function union( setA: Set, ...sets: Set[] ) { const union = new Set( setA ); sets.forEach( currentSet => { for( const elem of currentSet ) { union.add( elem ); } } ); return union; } /** * Removes the elements of `setB` from `setA` to produce the difference. Does * not mutate the input sets. */ export function difference( setA: Set, setB: Set ) { const difference = new Set( setA ); for( const elem of setB ) { difference.delete( elem ); } return difference; } ================================================ FILE: test/convert.spec.ts ================================================ import { expect } from 'chai'; import { createTsMorphProject } from "../src/create-ts-morph-project"; import { convert } from "../src/converter/convert"; import { SourceFile } from "ts-morph"; import * as fs from "fs"; import logger from "../src/logger/logger"; import { JsToTsConverterOptions } from "../src"; // Minimal logging for tests logger.setLogLevel( 'error' ); describe( 'convert()', () => { it( `should convert JS classes to TS-compilable classes by filling in field (property) declarations for properties consumed in the original JS classes`, () => { runTest( `${__dirname}/fixture/superclass-subclass` ); } ); it( `should ignore expressions (i.e. non-identifiers) in the 'extends' clause of a class (at least for the moment, this would be too much to parse and figure out - may support in the future)`, () => { runTest( `${__dirname}/fixture/expression-extends` ); } ); it( `should not fill in property declarations for properties that are already declared (such as if the utility is run against a typescript codebase), but should fill in any missing properties that are not declared`, () => { runTest( `${__dirname}/fixture/typescript-class` ); } ); it( `should handle 'var this = that' by adding 'that.xyz' as a class property declaration`, () => { runTest( `${__dirname}/fixture/function-expressions-and-declarations` ); } ); it( `should make function parameters optional when call sites are found to supply fewer arguments than there are parameters`, () => { runTest( `${__dirname}/fixture/function-calls-with-fewer-args-than-params` ); } ); it( `should not require node_modules to be installed in order to convert a codebase`, () => { runTest( `${__dirname}/fixture/superclass-subclass-node-modules-not-installed` ); } ); it( `should properly handle includePatterns and excludePatterns options`, () => { runTest( `${__dirname}/fixture/include-exclude-patterns`, { includePatterns: [ '**/included/**' ], excludePatterns: [ '**/included/excluded/**' ] } ); } ); it( `should properly convert a React .jsx file to .tsx`, () => { runTest( `${__dirname}/fixture/react-class-jsx` ); } ); it( `should properly convert a React .js file which has JSX within it to .tsx`, () => { runTest( `${__dirname}/fixture/react-class-js` ); } ); it( `should properly convert a React .js file which has only a self-closing JSX tag within it to .tsx (https://github.com/gregjacobs/js-to-ts-converter/issues/15), and also not error with a self-closing JSX element (https://github.com/gregjacobs/js-to-ts-converter/issues/4)`, () => { runTest( `${__dirname}/fixture/react-jsx-self-closing-element` ); } ); it( `should not do anything with a reference to this.constructor (https://github.com/gregjacobs/js-to-ts-converter/issues/9)`, () => { runTest( `${__dirname}/fixture/class-with-this-constructor-reference` ); } ); } ); /** * Runs a test of the conversion utility by passing it a directory that has * two subdirectories: * * - input * - expected * * The `input` directory will be converted, and then compared to the * `expected` directory. * * @param absolutePath Absolute path to the directory which has * `input` and `expected` subdirectories. * @param [inputFilesOptions] The options to configure the converter. */ function runTest( absolutePath: string, inputFilesOptions?: JsToTsConverterOptions ) { if( !fs.lstatSync( absolutePath ).isDirectory() ) { throw new Error( 'The absolute path: ' + absolutePath + ' is not a directory' ); } if( !fs.lstatSync( absolutePath + '/input' ).isDirectory() ) { throw new Error( 'The absolute path: ' + absolutePath + '/input is not a directory' ); } if( !fs.lstatSync( absolutePath + '/expected' ).isDirectory() ) { throw new Error( 'The absolute path: ' + absolutePath + '/expected is not a directory' ); } const inputFilesProject = createTsMorphProject( absolutePath + '/input', inputFilesOptions ); const expectedFilesProject = createTsMorphProject( absolutePath + '/expected' ); if( inputFilesProject.getSourceFiles().length === 0 ) { throw new Error( `No source files were found in the input directory: ${absolutePath}/input` ); } const convertedInputProject = convert( inputFilesProject ); const convertedSourceFiles = convertedInputProject.getSourceFiles(); const expectedSourceFiles = expectedFilesProject.getSourceFiles(); const convertedSourceFilePaths = convertedInputProject.getSourceFiles().map( sf => sf.getFilePath() ); const expectedSourceFilePaths = expectedFilesProject.getSourceFiles().map( sf => sf.getFilePath() ); // First, make sure that there are the same number of files in the converted // and expected projects if( convertedSourceFiles.length !== expectedSourceFiles.length ) { throw new Error( ` The number of converted source files (${convertedSourceFiles.length}) does not match the number of expected source files (${expectedSourceFiles.length}). Converted source files: ${convertedSourceFilePaths.join( '\n ' )} Expected source files: ${expectedSourceFilePaths.join( '\n ' )} `.replace( /^\t*/gm, '' ) ) } // Now check each converted source file against the expected output file convertedSourceFiles.forEach( ( convertedSourceFile: SourceFile ) => { const expectedSourceFilePath = convertedSourceFile.getFilePath().replace( /([\\\/])input[\\\/]/, '$1expected$1' ); const expectedSourceFile = expectedFilesProject.getSourceFile( expectedSourceFilePath ); if( !expectedSourceFile ) { throw new Error( ` The converted source file (below) does not have a matching 'expected' file: '${convertedSourceFile.getFilePath()}' Tried to find matching expected file: '${expectedSourceFilePath}' `.replace( /^\t*/gm, '' ) ); } expect( convertedSourceFile.getFullText() ) .to.equal( expectedSourceFile!.getFullText() ); } ); } ================================================ FILE: test/fixture/class-with-this-constructor-reference/expected/my-class.ts ================================================ export class MyClass { public name: any; constructor() {} myMethod() { this.name = this.constructor.name; } } ================================================ FILE: test/fixture/class-with-this-constructor-reference/input/my-class.js ================================================ export class MyClass { constructor() {} myMethod() { this.name = this.constructor.name; } } ================================================ FILE: test/fixture/expression-extends/expected/expression-extends.ts ================================================ class ExpressionExtends extends Mixin.mix( SomeClass1, SomeClass2 ) { public someProp: any; constructor() { this.someProp = 1; } } ================================================ FILE: test/fixture/expression-extends/input/expression-extends.js ================================================ class ExpressionExtends extends Mixin.mix( SomeClass1, SomeClass2 ) { constructor() { this.someProp = 1; } } ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/call-to-exported-function.ts ================================================ import { myExportedFunction } from "./exported-function"; myExportedFunction(); // no args - should mark all as optional ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/call-to-local-function-with-default-value.ts ================================================ function threeArg( arg1, arg2 = 1, arg3 = 2 ) { } threeArg( 1 ); ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/call-to-local-function.ts ================================================ function threeArg( arg1, arg2?, arg3? ) { } threeArg( 1 ); ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/call-to-sub-class-method.ts ================================================ import { SubClass } from "./sub-class"; const subClass = new SubClass(); subClass.subclassMethod( 1, 2 ); // should *not* mark any args as optional subClass.subclassMethod2(); // should mark both arg1 and arg2 as optional ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/call-to-super-class-method.ts ================================================ import { SuperClass } from "./super-class"; const superclass = new SuperClass( 1 ); // marks arg2 and arg3 as optional superclass.somePublicMethod( 1 ); // marks arg2 as optional ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/constructor-with-rest-param.ts ================================================ class ConstructorWithRestParam { constructor( ...args ) { // should *not* be marked as optional } methodWithRestParam( ...args ) {} // should *not* be marked as optional } const instance = new ConstructorWithRestParam(); instance.methodWithRestParam(); ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/exported-function.ts ================================================ export function myExportedFunction( arg1?, arg2? ) { // arg1 and arg2 made optional by call-to-exported-function.js } ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/sub-class.ts ================================================ import { SuperClass } from "./super-class"; export class SubClass extends SuperClass { subclassMethod( arg1, arg2 ) { // these should *not* be made optional by the call in call-to-sub-class-method.js // call superclass method this.superclassMethod(); // marks the arg as optional } subclassMethod2( arg1?, arg2? ) { // these should both be made optional by the call in call-to-sub-class-method.js } } ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/expected/super-class.ts ================================================ export class SuperClass { constructor( arg1, arg2?, arg3? ) { // arg2 and arg3 will be marked optional by call-to-superclass-method.js } superclassMethod( arg? ) { // arg will be marked optional by sub-class.js } somePublicMethod( arg1, arg2? ) { // arg2 will be marked optional by call-to-class-method.js } } ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/call-to-exported-function.js ================================================ import { myExportedFunction } from "./exported-function"; myExportedFunction(); // no args - should mark all as optional ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/call-to-local-function-with-default-value.js ================================================ function threeArg( arg1, arg2 = 1, arg3 = 2 ) { } threeArg( 1 ); ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/call-to-local-function.js ================================================ function threeArg( arg1, arg2, arg3 ) { } threeArg( 1 ); ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/call-to-sub-class-method.js ================================================ import { SubClass } from "./sub-class"; const subClass = new SubClass(); subClass.subclassMethod( 1, 2 ); // should *not* mark any args as optional subClass.subclassMethod2(); // should mark both arg1 and arg2 as optional ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/call-to-super-class-method.js ================================================ import { SuperClass } from "./super-class"; const superclass = new SuperClass( 1 ); // marks arg2 and arg3 as optional superclass.somePublicMethod( 1 ); // marks arg2 as optional ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/constructor-with-rest-param.js ================================================ class ConstructorWithRestParam { constructor( ...args ) { // should *not* be marked as optional } methodWithRestParam( ...args ) {} // should *not* be marked as optional } const instance = new ConstructorWithRestParam(); instance.methodWithRestParam(); ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/exported-function.js ================================================ export function myExportedFunction( arg1, arg2 ) { // arg1 and arg2 made optional by call-to-exported-function.js } ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/sub-class.js ================================================ import { SuperClass } from "./super-class"; export class SubClass extends SuperClass { subclassMethod( arg1, arg2 ) { // these should *not* be made optional by the call in call-to-sub-class-method.js // call superclass method this.superclassMethod(); // marks the arg as optional } subclassMethod2( arg1, arg2 ) { // these should both be made optional by the call in call-to-sub-class-method.js } } ================================================ FILE: test/fixture/function-calls-with-fewer-args-than-params/input/super-class.js ================================================ export class SuperClass { constructor( arg1, arg2, arg3 ) { // arg2 and arg3 will be marked optional by call-to-superclass-method.js } superclassMethod( arg ) { // arg will be marked optional by sub-class.js } somePublicMethod( arg1, arg2 ) { // arg2 will be marked optional by call-to-class-method.js } } ================================================ FILE: test/fixture/function-expressions-and-declarations/expected/class-with-function-expressions.ts ================================================ class ClassWithFunctionExpressions { public destructured1: any; public destructured2: any; public prop1: any; public prop2: any; public innerAccessedProp: any; public blah: any; myMethod() { var that = this; var myFn1 = function() { that.prop1 = 1; } var myFn2 = function(a, b) { that.prop2 = 1; that['prop3'] = 2; } } myMethod2() { var self = this, somethingElse = 1; var myFn1 = function() { self.prop1 = 1; var myNestedFn = function() { self.innerAccessedProp = 2; } } } myMethod3() { var somethingElse = 1, me = this; var myFn1 = function() { me.prop1 = 1; } } destructuredThis() { // should simply not throw an error on this construct, while populating // these variables as PropertyDeclarations const { destructured1, destructured2 } = this; } complexMethodWhichCausesErrorInTsSimpleAstTransforms() { const that = this; that.blah.blah2.blah3 = 42; that.blah.blah2.blah3.blah4 = 43; // below is potentially another test to check, but above seems to // display the previous bug // // if( this.asdf ) { // _.someFn( that.asdf.asdf2, () => { // _.someOtherFn( that.blah.blah2.blah3, () => { // } ); // } ); // } // if( !this.something ) { // this.something = this.someOtherThing.fn( () => { // const abc = []; // // _.forEach(that.model.something.else, (a) => { // // if( asdf ) { // // that.model.something = 1; // // } else { // // that.model.somethingElse = 2; // // } // } ); // // that.model.something = 42; // _.set( that.model.something, 'abc', 'def' ); // that.somethingElse = 11; // } ); // } } } ================================================ FILE: test/fixture/function-expressions-and-declarations/input/class-with-function-expressions.js ================================================ class ClassWithFunctionExpressions { myMethod() { var that = this; var myFn1 = function() { that.prop1 = 1; } var myFn2 = function(a, b) { that.prop2 = 1; that['prop3'] = 2; } } myMethod2() { var self = this, somethingElse = 1; var myFn1 = function() { self.prop1 = 1; var myNestedFn = function() { self.innerAccessedProp = 2; } } } myMethod3() { var somethingElse = 1, me = this; var myFn1 = function() { me.prop1 = 1; } } destructuredThis() { // should simply not throw an error on this construct, while populating // these variables as PropertyDeclarations const { destructured1, destructured2 } = this; } complexMethodWhichCausesErrorInTsSimpleAstTransforms() { const that = this; that.blah.blah2.blah3 = 42; that.blah.blah2.blah3.blah4 = 43; // below is potentially another test to check, but above seems to // display the previous bug // // if( this.asdf ) { // _.someFn( that.asdf.asdf2, () => { // _.someOtherFn( that.blah.blah2.blah3, () => { // } ); // } ); // } // if( !this.something ) { // this.something = this.someOtherThing.fn( () => { // const abc = []; // // _.forEach(that.model.something.else, (a) => { // // if( asdf ) { // // that.model.something = 1; // // } else { // // that.model.somethingElse = 2; // // } // } ); // // that.model.something = 42; // _.set( that.model.something, 'abc', 'def' ); // that.somethingElse = 11; // } ); // } } } ================================================ FILE: test/fixture/include-exclude-patterns/expected/included/included-file.ts ================================================ class IncludedFile { public someProp: any; constructor() { this.someProp = 1; } } ================================================ FILE: test/fixture/include-exclude-patterns/input/included/excluded/excluded-file.js ================================================ class ExcludedFile { constructor() { this.someProp = 1; } } ================================================ FILE: test/fixture/include-exclude-patterns/input/included/included-file.js ================================================ class IncludedFile { constructor() { this.someProp = 1; } } ================================================ FILE: test/fixture/include-exclude-patterns/input/other-file-that-should-not-be-included.js ================================================ class OtherFileThatShouldNotBeIncluded {} ================================================ FILE: test/fixture/react-class-js/expected/react-class.tsx ================================================ import * as React from "react"; import PropTypes from "prop-types"; import { TableHead, TableRow, TableCell, TableSortLabel, Checkbox } from "@material-ui/core"; import { Draggable } from "react-beautiful-dnd"; class TableHeader extends React.Component { public props: any; renderHeader() { const mapArr = this.props.columns.filter(columnDef => !columnDef.hidden && !(columnDef.tableData.groupOrder > -1)) .map((columnDef, index) => (
{(columnDef.sort !== false && columnDef.sorting !== false && this.props.sorting) ? { const orderDirection = columnDef.tableData.id !== this.props.orderBy ? "asc" : this.props.orderDirection === "asc" ? "desc" : "asc"; this.props.onOrderChange(columnDef.tableData.id, orderDirection); }} > {(this.props.grouping && columnDef.field) ? {(provided) => (
{columnDef.title}
)}
: columnDef.title }
: columnDef.title }
{columnDef.filter &&
{columnDef.filter}
}
)); return mapArr; } renderActionsHeader() { const localization = { ...TableHeader.defaultProps.localization, ...this.props.localization }; return ( {localization.actions} ); } renderSelectionHeader() { return ( 0 && this.props.selectedCount < this.props.dataCount} checked={this.props.selectedCount === this.props.dataCount} onChange={(event, checked) => this.props.onAllSelected && this.props.onAllSelected(checked)} /> ); } render() { const headers = this.renderHeader(); if (this.props.hasSelection && this.props.dataCount) { headers.splice(0, 0, this.renderSelectionHeader()); } if (this.props.showActionsColumn) { if (this.props.actionsHeaderIndex >= 0) { let endPos = 0; if (this.props.hasSelection) { endPos = 1; } headers.splice(this.props.actionsHeaderIndex + endPos, 0, this.renderActionsHeader()); } else if (this.props.actionsHeaderIndex === -1) { headers.push(this.renderActionsHeader()); } } if (this.props.hasDetailPanel) { headers.splice(0, 0, ); } this.props.columns .filter(columnDef => columnDef.tableData.groupOrder > -1) .forEach(columnDef => { headers.splice(0, 0, ); }); return ( {headers} ); } } TableHeader.defaultProps = { dataCount: 0, hasSelection: false, headerStyle: {}, selectedCount: 0, sorting: true, localization: { actions: "Actions" }, orderBy: undefined, orderDirection: "asc", actionsHeaderIndex: 0 }; TableHeader.propTypes = { columns: PropTypes.array.isRequired, dataCount: PropTypes.number, hasDetailPanel: PropTypes.bool.isRequired, hasSelection: PropTypes.bool, headerStyle: PropTypes.object, localization: PropTypes.object, selectedCount: PropTypes.number, sorting: PropTypes.bool, onAllSelected: PropTypes.func, onOrderChange: PropTypes.func, orderBy: PropTypes.number, orderDirection: PropTypes.string, actionsHeaderIndex: PropTypes.number, showActionsColumn: PropTypes.bool, }; export default TableHeader; ================================================ FILE: test/fixture/react-class-js/input/react-class.js ================================================ import * as React from "react"; import PropTypes from "prop-types"; import { TableHead, TableRow, TableCell, TableSortLabel, Checkbox } from "@material-ui/core"; import { Draggable } from "react-beautiful-dnd"; class TableHeader extends React.Component { renderHeader() { const mapArr = this.props.columns.filter(columnDef => !columnDef.hidden && !(columnDef.tableData.groupOrder > -1)) .map((columnDef, index) => (
{(columnDef.sort !== false && columnDef.sorting !== false && this.props.sorting) ? { const orderDirection = columnDef.tableData.id !== this.props.orderBy ? "asc" : this.props.orderDirection === "asc" ? "desc" : "asc"; this.props.onOrderChange(columnDef.tableData.id, orderDirection); }} > {(this.props.grouping && columnDef.field) ? {(provided) => (
{columnDef.title}
)}
: columnDef.title }
: columnDef.title }
{columnDef.filter &&
{columnDef.filter}
}
)); return mapArr; } renderActionsHeader() { const localization = { ...TableHeader.defaultProps.localization, ...this.props.localization }; return ( {localization.actions} ); } renderSelectionHeader() { return ( 0 && this.props.selectedCount < this.props.dataCount} checked={this.props.selectedCount === this.props.dataCount} onChange={(event, checked) => this.props.onAllSelected && this.props.onAllSelected(checked)} /> ); } render() { const headers = this.renderHeader(); if (this.props.hasSelection && this.props.dataCount) { headers.splice(0, 0, this.renderSelectionHeader()); } if (this.props.showActionsColumn) { if (this.props.actionsHeaderIndex >= 0) { let endPos = 0; if (this.props.hasSelection) { endPos = 1; } headers.splice(this.props.actionsHeaderIndex + endPos, 0, this.renderActionsHeader()); } else if (this.props.actionsHeaderIndex === -1) { headers.push(this.renderActionsHeader()); } } if (this.props.hasDetailPanel) { headers.splice(0, 0, ); } this.props.columns .filter(columnDef => columnDef.tableData.groupOrder > -1) .forEach(columnDef => { headers.splice(0, 0, ); }); return ( {headers} ); } } TableHeader.defaultProps = { dataCount: 0, hasSelection: false, headerStyle: {}, selectedCount: 0, sorting: true, localization: { actions: "Actions" }, orderBy: undefined, orderDirection: "asc", actionsHeaderIndex: 0 }; TableHeader.propTypes = { columns: PropTypes.array.isRequired, dataCount: PropTypes.number, hasDetailPanel: PropTypes.bool.isRequired, hasSelection: PropTypes.bool, headerStyle: PropTypes.object, localization: PropTypes.object, selectedCount: PropTypes.number, sorting: PropTypes.bool, onAllSelected: PropTypes.func, onOrderChange: PropTypes.func, orderBy: PropTypes.number, orderDirection: PropTypes.string, actionsHeaderIndex: PropTypes.number, showActionsColumn: PropTypes.bool, }; export default TableHeader; ================================================ FILE: test/fixture/react-class-jsx/expected/react-class.tsx ================================================ import * as React from "react"; import PropTypes from "prop-types"; import { TableHead, TableRow, TableCell, TableSortLabel, Checkbox } from "@material-ui/core"; import { Draggable } from "react-beautiful-dnd"; class TableHeader extends React.Component { public props: any; renderHeader() { const mapArr = this.props.columns.filter(columnDef => !columnDef.hidden && !(columnDef.tableData.groupOrder > -1)) .map((columnDef, index) => (
{(columnDef.sort !== false && columnDef.sorting !== false && this.props.sorting) ? { const orderDirection = columnDef.tableData.id !== this.props.orderBy ? "asc" : this.props.orderDirection === "asc" ? "desc" : "asc"; this.props.onOrderChange(columnDef.tableData.id, orderDirection); }} > {(this.props.grouping && columnDef.field) ? {(provided) => (
{columnDef.title}
)}
: columnDef.title }
: columnDef.title }
{columnDef.filter &&
{columnDef.filter}
}
)); return mapArr; } renderActionsHeader() { const localization = { ...TableHeader.defaultProps.localization, ...this.props.localization }; return ( {localization.actions} ); } renderSelectionHeader() { return ( 0 && this.props.selectedCount < this.props.dataCount} checked={this.props.selectedCount === this.props.dataCount} onChange={(event, checked) => this.props.onAllSelected && this.props.onAllSelected(checked)} /> ); } render() { const headers = this.renderHeader(); if (this.props.hasSelection && this.props.dataCount) { headers.splice(0, 0, this.renderSelectionHeader()); } if (this.props.showActionsColumn) { if (this.props.actionsHeaderIndex >= 0) { let endPos = 0; if (this.props.hasSelection) { endPos = 1; } headers.splice(this.props.actionsHeaderIndex + endPos, 0, this.renderActionsHeader()); } else if (this.props.actionsHeaderIndex === -1) { headers.push(this.renderActionsHeader()); } } if (this.props.hasDetailPanel) { headers.splice(0, 0, ); } this.props.columns .filter(columnDef => columnDef.tableData.groupOrder > -1) .forEach(columnDef => { headers.splice(0, 0, ); }); return ( {headers} ); } } TableHeader.defaultProps = { dataCount: 0, hasSelection: false, headerStyle: {}, selectedCount: 0, sorting: true, localization: { actions: "Actions" }, orderBy: undefined, orderDirection: "asc", actionsHeaderIndex: 0 }; TableHeader.propTypes = { columns: PropTypes.array.isRequired, dataCount: PropTypes.number, hasDetailPanel: PropTypes.bool.isRequired, hasSelection: PropTypes.bool, headerStyle: PropTypes.object, localization: PropTypes.object, selectedCount: PropTypes.number, sorting: PropTypes.bool, onAllSelected: PropTypes.func, onOrderChange: PropTypes.func, orderBy: PropTypes.number, orderDirection: PropTypes.string, actionsHeaderIndex: PropTypes.number, showActionsColumn: PropTypes.bool, }; export default TableHeader; ================================================ FILE: test/fixture/react-class-jsx/input/react-class.jsx ================================================ import * as React from "react"; import PropTypes from "prop-types"; import { TableHead, TableRow, TableCell, TableSortLabel, Checkbox } from "@material-ui/core"; import { Draggable } from "react-beautiful-dnd"; class TableHeader extends React.Component { renderHeader() { const mapArr = this.props.columns.filter(columnDef => !columnDef.hidden && !(columnDef.tableData.groupOrder > -1)) .map((columnDef, index) => (
{(columnDef.sort !== false && columnDef.sorting !== false && this.props.sorting) ? { const orderDirection = columnDef.tableData.id !== this.props.orderBy ? "asc" : this.props.orderDirection === "asc" ? "desc" : "asc"; this.props.onOrderChange(columnDef.tableData.id, orderDirection); }} > {(this.props.grouping && columnDef.field) ? {(provided) => (
{columnDef.title}
)}
: columnDef.title }
: columnDef.title }
{columnDef.filter &&
{columnDef.filter}
}
)); return mapArr; } renderActionsHeader() { const localization = { ...TableHeader.defaultProps.localization, ...this.props.localization }; return ( {localization.actions} ); } renderSelectionHeader() { return ( 0 && this.props.selectedCount < this.props.dataCount} checked={this.props.selectedCount === this.props.dataCount} onChange={(event, checked) => this.props.onAllSelected && this.props.onAllSelected(checked)} /> ); } render() { const headers = this.renderHeader(); if (this.props.hasSelection && this.props.dataCount) { headers.splice(0, 0, this.renderSelectionHeader()); } if (this.props.showActionsColumn) { if (this.props.actionsHeaderIndex >= 0) { let endPos = 0; if (this.props.hasSelection) { endPos = 1; } headers.splice(this.props.actionsHeaderIndex + endPos, 0, this.renderActionsHeader()); } else if (this.props.actionsHeaderIndex === -1) { headers.push(this.renderActionsHeader()); } } if (this.props.hasDetailPanel) { headers.splice(0, 0, ); } this.props.columns .filter(columnDef => columnDef.tableData.groupOrder > -1) .forEach(columnDef => { headers.splice(0, 0, ); }); return ( {headers} ); } } TableHeader.defaultProps = { dataCount: 0, hasSelection: false, headerStyle: {}, selectedCount: 0, sorting: true, localization: { actions: "Actions" }, orderBy: undefined, orderDirection: "asc", actionsHeaderIndex: 0 }; TableHeader.propTypes = { columns: PropTypes.array.isRequired, dataCount: PropTypes.number, hasDetailPanel: PropTypes.bool.isRequired, hasSelection: PropTypes.bool, headerStyle: PropTypes.object, localization: PropTypes.object, selectedCount: PropTypes.number, sorting: PropTypes.bool, onAllSelected: PropTypes.func, onOrderChange: PropTypes.func, orderBy: PropTypes.number, orderDirection: PropTypes.string, actionsHeaderIndex: PropTypes.number, showActionsColumn: PropTypes.bool, }; export default TableHeader; ================================================ FILE: test/fixture/react-jsx-self-closing-element/expected/react-self-closing-element.tsx ================================================ import * as React from "react"; /** * This example makes sure that .js is converted to .tsx when the only JSX * element in the file is self-closing */ export const MyComponent = () => { return
; }; ================================================ FILE: test/fixture/react-jsx-self-closing-element/input/react-self-closing-element.js ================================================ import * as React from "react"; /** * This example makes sure that .js is converted to .tsx when the only JSX * element in the file is self-closing */ export const MyComponent = () => { return
; }; ================================================ FILE: test/fixture/superclass-subclass/expected/another-sub-class.ts ================================================ import DefaultExportClass from "./default-export-class"; export class AnotherSubClass extends DefaultExportClass { public anotherSubClassProp: any; constructor() { this.defaultExportClassProp = 45; // from superclass this.anotherSubClassProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass/expected/default-export-class.ts ================================================ class DefaultExportClass { public defaultExportClassProp: any; constructor() { this.defaultExportClassProp = 1; } } export default DefaultExportClass; ================================================ FILE: test/fixture/superclass-subclass/expected/my-class.ts ================================================ import { MySuperClass } from './my-super-class'; export class MyClass extends MySuperClass { public myClassProp1: any; public myClassProp2: any; public myClassProp3: any; constructor() { this.mySuperClassProp = 99; this.myClassProp1 = 42; this.doSomething(); // should not become a property this.mySuperclassMethod(); // should not become a property as it is a method in the superclass } doSomething() { this.myClassProp2 = 78; console.log( this.myClassProp3 ); } } ================================================ FILE: test/fixture/superclass-subclass/expected/my-sub-class.ts ================================================ import { MyClass } from "./my-class"; export class MySubClass extends MyClass { public mySubClassProp: any; constructor() { this.mySuperClassProp = 42; // from superclass's superclass - should not be added as a prop this.myClassProp1 = 43; // from superclass - should not be added as a prop this.mySubClassProp = 1; this.mySuperclassMethod(); // should not be added as a property as it exists two superclasses up } } ================================================ FILE: test/fixture/superclass-subclass/expected/my-super-class.ts ================================================ export class MySuperClass { public mySuperClassProp: any; constructor() { this.mySuperclassMethod(); // should not be added as a property } mySuperclassMethod() { this.mySuperClassProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass/expected/superclass-in-node-modules.ts ================================================ import { Subject } from 'rxjs'; export class MySubClassWithSuperClassInNodeModules extends Subject { public myProp: any; mySuperclassMethod() { this.myProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass/expected/two-classes.ts ================================================ class Super { public superProp: any; someMethod() { this.superProp = 1; } } class Sub extends Super { public subProp: any; someMethod() { this.superProp = 2; this.subProp = 2; } } ================================================ FILE: test/fixture/superclass-subclass/input/another-sub-class.js ================================================ import DefaultExportClass from "./default-export-class"; export class AnotherSubClass extends DefaultExportClass { constructor() { this.defaultExportClassProp = 45; // from superclass this.anotherSubClassProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass/input/default-export-class.js ================================================ class DefaultExportClass { constructor() { this.defaultExportClassProp = 1; } } export default DefaultExportClass; ================================================ FILE: test/fixture/superclass-subclass/input/my-class.js ================================================ import { MySuperClass } from './my-super-class'; export class MyClass extends MySuperClass { constructor() { this.mySuperClassProp = 99; this.myClassProp1 = 42; this.doSomething(); // should not become a property this.mySuperclassMethod(); // should not become a property as it is a method in the superclass } doSomething() { this.myClassProp2 = 78; console.log( this.myClassProp3 ); } } ================================================ FILE: test/fixture/superclass-subclass/input/my-sub-class.js ================================================ import { MyClass } from "./my-class"; export class MySubClass extends MyClass { constructor() { this.mySuperClassProp = 42; // from superclass's superclass - should not be added as a prop this.myClassProp1 = 43; // from superclass - should not be added as a prop this.mySubClassProp = 1; this.mySuperclassMethod(); // should not be added as a property as it exists two superclasses up } } ================================================ FILE: test/fixture/superclass-subclass/input/my-super-class.js ================================================ export class MySuperClass { constructor() { this.mySuperclassMethod(); // should not be added as a property } mySuperclassMethod() { this.mySuperClassProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass/input/package.json ================================================ { "devDependencies": {}, "dependencies": { "rxjs": "^6.2.2" } } ================================================ FILE: test/fixture/superclass-subclass/input/superclass-in-node-modules.js ================================================ import { Subject } from 'rxjs'; export class MySubClassWithSuperClassInNodeModules extends Subject { mySuperclassMethod() { this.myProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass/input/two-classes.js ================================================ class Super { someMethod() { this.superProp = 1; } } class Sub extends Super { someMethod() { this.superProp = 2; this.subProp = 2; } } ================================================ FILE: test/fixture/superclass-subclass-node-modules-not-installed/expected/superclass-in-node-modules.ts ================================================ import { SomeSuperclass } from 'some-not-installed-module'; export class MyClassWithSuperClassInNodeModules extends SomeSuperclass { public myProp: any; myMethod() { this.myProp = 10; } } ================================================ FILE: test/fixture/superclass-subclass-node-modules-not-installed/input/superclass-in-node-modules.js ================================================ import { SomeSuperclass } from 'some-not-installed-module'; export class MyClassWithSuperClassInNodeModules extends SomeSuperclass { myMethod() { this.myProp = 10; } } ================================================ FILE: test/fixture/typescript-class/expected/declarations-in-superclass.ts ================================================ class SuperTypeScriptClass { public superProp: any; // *declaration* that should not be added to subclass } class SubTypeScriptClass extends SuperTypeScriptClass { public subProp: any; constructor() { super(); this.superProp = 1; // should not be added as a declaration in this class this.subProp = 2; // *should* be filled in as it is currently missing in this class and its superclass } } class SubSubTypeScriptClass extends SubTypeScriptClass { constructor() { super(); this.superProp = 1; // should not be added as a declaration in this class as it is declared 2 superclasses up } } ================================================ FILE: test/fixture/typescript-class/expected/typescript-class.ts ================================================ export class TypescriptClass { public prop: any; // shouldn't be duplicated constructor() { this.prop = 1; } } ================================================ FILE: test/fixture/typescript-class/expected/typescript-sub-class.ts ================================================ import { TypescriptClass } from "./typescript-class"; export class TypescriptSubClass extends TypescriptClass { public prop2: any; constructor() { super(); this.prop2 = 1; } } ================================================ FILE: test/fixture/typescript-class/input/declarations-in-superclass.ts ================================================ class SuperTypeScriptClass { public superProp: any; // *declaration* that should not be added to subclass } class SubTypeScriptClass extends SuperTypeScriptClass { constructor() { super(); this.superProp = 1; // should not be added as a declaration in this class this.subProp = 2; // *should* be filled in as it is currently missing in this class and its superclass } } class SubSubTypeScriptClass extends SubTypeScriptClass { constructor() { super(); this.superProp = 1; // should not be added as a declaration in this class as it is declared 2 superclasses up } } ================================================ FILE: test/fixture/typescript-class/input/typescript-class.ts ================================================ export class TypescriptClass { public prop: any; // shouldn't be duplicated constructor() { this.prop = 1; } } ================================================ FILE: test/fixture/typescript-class/input/typescript-sub-class.ts ================================================ import { TypescriptClass } from "./typescript-class"; export class TypescriptSubClass extends TypescriptClass { constructor() { super(); this.prop2 = 1; } } ================================================ FILE: test.ts ================================================ import { Project } from "ts-morph"; const tsAstProject = new Project(); const sourceFile = tsAstProject.createSourceFile('testfile.js', getSourceText()); // Just calling the below method is what causes the problem when moving later. // If this line is commented out, the move succeeds. sourceFile.getClass( 'TableHeader' )!; sourceFile.move('testfile.tsx'); function getSourceText() { return ` class TableHeader extends React.Component { renderHeader() { const mapArr = this.props.columns .map(columnDef => (
{(columnDef.sort !== false) ? 'test' : 'title' }
)); return mapArr; } } `; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Basic Options */ "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "skipLibCheck": true, // "lib": [], /* Specify library files to be included in the compilation: */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": true, /* Generates corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ "outDir": "./dist", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ "esModuleInterop": true, /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ /* Source Map Options */ // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }, "include": [ "src/**/*.ts" ] }