diff --git a/.gitignore b/.gitignore index af8a6a6..b899a48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist .DS_Store -coverage/ \ No newline at end of file +coverage/ +.log diff --git a/.prettierignore b/.prettierignore index 2d62f68..6835724 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ test/ dist/ +!test/transformers.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 23883b1..fff8a01 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,10 +7,10 @@ "env": { "NODE_ENV": "test" }, - "externalConsole": false, + "console": "internalConsole", "name": "Run Tests", - "outDir": "${workspaceRoot}/dist", - "preLaunchTask": "compile", + "outFiles": ["${workspaceRoot}/dist"], + "preLaunchTask": "tsc", "program": "${workspaceRoot}/node_modules/.bin/jest", "request": "launch", "runtimeArgs": [], diff --git a/package.json b/package.json index 4a3110f..af75515 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "transform": { ".ts": "/node_modules/ts-jest/preprocessor.js" }, - "testRegex": "test/runner.ts", + "testRegex": "(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", "moduleFileExtensions": ["ts", "js"] }, "lint-staged": { @@ -34,22 +34,26 @@ "dependencies": { "chalk": "^1.1.3", "commander": "^2.10.0", + "detect-indent": "^5.0.0", "glob": "^7.1.2", "lodash": "^4.17.4", + "prettier": "^1.10.2", "typescript": "^2.6.2" }, "devDependencies": { "@types/chalk": "^0.4.31", "@types/commander": "^2.9.1", + "@types/detect-indent": "^5.0.0", "@types/glob": "^5.0.30", "@types/jest": "^20.0.2", "@types/lodash": "^4.14.93", "@types/node": "^8.0.2", + "@types/prettier": "^1.10.0", "@types/react": "^15.0.31", + "dedent": "^0.7.0", "husky": "^0.14.3", "jest": "^20.0.4", "lint-staged": "^6.0.1", - "prettier": "^1.10.2", "ts-jest": "^20.0.6", "ts-node": "^3.1.0", "tslint": "^5.2.0" diff --git a/src/cli.ts b/src/cli.ts index 6cc2031..ed73b9b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,11 +4,22 @@ import * as program from 'commander'; import * as glob from 'glob'; import * as fs from 'fs'; import * as path from 'path'; +import * as prettier from 'prettier'; import { run } from '.'; program .version('1.0.0') + .option('--arrow-parens ', 'Include parentheses around a sole arrow function parameter.', 'avoid') + .option('--no-bracket-spacing', 'Do not print spaces between brackets.', false) + .option('--jsx-bracket-same-line', 'Put > on the last line instead of at a new line.', false) + .option('--print-width ', 'The line length where Prettier will try wrap.', 80) + .option('--prose-wrap How to wrap prose. (markdown)', 'preserve') + .option('--no-semi', 'Do not print semicolons, except at the beginning of lines which may need them', false) + .option('--single-quote', 'Use single quotes instead of double quotes.', false) + .option('--tab-width ', 'Number of spaces per indentation level.', 2) + .option('--trailing-comma ', 'Print trailing commas wherever possible when multi-line.', 'none') + .option('--use-tabs', 'Indent with tabs instead of spaces.', false) .usage('[options] ') .command('* ') .action(globPattern => { @@ -22,7 +33,19 @@ program try { fs.renameSync(filePath, newPath); - const result = run(newPath); + const prettierOptions: prettier.Options = { + arrowParens: program.arrowParens, + bracketSpacing: !program.noBracketSpacing, + jsxBracketSameLine: !!program.jsxBracketSameLine, + printWidth: parseInt(program.printWidth, 10), + proseWrap: program.proseWrap, + semi: !program.noSemi, + singleQuote: !!program.singleQuote, + tabWidth: parseInt(program.tabWidth, 10), + trailingComma: program.trailingComma, + useTabs: !!program.useTabs, + }; + const result = run(newPath, prettierOptions); fs.writeFileSync(newPath, result); } catch (error) { console.warn(`Failed to convert ${file}`); diff --git a/src/compiler.ts b/src/compiler.ts index acfce10..a13ebac 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,5 +1,10 @@ +import * as os from 'os'; +import * as fs from 'fs'; import * as ts from 'typescript'; import * as chalk from 'chalk'; +import * as _ from 'lodash'; +import * as prettier from 'prettier'; +import * as detectIndent from 'detect-indent'; import { TransformFactoryFactory } from '.'; @@ -7,7 +12,11 @@ import { TransformFactoryFactory } from '.'; * Compile and return result TypeScript * @param filePath Path to file to compile */ -export function compile(filePath: string, factoryFactories: TransformFactoryFactory[]) { +export function compile( + filePath: string, + factoryFactories: TransformFactoryFactory[], + incomingPrettierOptions: prettier.Options = {}, +) { const compilerOptions: ts.CompilerOptions = { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.ES2015, @@ -42,5 +51,70 @@ export function compile(filePath: string, factoryFactories: TransformFactoryFact const printer = ts.createPrinter(); // TODO: fix the index 0 access... What if program have multiple source files? - return printer.printNode(ts.EmitHint.SourceFile, result.transformed[0], sourceFiles[0]); + const printed = printer.printNode(ts.EmitHint.SourceFile, result.transformed[0], sourceFiles[0]); + + const inputSource = fs.readFileSync(filePath, 'utf-8'); + const prettierOptions = getPrettierOptions(filePath, inputSource, incomingPrettierOptions); + + return prettier.format(printed, incomingPrettierOptions); +} + +/** + * Get Prettier options based on style of a JavaScript + * @param filePath Path to source file + * @param source Body of a JavaScript + * @param options Existing prettier option + */ +export function getPrettierOptions(filePath: string, source: string, options: prettier.Options): prettier.Options { + const resolvedOptions = prettier.resolveConfig.sync(filePath); + if (resolvedOptions) { + _.defaults(resolvedOptions, options); + return resolvedOptions; + } + const { amount: indentAmount, type: indentType } = detectIndent(source); + const sourceWidth = getCodeWidth(source, 80); + const semi = getUseOfSemi(source); + const quotations = getQuotation(source); + + _.defaults(options, { + tabWidth: indentAmount, + useTabs: indentType && indentType === 'tab', + printWidth: sourceWidth, + semi, + singleQuote: quotations === 'single', + }); + + return options; +} + +/** + * Given body of a source file, return its code width + * @param source + */ +function getCodeWidth(source: string, defaultWidth: number): number { + return source.split(os.EOL).reduce((result, line) => Math.max(result, line.length), defaultWidth); +} + +/** + * Detect if a source file is using semicolon + * @todo: use an actual parser. This is not a proper implementation + * @param source + * @return true if code is using semicolons + */ +function getUseOfSemi(source: string): boolean { + return source.indexOf(';') !== -1; +} + +/** + * Detect if a source file is using single quotes or double quotes + * @todo use an actual parser. This is not a proper implementation + * @param source + */ +function getQuotation(source: string): 'single' | 'double' { + const numberOfSingleQuotes = (source.match(/\'/g) || []).length; + const numberOfDoubleQuotes = (source.match(/\"/g) || []).length; + if (numberOfSingleQuotes > numberOfDoubleQuotes) { + return 'single'; + } + return 'double'; } diff --git a/src/index.ts b/src/index.ts index 2901b7f..831dbd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import * as ts from 'typescript'; +import * as prettier from 'prettier'; import { compile } from './compiler'; import { reactJSMakePropsAndStateInterfaceTransformFactoryFactory } from './transforms/react-js-make-props-and-state-transform'; @@ -36,6 +37,6 @@ export type TransformFactoryFactory = (typeChecker: ts.TypeChecker) => ts.Transf * Run React JavaScript to TypeScript transform for file at `filePath` * @param filePath */ -export function run(filePath: string): string { - return compile(filePath, allTransforms); +export function run(filePath: string, prettierOptions: prettier.Options = {}): string { + return compile(filePath, allTransforms, prettierOptions); } diff --git a/src/untyped-modules.d.ts b/src/untyped-modules.d.ts new file mode 100644 index 0000000..f187021 --- /dev/null +++ b/src/untyped-modules.d.ts @@ -0,0 +1 @@ +declare module 'dedent'; diff --git a/test/collapse-intersection-interfaces-transform/advanced/output.tsx b/test/collapse-intersection-interfaces-transform/advanced/output.tsx index a272d5a..a579223 100644 --- a/test/collapse-intersection-interfaces-transform/advanced/output.tsx +++ b/test/collapse-intersection-interfaces-transform/advanced/output.tsx @@ -1,7 +1,7 @@ type Foo = { - foo: string; - stuff: boolean; - other: () => void; - bar: number; - [key: string]: number; + foo: string, + stuff: boolean, + other: () => void, + bar: number, + [key: string]: number, }; diff --git a/test/collapse-intersection-interfaces-transform/empty-empty/output.tsx b/test/collapse-intersection-interfaces-transform/empty-empty/output.tsx index ad4c7b6..579668f 100644 --- a/test/collapse-intersection-interfaces-transform/empty-empty/output.tsx +++ b/test/collapse-intersection-interfaces-transform/empty-empty/output.tsx @@ -1,2 +1 @@ -type Foo = { -}; +type Foo = {}; diff --git a/test/collapse-intersection-interfaces-transform/multiple/output.tsx b/test/collapse-intersection-interfaces-transform/multiple/output.tsx index cd722f1..f6d0c1d 100644 --- a/test/collapse-intersection-interfaces-transform/multiple/output.tsx +++ b/test/collapse-intersection-interfaces-transform/multiple/output.tsx @@ -1,8 +1,8 @@ type Foo = { - foo: string; - bar: number; + foo: string, + bar: number, }; type Bar = { - foo: number; - bar: string; + foo: number, + bar: string, }; diff --git a/test/collapse-intersection-interfaces-transform/simple/output.tsx b/test/collapse-intersection-interfaces-transform/simple/output.tsx index 97d43d9..d4374e3 100644 --- a/test/collapse-intersection-interfaces-transform/simple/output.tsx +++ b/test/collapse-intersection-interfaces-transform/simple/output.tsx @@ -1,4 +1,4 @@ type Foo = { - foo: string; - bar: number; + foo: string, + bar: number, }; diff --git a/test/end-to-end/basic/output.tsx b/test/end-to-end/basic/output.tsx index b2d81de..c98cfbd 100644 --- a/test/end-to-end/basic/output.tsx +++ b/test/end-to-end/basic/output.tsx @@ -1,7 +1,5 @@ import * as React from 'react'; -export default class MyComponent extends React.Component<{ - }, { - }> { +export default class MyComponent extends React.Component<{}, {}> { render() { return
; } diff --git a/test/end-to-end/initial-state-and-proprypes-and-set-state/output.tsx b/test/end-to-end/initial-state-and-proprypes-and-set-state/output.tsx index b120653..95fae0f 100644 --- a/test/end-to-end/initial-state-and-proprypes-and-set-state/output.tsx +++ b/test/end-to-end/initial-state-and-proprypes-and-set-state/output.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; type MyComponentProps = { - baz: string; + baz: string, }; type MyComponentState = { - dynamicState: number; - foo: number; - bar: string; + dynamicState: number, + foo: number, + bar: string, }; export default class MyComponent extends React.Component { state = { foo: 1, bar: 'str' }; diff --git a/test/end-to-end/initial-state-and-proprypes/output.tsx b/test/end-to-end/initial-state-and-proprypes/output.tsx index 93ed422..912e3b8 100644 --- a/test/end-to-end/initial-state-and-proprypes/output.tsx +++ b/test/end-to-end/initial-state-and-proprypes/output.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; type MyComponentProps = { - baz: string; + baz: string, }; type MyComponentState = { - foo: number; - bar: string; + foo: number, + bar: string, }; export default class MyComponent extends React.Component { state = { foo: 1, bar: 'str' }; diff --git a/test/end-to-end/multiple-components/output.tsx b/test/end-to-end/multiple-components/output.tsx index 4154dd3..5495538 100644 --- a/test/end-to-end/multiple-components/output.tsx +++ b/test/end-to-end/multiple-components/output.tsx @@ -1,33 +1,31 @@ type HelloProps = { - message?: string; + message?: string, }; const Hello: React.SFC = ({ message }) => { return
hello {message}
; }; type HeyProps = { - message?: string; + message?: string, }; const Hey: React.SFC = ({ name }) => { return
hey, {name}
; }; type MyComponentState = { - foo: number; - bar: number; + foo: number, + bar: number, }; -export default class MyComponent extends React.Component<{ - }, MyComponentState> { +export default class MyComponent extends React.Component<{}, MyComponentState> { render() { - return