├── .babelrc ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── getting_started.md └── issue_template.md ├── index.js.flow ├── package.json ├── src ├── __tests__ │ ├── __snapshots__ │ │ ├── basic.spec.ts.snap │ │ ├── bigint-literals.spec.ts.snap │ │ ├── boolean-literals.spec.ts.snap │ │ ├── classes.spec.ts.snap │ │ ├── computed.spec.ts.snap │ │ ├── conditional.spec.ts.snap │ │ ├── declaration-file.spec.ts.snap │ │ ├── duplicated-names.spec.ts.snap │ │ ├── enums.spec.ts.snap │ │ ├── exports.spec.ts.snap │ │ ├── function-exports.spec.ts.snap │ │ ├── global-declares.spec.ts.snap │ │ ├── global-this.spec.ts.snap │ │ ├── import.spec.ts.snap │ │ ├── imports.spec.ts.snap │ │ ├── indexers.spec.ts.snap │ │ ├── interface-exports.spec.ts.snap │ │ ├── interfaces.spec.ts.snap │ │ ├── jsdoc.spec.ts.snap │ │ ├── mapped-types.spec.ts.snap │ │ ├── module-identifiers.spec.ts.snap │ │ ├── modules.spec.ts.snap │ │ ├── namespaces.spec.ts.snap │ │ ├── spread.spec.ts.snap │ │ ├── string-literals.spec.ts.snap │ │ ├── tuples.spec.ts.snap │ │ ├── type-exports.spec.ts.snap │ │ ├── union-strings.spec.ts.snap │ │ ├── utility-types.spec.ts.snap │ │ ├── value-exports.spec.ts.snap │ │ └── variables.spec.ts.snap │ ├── basic.spec.ts │ ├── bigint-literals.spec.ts │ ├── boolean-literals.spec.ts │ ├── classes.spec.ts │ ├── computed.spec.ts │ ├── conditional.spec.ts │ ├── declaration-file.spec.ts │ ├── duplicated-names.spec.ts │ ├── enums.spec.ts │ ├── exports.spec.ts │ ├── function-exports.spec.ts │ ├── global-declares.spec.ts │ ├── global-this.spec.ts │ ├── import.spec.ts │ ├── imports.spec.ts │ ├── indexers.spec.ts │ ├── interface-exports.spec.ts │ ├── interfaces.spec.ts │ ├── jsdoc.spec.ts │ ├── mapped-types.spec.ts │ ├── module-identifiers.spec.ts │ ├── modules.spec.ts │ ├── namespaces.spec.ts │ ├── snippet │ │ ├── export-enum-file.ts │ │ ├── import-enum-file.ts │ │ ├── import-enum-type-file.ts │ │ ├── import-import-type.ts │ │ └── import-type.ts │ ├── spread.spec.ts │ ├── string-literals.spec.ts │ ├── tuples.spec.ts │ ├── type-exports.spec.ts │ ├── union-strings.spec.ts │ ├── utility-types.spec.ts │ ├── value-exports.spec.ts │ └── variables.spec.ts ├── checker.ts ├── cli │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── compiler.spec.ts.snap │ │ │ └── fixtures.spec.ts.snap │ │ ├── compiler.spec.ts │ │ ├── fixtures.spec.ts │ │ └── fixtures │ │ │ └── danger.d.ts │ ├── beautifier.ts │ ├── compiler.ts │ ├── default-exporter.ts │ ├── flow-typed-exporter.ts │ ├── index.ts │ ├── meta.ts │ ├── runner.h.ts │ ├── runner.ts │ └── util.ts ├── env.ts ├── errors │ └── error-message.ts ├── index.ts ├── logger.ts ├── namespace-manager.ts ├── nodename.ts ├── nodes │ ├── export-declaration.ts │ ├── export.ts │ ├── factory.ts │ ├── import.ts │ ├── module.ts │ ├── namespace.ts │ ├── node.ts │ └── property.ts ├── options.ts ├── parse │ ├── ast.ts │ ├── index.ts │ └── transformers.ts ├── printers │ ├── basics.ts │ ├── common.ts │ ├── declarations.ts │ ├── function.ts │ ├── identifiers.ts │ ├── index.ts │ ├── node.ts │ ├── relationships.ts │ ├── smart-identifiers.ts │ └── unionNode.ts ├── test-matchers.ts └── typescript-internals.ts ├── tsconfig.declaration.json ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "loose": true, 7 | "targets": { 8 | "node": "10" 9 | } 10 | } 11 | ], 12 | ["@babel/preset-typescript", { "loose": true }] 13 | ], 14 | "plugins": [["@babel/plugin-proposal-class-properties", { "loose": true }]] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /flow-typed/npm/**/*.js 2 | /node_modules 3 | /lib 4 | /src/cli/__tests__/fixtures/**/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "plugin:prettier/recommended", 7 | "plugin:jest/recommended" 8 | ], 9 | "plugins": ["jest"], 10 | "env": { 11 | "es6": true, 12 | "node": true 13 | }, 14 | "rules": { 15 | "no-redeclare": "off", 16 | "no-console": "off", 17 | "@typescript-eslint/no-unused-vars": [ 18 | 2, 19 | { 20 | "vars": "all", 21 | "args": "after-used", 22 | "argsIgnorePattern": "^_", 23 | "ignoreRestSiblings": true 24 | } 25 | ], 26 | "@typescript-eslint/no-use-before-define": 0, 27 | "@typescript-eslint/ban-types": 0, 28 | "@typescript-eslint/ban-ts-comment": 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | tester/node_modules 4 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib/__tests__/* 2 | node_modules 3 | lib 4 | tester/node_modules 5 | .idea 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | 6 | script: 7 | - yarn run lint && yarn tsc && yarn run test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Joar Wilk (joar.wilk@gmail.com) 2 | 3 | This project is free software released under the MIT license: 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flowgen 2 | 3 | ## The state of the converter 4 | It's surprisingly robust and non-lossy as it stands right now, in big part thanks to how similar flow and typescript definition files are. Please see the output in [this flow-typed PR](https://github.com/flowtype/flow-typed/pull/590) for the state of the output. 5 | 6 | | Supported? | Syntax | TypeScript | Flow | 7 | |---|---|---|---| 8 | | ✅ | Void type | `void` | `void` | 9 | | ✅ | Undefined type | `undefined` | `void` | 10 | | ✅ | Unknown type | `unknown` | `mixed` | 11 | | ✅ | Symbol type | `symbol` | `Symbol` | 12 | | | Unique symbol type | `unique symbol` | `Symbol` | 13 | | ✅ | Object type | `object` | `{[key: string]: any}` | 14 | | ✅ | Never type | `never` | `empty` | 15 | | ✅ | Variance | `interface A { readonly b: B, c: C }` | `interface A { +b: B, c: C }` | 16 | | ✅ | Functions | `(a: A, b: B) => C` | `(a: A, b: B) => C` | 17 | | ✅ | Indexers | `{[k: string]: string}` | `{[k: string]: string}` | 18 | | | This type | `(this: X, a: A, b: B) => C` | `(a: A, b: B) => C` | 19 | | | Type guards | `(a: X) => a is A` | `(a: X) => boolean` | 20 | | ✅ | Type parameter bounds | `function f(a:A){}` | `function f(a:A){}` | 21 | | ✅ | keyof X | `keyof X` | `$Keys` | 22 | | ✅ | X[keyof X] | `X[keyof X]` | `$ElementType>` | 23 | | ✅ | Partial | `Partial` | `$Rest` | 24 | | ✅ | Readonly | `Readonly` | `$ReadOnly` | 25 | | ✅ | ReadonlyArray | `ReadonlyArray` | `$ReadOnlyArray` | 26 | | ✅ | ReadonlySet | `ReadonlySet` | `$ReadOnlySet` | 27 | | ✅ | ReadonlyMap | `ReadonlyMap` | `$ReadOnlyMap` | 28 | | ✅ | Record | `Record` | `{ [key: K]: T }` | 29 | | | Pick | `Pick` | | 30 | | | Exclude | `Exclude` | | 31 | | | Extract | `Extract` | | 32 | | ✅ | NonNullable | `NonNullable` | `$NonMaybeType` | 33 | | ✅ | ReturnType | `ReturnType` | `$Call<((...args: any[]) => R) => R, F>` | 34 | | | InstanceType | `InstanceType` | | 35 | | | Required | `Required` | | 36 | | | ThisType | `ThisType` | | 37 | | ✅ | T['string'] | `T['string']` | `$PropertyType` | 38 | | ✅ | T[k] | `T[k]` | `$ElementType` | 39 | | ✅ | Mapped types | `{[K in keyof Obj]: Obj[K]}` | `$ObjMapi(K) => $ElementType>` | 40 | | | Conditional types | `A extends B ? C : D` | `any` | 41 | | ✅ | typeof operator | `typeof foo` | `typeof foo` | 42 | | ✅ | Tuple type | `[number, string]` | `[number, string]` | 43 | | ✅ | Type alias | `type A = string` | `type A = string` | 44 | | ✅ | type/typeof import | `import A from 'module'` | `import type A from 'module'` | 45 | 46 | ## Usage 47 | 48 | Install using `npm i flowgen --save` 49 | 50 | ```js 51 | import { compiler } from 'flowgen'; 52 | 53 | // To compile a d.ts file 54 | const flowdef = compiler.compileDefinitionFile(filename); 55 | 56 | // To compile a string 57 | const flowdef = compiler.compileDefinitionString(str); 58 | 59 | // To compile a typescript test file to JavaScript 60 | // esTarget = ES5/ES6 etc 61 | const testCase = compiler.compileTest(path, esTarget) 62 | ``` 63 | 64 | *Recommended second step:* 65 | 66 | ```js 67 | import { beautify } from 'flowgen'; 68 | 69 | // Make the definition human readable 70 | const readableDef = beautify(generatedFlowdef); 71 | ``` 72 | 73 | ### CLI 74 | 75 | Standard usage (will produce `export.flow.js`): 76 | ``` 77 | npm i -g flowgen 78 | flowgen lodash.d.ts 79 | ``` 80 | 81 | ### Options 82 | ``` 83 | -o / --output-file [outputFile]: Specifies the filename of the exported file, defaults to export.flow.js 84 | ``` 85 | 86 | ### Flags for specific cases 87 | ``` 88 | --flow-typed-format: Format output so it fits in the flow-typed repo 89 | --compile-tests: Compile any sibling -tests.ts files found 90 | --no-inexact: Do not mark object types as inexact (using `...`) 91 | --no-module-exports: Convert `export = Type` only to default export, instead of `declare module.exports: Type` 92 | --interface-records: Convert TypeScript interfaces to Exact Objects 93 | --no-jsdoc: Ignore TypeScript JSDoc 94 | --add-flow-header: Add `// @flow` to generated files. Should be used for libs. 95 | ``` 96 | 97 | ## The difficult parts 98 | 99 | ### Namespaces 100 | Namespaces have been a big headache. What it does right now is that it splits any namespace out into prefixed global scope declarations instead. It works OK, but its not pretty and there's some drawbacks to it. 101 | 102 | ### External library imports 103 | Definitions in TS and flow are often quite different, and imported types from other libraries don't usually have 104 | a one-to-one mapping. Common cases are `React.ReactElement`, `React.CSSProps`etc. 105 | This might require manual processing, or we add a set of hardcoded mutations that handle common cases. 106 | 107 | ### Odd TS conventions 108 | [Lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9fb1696ad55c0ac54bbf6e477f21b52536211a1e/types/lodash/index.d.ts) has been one of the reference libraries i've worked with when creating the 109 | converter. The definition is mostly just a series of interfaces with the same name being re-declared over and over again for each function, which doesn't translate to flow at all. There's multiple ways of solving this but I don't have a great solution for it in place yet. 110 | 111 | ### Sample of finding all typescript definition files and generate flow file with shell script 112 | If your typescript definition files are built in `lib` add below shell script and run it. 113 | 114 | ```sh 115 | for i in $(find lib -type f -name "*.d.ts"); 116 | do sh -c "flowgen $i -o ${i%.*.*}.js.flow"; 117 | done; 118 | ``` 119 | 120 | So if you have definition files in different dir, you can rename `lib` and run the script. 121 | 122 | Here’s an example of the above as an npm script in `package.json` that excludes any typescript definition files found inside `node_modules`: 123 | ```json 124 | "scripts": { 125 | "build:flowtypes": "find . -type f -not -path './node_modules/*' -name '*.d.ts' -exec sh -c 'yarn flowgen --add-flow-header $1 -o ${1%.*.*}.js.flow' _ '{}' \\;" 126 | } 127 | ``` 128 | 129 | You can then have a `build` script that generates flow types along the lines of `tsc --build && npm run build:flowtypes`. 130 | 131 | ## Contributing 132 | 133 | All help is appreciated. Please [tweet at me](https://twitter.com/joarwilk) if you want some help getting started, or just want to discuss ideas on how to solve the trickier parts. 134 | 135 | ## Distribution 136 | 137 | * `git pull origin master` 138 | * `yarn compile` 139 | * Change the version in `package.json` 140 | * `git add .` 141 | * `git commit -m "New release" 142 | * `npm publish` 143 | * `git push` 144 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | Hey so you want to take a look at fixing your bug, we have three steps: 2 | 3 | * Clone the repo: 4 | 5 | ```sh 6 | git clone https://github.com/joarwilk/flowgen.git 7 | cd flowgen 8 | ``` 9 | 10 | * Setup dependencies 11 | 12 | ```sh 13 | yarn install 14 | ``` 15 | 16 | * Run tests 17 | 18 | ```sh 19 | yarn test 20 | ``` 21 | 22 | From there you should add a new test file with a chunk of your TypeScript interface. For example, I created the file `src/__tests__/union_strings.spec.js` and added 23 | 24 | ```js 25 | import { compiler, beautify } from ".."; 26 | import "./matchers"; 27 | 28 | it('should handle union strings', () => { 29 | const ts = ` 30 | interface MyObj { 31 | state?: "APPROVED" | "REQUEST_CHANGES" | "COMMENT" | "PENDING" 32 | } 33 | `; 34 | 35 | const result = compiler.compileDefinitionString(ts); 36 | 37 | expect(beautify(result)).toMatchSnapshot() 38 | expect(result).toBeValidFlowTypeDeclarations(); 39 | }); 40 | ``` 41 | 42 | Running `yarn test` created a snapshot like this: 43 | 44 | ```js 45 | // Jest Snapshot v1, https://goo.gl/fbAQLP 46 | 47 | exports[`should handle union strings 1`] = ` 48 | "declare interface MyObj { 49 | state?: 'APPROVED' | 'REQUEST_CHANGES' | 'COMMENT' | 'PENDING' 50 | }" 51 | `; 52 | ``` 53 | 54 | Which I could then use as a reference for the output. You can replace the string in `ts` with whatever broken interface code you have in a new spec file and you'll have a full integration test for your changes. You can see this in commit [ccfbea](https://github.com/joarwilk/flowgen/commit/ccfbeaa189b14ee70f675601c731bf3c7cb6a88b). 55 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | Hi there, thanks for the issue! 2 | 3 | This project is being maintained, but it's in a place where most people 4 | should consider trying to fix their own problems instead of hoping 5 | for a maintainer to handle it. It works as-is for most of our use cases. 6 | 7 | The [getting_started.md](./getting_started.md) guide shows how you can get 8 | a reproduction of your bug in a test really quickly, and the maintainers 9 | can help figure out how to go from red to green if you can't get it. 10 | -------------------------------------------------------------------------------- /index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | 3 | export type Options = {| 4 | jsdoc?: boolean, 5 | interfaceRecords?: boolean, 6 | moduleExports?: boolean, 7 | quiet?: boolean, 8 | inexact?: boolean, 9 | |}; 10 | 11 | export type Compiler = {| 12 | compileTest(path: string, target: string): void, 13 | compileDefinitionString( 14 | string: string, 15 | options?: Options, 16 | mapSourceCode?: (source: string | void, fileName: string) => string | void, 17 | ): string, 18 | compileDefinitionFile( 19 | path: string, 20 | options?: Options, 21 | mapSourceCode?: (source: string | void, fileName: string) => string | void, 22 | ): string, 23 | 24 | // Low-level exports 25 | reset(options?: Options): void, 26 | setChecker(checker: $FlowFixMe /* ts.TypeChecker */): void, 27 | compile(sourceFile: $FlowFixMe /* ts.SourceFile */): string, 28 | |}; 29 | 30 | declare type Flowgen = {| 31 | beautify(str: string): string, 32 | compiler: Compiler, 33 | |}; 34 | 35 | declare export default Flowgen; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flowgen", 3 | "description": "Generate flowtype definition files from TypeScript", 4 | "version": "1.21.0", 5 | "bin": { 6 | "flowgen": "./lib/cli/index.js" 7 | }, 8 | "jest": { 9 | "testMatch": [ 10 | "**/__tests__/**/*.ts", 11 | "**/?(*.)+(spec|test).ts", 12 | "!**/*.d.ts" 13 | ] 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/joarwilk/flowgen.git" 18 | }, 19 | "dependencies": { 20 | "@babel/code-frame": "^7.16.7", 21 | "@babel/highlight": "^7.16.7", 22 | "commander": "^6.1.0", 23 | "lodash": "^4.17.20", 24 | "prettier": "^2.5.1", 25 | "shelljs": "^0.8.4", 26 | "typescript": "~4.4.4", 27 | "typescript-compiler": "^1.4.1-2" 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.16.7", 31 | "@babel/core": "^7.16.7", 32 | "@babel/plugin-proposal-class-properties": "^7.16.7", 33 | "@babel/preset-env": "^7.16.7", 34 | "@babel/preset-typescript": "^7.16.7", 35 | "@types/babel__code-frame": "^7.0.3", 36 | "@types/jest": "^26.0.13", 37 | "@types/lodash": "^4.14.161", 38 | "@types/node": "^14.10.0", 39 | "@types/prettier": "^2.4.2", 40 | "@types/react": "^16.9.49", 41 | "@types/shelljs": "^0.8.7", 42 | "@typescript-eslint/eslint-plugin": "^5.9.0", 43 | "@typescript-eslint/parser": "^5.9.0", 44 | "babel-core": "^7.0.0-bridge.0", 45 | "babel-eslint": "^10.1.0", 46 | "babel-jest": "^27.4.6", 47 | "chalk": "^4.1.0", 48 | "eslint": "^8.6.0", 49 | "eslint-config-prettier": "^8.3.0", 50 | "eslint-plugin-import": "^2.25.4", 51 | "eslint-plugin-jest": "^25.3.4", 52 | "eslint-plugin-prettier": "^4.0.0", 53 | "flow-bin": "^0.195.0", 54 | "jest": "^27.4.7", 55 | "mongodb": "^4.1.3" 56 | }, 57 | "files": [ 58 | "lib", 59 | "index.js.flow" 60 | ], 61 | "license": "ISC", 62 | "main": "lib/index.js", 63 | "types": "lib/index.d.ts", 64 | "scripts": { 65 | "prepublishOnly": "npm run compile & npm run type", 66 | "type": "tsc -p tsconfig.declaration.json", 67 | "compile": "babel ./src --extensions '.ts,.tsx,.js' --out-dir lib --delete-dir-on-start --ignore 'src/**/*.spec.ts' --ignore 'src/__tests__/'", 68 | "compile:watch": "npm run compile -- -w", 69 | "test": "jest src/**/*.spec.ts", 70 | "test:watch": "jest src/**/*.spec.ts --watch", 71 | "lint": "eslint '**/*.{js,ts,tsx}' --quiet" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/basic.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle basic keywords 1`] = ` 4 | "declare type A = { 5 | a: void, 6 | b: string, 7 | c: any, 8 | d: number, 9 | e: boolean, 10 | f: null, 11 | g: void, 12 | h: { [key: string]: any }, 13 | i: 1, 14 | j: 2, 15 | k: true, 16 | l: false, 17 | m: \\"foo\\", 18 | n: \\"bar\\", 19 | o: empty, 20 | p: mixed, 21 | r: -1, 22 | s: -2, 23 | t: Symbol, 24 | u: Symbol, 25 | v: [1, 2, 3], 26 | w: $ReadOnlyArray, 27 | x: RegExp$matchResult, 28 | ... 29 | }; 30 | " 31 | `; 32 | 33 | exports[`should handle basic keywords 2`] = ` 34 | "declare type A = {| 35 | a: void, 36 | b: string, 37 | c: any, 38 | d: number, 39 | e: boolean, 40 | f: null, 41 | g: void, 42 | h: { [key: string]: any }, 43 | i: 1, 44 | j: 2, 45 | k: true, 46 | l: false, 47 | m: \\"foo\\", 48 | n: \\"bar\\", 49 | o: empty, 50 | p: mixed, 51 | r: -1, 52 | s: -2, 53 | t: Symbol, 54 | u: Symbol, 55 | v: [1, 2, 3], 56 | w: $ReadOnlyArray, 57 | x: RegExp$matchResult, 58 | |}; 59 | " 60 | `; 61 | 62 | exports[`should handle class types 1`] = ` 63 | "declare export class Foo {} 64 | " 65 | `; 66 | 67 | exports[`should handle class types 2`] = ` 68 | "declare export class Foo {} 69 | " 70 | `; 71 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/bigint-literals.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle bigint literals in type 1`] = ` 4 | "declare type MyBigType = bigint; 5 | declare type bigint_like = number | bigint | string; 6 | declare var literal: 9007199254740991n; 7 | " 8 | `; 9 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/boolean-literals.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle boolean literals in type 1`] = ` 4 | "declare type MyFalsyType = string | false; 5 | declare type MyTruthyType = true | string; 6 | declare var foo: true; 7 | " 8 | `; 9 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/classes.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle class extends 1`] = ` 4 | "declare class extension { 5 | getString(): string; 6 | } 7 | declare class extender mixins extension { 8 | getNumber(): number; 9 | } 10 | " 11 | `; 12 | 13 | exports[`should handle class implements 1`] = ` 14 | "declare interface implementation { 15 | getString(): string; 16 | } 17 | declare class implementor implements implementation { 18 | getString(): string; 19 | } 20 | " 21 | `; 22 | 23 | exports[`should handle class implements and extends 1`] = ` 24 | "declare interface implementation1 { 25 | getString(): string; 26 | } 27 | declare interface implementation2 { 28 | getNumber(): number; 29 | } 30 | declare class extension {} 31 | declare class implementor 32 | mixins extension 33 | implements implementation1, implementation2 34 | { 35 | getString(): string; 36 | getNumber(): number; 37 | } 38 | " 39 | `; 40 | 41 | exports[`should handle static methods ES6 classes 1`] = ` 42 | "declare class Subscribable {} 43 | declare class Operator {} 44 | declare class Observable mixins Subscribable { 45 | create: Function; 46 | static create: Function; 47 | lift(operator: Operator): Observable; 48 | static lift(operator: Operator): Observable; 49 | +foo: number; 50 | static +bar: string; 51 | baz?: string; 52 | +quux?: number; 53 | static quick?: Symbol; 54 | static +fox?: string; 55 | jump?: () => void; 56 | +jump?: () => void; 57 | static +jump?: () => void; 58 | cfnProperties: { 59 | [key: string]: any, 60 | }; 61 | static fooGet: string; 62 | } 63 | " 64 | `; 65 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/computed.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should approximate unsupported keys 1`] = ` 4 | "declare type A = { 5 | [typeof Foo]: (() => any) | void, 6 | +[typeof Foo]: (() => any) | void, 7 | [typeof Foo]: () => any, 8 | +[typeof Foo]: () => any, 9 | [typeof Foo]: any | void, 10 | +[typeof Foo]: any | void, 11 | [typeof Foo]: any, 12 | +[typeof Foo]: any, 13 | ... 14 | }; 15 | declare class B { 16 | [typeof Foo]: (() => any) | void; 17 | +[typeof Foo]: (() => any) | void; 18 | [typeof Foo]: () => any; 19 | +[typeof Foo]: () => any; 20 | [typeof Foo]: any | void; 21 | +[typeof Foo]: any | void; 22 | [typeof Foo]: any; 23 | +[typeof Foo]: any; 24 | } 25 | declare interface C { 26 | [typeof Foo]: (() => any) | void; 27 | +[typeof Foo]: (() => any) | void; 28 | [typeof Foo]: () => any; 29 | +[typeof Foo]: () => any; 30 | [typeof Foo]: any | void; 31 | +[typeof Foo]: any | void; 32 | [typeof Foo]: any; 33 | +[typeof Foo]: any; 34 | } 35 | " 36 | `; 37 | 38 | exports[`should handle computed Symbol.iterator and Symbol.asyncIterator 1`] = ` 39 | "declare type A = { 40 | @@asyncIterator: (() => any) | void, 41 | @@iterator: (() => any) | void, 42 | +@@asyncIterator: (() => any) | void, 43 | +@@iterator: (() => any) | void, 44 | @@asyncIterator: () => any, 45 | @@iterator: () => any, 46 | +@@asyncIterator: () => any, 47 | +@@iterator: () => any, 48 | @@asyncIterator: any | void, 49 | @@iterator: any | void, 50 | +@@asyncIterator: any | void, 51 | +@@iterator: any | void, 52 | @@asyncIterator: any, 53 | @@iterator: any, 54 | +@@asyncIterator: any, 55 | +@@iterator: any, 56 | ... 57 | }; 58 | declare class B { 59 | @@asyncIterator: (() => any) | void; 60 | @@iterator: (() => any) | void; 61 | +@@asyncIterator: (() => any) | void; 62 | +@@iterator: (() => any) | void; 63 | @@asyncIterator: () => any; 64 | @@iterator: () => any; 65 | +@@asyncIterator: () => any; 66 | +@@iterator: () => any; 67 | @@asyncIterator: any | void; 68 | @@iterator: any | void; 69 | +@@asyncIterator: any | void; 70 | +@@iterator: any | void; 71 | @@asyncIterator: any; 72 | @@iterator: any; 73 | +@@asyncIterator: any; 74 | +@@iterator: any; 75 | } 76 | declare interface C { 77 | @@asyncIterator: (() => any) | void; 78 | @@iterator: (() => any) | void; 79 | +@@asyncIterator: (() => any) | void; 80 | +@@iterator: (() => any) | void; 81 | @@asyncIterator: () => any; 82 | @@iterator: () => any; 83 | +@@asyncIterator: () => any; 84 | +@@iterator: () => any; 85 | @@asyncIterator: any | void; 86 | @@iterator: any | void; 87 | +@@asyncIterator: any | void; 88 | +@@iterator: any | void; 89 | @@asyncIterator: any; 90 | @@iterator: any; 91 | +@@asyncIterator: any; 92 | +@@iterator: any; 93 | } 94 | " 95 | `; 96 | 97 | exports[`should handle string literals 1`] = ` 98 | "declare type A = { 99 | foo: (() => any) | void, 100 | +foo: (() => any) | void, 101 | foo: () => any, 102 | +foo: () => any, 103 | foo: any | void, 104 | +foo: any | void, 105 | foo: any, 106 | +foo: any, 107 | ... 108 | }; 109 | declare class B { 110 | foo: (() => any) | void; 111 | +foo: (() => any) | void; 112 | foo: () => any; 113 | +foo: () => any; 114 | foo: any | void; 115 | +foo: any | void; 116 | foo: any; 117 | +foo: any; 118 | } 119 | declare interface C { 120 | foo: (() => any) | void; 121 | +foo: (() => any) | void; 122 | foo: () => any; 123 | +foo: () => any; 124 | foo: any | void; 125 | +foo: any | void; 126 | foo: any; 127 | +foo: any; 128 | } 129 | " 130 | `; 131 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/conditional.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle exported interfaces 1`] = ` 4 | "// see https://gist.github.com/thecotne/6e5969f4aaf8f253985ed36b30ac9fe0 5 | type $FlowGen$If = $Call< 6 | ((true, Then, Else) => Then) & ((false, Then, Else) => Else), 7 | X, 8 | Then, 9 | Else 10 | >; 11 | 12 | type $FlowGen$Assignable = $Call< 13 | ((...r: [B]) => true) & ((...r: [A]) => false), 14 | A 15 | >; 16 | 17 | declare export function add( 18 | a: T, 19 | b: T 20 | ): $FlowGen$If<$FlowGen$Assignable, string, number>; 21 | " 22 | `; 23 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/declaration-file.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle basic keywords cll 1`] = ` 4 | "declare module \\"test\\" { 5 | declare interface A { 6 | bar: string; 7 | } 8 | declare export var ok: number; 9 | } 10 | " 11 | `; 12 | 13 | exports[`should handle basic keywords 1`] = ` 14 | "declare module \\"test\\" { 15 | declare interface A { 16 | bar: string; 17 | } 18 | declare var ok: number; 19 | } 20 | " 21 | `; 22 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/duplicated-names.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should be scoped to main file 1`] = ` 4 | "import { Buffer } from \\"buffer\\"; 5 | export type BufferAlias = Buffer; 6 | " 7 | `; 8 | 9 | exports[`should generate unique names 1`] = ` 10 | "export type AuthMechanismType = string; 11 | declare export var AuthMechanism: { 12 | +MONGODB_AWS: \\"MONGODB-AWS\\", 13 | +MONGODB_CR: \\"MONGODB-CR\\", 14 | ... 15 | }; 16 | export type AuthMechanismType1 = $ElementType< 17 | typeof AuthMechanism, 18 | $Keys 19 | >; 20 | " 21 | `; 22 | 23 | exports[`should handle variable & type having same name 1`] = ` 24 | "declare export var AuthMechanism: { 25 | +MONGODB_AWS: \\"MONGODB-AWS\\", 26 | +MONGODB_CR: \\"MONGODB-CR\\", 27 | ... 28 | }; 29 | export type AuthMechanismType = $ElementType< 30 | typeof AuthMechanism, 31 | $Keys 32 | >; 33 | " 34 | `; 35 | 36 | exports[`should support generic type rename 1`] = ` 37 | "declare export var ProfilingLevel: $ReadOnly<{ 38 | +off: \\"off\\", 39 | ... 40 | }>; 41 | export type ProfilingLevelType = $ElementType< 42 | typeof ProfilingLevel, 43 | $Keys 44 | >; 45 | export type Callback = (error?: Error, result?: T) => void; 46 | declare export var callback: Callback; 47 | " 48 | `; 49 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/enums.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle basic enums: class 1`] = ` 4 | "declare var Label: {| 5 | +LABEL_OPTIONAL: 0, // 0 6 | +LABEL_REQUIRED: 1, // 1 7 | +LABEL_REPEATED: 2, // 2 8 | |}; 9 | declare type A = $Values; 10 | declare type B = typeof Label.LABEL_OPTIONAL; 11 | " 12 | `; 13 | 14 | exports[`should handle empty enums: class 1`] = ` 15 | "declare var Empty: {||}; 16 | " 17 | `; 18 | 19 | exports[`should handle importing enum types: class 1`] = ` 20 | "declare export var Label: {| 21 | +A: \\"A\\", // \\"A\\" 22 | +B: \\"B\\", // \\"B\\" 23 | |}; 24 | " 25 | `; 26 | 27 | exports[`should handle importing enum types: class 2`] = ` 28 | "import typeof { Label } from \\"./export-enum-file\\"; 29 | declare export function foo(label: $Values { 274 | +d: A; 275 | b: number; 276 | } 277 | 278 | declare class A$B$D {} 279 | 280 | declare var npm$namespace$A$B$C: {| 281 | N: typeof A$B$C$N, 282 | |}; 283 | declare class A$B$C$N mixins A$B$D implements A$B$S { 284 | a: string; 285 | } 286 | " 287 | `; 288 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/spread.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should not insert spread when performing union of class types 1`] = ` 4 | "declare class Foo {} 5 | declare class Bar {} 6 | declare var combination: Foo & Bar; 7 | " 8 | `; 9 | 10 | exports[`should use spread when performing union of object types 1`] = ` 11 | "declare type Foo = { 12 | foo: number, 13 | ... 14 | }; 15 | declare type Bar = { 16 | bar: string, 17 | ... 18 | }; 19 | declare var combination: { ...Foo, ...Bar }; 20 | " 21 | `; 22 | 23 | exports[`should use spread when performing union of object types 2`] = ` 24 | "declare type Foo = {| 25 | foo: number, 26 | |}; 27 | declare type Bar = {| 28 | bar: string, 29 | |}; 30 | declare var combination: {| ...Foo, ...Bar |}; 31 | " 32 | `; 33 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/string-literals.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle exported constant string literals 1`] = ` 4 | "declare export var SET_NAME: \\"my/lib/SET_NAME\\"; 5 | declare export var SET_STAGE: \\"my/lib/SET_STAGE\\"; 6 | " 7 | `; 8 | 9 | exports[`should handle string literals in function argument "overloading" 1`] = ` 10 | "declare interface MyObj { 11 | on(event: \\"error\\", cb: (err: Error) => void): void; 12 | on(event: \\"close\\", cb: (code: number, message: string) => void): void; 13 | on( 14 | event: \\"message\\", 15 | cb: ( 16 | data: any, 17 | flags: { 18 | binary: boolean, 19 | ... 20 | } 21 | ) => void 22 | ): void; 23 | on( 24 | event: \\"ping\\", 25 | cb: ( 26 | data: any, 27 | flags: { 28 | binary: boolean, 29 | ... 30 | } 31 | ) => void 32 | ): void; 33 | on( 34 | event: \\"pong\\", 35 | cb: ( 36 | data: any, 37 | flags: { 38 | binary: boolean, 39 | ... 40 | } 41 | ) => void 42 | ): void; 43 | on(event: \\"open\\", cb: () => void): void; 44 | on(event: string, listener: (...args: any[]) => void): void; 45 | } 46 | " 47 | `; 48 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/tuples.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle tuples 1`] = ` 4 | "declare type T1 = [number, string | void]; 5 | declare type T2 = [number] & string[]; 6 | " 7 | `; 8 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/type-exports.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle export list syntax 1`] = ` 4 | "declare type ComplexType = 5 | | { 6 | type: number, 7 | ... 8 | } 9 | | { 10 | type: string, 11 | ... 12 | }; 13 | export type { ComplexType }; 14 | declare var foo: 5; 15 | declare export { foo }; 16 | " 17 | `; 18 | 19 | exports[`should handle exported types 1`] = ` 20 | "export type FactoryOrValue = T | (() => T); 21 | export type Maybe = 22 | | { 23 | type: \\"just\\", 24 | value: T, 25 | ... 26 | } 27 | | { 28 | type: \\"nothing\\", 29 | ... 30 | }; 31 | " 32 | `; 33 | 34 | exports[`should handle inline export list syntax 1`] = ` 35 | "declare type ComplexType = 36 | | { 37 | type: number, 38 | ... 39 | } 40 | | { 41 | type: string, 42 | ... 43 | }; 44 | declare var foo: 5; 45 | export type { ComplexType }; 46 | declare export { foo }; 47 | " 48 | `; 49 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/union-strings.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle union strings 1`] = ` 4 | "declare interface MyObj { 5 | state?: \\"APPROVED\\" | \\"REQUEST_CHANGES\\" | \\"COMMENT\\" | \\"PENDING\\"; 6 | } 7 | declare type CompletionsTriggerCharacter = '\\"' | \\"'\\"; 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/utility-types.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle Omit type 1`] = ` 4 | "declare type A = $Diff< 5 | { 6 | a: string, 7 | b: number, 8 | ... 9 | }, 10 | { a: any } 11 | >; 12 | declare type B = $Diff< 13 | { 14 | a: string, 15 | b: number, 16 | ... 17 | }, 18 | { a: any, b: any } 19 | >; 20 | declare type O = { 21 | a: string, 22 | b: number, 23 | ... 24 | }; 25 | declare type U = \\"a\\"; 26 | declare type C = $Diff; 27 | " 28 | `; 29 | 30 | exports[`should handle utility types 1`] = ` 31 | "declare type A = $ReadOnly<{ 32 | a: number, 33 | ... 34 | }>; 35 | declare type B = $Rest< 36 | { 37 | a: number, 38 | ... 39 | }, 40 | { ... } 41 | >; 42 | declare type C = $NonMaybeType; 43 | declare type D = $ReadOnlyArray; 44 | declare type E = $Call<((...args: any[]) => R) => R, () => string>; 45 | declare type F = { [key: string]: number, ... }; 46 | declare type G = $ReadOnlySet; 47 | declare type H = $ReadOnlyMap; 48 | declare type A1 = Readonly; 49 | declare type B1 = Partial; 50 | declare type C1 = NonNullable; 51 | declare type D1 = ReadonlyArray; 52 | declare type E1 = ReturnType; 53 | declare type F1 = Record; 54 | declare type A2 = $ReadOnly; 55 | declare type B2 = $Rest; 56 | declare type C2 = $NonMaybeType; 57 | declare type D2 = $ReadOnlyArray; 58 | declare type E2 = $Call<((...args: any[]) => R) => R, () => T>; 59 | declare type F2 = { [key: T]: U, ... }; 60 | " 61 | `; 62 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/value-exports.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle default exported es module values 1`] = ` 4 | "declare var test: { 5 | a: number, 6 | ... 7 | }; 8 | declare export default typeof test; 9 | " 10 | `; 11 | 12 | exports[`should handle exported es module values 1`] = ` 13 | "declare var test: { 14 | a: number, 15 | ... 16 | }; 17 | declare export { test }; 18 | " 19 | `; 20 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/variables.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle declares 1`] = ` 4 | "declare var test: { 5 | a: number, 6 | ... 7 | }; 8 | declare var foo: string; 9 | declare var bar: number; 10 | declare var baz: number; 11 | declare var quuz: any; 12 | declare var quuuz: string; 13 | declare var quuuuz: number; 14 | declare var quuuuuz: string; 15 | declare var fox: number; 16 | " 17 | `; 18 | -------------------------------------------------------------------------------- /src/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle basic keywords", () => { 5 | const ts = `type A = { 6 | a: void, 7 | b: string, 8 | c: any, 9 | d: number, 10 | e: boolean, 11 | f: null, 12 | g: undefined, 13 | h: object, 14 | i: 1, 15 | j: 2, 16 | k: true, 17 | l: false, 18 | m: 'foo', 19 | n: 'bar', 20 | o: never, 21 | p: unknown, 22 | r: -1, 23 | s: -2, 24 | t: symbol, 25 | u: unique symbol, 26 | v: readonly [1, 2, 3], 27 | w: readonly string[], 28 | x: RegExpMatchArray 29 | }`; 30 | 31 | { 32 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 33 | expect(beautify(result)).toMatchSnapshot(); 34 | expect(result).toBeValidFlowTypeDeclarations(); 35 | } 36 | 37 | { 38 | const result = compiler.compileDefinitionString(ts, { 39 | quiet: true, 40 | inexact: false, 41 | }); 42 | expect(beautify(result)).toMatchSnapshot(); 43 | expect(result).toBeValidFlowTypeDeclarations(); 44 | } 45 | }); 46 | 47 | it("should handle class types", () => { 48 | const ts = ` 49 | declare export class Foo { 50 | }`; 51 | 52 | { 53 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 54 | expect(beautify(result)).toMatchSnapshot(); 55 | expect(result).toBeValidFlowTypeDeclarations(); 56 | } 57 | 58 | { 59 | const result = compiler.compileDefinitionString(ts, { 60 | quiet: true, 61 | inexact: false, 62 | }); 63 | expect(beautify(result)).toMatchSnapshot(); 64 | expect(result).toBeValidFlowTypeDeclarations(); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /src/__tests__/bigint-literals.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle bigint literals in type", () => { 5 | const ts = ` 6 | type MyBigType = bigint; 7 | type bigint_like = number | bigint | string; 8 | const literal = 9007199254740991n; 9 | `; 10 | 11 | // unsupported expressions that should be supported 12 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt 13 | // const ts = ` 14 | // const ctor = BigInt("9007199254740991"); 15 | // const add = 4n + 5n; 16 | // const mult = 4n * 5n; 17 | // const sub = 4n - 5n; 18 | // const mod = 4n % 3n; 19 | // const exp = 4n ** 2n; 20 | // const div = 4n / 2n; 21 | // const eq = 4n == 0; 22 | // const cmp = 4n > 0; 23 | // const mixed = [4n, 6, -12n, 10, 4, 0, 0n]; 24 | // const boolCast = !12n; 25 | // `; 26 | 27 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 28 | 29 | expect(beautify(result)).toMatchSnapshot(); 30 | expect(result).toBeValidFlowTypeDeclarations(); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/boolean-literals.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle boolean literals in type", () => { 5 | const ts = ` 6 | type MyFalsyType = string | false; 7 | type MyTruthyType = true | string; 8 | const foo = true; 9 | `; 10 | 11 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 12 | 13 | expect(beautify(result)).toMatchSnapshot(); 14 | expect(result).toBeValidFlowTypeDeclarations(); 15 | }); 16 | -------------------------------------------------------------------------------- /src/__tests__/classes.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle static methods ES6 classes", () => { 5 | const ts = ` 6 | class Subscribable {} 7 | class Operator {} 8 | class Observable implements Subscribable { 9 | create: Function; 10 | static create: Function; 11 | lift(operator: Operator): Observable; 12 | static lift(operator: Operator): Observable; 13 | readonly foo: number; 14 | static readonly bar: string; 15 | baz?: string; 16 | readonly quux?: number; 17 | static quick?: symbol; 18 | static readonly fox?: string; 19 | jump?(): void; 20 | readonly jump?(): void; 21 | static readonly jump?(): void; 22 | protected get cfnProperties(): { 23 | [key: string]: any; 24 | }; 25 | static get fooGet(): string; 26 | } 27 | `; 28 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 29 | expect(beautify(result)).toMatchSnapshot(); 30 | expect(result).toBeValidFlowTypeDeclarations(); 31 | }); 32 | 33 | it("should handle class extends", () => { 34 | const ts = ` 35 | class extension { 36 | getString(): string 37 | } 38 | class extender extends extension { 39 | getNumber(): number 40 | } 41 | `; 42 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 43 | expect(beautify(result)).toMatchSnapshot(); 44 | expect(result).toBeValidFlowTypeDeclarations(); 45 | }); 46 | 47 | it("should handle class implements", () => { 48 | const ts = ` 49 | interface implementation { 50 | getString(): string 51 | } 52 | class implementor implements implementation { 53 | getString(): string 54 | } 55 | `; 56 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 57 | expect(beautify(result)).toMatchSnapshot(); 58 | expect(result).toBeValidFlowTypeDeclarations(); 59 | }); 60 | 61 | it("should handle class implements and extends", () => { 62 | const ts = ` 63 | interface implementation1 { 64 | getString(): string 65 | } 66 | interface implementation2 { 67 | getNumber(): number 68 | } 69 | class extension {} 70 | class implementor extends extension implements implementation1, implementation2 { 71 | getString(): string 72 | getNumber(): number 73 | } 74 | `; 75 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 76 | expect(beautify(result)).toMatchSnapshot(); 77 | expect(result).toBeValidFlowTypeDeclarations(); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/computed.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle computed Symbol.iterator and Symbol.asyncIterator", () => { 5 | const ts = ` 6 | type A = { 7 | [Symbol.asyncIterator]?(): any, 8 | [Symbol.iterator]?(): any, 9 | readonly [Symbol.asyncIterator]?(): any, 10 | readonly [Symbol.iterator]?(): any, 11 | [Symbol.asyncIterator](): any, 12 | [Symbol.iterator](): any, 13 | readonly [Symbol.asyncIterator](): any, 14 | readonly [Symbol.iterator](): any, 15 | [Symbol.asyncIterator]?: any, 16 | [Symbol.iterator]?: any, 17 | readonly [Symbol.asyncIterator]?: any, 18 | readonly [Symbol.iterator]?: any, 19 | [Symbol.asyncIterator]: any, 20 | [Symbol.iterator]: any, 21 | readonly [Symbol.asyncIterator]: any, 22 | readonly [Symbol.iterator]: any, 23 | } 24 | declare class B { 25 | [Symbol.asyncIterator]?(): any, 26 | [Symbol.iterator]?(): any, 27 | readonly [Symbol.asyncIterator]?(): any, 28 | readonly [Symbol.iterator]?(): any, 29 | [Symbol.asyncIterator](): any, 30 | [Symbol.iterator](): any, 31 | readonly [Symbol.asyncIterator](): any, 32 | readonly [Symbol.iterator](): any, 33 | [Symbol.asyncIterator]?: any, 34 | [Symbol.iterator]?: any, 35 | readonly [Symbol.asyncIterator]?: any, 36 | readonly [Symbol.iterator]?: any, 37 | [Symbol.asyncIterator]: any, 38 | [Symbol.iterator]: any, 39 | readonly [Symbol.asyncIterator]: any, 40 | readonly [Symbol.iterator]: any, 41 | } 42 | interface C { 43 | [Symbol.asyncIterator]?(): any, 44 | [Symbol.iterator]?(): any, 45 | readonly [Symbol.asyncIterator]?(): any, 46 | readonly [Symbol.iterator]?(): any, 47 | [Symbol.asyncIterator](): any, 48 | [Symbol.iterator](): any, 49 | readonly [Symbol.asyncIterator](): any, 50 | readonly [Symbol.iterator](): any, 51 | [Symbol.asyncIterator]?: any, 52 | [Symbol.iterator]?: any, 53 | readonly [Symbol.asyncIterator]?: any, 54 | readonly [Symbol.iterator]?: any, 55 | [Symbol.asyncIterator]: any, 56 | [Symbol.iterator]: any, 57 | readonly [Symbol.asyncIterator]: any, 58 | readonly [Symbol.iterator]: any, 59 | } 60 | `; 61 | 62 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 63 | 64 | expect(beautify(result)).toMatchSnapshot(); 65 | expect(result).toBeValidFlowTypeDeclarations(); 66 | }); 67 | 68 | it("should handle string literals", () => { 69 | const ts = ` 70 | type A = { 71 | ["foo"]?(): any, 72 | readonly ["foo"]?(): any, 73 | ["foo"](): any, 74 | readonly ["foo"](): any, 75 | ["foo"]?: any, 76 | readonly ["foo"]?: any, 77 | ["foo"]: any, 78 | readonly ["foo"]: any, 79 | } 80 | declare class B { 81 | ["foo"]?(): any, 82 | readonly ["foo"]?(): any, 83 | ["foo"](): any, 84 | readonly ["foo"](): any, 85 | ["foo"]?: any, 86 | readonly ["foo"]?: any, 87 | ["foo"]: any, 88 | readonly ["foo"]: any, 89 | } 90 | interface C { 91 | ["foo"]?(): any, 92 | readonly ["foo"]?(): any, 93 | ["foo"](): any, 94 | readonly ["foo"](): any, 95 | ["foo"]?: any, 96 | readonly ["foo"]?: any, 97 | ["foo"]: any, 98 | readonly ["foo"]: any, 99 | } 100 | `; 101 | 102 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 103 | 104 | expect(beautify(result)).toMatchSnapshot(); 105 | expect(result).toBeValidFlowTypeDeclarations(); 106 | }); 107 | 108 | it("should approximate unsupported keys", () => { 109 | const ts = ` 110 | type A = { 111 | [Foo]?(): any, 112 | readonly [Foo]?(): any, 113 | [Foo](): any, 114 | readonly [Foo](): any, 115 | [Foo]?: any, 116 | readonly [Foo]?: any, 117 | [Foo]: any, 118 | readonly [Foo]: any, 119 | } 120 | declare class B { 121 | [Foo]?(): any, 122 | readonly [Foo]?(): any, 123 | [Foo](): any, 124 | readonly [Foo](): any, 125 | [Foo]?: any, 126 | readonly [Foo]?: any, 127 | [Foo]: any, 128 | readonly [Foo]: any, 129 | } 130 | interface C { 131 | [Foo]?(): any, 132 | readonly [Foo]?(): any, 133 | [Foo](): any, 134 | readonly [Foo](): any, 135 | [Foo]?: any, 136 | readonly [Foo]?: any, 137 | [Foo]: any, 138 | readonly [Foo]: any, 139 | } 140 | `; 141 | 142 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 143 | 144 | expect(beautify(result)).toMatchSnapshot(); 145 | expect(result).not.toBeValidFlowTypeDeclarations(); // unsupported-syntax 146 | }); 147 | -------------------------------------------------------------------------------- /src/__tests__/conditional.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle exported interfaces", () => { 5 | const ts = `export function add(a: T, b: T): T extends string ? string : number;`; 6 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 7 | expect(beautify(result)).toMatchSnapshot(); 8 | expect(result).toBeValidFlowTypeDeclarations(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__tests__/declaration-file.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle basic keywords", () => { 5 | const ts = ` 6 | declare interface A { 7 | bar: string 8 | } 9 | 10 | export declare const ok: number 11 | `; 12 | 13 | const result = compiler.compileDefinitionString(ts, { 14 | quiet: true, 15 | asModule: "test", 16 | }); 17 | expect(beautify(result)).toMatchSnapshot(); 18 | expect(result).toBeValidFlowTypeDeclarations(); 19 | }); 20 | 21 | it("should handle basic keywords cll", () => { 22 | const ts = ` 23 | declare module 'test' { 24 | interface A { 25 | bar: string 26 | } 27 | export const ok: number 28 | }`; 29 | 30 | const result = compiler.compileDefinitionString(ts, { 31 | quiet: true, 32 | asModule: "test", 33 | }); 34 | expect(beautify(result)).toMatchSnapshot(); 35 | expect(result).toBeValidFlowTypeDeclarations(); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/duplicated-names.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle variable & type having same name", () => { 5 | const ts = ` 6 | export declare const AuthMechanism: { 7 | readonly MONGODB_AWS: "MONGODB-AWS"; 8 | readonly MONGODB_CR: "MONGODB-CR"; 9 | }; 10 | export declare type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism]; 11 | `; 12 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 13 | expect(beautify(result)).toMatchSnapshot(); 14 | expect(result).toBeValidFlowTypeDeclarations(); 15 | }); 16 | 17 | it("should generate unique names", () => { 18 | const ts = ` 19 | export declare type AuthMechanismType = string; 20 | export declare const AuthMechanism: { 21 | readonly MONGODB_AWS: "MONGODB-AWS"; 22 | readonly MONGODB_CR: "MONGODB-CR"; 23 | }; 24 | export declare type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism]; 25 | `; 26 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 27 | expect(beautify(result)).toMatchSnapshot(); 28 | expect(result).toBeValidFlowTypeDeclarations(); 29 | }); 30 | 31 | it("should be scoped to main file", () => { 32 | const ts = ` 33 | import { Buffer } from 'buffer'; 34 | export declare type BufferAlias = Buffer; 35 | `; 36 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 37 | expect(beautify(result)).toMatchSnapshot(); 38 | // FlowJs is not valid due to `import` not transpiled to typeof 39 | // expect(result).toBeValidFlowTypeDeclarations(); 40 | }); 41 | 42 | it("should support generic type rename", () => { 43 | const ts = ` 44 | export declare const ProfilingLevel: Readonly<{ 45 | readonly off: "off"; 46 | }>; 47 | export declare type ProfilingLevel = typeof ProfilingLevel[keyof typeof ProfilingLevel]; 48 | 49 | export declare type Callback = (error?: Error, result?: T) => void; 50 | 51 | export declare const callback: Callback; 52 | `; 53 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 54 | expect(beautify(result)).toMatchSnapshot(); 55 | expect(result).toBeValidFlowTypeDeclarations(); 56 | }); 57 | -------------------------------------------------------------------------------- /src/__tests__/enums.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle empty enums", () => { 5 | const ts = `enum Empty { }`; 6 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 7 | expect(beautify(result)).toMatchSnapshot("class"); 8 | expect(result).toBeValidFlowTypeDeclarations(); 9 | }); 10 | 11 | it("should handle basic enums", () => { 12 | const ts = `enum Label { 13 | LABEL_OPTIONAL, 14 | LABEL_REQUIRED, 15 | LABEL_REPEATED, 16 | } 17 | type A = Label 18 | type B = Label.LABEL_OPTIONAL 19 | `; 20 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 21 | expect(beautify(result)).toMatchSnapshot("class"); 22 | expect(result).toBeValidFlowTypeDeclarations(); 23 | }); 24 | 25 | it("should handle number enums", () => { 26 | const ts = `enum Label { 27 | ONE = 1, 28 | TWO = 2, 29 | THREE = 3, 30 | NEGATIVE = -123, 31 | DECIMAL = 3.14, 32 | } 33 | type A = Label 34 | type B = Label.TWO 35 | `; 36 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 37 | expect(beautify(result)).toMatchSnapshot("class"); 38 | expect(result).toBeValidFlowTypeDeclarations(); 39 | }); 40 | 41 | it("should handle string enums", () => { 42 | const ts = `enum Label { 43 | LABEL_OPTIONAL = 'LABEL_OPTIONAL', 44 | LABEL_REQUIRED = 'LABEL_REQUIRED', 45 | LABEL_REPEATED = 'LABEL_REPEATED', 46 | } 47 | type A = Label 48 | type B = Label.LABEL_REQUIRED 49 | `; 50 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 51 | expect(beautify(result)).toMatchSnapshot("class"); 52 | expect(result).toBeValidFlowTypeDeclarations(); 53 | }); 54 | 55 | it("should handle importing enum types", () => { 56 | const results = compiler.compileDefinitionFiles( 57 | [ 58 | "src/__tests__/snippet/export-enum-file.ts", 59 | "src/__tests__/snippet/import-enum-type-file.ts", 60 | ], 61 | { 62 | quiet: false, 63 | }, 64 | ); 65 | for (const result of results) { 66 | expect(beautify(result[1])).toMatchSnapshot("class"); 67 | // TODO: this function only runs flow on one file at a time, so it errors when trying to import 68 | // expect(result[1]).toBeValidFlowTypeDeclarations(); 69 | } 70 | }); 71 | 72 | it("should handle importing enums", () => { 73 | const results = compiler.compileDefinitionFiles( 74 | [ 75 | "src/__tests__/snippet/export-enum-file.ts", 76 | "src/__tests__/snippet/import-enum-file.ts", 77 | ], 78 | { 79 | quiet: false, 80 | }, 81 | ); 82 | for (const result of results) { 83 | expect(beautify(result[1])).toMatchSnapshot("class"); 84 | // TODO: this function only runs flow on one file at a time, so it errors when trying to import 85 | // expect(result[1]).toBeValidFlowTypeDeclarations(); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /src/__tests__/exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle exports", () => { 5 | const ts = ` 6 | export default module 7 | export { module } 8 | export { module as newModule } 9 | export { GeneratorOptions } from "@babel/generator"; 10 | export { GeneratorOptions as NewGeneratorOptions } from "@babel/generator"; 11 | export * from 'typescript'; 12 | export * as t from "@babel/types"; 13 | //enable when typescript supports 14 | //export traverse, { Visitor, NodePath } from "@babel/traverse"; 15 | //export template from "@babel/template";`; 16 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 17 | expect(beautify(result)).toMatchSnapshot(); 18 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-module 19 | }); 20 | 21 | test("should handle unnamed default export", () => { 22 | const ts = ` 23 | export default function(): void; 24 | `; 25 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 26 | expect(beautify(result)).toMatchSnapshot(); 27 | expect(result).toBeValidFlowTypeDeclarations(); 28 | }); 29 | -------------------------------------------------------------------------------- /src/__tests__/function-exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle exported es module functions", () => { 5 | const ts = ` 6 | class Action {} 7 | class RouterState {} 8 | class Store {} 9 | class SyncHistoryWithStoreOptions {} 10 | class HistoryUnsubscribe {} 11 | 12 | export function routerReducer(state?: RouterState, action?: Action): RouterState; 13 | export function syncHistoryWithStore(history: History, store: Store, options?: SyncHistoryWithStoreOptions): History & HistoryUnsubscribe; 14 | `; 15 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 16 | expect(beautify(result)).toMatchSnapshot(); 17 | expect(result).toBeValidFlowTypeDeclarations(); 18 | }); 19 | 20 | it("should handle toString function overload", () => { 21 | const ts = `export function toString(): void; 22 | export function toString(e: number): void; 23 | export function toString(b: string): void; 24 | `; 25 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 26 | expect(beautify(result)).toMatchSnapshot(); 27 | expect(result).toBeValidFlowTypeDeclarations(); 28 | }); 29 | 30 | it("should handle default exported es module functions", () => { 31 | const ts = ` 32 | class Action {} 33 | class RouterState {} 34 | 35 | export default function routerReducer(state?: RouterState, action?: Action): RouterState;`; 36 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 37 | expect(beautify(result)).toMatchSnapshot(); 38 | expect(result).toBeValidFlowTypeDeclarations(); 39 | }); 40 | 41 | it("should handle function overload es module functions", () => { 42 | const ts = ` 43 | class Action {} 44 | class RouterState {} 45 | 46 | export function routerReducer(state?: RouterState, action?: Action): RouterState; 47 | export function routerReducer(state?: RouterState): RouterState; 48 | `; 49 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 50 | expect(beautify(result)).toMatchSnapshot(); 51 | expect(result).toBeValidFlowTypeDeclarations(); 52 | }); 53 | 54 | it("should remove this annotation from functions", () => { 55 | const ts = 56 | "function addClickListener(onclick: (this: void, e: Event) => void): void;"; 57 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 58 | expect(beautify(result)).toMatchSnapshot(); 59 | expect(result).toBeValidFlowTypeDeclarations(); 60 | }); 61 | 62 | it("should remove default parameters from functions", () => { 63 | const ts = 64 | "function addClickListener(onclick: (e: Event) => void): T;"; 65 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 66 | expect(beautify(result)).toMatchSnapshot(); 67 | expect(result).toBeValidFlowTypeDeclarations(); 68 | }); 69 | 70 | it("should not break with Promise return types - issue 156", () => { 71 | const ts = "export declare const fn: () => Promise;"; 72 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 73 | expect(beautify(result)).toMatchSnapshot(); 74 | expect(result).toBeValidFlowTypeDeclarations(); 75 | }); 76 | -------------------------------------------------------------------------------- /src/__tests__/global-declares.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle declared interfaces", () => { 5 | const ts = ` 6 | declare interface ICustomMessage { 7 | method(test: string): void; 8 | otherMethod(literal: "A"|"B"): void; 9 | } 10 | `; 11 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 12 | expect(beautify(result)).toMatchSnapshot(); 13 | expect(result).toBeValidFlowTypeDeclarations(); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__tests__/global-this.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should not crash when getting globalThis in code", () => { 5 | const ts = `import * as React from 'react'; 6 | export default class MenuStatefulContainer extends React.Component { 7 | handleItemClick: ( 8 | event: React.MouseEvent 9 | ) => void; 10 | render(): React.ReactNode; 11 | } 12 | `; 13 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 14 | expect(beautify(result)).toMatchSnapshot(); 15 | expect(result).not.toBeValidFlowTypeDeclarations(); // missing-type-arg,prop-missing 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/import.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle dynamic imports", () => { 5 | const ts = ` 6 | // whole module 7 | type A = import('react'); 8 | 9 | // type alias inside module 10 | type B = import('react').ReactNode; 11 | 12 | // generic type alias inside module, with type arguments 13 | type C = import('react').ComponentType<{}>; 14 | 15 | // class inside module 16 | type D = import('zlib').Zlib; 17 | `; 18 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 19 | expect(beautify(result)).toMatchSnapshot(); 20 | expect(result).toBeValidFlowTypeDeclarations(); 21 | }); 22 | 23 | it("should handle imports from odd names", () => { 24 | const ts = ` 25 | type A = import('..'); 26 | type B = import('@!-/./'); 27 | `; 28 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 29 | expect(beautify(result)).toMatchSnapshot(); 30 | // expect(result).toBeValidFlowTypeDeclarations(); // would need actual modules at those names 31 | }); 32 | 33 | it("should handle import nested in type arguments of import", () => { 34 | // In other words, test that our visitor for this feature didn't forget to 35 | // visit the type arguments in the case where it's rewriting something. 36 | const ts = ` 37 | type A = import("react").ComponentType>; 38 | `; 39 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 40 | expect(beautify(result)).toMatchSnapshot(); 41 | expect(result).toBeValidFlowTypeDeclarations(); 42 | }); 43 | 44 | it("should handle import in an imported file", () => { 45 | // There once was a bug where transformers wouldn't get run on the second 46 | // (or later) file in a list like this. Test that the transformer 47 | // implementing this feature does indeed run there. 48 | const results = compiler.compileDefinitionFiles( 49 | [ 50 | "src/__tests__/snippet/import-import-type.ts", 51 | "src/__tests__/snippet/import-type.ts", 52 | ], 53 | { quiet: false }, 54 | ); 55 | for (const result of results) { 56 | expect(beautify(result[1])).toMatchSnapshot(result[0]); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /src/__tests__/imports.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle imports", () => { 5 | const ts = `import { GeneratorOptions } from "@babel/generator"; 6 | import traverse, { Visitor, NodePath } from "@babel/traverse"; 7 | import { Visitor as NewVisitor } from "@babel/traverse"; 8 | import template from "@babel/template"; 9 | import * as t from "@babel/types"; 10 | import v, * as d from 'typescript';`; 11 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 12 | expect(beautify(result)).toMatchSnapshot(); 13 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-module 14 | }); 15 | 16 | it("should handle imports inside module", () => { 17 | const ts = ` 18 | declare module '@babel/core' { 19 | import { GeneratorOptions } from "@babel/generator"; 20 | import traverse, { Visitor, NodePath } from "@babel/traverse"; 21 | import { Visitor as NewVisitor } from "@babel/traverse"; 22 | import template from "@babel/template"; 23 | import * as t from "@babel/types"; 24 | import v, * as d from 'typescript'; 25 | } 26 | `; 27 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 28 | expect(beautify(result)).toMatchSnapshot(); 29 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-module 30 | }); 31 | 32 | it("should handle import type", () => { 33 | const ts = ` 34 | type S = typeof import('http') 35 | `; 36 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 37 | expect(beautify(result)).toMatchSnapshot(); 38 | expect(result).toBeValidFlowTypeDeclarations(); 39 | }); 40 | 41 | it("should handle type imports", () => { 42 | const ts = `import type { GeneratorOptions } from "@babel/generator"; 43 | import type traverse from "@babel/traverse"; 44 | import type { Visitor as NewVisitor } from "@babel/traverse"; 45 | `; 46 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 47 | expect(beautify(result)).toMatchSnapshot(); 48 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-module 49 | }); 50 | -------------------------------------------------------------------------------- /src/__tests__/indexers.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle indexers", () => { 5 | const ts = ` 6 | type Map = { 7 | [key: string]: number 8 | } 9 | `; 10 | { 11 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 12 | expect(beautify(result)).toMatchSnapshot(); 13 | expect(result).toBeValidFlowTypeDeclarations(); 14 | } 15 | 16 | { 17 | const result = compiler.compileDefinitionString(ts, { 18 | quiet: true, 19 | inexact: false, 20 | }); 21 | expect(beautify(result)).toMatchSnapshot(); 22 | expect(result).toBeValidFlowTypeDeclarations(); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/interface-exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle exported interfaces", () => { 5 | const ts = `export interface UnaryFunction { 6 | (source: T): R; 7 | } 8 | `; 9 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 10 | expect(beautify(result)).toMatchSnapshot(); 11 | expect(result).toBeValidFlowTypeDeclarations(); 12 | }); 13 | 14 | it("should handle exported interfaces within a module", () => { 15 | const ts = `declare module "my-module" { 16 | export interface UnaryFunction { 17 | (source: T): R; 18 | } 19 | } 20 | `; 21 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 22 | expect(beautify(result)).toMatchSnapshot(); 23 | expect(result).toBeValidFlowTypeDeclarations(); 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/interfaces.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle single interface", () => { 5 | const ts = ` 6 | interface User { 7 | firstName: string 8 | } 9 | `; 10 | const result = compiler.compileDefinitionString(ts); 11 | expect(beautify(result)).toMatchSnapshot(); 12 | expect(result).toBeValidFlowTypeDeclarations(); 13 | const result2 = compiler.compileDefinitionString(ts, { 14 | interfaceRecords: true, 15 | }); 16 | expect(beautify(result2)).toMatchSnapshot(); 17 | expect(result2).toBeValidFlowTypeDeclarations(); 18 | }); 19 | 20 | it("should handle interface inheritance", () => { 21 | const ts = ` 22 | interface User { 23 | firstName: string 24 | } 25 | interface SpecialUser extends User { 26 | nice: number 27 | } 28 | `; 29 | const result = compiler.compileDefinitionString(ts); 30 | expect(beautify(result)).toMatchSnapshot(); 31 | expect(result).toBeValidFlowTypeDeclarations(); 32 | const result2 = compiler.compileDefinitionString(ts, { 33 | interfaceRecords: true, 34 | }); 35 | expect(beautify(result2)).toMatchSnapshot(); 36 | expect(result2).toBeValidFlowTypeDeclarations(); 37 | 38 | const result3 = compiler.compileDefinitionString(ts, { 39 | interfaceRecords: true, 40 | inexact: false, 41 | }); 42 | expect(beautify(result3)).toMatchSnapshot(); 43 | expect(result3).toBeValidFlowTypeDeclarations(); 44 | }); 45 | 46 | it("should handle interface merging", () => { 47 | const ts = ` 48 | interface User { 49 | firstName: string 50 | } 51 | interface User { 52 | lastName: string 53 | } 54 | interface User { 55 | username: string 56 | } 57 | `; 58 | const result = compiler.compileDefinitionString(ts); 59 | expect(beautify(result)).toMatchSnapshot(); 60 | expect(result).toBeValidFlowTypeDeclarations(); 61 | const result2 = compiler.compileDefinitionString(ts, { 62 | interfaceRecords: true, 63 | }); 64 | expect(beautify(result2)).toMatchSnapshot(); 65 | expect(result2).toBeValidFlowTypeDeclarations(); 66 | }); 67 | 68 | it("should handle all properties", () => { 69 | const ts = ` 70 | interface Props { 71 | "aria-label": string; 72 | "aria-labelledby"?: number; 73 | color: string; 74 | [key: string]: string; 75 | } 76 | `; 77 | const result = compiler.compileDefinitionString(ts); 78 | expect(beautify(result)).toMatchSnapshot(); 79 | expect(result).not.toBeValidFlowTypeDeclarations(); // unsupported-syntax 80 | }); 81 | 82 | it("should support readonly modifier", () => { 83 | const ts = ` 84 | interface Helper { 85 | readonly name: string; 86 | readonly callback(): void; 87 | } 88 | `; 89 | const result = compiler.compileDefinitionString(ts); 90 | expect(beautify(result)).toMatchSnapshot(); 91 | expect(result).toBeValidFlowTypeDeclarations(); 92 | }); 93 | 94 | it("should support call signature", () => { 95 | const ts = ` 96 | interface ObjectSchema {} 97 | interface ObjectSchemaDefinition {} 98 | declare interface ObjectSchemaConstructor { 99 | (fields?: ObjectSchemaDefinition): ObjectSchema; 100 | new (): ObjectSchema<{}>; 101 | } 102 | `; 103 | const result = compiler.compileDefinitionString(ts); 104 | expect(beautify(result)).toMatchSnapshot(); 105 | expect(result).toBeValidFlowTypeDeclarations(); 106 | }); 107 | 108 | it("should remove this in call signature", () => { 109 | const ts = ` 110 | interface Arc { 111 | (this: This, d: Datum, ...args: any[]): string | null; 112 | } 113 | 114 | interface D { 115 | new (this: This, d: Datum, ...args: any[]); 116 | } 117 | 118 | interface C { 119 | (this: This, d: Datum, ...args: any[]); 120 | } 121 | `; 122 | const result = compiler.compileDefinitionString(ts); 123 | expect(beautify(result)).toMatchSnapshot(); 124 | expect(result).toBeValidFlowTypeDeclarations(); 125 | }); 126 | 127 | it("should remove generic defaults in call signature", () => { 128 | const ts = ` 129 | interface AbstractLevelDOWN {} 130 | interface AbstractLevelDOWNConstructor { 131 | (location: string): AbstractLevelDOWN; 132 | } 133 | `; 134 | const result = compiler.compileDefinitionString(ts); 135 | expect(beautify(result)).toMatchSnapshot(); 136 | expect(result).toBeValidFlowTypeDeclarations(); 137 | }); 138 | 139 | it("should support omitting generic defaults in types, classes, interfaces", () => { 140 | const ts = ` 141 | interface Foo {} 142 | interface FooBar extends Foo {} 143 | type Bar = {} 144 | class Baz {} 145 | 146 | declare var a: Foo 147 | declare var b: Bar 148 | declare var c: Baz 149 | 150 | declare var d: Foo 151 | declare var e: Bar 152 | declare var f: Baz 153 | `; 154 | const result = compiler.compileDefinitionString(ts); 155 | expect(beautify(result)).toMatchSnapshot(); 156 | expect(result).toBeValidFlowTypeDeclarations(); 157 | }); 158 | 159 | it("should support optional methods", () => { 160 | const ts = ` 161 | interface Example { 162 | required(value: any, state: State): true; 163 | optional?(value: any, state: State): false; 164 | } 165 | `; 166 | const result = compiler.compileDefinitionString(ts); 167 | expect(beautify(result)).toMatchSnapshot(); 168 | expect(result).toBeValidFlowTypeDeclarations(); 169 | }); 170 | 171 | it("should handle toString property name", () => { 172 | const ts = ` 173 | interface A { 174 | toString(): string; 175 | } 176 | `; 177 | const result = compiler.compileDefinitionString(ts); 178 | expect(beautify(result)).toMatchSnapshot(); 179 | expect(result).toBeValidFlowTypeDeclarations(); 180 | }); 181 | 182 | it("should handle untyped object binding pattern", () => { 183 | const ts = ` 184 | interface ObjectBinding { 185 | (): void; 186 | ({}): void; 187 | ({ a, b }): void; 188 | } 189 | `; 190 | const result = compiler.compileDefinitionString(ts); 191 | expect(beautify(result)).toMatchSnapshot(); 192 | expect(result).toBeValidFlowTypeDeclarations(); 193 | }); 194 | 195 | it("should handle untyped array binding pattern", () => { 196 | const ts = ` 197 | interface ArrayBinding { 198 | (): void; 199 | ([]): void; 200 | ([ a, b ]): void; 201 | } 202 | `; 203 | const result = compiler.compileDefinitionString(ts); 204 | expect(beautify(result)).toMatchSnapshot(); 205 | expect(result).toBeValidFlowTypeDeclarations(); 206 | }); 207 | 208 | it("should handle typed object binding pattern", () => { 209 | const ts = ` 210 | interface ObjectBinding { 211 | (): void; 212 | ({}: any): void; 213 | ({ a, b }: { a: string, b: number }): void; 214 | } 215 | `; 216 | const result = compiler.compileDefinitionString(ts); 217 | expect(beautify(result)).toMatchSnapshot(); 218 | expect(result).toBeValidFlowTypeDeclarations(); 219 | }); 220 | 221 | it("should handle typed array binding pattern", () => { 222 | const ts = ` 223 | interface ArrayBinding { 224 | (): void; 225 | ([]: []): void; 226 | ([ a, b ]: [string, number]): void; 227 | } 228 | `; 229 | const result = compiler.compileDefinitionString(ts); 230 | expect(beautify(result)).toMatchSnapshot(); 231 | expect(result).toBeValidFlowTypeDeclarations(); 232 | }); 233 | 234 | it("should handle mutli-extends pattern", () => { 235 | const ts = ` 236 | interface Shape { 237 | color: string; 238 | } 239 | 240 | interface PenStroke { 241 | penWidth: number; 242 | } 243 | interface Square extends Shape, PenStroke { 244 | sideLength: number; 245 | } 246 | `; 247 | const result = compiler.compileDefinitionString(ts); 248 | expect(beautify(result)).toMatchSnapshot(); 249 | expect(result).toBeValidFlowTypeDeclarations(); 250 | }); 251 | -------------------------------------------------------------------------------- /src/__tests__/jsdoc.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle basic jsdoc", () => { 5 | const ts = `const skip: number 6 | /** 7 | * @param {string} userToken User authentication token 8 | * @returns {Promise} 9 | */ 10 | declare function authorize(userToken: string): Promise 11 | 12 | /** 13 | * @param {*} any_type 14 | * @returns {void} 15 | */ 16 | declare function untyped(any_type: any): void; 17 | 18 | type F = { 19 | /** 20 | * Get instance or instances of type registered under the provided name and optional hint. 21 | * @param {string} name The binding name. 22 | * @param {string} hint The binding hint. 23 | * @param {...args} args Additional args. 24 | */ 25 | get(name: string, hint?: string, ...args: any[]): T; 26 | /** 27 | * This method inserts an entry to the list. Optionally it can place new entry at provided index. 28 | * @param entry {?} - The entry to insert 29 | * @param opt_idx {number=} - The index where the new entry should be inserted; if omitted or greater then the current size of the list, the entry is added at the end of the list; 30 | * a negative index is treated as being relative from the end of the list 31 | */ 32 | add(entry: any, opt_idx?: number): void; 33 | } 34 | 35 | /** 36 | * Patches an Element with the the provided function. Exactly one top level 37 | * element call should be made corresponding to \`node\`. 38 | * @param {!Element} node The Element where the patch should start. 39 | * @param {!function(T)} fn A function containing open/close/etc. calls that 40 | * describe the DOM. This should have at most one top level element call. 41 | * @param {T=} data An argument passed to fn to represent DOM state. 42 | * @return {?Node} The node if it was updated, its replacedment or null if it 43 | * was removed. 44 | * @template T 45 | * @see https://thereShouldNotBeASpaceAfterHttps.com 46 | * @see HelloWorld 47 | */ 48 | declare var patchOuter: ( 49 | node: Element, 50 | fn: (data: T) => void, 51 | data?: T, 52 | ) => Node | null; 53 | 54 | declare class ECharts {} 55 | 56 | /** 57 | * Creates an ECharts instance, and returns an echartsInstance. You shall 58 | * not initialize multiple ECharts instances on a single container. 59 | * 60 | * @param {HTMLDivElement | HTMLCanvasElement} dom Instance container, 61 | * usually is a \`div\` element with height and width defined. 62 | * @param {object | string} [theme] Theme to be applied. 63 | * This can be a configuring object of a theme, or a theme name 64 | * registered through [echarts.registerTheme](https://echarts.apache.org/api.html#echarts.registerTheme). 65 | * @param {object} [opts] Chart configurations. 66 | * @param {number} [opts.devicePixelRatio] Ratio of one physical pixel to 67 | * the size of one device independent pixels. Browser's 68 | * \`window.devicePixelRatio\` is used by default. 69 | * @param {string} [opts.renderer] Supports \`'canvas'\` or \`'svg'\`. 70 | * See [Render by Canvas or SVG](https://echarts.apache.org/tutorial.html#Render%20by%20Canvas%20or%20SVG). 71 | * @param {number} [opts.width] Specify width explicitly, in pixel. 72 | * If setting to \`null\`/\`undefined\`/\`'auto'\`, width of \`dom\` 73 | * (instance container) will be used. 74 | * @param {number} [opts.height] Specify height explicitly, in pixel. 75 | * If setting to \`null\`/\`undefined\`/\`'auto'\`, height of \`dom\` 76 | * (instance container) will be used. 77 | */ 78 | function init( 79 | dom: HTMLDivElement | HTMLCanvasElement, 80 | theme?: object | string, 81 | opts?: { 82 | devicePixelRatio?: number; 83 | renderer?: string; 84 | width?: number | string; 85 | height?: number | string; 86 | }, 87 | ): ECharts; 88 | 89 | /** 90 | * Plain comment 91 | */ 92 | declare function test(): Promise 93 | `; 94 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 95 | expect(beautify(result)).toMatchSnapshot(); 96 | expect(result).toBeValidFlowTypeDeclarations(); 97 | }); 98 | 99 | it("should remove jsdoc", () => { 100 | const ts = ` 101 | /** 102 | * @param {string} userToken User authentication token 103 | * @returns {Promise} 104 | */ 105 | declare function authorize(userToken: string): Promise 106 | `; 107 | const result = compiler.compileDefinitionString(ts, { 108 | quiet: true, 109 | jsdoc: false, 110 | }); 111 | expect(beautify(result)).toMatchSnapshot(); 112 | expect(result).toBeValidFlowTypeDeclarations(); 113 | }); 114 | -------------------------------------------------------------------------------- /src/__tests__/mapped-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle mapped types", () => { 5 | const ts = ` 6 | type Ref = {current: T | null} 7 | type SourceUnion = 'a' | 'b' | 'c' 8 | type SourceObject = { 9 | a: number, 10 | d: string 11 | } 12 | type MappedUnion = { 13 | [K in SourceUnion]: Ref 14 | } 15 | type MappedObj = { 16 | [K in keyof SourceObject]: Ref 17 | } 18 | type ConstantKey = MappedObj["a"] 19 | `; 20 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 21 | expect(beautify(result)).toMatchSnapshot(); 22 | expect(result).toBeValidFlowTypeDeclarations(); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/module-identifiers.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle react types", () => { 5 | const ts = ` 6 | import type {ReactNode, ReactElement} from 'react' 7 | import * as React from 'react' 8 | declare function s(node: ReactNode): void; 9 | declare function s(node: React.ReactNode): void; 10 | declare function s(node: ReactElement<'div'>): void; 11 | declare function s(node: React.ReactElement<'div'>): void; 12 | `; 13 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 14 | expect(beautify(result)).toMatchSnapshot(); 15 | expect(result).toBeValidFlowTypeDeclarations(); 16 | }); 17 | 18 | describe("should handle global types", () => { 19 | test("jsx", () => { 20 | const ts = ` 21 | import * as React from 'react' 22 | declare function s(node: JSX.Element): void; 23 | 24 | type Props = {children: JSX.Element} 25 | 26 | declare class Component extends React.Component { 27 | render(): JSX.Element 28 | } 29 | `; 30 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 31 | expect(beautify(result)).toMatchSnapshot(); 32 | expect(result).toBeValidFlowTypeDeclarations(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/modules.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle module", () => { 5 | const ts = ` 6 | declare module 'test' { 7 | declare export type Test = 'ok' | 'error' 8 | declare type Test2 = 'ok' | 'error' 9 | type Maybe = {type: 'just', value: T} | {type: 'nothing'} 10 | export type Ref = { current: T } 11 | 12 | export const ok: number 13 | } 14 | `; 15 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 16 | expect(beautify(result)).toMatchSnapshot(); 17 | expect(result).toBeValidFlowTypeDeclarations(); 18 | }); 19 | 20 | it("should handle module merging", () => { 21 | const ts = ` 22 | declare module 'test' { 23 | interface A { 24 | bar: string 25 | } 26 | export const ok: number 27 | } 28 | declare module 'test' { 29 | interface A { 30 | baz: string 31 | } 32 | export const error: string 33 | } 34 | `; 35 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 36 | expect(beautify(result)).toMatchSnapshot(); 37 | expect(result).toBeValidFlowTypeDeclarations(); 38 | }); 39 | 40 | it("should not merge distinct modules", () => { 41 | const ts = ` 42 | declare module 'A' { 43 | export interface A { 44 | foo: string; 45 | } 46 | } 47 | declare module 'B' { 48 | export interface A { 49 | baz: string; 50 | } 51 | } 52 | export interface A { 53 | bar: string 54 | } 55 | `; 56 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 57 | expect(beautify(result)).toMatchSnapshot(); 58 | expect(result).toBeValidFlowTypeDeclarations(); 59 | }); 60 | 61 | it("should handle module function merging", () => { 62 | const ts = ` 63 | declare module 'test' { 64 | declare function test(err: number): void 65 | } 66 | declare module 'test' { 67 | declare function test(response: string): string 68 | } 69 | `; 70 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 71 | expect(beautify(result)).toMatchSnapshot(); 72 | expect(result).toBeValidFlowTypeDeclarations(); 73 | }); 74 | -------------------------------------------------------------------------------- /src/__tests__/namespaces.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | describe("should handle merging with other types", () => { 5 | describe("function", () => { 6 | test("interface", () => { 7 | const ts = ` 8 | declare function test(foo: number): string; 9 | namespace test { 10 | export interface Foo { 11 | bar: number 12 | } 13 | } 14 | `; 15 | const result = compiler.compileDefinitionString(ts); 16 | expect(beautify(result)).toMatchSnapshot(); 17 | expect(result).toBeValidFlowTypeDeclarations(); 18 | }); 19 | 20 | test("type", () => { 21 | const ts = ` 22 | declare function test(foo: number): string; 23 | namespace test { 24 | export type Foo = { 25 | bar: number 26 | } 27 | } 28 | `; 29 | const result = compiler.compileDefinitionString(ts); 30 | expect(beautify(result)).toMatchSnapshot(); 31 | expect(result).toBeValidFlowTypeDeclarations(); 32 | }); 33 | 34 | test("const", () => { 35 | const ts = ` 36 | declare function test(foo: number): string; 37 | namespace test { 38 | export const ok: number 39 | } 40 | `; 41 | const result = compiler.compileDefinitionString(ts); 42 | expect(beautify(result)).toMatchSnapshot(); 43 | expect(result).toBeValidFlowTypeDeclarations(); 44 | }); 45 | }); 46 | 47 | test("class", () => { 48 | const ts = ` 49 | declare class Album { 50 | label: Album.AlbumLabel; 51 | } 52 | namespace Album { 53 | export declare class AlbumLabel { } 54 | } 55 | `; 56 | const result = compiler.compileDefinitionString(ts); 57 | expect(beautify(result)).toMatchSnapshot(); 58 | expect(result).toBeValidFlowTypeDeclarations(); 59 | }); 60 | 61 | test("enum", () => { 62 | const ts = ` 63 | // TODO: implement enum merging 64 | enum Color { 65 | red = 1, 66 | green = 2, 67 | blue = 4 68 | } 69 | namespace Color { 70 | export declare function mixColor(colorName: string): number; 71 | } 72 | `; 73 | const result = compiler.compileDefinitionString(ts); 74 | expect(beautify(result)).toMatchSnapshot(); 75 | expect(result).not.toBeValidFlowTypeDeclarations(); // TODO: prop-missing 76 | }); 77 | }); 78 | 79 | it("should handle namespaces", () => { 80 | const ts = ` 81 | namespace test { 82 | export const ok: number 83 | } 84 | `; 85 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 86 | expect(beautify(result)).toMatchSnapshot(); 87 | expect(result).toBeValidFlowTypeDeclarations(); 88 | }); 89 | 90 | it("should handle namespace merging", () => { 91 | const ts = ` 92 | namespace test { 93 | export const ok: number 94 | } 95 | namespace test { 96 | export const error: string 97 | } 98 | `; 99 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 100 | expect(beautify(result)).toMatchSnapshot(); 101 | expect(result).toBeValidFlowTypeDeclarations(); 102 | }); 103 | 104 | it("should handle namespace function merging", () => { 105 | const ts = ` 106 | namespace test { 107 | declare function test(err: number): void 108 | } 109 | namespace test { 110 | declare function test(response: string): string 111 | } 112 | `; 113 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 114 | expect(beautify(result)).toMatchSnapshot(); 115 | expect(result).toBeValidFlowTypeDeclarations(); 116 | }); 117 | 118 | it("should handle exported interfaces and types", () => { 119 | const ts = ` 120 | namespace Example { 121 | export interface StoreModel {} 122 | } 123 | `; 124 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 125 | expect(beautify(result)).toMatchSnapshot(); 126 | expect(result).toBeValidFlowTypeDeclarations(); 127 | }); 128 | 129 | it("should handle nested namespaces", () => { 130 | const ts = ` 131 | import * as external from "external"; 132 | 133 | declare namespace E0 { 134 | type A = external.type; 135 | namespace U1 { 136 | declare interface S3 { 137 | a: string; 138 | } 139 | } 140 | namespace U1 { 141 | declare var e2: number; 142 | enum E2 { 143 | E = 1, 144 | } 145 | declare interface S3 { 146 | b: string; 147 | } 148 | namespace D1 { 149 | namespace S2 { 150 | declare interface S3 { 151 | b: string; 152 | } 153 | declare var n3: symbol; 154 | class N3 {} 155 | } 156 | } 157 | namespace DD1 { 158 | namespace S2 { 159 | declare interface S3 { 160 | e: number; 161 | } 162 | } 163 | } 164 | } 165 | namespace S1 { 166 | declare var m3: string; 167 | } 168 | declare var s1: string; 169 | } 170 | `; 171 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 172 | expect(beautify(result)).toMatchSnapshot(); 173 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-module 174 | }); 175 | 176 | describe("should handle nested namespace merging", () => { 177 | describe("function", () => { 178 | test("interface", () => { 179 | const ts = ` 180 | namespace ns { 181 | declare function test(foo: number): string; 182 | namespace test { 183 | export interface Foo { 184 | bar: number 185 | } 186 | } 187 | } 188 | `; 189 | const result = compiler.compileDefinitionString(ts); 190 | expect(beautify(result)).toMatchSnapshot(); 191 | }); 192 | 193 | test("type", () => { 194 | const ts = ` 195 | namespace ns { 196 | declare function test(foo: number): string; 197 | namespace test { 198 | export type Foo = { 199 | bar: number 200 | } 201 | } 202 | } 203 | `; 204 | const result = compiler.compileDefinitionString(ts); 205 | expect(beautify(result)).toMatchSnapshot(); 206 | }); 207 | 208 | test("const", () => { 209 | const ts = ` 210 | namespace ns { 211 | declare function test(foo: number): string; 212 | namespace test { 213 | export const ok: number 214 | } 215 | } 216 | `; 217 | const result = compiler.compileDefinitionString(ts); 218 | expect(beautify(result)).toMatchSnapshot(); 219 | }); 220 | }); 221 | 222 | test("class", () => { 223 | const ts = ` 224 | namespace ns { 225 | declare class Album { 226 | label: ns.Album.AlbumLabel; 227 | } 228 | namespace Album { 229 | export declare class AlbumLabel { } 230 | } 231 | } 232 | `; 233 | const result = compiler.compileDefinitionString(ts); 234 | expect(beautify(result)).toMatchSnapshot(); 235 | }); 236 | 237 | test("enum", () => { 238 | const ts = ` 239 | namespace ns { 240 | // TODO: implement enum merging inside namespaces 241 | enum Color { 242 | red = 1, 243 | green = 2, 244 | blue = 4 245 | } 246 | namespace Color { 247 | export declare function mixColor(colorName: string): number; 248 | } 249 | } 250 | `; 251 | const result = compiler.compileDefinitionString(ts); 252 | expect(beautify(result)).toMatchSnapshot(); 253 | }); 254 | }); 255 | 256 | test("should handle qualified namespaces", () => { 257 | const ts = ` 258 | declare namespace A.B { 259 | interface S { 260 | readonly d: A; 261 | b: number; 262 | } 263 | declare class D {} 264 | } 265 | 266 | declare namespace A.B.C { 267 | declare class N extends D implements S { 268 | a: string; 269 | } 270 | }`; 271 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 272 | expect(beautify(result)).toMatchSnapshot(); 273 | expect(result).not.toBeValidFlowTypeDeclarations(); // TODO: type-as-value 274 | }); 275 | 276 | test("should handle global augmentation", () => { 277 | const ts = ` 278 | declare global { 279 | interface Array {} 280 | }`; 281 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 282 | expect(beautify(result)).toMatchSnapshot(); 283 | expect(result).toBeValidFlowTypeDeclarations(); 284 | }); 285 | 286 | test("should handle import equals declaration", () => { 287 | const ts = ` 288 | import hello = A.B; 289 | `; 290 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 291 | expect(beautify(result)).toMatchSnapshot(); 292 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-name 293 | }); 294 | -------------------------------------------------------------------------------- /src/__tests__/snippet/export-enum-file.ts: -------------------------------------------------------------------------------- 1 | export enum Label { 2 | A = "A", 3 | B = "B", 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/snippet/import-enum-file.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "./export-enum-file"; 2 | export function foo(label: Label): void { 3 | console.log(label); 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/snippet/import-enum-type-file.ts: -------------------------------------------------------------------------------- 1 | import type { Label } from "./export-enum-file"; 2 | export function foo(label: Label): void { 3 | console.log(label); 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/snippet/import-import-type.ts: -------------------------------------------------------------------------------- 1 | export { R } from "./import-type"; 2 | -------------------------------------------------------------------------------- /src/__tests__/snippet/import-type.ts: -------------------------------------------------------------------------------- 1 | export declare const R: import("react").RefAttributes; 2 | -------------------------------------------------------------------------------- /src/__tests__/spread.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should use spread when performing union of object types", () => { 5 | const ts = ` 6 | type Foo = { foo: number }; 7 | type Bar = { bar: string }; 8 | const combination: Foo & Bar; 9 | `; 10 | 11 | { 12 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 13 | expect(beautify(result)).toMatchSnapshot(); 14 | expect(result).toBeValidFlowTypeDeclarations(); 15 | } 16 | 17 | { 18 | const result = compiler.compileDefinitionString(ts, { 19 | quiet: true, 20 | inexact: false, 21 | }); 22 | expect(beautify(result)).toMatchSnapshot(); 23 | expect(result).toBeValidFlowTypeDeclarations(); 24 | } 25 | }); 26 | 27 | it("should not insert spread when performing union of class types", () => { 28 | const ts = ` 29 | class Foo {} 30 | class Bar {} 31 | const combination: Foo & Bar; 32 | `; 33 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 34 | expect(beautify(result)).toMatchSnapshot(); 35 | expect(result).toBeValidFlowTypeDeclarations(); 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/string-literals.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it('should handle string literals in function argument "overloading"', () => { 5 | const ts = ` 6 | interface MyObj { 7 | on(event: 'error', cb: (err: Error) => void): void; 8 | on(event: 'close', cb: (code: number, message: string) => void): void; 9 | on(event: 'message', cb: (data: any, flags: { binary: boolean }) => void): void; 10 | on(event: 'ping', cb: (data: any, flags: { binary: boolean }) => void): void; 11 | on(event: 'pong', cb: (data: any, flags: { binary: boolean }) => void): void; 12 | on(event: 'open', cb: () => void): void; 13 | on(event: string, listener: (...args: any[]) => void): void; 14 | } 15 | `; 16 | 17 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 18 | 19 | expect(beautify(result)).toMatchSnapshot(); 20 | expect(result).toBeValidFlowTypeDeclarations(); 21 | }); 22 | 23 | it("should handle exported constant string literals", () => { 24 | const ts = ` 25 | export declare const SET_NAME = "my/lib/SET_NAME"; 26 | export declare const SET_STAGE = "my/lib/SET_STAGE"; 27 | `; 28 | 29 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 30 | 31 | expect(beautify(result)).toMatchSnapshot(); 32 | expect(result).toBeValidFlowTypeDeclarations(); 33 | }); 34 | -------------------------------------------------------------------------------- /src/__tests__/tuples.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle tuples", () => { 5 | const ts = ` 6 | type T1 = [number, string?]; 7 | type T2 = [number, ...string[]]; 8 | `; 9 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 10 | expect(beautify(result)).toMatchSnapshot(); 11 | expect(result).toBeValidFlowTypeDeclarations(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/type-exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle exported types", () => { 5 | const ts = ` 6 | export declare type FactoryOrValue = T | (() => T); 7 | export type Maybe = {type: 'just', value: T} | {type: 'nothing'} 8 | `; 9 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 10 | expect(beautify(result)).toMatchSnapshot(); 11 | expect(result).toBeValidFlowTypeDeclarations(); 12 | }); 13 | 14 | it("should handle export list syntax", () => { 15 | const ts = ` 16 | declare type ComplexType = { 17 | type: number 18 | } | { 19 | type: string 20 | }; 21 | export type { ComplexType }; 22 | const foo = 5; 23 | export { foo }; 24 | `; 25 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 26 | expect(beautify(result)).toMatchSnapshot(); 27 | expect(result).toBeValidFlowTypeDeclarations(); 28 | }); 29 | 30 | it("should handle inline export list syntax", () => { 31 | const ts = ` 32 | declare type ComplexType = { 33 | type: number 34 | } | { 35 | type: string 36 | }; 37 | const foo = 5; 38 | export { type ComplexType, foo }; 39 | `; 40 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 41 | expect(beautify(result)).toMatchSnapshot(); 42 | expect(result).toBeValidFlowTypeDeclarations(); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/union-strings.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle union strings", () => { 5 | const ts = ` 6 | interface MyObj { 7 | state?: "APPROVED" | "REQUEST_CHANGES" | "COMMENT" | "PENDING" 8 | } 9 | type CompletionsTriggerCharacter = '"' | "'"; 10 | `; 11 | 12 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 13 | 14 | expect(beautify(result)).toMatchSnapshot(); 15 | expect(result).toBeValidFlowTypeDeclarations(); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/utility-types.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle utility types", () => { 5 | const ts = ` 6 | type A = Readonly<{a: number}> 7 | type B = Partial<{a: number}> 8 | type C = NonNullable 9 | type D = ReadonlyArray 10 | type E = ReturnType<() => string> 11 | type F = Record 12 | type G = ReadonlySet 13 | type H = ReadonlyMap 14 | 15 | type A1 = Readonly 16 | type B1 = Partial 17 | type C1 = NonNullable 18 | type D1 = ReadonlyArray 19 | type E1 = ReturnType 20 | type F1 = Record 21 | 22 | type A2 = Readonly 23 | type B2 = Partial 24 | type C2 = NonNullable 25 | type D2 = ReadonlyArray 26 | type E2 = ReturnType<() => T> 27 | type F2 = Record 28 | `; 29 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 30 | expect(beautify(result)).toMatchSnapshot(); 31 | expect(result).toBeValidFlowTypeDeclarations(); 32 | }); 33 | 34 | it("should handle Omit type", () => { 35 | const ts = ` 36 | type A = Omit<{ a: string, b: number }, "a"> 37 | type B = Omit<{ a: string, b: number }, "a" | "b"> 38 | 39 | type O = { a: string, b: number }; 40 | type U = "a"; 41 | type C = Omit; 42 | `; 43 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 44 | expect(beautify(result)).toMatchSnapshot(); 45 | expect(result).toBeValidFlowTypeDeclarations(); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/value-exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle exported es module values", () => { 5 | const ts = `declare var test: {a: number}; 6 | export {test}; 7 | `; 8 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 9 | expect(beautify(result)).toMatchSnapshot(); 10 | expect(result).toBeValidFlowTypeDeclarations(); 11 | }); 12 | 13 | it("should handle default exported es module values", () => { 14 | const ts = `declare var test: {a: number}; 15 | export default test; 16 | `; 17 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 18 | expect(beautify(result)).toMatchSnapshot(); 19 | expect(result).toBeValidFlowTypeDeclarations(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/variables.spec.ts: -------------------------------------------------------------------------------- 1 | import { compiler, beautify } from ".."; 2 | import "../test-matchers"; 3 | 4 | it("should handle declares", () => { 5 | const ts = ` 6 | declare const test: {a: number}; 7 | declare const foo: string, bar: number; 8 | 9 | declare var baz: number; 10 | declare var quuz: any, quuuz: string; 11 | 12 | declare let quuuuz: number; 13 | declare let quuuuuz: string, fox: number; 14 | `; 15 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 16 | expect(beautify(result)).toMatchSnapshot(); 17 | expect(result).toBeValidFlowTypeDeclarations(); 18 | }); 19 | -------------------------------------------------------------------------------- /src/checker.ts: -------------------------------------------------------------------------------- 1 | import type { TypeChecker } from "typescript"; 2 | 3 | export const checker: { 4 | current: TypeChecker | null; 5 | } = { current: null }; 6 | -------------------------------------------------------------------------------- /src/cli/__tests__/__snapshots__/compiler.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should handle bounded polymorphism 1`] = ` 4 | "declare function fooGood< 5 | T: { 6 | x: number, 7 | ... 8 | } 9 | >( 10 | obj: T 11 | ): T; 12 | " 13 | `; 14 | 15 | exports[`should handle maybe & nullable type 1`] = `"declare var a: string | null | void;"`; 16 | -------------------------------------------------------------------------------- /src/cli/__tests__/compiler.spec.ts: -------------------------------------------------------------------------------- 1 | import compiler from "../compiler"; 2 | import beautify from "../beautifier"; 3 | import "../../test-matchers"; 4 | 5 | it("should handle maybe & nullable type", () => { 6 | const result = compiler.compileDefinitionString( 7 | "let a: string | null | undefined", 8 | { quiet: true }, 9 | ); 10 | 11 | expect(result).toMatchSnapshot(); 12 | expect(result).toBeValidFlowTypeDeclarations(); 13 | }); 14 | 15 | it("should handle bounded polymorphism", () => { 16 | const ts = ` 17 | function fooGood(obj: T): T { 18 | console.log(Math.abs(obj.x)); 19 | return obj; 20 | } 21 | `; 22 | 23 | const result = compiler.compileDefinitionString(ts, { quiet: true }); 24 | 25 | expect(beautify(result)).toMatchSnapshot(); 26 | expect(result).toBeValidFlowTypeDeclarations(); 27 | }); 28 | -------------------------------------------------------------------------------- /src/cli/__tests__/fixtures.spec.ts: -------------------------------------------------------------------------------- 1 | import compiler from "../compiler"; 2 | import beautify from "../beautifier"; 3 | import "../../test-matchers"; 4 | import fs from "fs"; 5 | 6 | it("handles the danger.d.ts correctly", () => { 7 | const dangerDTS = fs.readFileSync( 8 | `${__dirname}/fixtures/danger.d.ts`, 9 | "utf8", 10 | ); 11 | const result = compiler.compileDefinitionString(dangerDTS, { quiet: true }); 12 | 13 | expect(beautify(result)).toMatchSnapshot(); 14 | expect(result).not.toBeValidFlowTypeDeclarations(); // cannot-resolve-module 15 | }); 16 | -------------------------------------------------------------------------------- /src/cli/beautifier.ts: -------------------------------------------------------------------------------- 1 | import prettier from "prettier"; 2 | 3 | export default function beautify(str: string): string { 4 | return prettier.format(str, { parser: "babel-flow" }); 5 | } 6 | -------------------------------------------------------------------------------- /src/cli/compiler.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import ts, { 3 | createProgram, 4 | createCompilerHost, 5 | createSourceFile, 6 | ScriptTarget, 7 | transform, 8 | } from "typescript"; 9 | import type { SourceFile } from "typescript"; 10 | import tsc from "typescript-compiler"; 11 | 12 | import namespaceManager from "../namespace-manager"; 13 | import { assignOptions, resetOptions } from "../options"; 14 | import type { Options } from "../options"; 15 | import { checker } from "../checker"; 16 | import * as logger from "../logger"; 17 | import { withEnv } from "../env"; 18 | import { 19 | importEqualsTransformer, 20 | legacyModules, 21 | declarationFileTransform, 22 | importTypeToImportDeclaration, 23 | } from "../parse/transformers"; 24 | import { recursiveWalkTree } from "../parse"; 25 | import { printFlowGenHelper } from "../printers/node"; 26 | 27 | const compile = withEnv( 28 | (env: any, sourceFile: SourceFile): string => { 29 | const rootNode = recursiveWalkTree(sourceFile); 30 | 31 | const output = rootNode 32 | .getChildren() 33 | .map(child => { 34 | return child.print(); 35 | }) 36 | .join(""); 37 | 38 | const helpersOutputs = printFlowGenHelper(env); 39 | 40 | return `${helpersOutputs}\n\n${output}`; 41 | }, 42 | ); 43 | 44 | const reset = (options?: Options): void => { 45 | resetOptions(); 46 | if (options) { 47 | assignOptions(options); 48 | } 49 | namespaceManager.reset(); 50 | }; 51 | 52 | const compilerOptions = { 53 | noLib: true, 54 | target: ScriptTarget.Latest, 55 | }; 56 | 57 | const getTransformers = (options?: Options) => [ 58 | legacyModules(), 59 | importEqualsTransformer(), 60 | declarationFileTransform(options), 61 | importTypeToImportDeclaration(), 62 | ]; 63 | 64 | const transformFile = ( 65 | fileName: string, 66 | sourceText: string, 67 | languageVersion: ScriptTarget, 68 | options?: Options, 69 | ) => { 70 | const transformedAst = transform( 71 | //$todo Flow has problems when switching variables instead of literals 72 | createSourceFile(fileName, sourceText, languageVersion, true), 73 | getTransformers(options), 74 | compilerOptions, 75 | ).transformed[0]; 76 | const transformedText = ts.createPrinter().printFile(transformedAst); 77 | return createSourceFile(fileName, transformedText, languageVersion, true); 78 | }; 79 | 80 | /** 81 | * Compiles typescript files 82 | */ 83 | export default { 84 | reset, 85 | 86 | compile: compile.withEnv({}), 87 | 88 | setChecker(typeChecker: ts.TypeChecker) { 89 | checker.current = typeChecker; 90 | }, 91 | 92 | getTransformers(options?: Options) { 93 | return getTransformers(options); 94 | }, 95 | 96 | compileTest: (testPath: string, target: string): void => { 97 | tsc.compile(testPath, "--module commonjs -t ES6 --out " + target); 98 | }, 99 | 100 | compileDefinitionString: (string: string, options?: Options): string => { 101 | reset(options); 102 | 103 | const compilerHost = createCompilerHost({}, true); 104 | const oldSourceFile = compilerHost.getSourceFile; 105 | compilerHost.getSourceFile = (file, languageVersion) => { 106 | if (file === "file.ts") { 107 | return transformFile("/dev/null", string, languageVersion, options); 108 | } 109 | return oldSourceFile(file, languageVersion); 110 | }; 111 | 112 | const program = createProgram(["file.ts"], compilerOptions, compilerHost); 113 | 114 | checker.current = program.getTypeChecker(); 115 | const sourceFile = program.getSourceFile("file.ts"); 116 | 117 | if (!sourceFile) return ""; 118 | 119 | logger.setSourceFile(sourceFile); 120 | 121 | return compile.withEnv({})(sourceFile); 122 | }, 123 | 124 | compileDefinitionFile: ( 125 | definitionPath: string, 126 | options?: Options, 127 | mapSourceCode: ( 128 | source: string | undefined, 129 | fileName: string, 130 | ) => string | undefined = a => a, 131 | ): string => { 132 | reset(options); 133 | 134 | const compilerHost = createCompilerHost({}, true); 135 | const oldSourceFile = compilerHost.getSourceFile; 136 | const oldReadFile = compilerHost.readFile; 137 | compilerHost.readFile = fileName => 138 | mapSourceCode(oldReadFile(fileName), fileName); 139 | const absolutePath = path.resolve(definitionPath); 140 | compilerHost.getSourceFile = (file, languageVersion) => { 141 | if (path.resolve(file) === absolutePath) { 142 | const sourceText = compilerHost.readFile(file); 143 | return transformFile(file, sourceText, languageVersion, options); 144 | } 145 | return oldSourceFile(file, languageVersion); 146 | }; 147 | 148 | const program = createProgram( 149 | [definitionPath], 150 | compilerOptions, 151 | compilerHost, 152 | ); 153 | 154 | checker.current = program.getTypeChecker(); 155 | const sourceFile = program.getSourceFile(definitionPath); 156 | 157 | if (!sourceFile) return ""; 158 | 159 | logger.setSourceFile(sourceFile); 160 | 161 | return compile.withEnv({})(sourceFile); 162 | }, 163 | 164 | compileDefinitionFiles: ( 165 | definitionPaths: string[], 166 | options?: Options, 167 | mapSourceCode: ( 168 | source: string | undefined, 169 | fileName: string, 170 | ) => string | undefined = a => a, 171 | ): Array<[string, string]> => { 172 | const compilerHost = createCompilerHost({}, true); 173 | const oldSourceFile = compilerHost.getSourceFile; 174 | const oldReadFile = compilerHost.readFile; 175 | compilerHost.readFile = fileName => 176 | mapSourceCode(oldReadFile(fileName), fileName); 177 | const absolutePaths = new Set(definitionPaths.map(p => path.resolve(p))); 178 | compilerHost.getSourceFile = (file, languageVersion) => { 179 | if (absolutePaths.has(path.resolve(file))) { 180 | const sourceText = compilerHost.readFile(file); 181 | return transformFile(file, sourceText, languageVersion, options); 182 | } 183 | return oldSourceFile(file, languageVersion); 184 | }; 185 | 186 | const program = createProgram( 187 | definitionPaths, 188 | compilerOptions, 189 | compilerHost, 190 | ); 191 | 192 | checker.current = program.getTypeChecker(); 193 | 194 | return definitionPaths.map(definitionPath => { 195 | const sourceFile = program.getSourceFile(definitionPath); 196 | if (!sourceFile) return [definitionPath, ""]; 197 | logger.setSourceFile(sourceFile); 198 | reset(options); 199 | return [definitionPath, compile.withEnv({})(sourceFile)]; 200 | }); 201 | }, 202 | }; 203 | -------------------------------------------------------------------------------- /src/cli/default-exporter.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import shell from "shelljs"; 3 | import path from "path"; 4 | import { promisify } from "./util"; 5 | 6 | const open: (filename: string, flags: string | number) => Promise = 7 | promisify(fs.open); 8 | const writeFile: (fd: number, output: string) => Promise = promisify( 9 | fs.writeFile, 10 | ); 11 | 12 | /** 13 | * Takes a path and some content and performs a write call. Simple. 14 | */ 15 | export default async function exportDefault( 16 | fileName: string, 17 | output: string, 18 | _index: number, 19 | ): Promise { 20 | const folderName = path.dirname(fileName); 21 | let handle; 22 | try { 23 | handle = await open(fileName, "w"); 24 | } catch { 25 | shell.mkdir("-p", folderName); 26 | handle = await open(fileName, "w"); 27 | } 28 | await writeFile(handle, output); 29 | return fileName; 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/flow-typed-exporter.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import shell from "shelljs"; 3 | import program from "commander"; 4 | import { promisify } from "./util"; 5 | 6 | import type { DirectoryFlowTypedFile } from "./runner.h"; 7 | 8 | const exists: (filename: string) => Promise = promisify(fs.exists); 9 | const writeFile: (filename: string, data: string) => Promise = promisify( 10 | fs.writeFile, 11 | ); 12 | const appendFile: (filename: string, data: string) => Promise = promisify( 13 | fs.appendFile, 14 | ); 15 | 16 | export async function flowTypedExporter( 17 | moduleName: string, 18 | output: string, 19 | _index: number, 20 | ): Promise { 21 | const opts = program.opts(); 22 | const out = 23 | typeof opts.flowTypedFormat === "string" ? opts.flowTypedFormat : "exports"; 24 | const folder = `./${out}/${moduleName}_v1.x.x`; 25 | const flowFolder = `${folder}/flow_v0.35.x-`; 26 | const outputFile = `${folder}/flow_v0.35.x-/${moduleName}.js`; 27 | 28 | const testfilePath = `${folder}/test_${moduleName}.js`; 29 | 30 | if (!(await exists(flowFolder))) { 31 | shell.mkdir("-p", flowFolder); 32 | } 33 | await writeFile(testfilePath, ""); 34 | await writeFile(outputFile, output); 35 | return testfilePath; 36 | } 37 | 38 | export async function flowTypedDirectoryExporter( 39 | { rootModuleName }: DirectoryFlowTypedFile, 40 | output: string, 41 | index: number, 42 | ): Promise { 43 | const opts = program.opts(); 44 | const out = 45 | typeof opts.flowTypedFormat === "string" ? opts.flowTypedFormat : "exports"; 46 | const flowFolder = `${out}/flow_v0.35.x-`; 47 | const outputFile = `${out}/flow_v0.35.x-/${rootModuleName}.js`; 48 | 49 | const testfilePath = `${out}/test_${rootModuleName}.js`; 50 | 51 | if (!(await exists(flowFolder))) { 52 | shell.mkdir("-p", flowFolder); 53 | } 54 | if (index === 0 && (await exists(outputFile))) { 55 | await writeFile(outputFile, ""); 56 | } 57 | await writeFile(testfilePath, ""); 58 | await appendFile(outputFile, output); 59 | return testfilePath; 60 | } 61 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import runner from "./runner"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | const pkg = require("../../package.json"); 6 | 7 | import program from "commander"; 8 | 9 | program 10 | .version(pkg.version) 11 | .option( 12 | "-o --output-file [outputFile]", 13 | "name for output file, defaults to export.flow.js", 14 | "export.flow.js", 15 | ) 16 | .option("--no-module-exports", "use only default exports") 17 | .option( 18 | "--interface-records", 19 | "exact records instead of interfaces in output", 20 | ) 21 | .option("--quiet", "output without logs") 22 | .option("--no-jsdoc", "output without jsdoc") 23 | .option("--no-inexact", "output without inexact types") 24 | .option("--flow-typed-format [dirname]", "format output for flow-typed") 25 | .option( 26 | "--add-flow-header", 27 | "adds '// @flow' to the generated files (for libs)", 28 | ) 29 | .option("--compile-tests", "compile any -tests.ts files found") 30 | .option( 31 | "--as-module [asModule]", 32 | "wrap the output as a module declaration (for libs)", 33 | ) 34 | .arguments("[files...]") 35 | .action((files, options) => { 36 | runner({ 37 | interfaceRecords: options.interfaceRecords, 38 | moduleExports: options.moduleExports, 39 | jsdoc: options.jsdoc, 40 | quiet: options.quiet, 41 | inexact: options.inexact, 42 | flowTypedFormat: options.flowTypedFormat, 43 | addFlowHeader: options.addFlowHeader, 44 | compileTests: options.compileTests, 45 | out: options.outputFile, 46 | version: pkg.version, 47 | asModule: options.asModule, 48 | }).compile(files); 49 | }); 50 | 51 | program.parse(process.argv); 52 | 53 | if (!process.argv.slice(2).length) { 54 | program.outputHelp(); 55 | } 56 | -------------------------------------------------------------------------------- /src/cli/meta.ts: -------------------------------------------------------------------------------- 1 | export default ( 2 | moduleName: string, 3 | version: string, 4 | flowHeader: boolean, 5 | ): string => `/** 6 | * Flowtype definitions for ${moduleName} 7 | * Generated by Flowgen from a Typescript Definition 8 | * Flowgen v${version}${flowHeader ? `\n* @flow` : ""} 9 | */ 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/cli/runner.h.ts: -------------------------------------------------------------------------------- 1 | export type RunnerOptions = { 2 | jsdoc: boolean; 3 | quiet: boolean; 4 | interfaceRecords: boolean; 5 | moduleExports: boolean; 6 | inexact: boolean; 7 | version: string; 8 | out: string; 9 | flowTypedFormat: boolean; 10 | addFlowHeader: boolean; 11 | compileTests: boolean; 12 | asModule: string; 13 | }; 14 | 15 | export type Mode = "directory" | "directory-flow-typed" | "flow-typed" | "file"; 16 | 17 | export type DirectoryFlowTypedFile = { 18 | rootModuleName: string; 19 | moduleName: string; 20 | filename: string; 21 | }; 22 | export type OutputFile = DirectoryFlowTypedFile | string; 23 | 24 | export type File = { 25 | index: number; 26 | mode: Mode; 27 | moduleName: string; 28 | isMain: boolean; 29 | file: string; 30 | readonly outputFile: OutputFile; 31 | intro: string; 32 | }; 33 | -------------------------------------------------------------------------------- /src/cli/runner.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import { promisify } from "./util"; 4 | 5 | import meta from "./meta"; 6 | import beautify from "./beautifier"; 7 | 8 | import compiler from "./compiler"; 9 | 10 | import defaultExporter from "./default-exporter"; 11 | import { 12 | flowTypedExporter, 13 | flowTypedDirectoryExporter, 14 | } from "./flow-typed-exporter"; 15 | import type { File, RunnerOptions, Mode, OutputFile } from "./runner.h"; 16 | 17 | const readDir = promisify(fs.readdir); 18 | const readFile = promisify(fs.readFile); 19 | 20 | async function outputFile( 21 | flowDefinitions: string, 22 | { intro, index, file, moduleName, outputFile }: File, 23 | options: RunnerOptions, 24 | writeFile, 25 | ): Promise { 26 | // Produce the flow library content 27 | try { 28 | // Write the output to disk 29 | let code = intro + flowDefinitions; 30 | try { 31 | code = beautify(code); 32 | } catch (e) { 33 | console.error(e); 34 | } 35 | const absoluteOutputFilePath: string = await writeFile( 36 | outputFile, 37 | code, 38 | index, 39 | ); 40 | 41 | // Check if we should compile tests as well 42 | if (options.compileTests) { 43 | // Assume tests file is in same dir, named -tests.ts 44 | // Based on DD conventions 45 | const testFileName = path.dirname(file) + "/" + moduleName + "-tests.ts"; 46 | const testFileOutput = 47 | path.dirname(absoluteOutputFilePath) + "/test_" + moduleName + ".js"; 48 | 49 | // Try to compile the test file. Will fail silently if not present. 50 | compiler.compileTest(testFileName, testFileOutput); 51 | } 52 | } catch (e) { 53 | console.error("Parsing", file, "failed"); 54 | console.error(e); 55 | } 56 | } 57 | 58 | function getFile( 59 | file: string, 60 | options: RunnerOptions, 61 | rawFile: string, 62 | isInDir: boolean, 63 | index: number, 64 | pkg?: { 65 | [key: string]: any; 66 | }, 67 | ): File { 68 | // Get the module name from the file name 69 | const moduleName = getModuleNameFromFile(file, pkg); 70 | const mode = getMode(options, file, isInDir); 71 | const mainTypes: string = pkg ? pkg.typings || pkg.types || "" : ""; 72 | const isMain = path.join(rawFile, mainTypes) === file; 73 | 74 | // The format of the output argument varies a bit based on which 75 | // exporting format we're using. For flow-typed, only the module name 76 | // is required, otherwise we use the cli arg. 77 | const outputFile = getOutputFile(options, file, rawFile, mode, moduleName); 78 | 79 | // Get the intro text 80 | let intro = meta( 81 | moduleName, 82 | options.version, 83 | options.addFlowHeader || mode === "directory", 84 | ); 85 | 86 | if (mode === "directory-flow-typed") intro = ""; 87 | 88 | return { file, index, isMain, outputFile, moduleName, intro, mode }; 89 | } 90 | 91 | async function bfs( 92 | rootDir: string, 93 | options: RunnerOptions, 94 | ): Promise> { 95 | const queue: Array = []; 96 | const files: Array = []; 97 | let pkg; 98 | try { 99 | pkg = JSON.parse( 100 | (await readFile(path.join(rootDir, "package.json"))).toString(), 101 | ); 102 | } catch (err) { 103 | // ignored 104 | } 105 | queue.push(rootDir); 106 | let current; 107 | while (queue.length) { 108 | current = queue.shift(); 109 | try { 110 | const dir = await readDir(current, { withFileTypes: true }); 111 | for (const file of dir) { 112 | if (file.isDirectory()) { 113 | if (file.name === "node_modules") continue; 114 | queue.push(path.join(current, file.name)); 115 | } else { 116 | if (!file.name.endsWith(".d.ts")) continue; 117 | files.push( 118 | getFile( 119 | path.join(current, file.name), 120 | options, 121 | rootDir, 122 | true, 123 | files.length, 124 | pkg, 125 | ), 126 | ); 127 | } 128 | } 129 | } catch { 130 | files.push(getFile(current, options, rootDir, false, files.length, pkg)); 131 | } 132 | } 133 | return files; 134 | } 135 | 136 | export default (options: RunnerOptions) => { 137 | const fileOptions = { 138 | jsdoc: options.jsdoc, 139 | quiet: options.quiet, 140 | interfaceRecords: options.interfaceRecords, 141 | moduleExports: options.moduleExports, 142 | inexact: options.inexact, 143 | asModule: options.asModule, 144 | }; 145 | // No real reason to return an object here instead of combining 146 | // the compile function into the wrapper, but I like the API it produces. 147 | return { 148 | compile: async (rawFiles: Array): Promise => { 149 | const files: Array = []; 150 | // Iterate all the files the user has passed in 151 | for (const rawFile of rawFiles) { 152 | files.push(...(await bfs(rawFile, options))); 153 | } 154 | const filesByPath = files.reduce((acc, file) => { 155 | acc.set(file.file, file); 156 | return acc; 157 | }, new Map()); 158 | const fileMapper = (sourceCode, fileName) => { 159 | if (!sourceCode) return; 160 | const file = filesByPath.get(fileName); 161 | if (!file) return sourceCode; 162 | if (file.mode !== "directory-flow-typed") return sourceCode; 163 | if (file.isMain) { 164 | return `declare module "${file.outputFile.moduleName}" {${sourceCode}} 165 | declare module "${file.outputFile.rootModuleName}" { 166 | declare export * from "${file.outputFile.moduleName}"; 167 | } 168 | `; 169 | } 170 | return `declare module "${file.outputFile.moduleName}" {${sourceCode}}`; 171 | }; 172 | if (files.length > 1) { 173 | const sources = compiler.compileDefinitionFiles( 174 | files.map(v => v.file), 175 | fileOptions, 176 | fileMapper, 177 | ); 178 | for (let index = 0; index < sources.length; index++) { 179 | const [, flowDefinitions] = sources[index]; 180 | const file = files[index]; 181 | let writeFile: any = defaultExporter; 182 | if (file.mode === "flow-typed") writeFile = flowTypedExporter; 183 | if (file.mode === "directory-flow-typed") 184 | writeFile = flowTypedDirectoryExporter; 185 | // Let the user know what's going on 186 | if (files.length >= 3) { 187 | // If we're compiling a lot of files, show more stats 188 | const progress = Math.round(((index + 1) / files.length) * 100); 189 | process.stdout.write("\r\x1b[K"); 190 | process.stdout.write(progress + "% | " + file.moduleName); 191 | } else { 192 | console.log("Parsing", file.moduleName); 193 | } 194 | outputFile(flowDefinitions, file, options, writeFile); 195 | } 196 | } else { 197 | const file = files[0]; 198 | const flowDefinitions = compiler.compileDefinitionFile( 199 | file.file, 200 | fileOptions, 201 | fileMapper, 202 | ); 203 | 204 | let writeFile: any = defaultExporter; 205 | if (file.mode === "flow-typed") writeFile = flowTypedExporter; 206 | if (file.mode === "directory-flow-typed") 207 | writeFile = flowTypedDirectoryExporter; 208 | // Let the user know what's going on 209 | console.log("Parsing", file.moduleName); 210 | outputFile(flowDefinitions, file, options, writeFile); 211 | } 212 | if (files.length >= 3) process.stdout.write("\n"); 213 | }, 214 | }; 215 | }; 216 | 217 | function getModuleNameFromFile( 218 | fileName: string, 219 | pkg?: { 220 | [key: string]: any; 221 | }, 222 | ): string { 223 | if (pkg) return pkg.name; 224 | return path.basename(fileName).replace(".d.ts", ""); 225 | } 226 | 227 | function getMode(options: RunnerOptions, file: string, isDir: boolean): Mode { 228 | if (isDir && options.flowTypedFormat) return "directory-flow-typed"; 229 | if (isDir) return "directory"; 230 | if (options.flowTypedFormat) return "flow-typed"; 231 | return "file"; 232 | } 233 | 234 | function getOutputFile( 235 | options: RunnerOptions, 236 | file: string, 237 | prefix: string, 238 | mode: Mode, 239 | moduleName: string, 240 | ): OutputFile { 241 | switch (mode) { 242 | case "directory-flow-typed": 243 | return { 244 | rootModuleName: moduleName, 245 | moduleName: path.join( 246 | moduleName, 247 | path.relative(prefix, file).replace(".d.ts", ""), 248 | ), 249 | filename: path.normalize(file.replace(prefix, "").replace(".d.ts", "")), 250 | }; 251 | case "directory": { 252 | const basedir = options.out ?? "exports"; 253 | return path.normalize( 254 | file 255 | .replace(prefix, `${basedir}${path.sep}`) 256 | .replace(".d.ts", ".js.flow"), 257 | ); 258 | } 259 | case "flow-typed": 260 | return moduleName; 261 | default: 262 | return options.out; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/cli/util.ts: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | 3 | export const promisify = util.promisify; 4 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | let i = 0; 2 | const envs = {}; 3 | 4 | export function withEnv, B>( 5 | callback: (env: Env, ...args: A) => B, 6 | ): { 7 | withEnv(env: T): (...args: A) => B; 8 | (...args: A): B; 9 | } { 10 | function fn(...args: A) { 11 | return callback(envs[i - 1], ...args); 12 | } 13 | fn.withEnv = env => { 14 | envs[i] = env; 15 | i++; 16 | return (...args: A) => { 17 | return callback(env, ...args); 18 | }; 19 | }; 20 | return fn; 21 | } 22 | -------------------------------------------------------------------------------- /src/errors/error-message.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxKind } from "typescript"; 2 | 3 | export type ErrorMessage = 4 | | { 5 | readonly type: "UnsupportedComputedProperty"; 6 | } 7 | | { 8 | readonly type: "UnsupportedUniqueSymbol"; 9 | } 10 | | { 11 | readonly type: "UnsupportedConditionalType"; 12 | } 13 | | { 14 | readonly type: "UnsupportedGlobalAugmentation"; 15 | } 16 | | { 17 | readonly type: "UnsupportedNestedModule"; 18 | } 19 | | { 20 | readonly type: "UnsupportedTypeOperator"; 21 | readonly operator: typeof SyntaxKind[keyof typeof SyntaxKind]; 22 | } 23 | | { 24 | readonly type: "UnexpectedTsSyntax"; 25 | readonly description: string; 26 | } 27 | | { 28 | readonly type: "FlowgenInternalError"; 29 | readonly description: string; 30 | } 31 | | { 32 | readonly type: "MissingFunctionName"; 33 | }; 34 | 35 | export function printErrorMessage(error: ErrorMessage): string { 36 | switch (error.type) { 37 | case "UnsupportedComputedProperty": 38 | return "Flow doesn't support computed property names"; 39 | 40 | case "UnsupportedUniqueSymbol": 41 | return "Flow doesn't support `unique symbol`"; 42 | 43 | case "UnsupportedConditionalType": 44 | return "Flow doesn't support conditional types, use `$Call` utility type"; 45 | 46 | case "MissingFunctionName": 47 | return "Flow doesn't support unnamed functions"; 48 | 49 | case "UnsupportedGlobalAugmentation": 50 | return "Flow doesn't support global augmentation"; 51 | 52 | case "UnsupportedNestedModule": 53 | return "Flow doesn't support nested modules"; 54 | 55 | case "UnsupportedTypeOperator": 56 | return `Unsupported type operator: ${SyntaxKind[error.operator]}`; 57 | 58 | case "UnexpectedTsSyntax": 59 | return `Unexpected TypeScript syntax: ${error.description}. Please report this at https://github.com/joarwilk/flowgen/issues`; 60 | 61 | case "FlowgenInternalError": 62 | return `Flowgen internal error: ${error.description}. Please report this at https://github.com/joarwilk/flowgen/issues`; 63 | 64 | default: 65 | error as never; 66 | return "Unknown error. Please report this in https://github.com/joarwilk/flowgen/issues"; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import compiler from "./cli/compiler"; 2 | import beautify from "./cli/beautifier"; 3 | 4 | export { default as compiler } from "./cli/compiler"; 5 | 6 | export { default as beautify } from "./cli/beautifier"; 7 | 8 | export default { 9 | beautify, 10 | compiler, 11 | }; 12 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import type { SourceFile } from "typescript"; 2 | import ts from "typescript"; 3 | import { opts } from "./options"; 4 | import path from "path"; 5 | import { codeFrameColumns } from "@babel/code-frame"; 6 | import { getChalk } from "@babel/highlight"; 7 | 8 | import { printErrorMessage } from "./errors/error-message"; 9 | import type { ErrorMessage } from "./errors/error-message"; 10 | 11 | const sourceFile: { 12 | current: SourceFile | null; 13 | } = { current: null }; 14 | 15 | export function setSourceFile(file: SourceFile): void { 16 | sourceFile.current = file; 17 | } 18 | 19 | function padDashes(consumedWidth: number) { 20 | return "-".repeat(Math.max(4, process.stdout.columns - consumedWidth)); 21 | } 22 | 23 | export function error(node: ts.Node, message: ErrorMessage): void { 24 | if (opts().quiet) return; 25 | const options = { 26 | highlightCode: true, 27 | message: printErrorMessage(message), 28 | }; 29 | const chalk = getChalk(options); 30 | if (sourceFile.current !== null) { 31 | const currentSourceFile = sourceFile.current; 32 | const code = currentSourceFile.text; 33 | const tsStartLocation = currentSourceFile.getLineAndCharacterOfPosition( 34 | node.getStart(sourceFile.current), 35 | ); 36 | const tsEndLocation = currentSourceFile.getLineAndCharacterOfPosition( 37 | node.getEnd(), 38 | ); 39 | const babelLocation = { 40 | start: { 41 | line: tsStartLocation.line + 1, 42 | column: tsStartLocation.character + 1, 43 | }, 44 | end: { 45 | line: tsEndLocation.line + 1, 46 | column: tsEndLocation.character + 1, 47 | }, 48 | }; 49 | const result = codeFrameColumns(code, babelLocation, options); 50 | const position = `:${babelLocation.start.line}:${babelLocation.start.column}`; 51 | const fileName = path.relative(process.cwd(), currentSourceFile.fileName); 52 | console.log( 53 | chalk.red.bold( 54 | `Error ${padDashes( 55 | 7 + fileName.length + position.length, 56 | )} ${fileName}${position}`, 57 | ), 58 | ); 59 | console.log("\n"); 60 | console.log(result); 61 | console.log("\n"); 62 | } else { 63 | console.log(printErrorMessage(message)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/namespace-manager.ts: -------------------------------------------------------------------------------- 1 | let namespaceProps = Object.create(null); 2 | let namespaces: Array = []; 3 | 4 | export default { 5 | register: (name: string): number => namespaces.push(name), 6 | registerProp: (namespace: string, name: string): string => 7 | (namespaceProps[name] = namespace), 8 | nsExists: (name: string): boolean => namespaces.includes(name), 9 | nsPropExists: (name: string): boolean => 10 | Object.keys(namespaceProps).includes(name), 11 | getNSForProp: (name: string): any => namespaceProps[name], 12 | reset: (): void => { 13 | namespaceProps = Object.create(null); 14 | namespaces = []; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/nodename.ts: -------------------------------------------------------------------------------- 1 | import { SyntaxKind } from "typescript"; 2 | 3 | import type { Node } from "typescript"; 4 | 5 | export default function getNodeName(node: Node): string { 6 | return SyntaxKind[node.kind] || node.constructor + ""; 7 | } 8 | -------------------------------------------------------------------------------- /src/nodes/export-declaration.ts: -------------------------------------------------------------------------------- 1 | import type { RawNode } from "./node"; 2 | import type { Expression, ExportDeclaration as RawExport } from "typescript"; 3 | import { isNamespaceExport } from "typescript"; 4 | import * as printers from "../printers"; 5 | import Node from "./node"; 6 | 7 | type ExportDeclarationType = RawExport & { 8 | moduleSpecifier?: Expression & { 9 | text: string; 10 | }; 11 | }; 12 | 13 | export default class ExportDeclaration extends Node { 14 | constructor(node: RawNode) { 15 | super(node); 16 | } 17 | 18 | print(): string { 19 | //TODO: move to printers 20 | if (this.raw.exportClause) { 21 | const isTypeImport = this.raw.isTypeOnly; 22 | 23 | let specifier = ""; 24 | if (this.raw.moduleSpecifier) 25 | specifier = `from '${this.raw.moduleSpecifier.text}';`; 26 | 27 | if (isNamespaceExport(this.raw.exportClause)) { 28 | return `declare export * as ${this.raw.exportClause.name.escapedText} ${specifier}\n`; 29 | } 30 | 31 | // split exports into type and value exports 32 | const rawElements = this.raw.exportClause.elements; 33 | let typeExports, valueExports; 34 | if (isTypeImport) { 35 | typeExports = rawElements; 36 | valueExports = []; 37 | } else { 38 | typeExports = []; 39 | valueExports = []; 40 | let nextIsType = false; 41 | for (const node of rawElements) { 42 | if (nextIsType) { 43 | typeExports.push(node); 44 | nextIsType = false; 45 | } else if (node.name.originalKeywordKind === 150) { 46 | nextIsType = true; 47 | } else { 48 | valueExports.push(node); 49 | } 50 | } 51 | } 52 | 53 | const generateOutput = (prefix, elems) => { 54 | return `${prefix} { 55 | ${elems.map(node => printers.node.printType(node))} 56 | }${specifier}\n`; 57 | }; 58 | 59 | let result = ""; 60 | if (typeExports.length) { 61 | result += generateOutput(`export type`, typeExports); 62 | } 63 | if (valueExports.length) { 64 | result += generateOutput(`declare export`, valueExports); 65 | } 66 | return result; 67 | } else { 68 | return `declare export * from '${this.raw.moduleSpecifier.text}';\n`; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/nodes/export.ts: -------------------------------------------------------------------------------- 1 | import type { RawNode } from "./node"; 2 | import Node from "./node"; 3 | 4 | import * as printers from "../printers"; 5 | 6 | export default class Export extends Node { 7 | constructor(node: RawNode) { 8 | super(node); 9 | } 10 | 11 | print() { 12 | return printers.relationships.moduleExports(this.raw); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/nodes/factory.ts: -------------------------------------------------------------------------------- 1 | import type { RawNode } from "./node"; 2 | 3 | import type Node from "./node"; 4 | import ImportNode from "./import"; 5 | import ExportNode from "./export"; 6 | import ExportDeclarationNode from "./export-declaration"; 7 | import ModuleNode from "./module"; 8 | import PropertyNode from "./property"; 9 | import NamespaceNode from "./namespace"; 10 | 11 | import { getMembersFromNode } from "../parse/ast"; 12 | import { checker } from "../checker"; 13 | import { getFullyQualifiedName } from "../printers/node"; 14 | 15 | export class Factory { 16 | _modules: { 17 | [key: string]: ModuleNode; 18 | }; 19 | _propDeclarations: { 20 | [key: string]: PropertyNode; 21 | }; 22 | _functionDeclarations: { 23 | [key: string]: Array; 24 | }; 25 | 26 | constructor() { 27 | //$todo 28 | this._modules = Object.create(null); 29 | //$todo 30 | this._propDeclarations = Object.create(null); 31 | //$todo 32 | this._functionDeclarations = Object.create(null); 33 | } 34 | 35 | // If multiple declarations are found for the same module name 36 | // return the memoized instance of the module instead 37 | createModuleNode(node: RawNode, name: string): ModuleNode { 38 | if (Object.keys(this._modules).includes(name)) { 39 | return this._modules[name]; 40 | } 41 | 42 | const module = new ModuleNode(node, name); 43 | 44 | this._modules[name] = module; 45 | 46 | return module; 47 | } 48 | 49 | createFunctionDeclaration( 50 | node: RawNode, 51 | rawName: string, 52 | context: Node, 53 | ): void { 54 | let name = rawName; 55 | const propNode = new PropertyNode(node); 56 | 57 | if (context instanceof ModuleNode) { 58 | name = context.name + "$" + rawName; 59 | } 60 | if (context instanceof NamespaceNode && checker.current) { 61 | const symbol = checker.current.getSymbolAtLocation(node.name); 62 | name = getFullyQualifiedName(symbol, node, false); 63 | } 64 | 65 | if (!this._functionDeclarations[name]) { 66 | this._functionDeclarations[name] = [propNode]; 67 | } else if (Object.keys(this._functionDeclarations).includes(name)) { 68 | this._functionDeclarations[name].push(propNode); 69 | } 70 | 71 | context.addChild(name + this._functionDeclarations[name].length, propNode); 72 | } 73 | 74 | // Some definition files (like lodash) declare the same 75 | // interface/type/function multiple times as a way of overloading. 76 | // Flow does not support that, and this is where we handle that 77 | createPropertyNode( 78 | node: RawNode, 79 | name?: string, 80 | context?: Node, 81 | ): PropertyNode { 82 | if (typeof name === "undefined") { 83 | return new PropertyNode(node); 84 | } 85 | 86 | if (context instanceof ModuleNode) { 87 | name = context.name + "$" + name; 88 | } 89 | if (context instanceof NamespaceNode && checker.current) { 90 | const symbol = checker.current.getSymbolAtLocation(node.name); 91 | name = getFullyQualifiedName(symbol, node, false); 92 | } 93 | 94 | if (Object.keys(this._propDeclarations).includes(name)) { 95 | this._propDeclarations[name].maybeAddMember(getMembersFromNode(node)); 96 | 97 | return this._propDeclarations[name]; 98 | } 99 | 100 | const propNode = new PropertyNode(node); 101 | this._propDeclarations[name] = propNode; 102 | return propNode; 103 | } 104 | 105 | createNamespaceNode = ( 106 | node: RawNode, 107 | name: string, 108 | context: Node, 109 | ): NamespaceNode => { 110 | let contextName; 111 | if (context instanceof ModuleNode) { 112 | contextName = context.name + "$" + name; 113 | } 114 | if (context instanceof NamespaceNode && checker.current) { 115 | const symbol = checker.current.getSymbolAtLocation(node.name); 116 | contextName = getFullyQualifiedName(symbol, node, false); 117 | } 118 | if (typeof contextName !== "undefined") { 119 | if (this._functionDeclarations[contextName]) { 120 | for (const prop of this._functionDeclarations[contextName]) 121 | prop.skipNode(); 122 | } 123 | if (this._propDeclarations[contextName]) { 124 | this._propDeclarations[contextName].skipNode(); 125 | } 126 | return new NamespaceNode( 127 | name, 128 | this._functionDeclarations[contextName], 129 | this._propDeclarations[contextName], 130 | ); 131 | } else { 132 | return new NamespaceNode(name); 133 | } 134 | }; 135 | createImportNode = (node: RawNode): ImportNode => new ImportNode(node); 136 | createExportNode = (node: RawNode): ExportNode => new ExportNode(node); 137 | createExportDeclarationNode = (node: RawNode): ExportDeclarationNode => 138 | new ExportDeclarationNode(node); 139 | } 140 | 141 | export default { 142 | create: (): Factory => new Factory(), 143 | }; 144 | -------------------------------------------------------------------------------- /src/nodes/import.ts: -------------------------------------------------------------------------------- 1 | import type { RawNode } from "./node"; 2 | import Node from "./node"; 3 | import * as printers from "../printers"; 4 | import { checker } from "../checker"; 5 | import * as ts from "typescript"; 6 | 7 | export default class Import extends Node { 8 | constructor(node: RawNode) { 9 | super(node); 10 | } 11 | 12 | print(): string { 13 | //TODO: move to printers 14 | if (this.raw.importClause) { 15 | const bindings = this.raw.importClause.namedBindings; 16 | const name = this.raw.importClause.name; 17 | const isTypeImport = this.raw.importClause.isTypeOnly; 18 | 19 | // in Typescript, you can use "import type" on an enum 20 | // however, flowgen converts typescript enums to regular objects 21 | // so that means "import type" would fail on them (can't import type a regular object) 22 | // instead, we mimic this by using the import { typeof } notation 23 | const splitTypeImports = elements => { 24 | const enumElems = []; 25 | const regularElems = []; 26 | for (const elem of elements) { 27 | // if we're not using import type, no need to do anything special 28 | if (!isTypeImport) { 29 | regularElems.push(elem); 30 | continue; 31 | } 32 | const elemSymbol = checker.current.getTypeAtLocation(elem).symbol; 33 | const isEnum = 34 | elemSymbol && 35 | elemSymbol.declarations && 36 | elemSymbol.declarations[0].kind === ts.SyntaxKind.EnumDeclaration; 37 | if (isEnum) { 38 | enumElems.push(elem); 39 | } else { 40 | regularElems.push(elem); 41 | } 42 | } 43 | return { enumElems, regularElems }; 44 | }; 45 | 46 | if (name && bindings) { 47 | const elements = bindings.elements; 48 | if (elements) { 49 | const { enumElems, regularElems } = splitTypeImports(elements); 50 | 51 | let result = ""; 52 | if (regularElems.length > 0) { 53 | result += `import${ 54 | this.module === "root" && !isTypeImport ? "" : " type" 55 | } ${name.text}, { 56 | ${elements.map(node => printers.node.printType(node))} 57 | } from '${this.raw.moduleSpecifier.text}';\n`; 58 | } 59 | if (enumElems.length > 0) { 60 | result += `import typeof ${name.text}, { 61 | ${elements.map(node => printers.node.printType(node))} 62 | } from '${this.raw.moduleSpecifier.text}';\n`; 63 | } 64 | 65 | return result; 66 | } else { 67 | const namespace = bindings.name.text; 68 | return `import${this.module === "root" ? "" : " typeof"} ${ 69 | name.text 70 | }, * as ${namespace} from '${this.raw.moduleSpecifier.text}';\n`; 71 | } 72 | } 73 | if (name) { 74 | return `import${this.module === "root" ? "" : " typeof"} ${ 75 | name.text 76 | } from '${this.raw.moduleSpecifier.text}';\n`; 77 | } 78 | if (bindings) { 79 | const elements = bindings.elements; 80 | if (elements) { 81 | const { enumElems, regularElems } = splitTypeImports(elements); 82 | 83 | let result = ""; 84 | if (regularElems.length > 0) { 85 | result += `import${ 86 | this.module === "root" && !isTypeImport ? "" : " type" 87 | } { 88 | ${regularElems.map(node => printers.node.printType(node))} 89 | } from '${this.raw.moduleSpecifier.text}';\n`; 90 | } 91 | if (enumElems.length > 0) { 92 | result += `import typeof { 93 | ${enumElems.map(node => printers.node.printType(node))} 94 | } from '${this.raw.moduleSpecifier.text}';\n`; 95 | } 96 | return result; 97 | } else { 98 | const name = bindings.name.text; 99 | return `import${ 100 | this.module === "root" ? "" : " typeof" 101 | } * as ${name} from '${this.raw.moduleSpecifier.text}';\n`; 102 | } 103 | } 104 | } 105 | return this.module === "root" 106 | ? `import '${this.raw.moduleSpecifier.text}';\n` 107 | : ""; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/nodes/module.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import type { RawNode } from "./node"; 3 | import * as logger from "../logger"; 4 | import Node from "./node"; 5 | import Namespace from "./namespace"; 6 | 7 | export default class Module extends Node { 8 | name: string; 9 | 10 | constructor(node: RawNode | undefined | null, name: string) { 11 | super(node); 12 | 13 | this.name = name; 14 | } 15 | 16 | addChild(name: string, child: Node): void { 17 | child.module = this.name; 18 | this.children[name] = child; 19 | } 20 | 21 | addChildren(name: string, child: Node): void { 22 | child.module = this.name; 23 | if ( 24 | child instanceof Namespace && 25 | this.children[child.name] && 26 | this.children[child.name].raw && 27 | this.children[child.name].raw.kind === ts.SyntaxKind.ClassDeclaration 28 | ) { 29 | name = child.name; 30 | } 31 | if (!this.children[name]) { 32 | this.children[name] = child; 33 | return; 34 | } 35 | if (this.children[name]) { 36 | for (const key in child.children) { 37 | this.children[name].addChildren(key, child.children[key]); 38 | } 39 | return; 40 | } 41 | } 42 | 43 | print(namespace?: string, module?: string, depth = 0): string { 44 | const children = this.getChildren() 45 | .map(child => { 46 | return child.print(undefined, this.name, depth + 1); 47 | }) 48 | .join("\n\t"); 49 | const node = ` 50 | declare module '${this.name}' { 51 | ${children} 52 | } 53 | `; 54 | if (module !== undefined && depth === 1) { 55 | logger.error(this.raw, { type: "UnsupportedNestedModule" }); 56 | return `/* ${node} */\n`; 57 | } 58 | return `${node}\n`; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/nodes/namespace.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { orderBy, uniqBy, flatten } from "lodash"; 3 | import PropertyNode from "./property"; 4 | import Node from "./node"; 5 | 6 | import namespaceManager from "../namespace-manager"; 7 | import * as printers from "../printers"; 8 | 9 | export default class Namespace extends Node { 10 | name: string; 11 | functions: Array; 12 | property: PropertyNode | undefined; 13 | 14 | constructor( 15 | name: string, 16 | functions?: Array, 17 | property?: PropertyNode, 18 | ) { 19 | super(null); 20 | 21 | this.name = name; 22 | this.functions = functions || []; 23 | this.property = property; 24 | 25 | namespaceManager.register(name); 26 | } 27 | 28 | addChild(name: string, child: Node): void { 29 | child.namespace = this.name; 30 | child.isValue = child.getChildren().some(node => { 31 | return ( 32 | node instanceof Namespace || 33 | node.raw.kind === ts.SyntaxKind.VariableStatement || 34 | node.raw.kind === ts.SyntaxKind.ClassDeclaration || 35 | node.raw.kind === ts.SyntaxKind.FunctionDeclaration || 36 | node.raw.kind === ts.SyntaxKind.EnumDeclaration 37 | ); 38 | }); 39 | namespaceManager.registerProp(this.name, child.name); 40 | 41 | this.children[name] = child; 42 | } 43 | 44 | addChildren(name: string, child: Node): void { 45 | child.namespace = this.name; 46 | child.isValue = child 47 | .getChildren() 48 | .some( 49 | node => 50 | node instanceof Namespace || 51 | node.raw.kind === ts.SyntaxKind.VariableStatement || 52 | node.raw.kind === ts.SyntaxKind.ClassDeclaration || 53 | node.raw.kind === ts.SyntaxKind.FunctionDeclaration || 54 | node.raw.kind === ts.SyntaxKind.EnumDeclaration, 55 | ); 56 | 57 | if ( 58 | child instanceof Namespace && 59 | this.children[child.name] && 60 | this.children[child.name].raw && 61 | this.children[child.name].raw.kind === ts.SyntaxKind.ClassDeclaration 62 | ) { 63 | name = child.name; 64 | } 65 | namespaceManager.registerProp(this.name, child.name); 66 | 67 | if (!this.children[name]) { 68 | this.children[name] = child; 69 | return; 70 | } 71 | if (this.children[name]) { 72 | for (const key in child.children) { 73 | this.children[name].addChildren(key, child.children[key]); 74 | } 75 | return; 76 | } 77 | } 78 | 79 | print = (namespace = "", mod = "root", depth?: number): string => { 80 | const children = uniqBy( 81 | orderBy(this.getChildren(), [a => a.isValue], ["desc"]), 82 | // @ts-expect-error todo(flow->ts) 83 | child => child.name.text || child.name, 84 | ); 85 | let name = this.name; 86 | if (namespace) { 87 | name = namespace + "$" + this.name; 88 | } 89 | 90 | const childrenDeclarations = this.functions 91 | // @ts-expect-error The .raw on a this.functions node is a ts.FunctionDeclaration. 92 | .map(propNode => printers.functions.functionType(propNode.raw, true)) 93 | .concat(Namespace.formatChildren(children, name)); 94 | 95 | const childrenNode = `${this.getChildren() 96 | .map(child => { 97 | return child.print(name, mod, depth); 98 | }) 99 | .join("\n\n")}`; 100 | 101 | if (childrenDeclarations.length > 0) { 102 | let topLevel = ""; 103 | const nsGroup = ` 104 | declare var npm$namespace$${name}: {| 105 | ${childrenDeclarations.map(declaration => `${declaration},`).join("\n")} 106 | |}\n`; 107 | if (namespace === "") { 108 | topLevel = `declare var ${name}: typeof npm$namespace$${name};\n`; 109 | } 110 | 111 | return topLevel + nsGroup + childrenNode; 112 | } 113 | 114 | return childrenNode; 115 | }; 116 | 117 | static formatChildren( 118 | children: ReadonlyArray, 119 | childrenNamespace: string, 120 | ): string[] { 121 | const functions = children.filter( 122 | child => 123 | child.raw && child.raw.kind === ts.SyntaxKind.FunctionDeclaration, 124 | ); 125 | const variables = flatten( 126 | children 127 | .filter( 128 | child => 129 | child.raw && child.raw.kind === ts.SyntaxKind.VariableStatement, 130 | ) 131 | .map(child => child.raw.declarationList.declarations), 132 | ); 133 | const enums = children.filter( 134 | child => child.raw && child.raw.kind === ts.SyntaxKind.EnumDeclaration, 135 | ); 136 | // Interfaces with type parameters are not expressible inside namespaces. 137 | const interfaces = children.filter( 138 | child => 139 | child.raw && 140 | child.raw.kind === ts.SyntaxKind.InterfaceDeclaration && 141 | !(child.raw.typeParameters && child.raw.typeParameters.length), 142 | ); 143 | const classes = children.filter( 144 | child => child.raw && child.raw.kind === ts.SyntaxKind.ClassDeclaration, 145 | ); 146 | const namespaces = children.filter(child => { 147 | return child.isValue; 148 | }); 149 | 150 | return [].concat( 151 | functions.map(child => { 152 | return `${child.name}: typeof ${childrenNamespace}$${child.name}`; 153 | }), 154 | variables.map(child => { 155 | return `${child.name.text}: typeof ${childrenNamespace}$${child.name.text}`; 156 | }), 157 | enums.map(child => { 158 | return `${child.name}: typeof ${childrenNamespace}$${child.name}`; 159 | }), 160 | interfaces.map(child => { 161 | return `${child.name}: Class<${childrenNamespace}$${child.name}>`; 162 | }), 163 | classes.map(child => { 164 | return `${child.name}: typeof ${childrenNamespace}$${child.name}`; 165 | }), 166 | namespaces.map(child => { 167 | return `${child.name}: typeof npm$namespace$${childrenNamespace}$${child.name}`; 168 | }), 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/nodes/node.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import type { Node as TSNode } from "typescript"; 3 | import { parseNameFromNode, stripDetailsFromTree } from "../parse/ast"; 4 | 5 | import * as printers from "../printers"; 6 | 7 | export type RawNode = any; 8 | 9 | class Node { 10 | children: { 11 | [key: string]: Node; 12 | }; 13 | kind: string; 14 | name: string; 15 | raw: NodeType; 16 | namespace: string | undefined | null; 17 | module: string | undefined | null; 18 | 19 | constructor(node?: NodeType | null) { 20 | //$off 21 | this.children = Object.create(null); 22 | 23 | if (node !== null) { 24 | this.raw = stripDetailsFromTree(node); 25 | this.name = parseNameFromNode(node); 26 | } 27 | } 28 | 29 | addChild(name: string, node: Node): void { 30 | this.children[name] = node; 31 | } 32 | 33 | //TODO: remove this 34 | addChildren(name: string, node: Node): void { 35 | if (!this.children[name]) { 36 | this.children[name] = node; 37 | return; 38 | } 39 | if (this.children[name]) { 40 | for (const key in node.children) { 41 | this.children[name].addChildren(key, node.children[key]); 42 | } 43 | return; 44 | } 45 | } 46 | 47 | /** 48 | * Used for overloading the props of some types 49 | */ 50 | maybeAddMember(members: any | ReadonlyArray): void { 51 | const rawMembers: Array | void = (this.raw as any).members; 52 | if (!rawMembers) { 53 | return; 54 | } 55 | if (Array.isArray(members)) { 56 | members.forEach(member => { 57 | rawMembers.push(stripDetailsFromTree(member)); 58 | }); 59 | } else { 60 | rawMembers.push(stripDetailsFromTree(members)); 61 | } 62 | } 63 | 64 | getChildren(): ReadonlyArray { 65 | return _.toArray(this.children); 66 | } 67 | 68 | //eslint-disable-next-line 69 | print(namespace?: string, module?: string, depth?: number): string { 70 | return printers.node.printType(this.raw); 71 | } 72 | } 73 | interface Node { 74 | [k: string]: any; 75 | } 76 | 77 | export default Node; 78 | -------------------------------------------------------------------------------- /src/nodes/property.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FunctionDeclaration, 3 | ClassDeclaration, 4 | InterfaceDeclaration, 5 | TypeAliasDeclaration, 6 | EnumDeclaration, 7 | VariableStatement, 8 | } from "typescript"; 9 | import * as ts from "typescript"; 10 | import Node from "./node"; 11 | 12 | import * as printers from "../printers"; 13 | import namespaceManager from "../namespace-manager"; 14 | import { parseNameFromNode } from "../parse/ast"; 15 | 16 | type PropertyNode = 17 | | FunctionDeclaration 18 | | ClassDeclaration 19 | | InterfaceDeclaration 20 | | TypeAliasDeclaration 21 | | EnumDeclaration 22 | | VariableStatement; 23 | 24 | export default class Property extends Node { 25 | name: string; 26 | skip: boolean; 27 | 28 | constructor(node: PropertyNode) { 29 | super(node); 30 | 31 | this.name = parseNameFromNode(node); 32 | this.skip = false; 33 | } 34 | 35 | skipNode() { 36 | this.skip = true; 37 | } 38 | 39 | print(namespace = "", mod = "root"): string { 40 | let out = ""; 41 | let name = this.name; 42 | 43 | if (namespace) { 44 | namespaceManager.registerProp(namespace, this.name); 45 | } 46 | 47 | if (namespace) { 48 | name = namespace + "$" + name; 49 | } 50 | 51 | out += printers.common.jsdoc(this.raw); 52 | 53 | const isDeclare = mod !== "root"; 54 | const exporter = printers.relationships.exporter(this.raw); 55 | const modifier = exporter 56 | ? `${isDeclare ? "declare " : ""}${exporter}` 57 | : "declare "; 58 | 59 | if (this.skip) return out; 60 | 61 | switch (this.raw.kind) { 62 | case ts.SyntaxKind.FunctionDeclaration: 63 | out += printers.functions.functionDeclaration(name, this.raw); 64 | break; 65 | case ts.SyntaxKind.ClassDeclaration: { 66 | const classChildren = this.getChildren(); 67 | out += printers.declarations.classDeclaration( 68 | name, 69 | this.raw, 70 | classChildren, 71 | ); 72 | if (classChildren.length) { 73 | out += `\n\n${classChildren 74 | .map(child => { 75 | return child.print(name, mod); 76 | }) 77 | .join("\n\n")}`; 78 | } 79 | break; 80 | } 81 | case ts.SyntaxKind.InterfaceDeclaration: 82 | out += printers.declarations.interfaceDeclaration( 83 | name, 84 | this.raw, 85 | modifier, 86 | ); 87 | break; 88 | case ts.SyntaxKind.TypeAliasDeclaration: 89 | out += printers.declarations.typeDeclaration(name, this.raw, modifier); 90 | break; 91 | case ts.SyntaxKind.EnumDeclaration: 92 | out += printers.declarations.enumDeclaration(name, this.raw); 93 | break; 94 | case ts.SyntaxKind.VariableStatement: 95 | for (const decl of this.raw.declarationList.declarations) { 96 | if (namespace && decl.name.kind === ts.SyntaxKind.Identifier) { 97 | const text = decl.name.text; 98 | namespaceManager.registerProp(namespace, text); 99 | } 100 | } 101 | out += printers.declarations.variableDeclaration(this.raw); 102 | break; 103 | default: 104 | /*::;(this.raw.kind: empty)*/ 105 | break; 106 | } 107 | return out; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | jsdoc?: boolean; 3 | interfaceRecords?: boolean; 4 | moduleExports?: boolean; 5 | quiet?: boolean; 6 | inexact?: boolean; 7 | asModule?: string; 8 | }; 9 | 10 | const defaultOptions: Options = Object.freeze({ 11 | jsdoc: true, 12 | interfaceRecords: false, 13 | moduleExports: true, 14 | quiet: false, 15 | inexact: true, 16 | }); 17 | 18 | const options: Options = { ...defaultOptions }; 19 | 20 | export function assignOptions(newOptions: Partial): void { 21 | Object.assign(options, newOptions); 22 | } 23 | 24 | export function resetOptions(): void { 25 | Object.assign(options, defaultOptions); 26 | } 27 | 28 | export function opts(): Options { 29 | return options; 30 | } 31 | -------------------------------------------------------------------------------- /src/parse/ast.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import type { RawNode } from "../nodes/node"; 3 | import util from "util"; 4 | import * as logger from "../logger"; 5 | 6 | import * as printers from "../printers"; 7 | 8 | const inspect = Symbol.for("nodejs.util.inspect.custom"); 9 | 10 | export const parseNameFromNode = (node: RawNode): string => { 11 | if (node.name && node.name.text) { 12 | return node.name.text; 13 | } else if (node.type && node.type.typeName) { 14 | return node.type.typeName.text; 15 | } else if (node.exportClause) { 16 | const names = []; 17 | ts.forEachChild(node.exportClause, child => { 18 | names.push(parseNameFromNode(child)); 19 | }); 20 | return names.join(","); 21 | } else if (node.importClause && node.importClause.namedBindings) { 22 | return parseNameFromNode(node.importClause.namedBindings); 23 | } else if (node.moduleSpecifier) { 24 | return node.moduleSpecifier.text; 25 | } else if (node.expression) { 26 | return printers.node.printType(stripDetailsFromTree(node.expression)); 27 | } else if (node.declarationList) { 28 | const declarations = node.declarationList.declarations 29 | .map(stripDetailsFromTree) 30 | .map(printers.node.printType) 31 | .join(" "); 32 | 33 | return declarations; 34 | } else if (node.kind === ts.SyntaxKind.NamedImports) { 35 | const names = []; 36 | ts.forEachChild(node, child => { 37 | names.push(parseNameFromNode(child)); 38 | }); 39 | return names.join(","); 40 | } else if (ts.isIdentifier(node)) { 41 | /* 42 | * Parse name for NamespaceExport, please refer to the PR: https://github.com/joarwilk/flowgen/pull/131 43 | * Based on the test, seems it only affects NamespaceExport 44 | * May need someone to update the implementation later if there are any issues 45 | */ 46 | if (node.escapedText && typeof node.escapedText === "string") { 47 | return node.escapedText; 48 | } 49 | } 50 | switch (node.kind) { 51 | case ts.SyntaxKind.FunctionDeclaration: 52 | logger.error(node.modifiers || node, { type: "MissingFunctionName" }); 53 | break; 54 | default: 55 | console.log("INVALID NAME", ts.SyntaxKind[node.kind]); 56 | break; 57 | } 58 | return "INVALID NAME REF"; 59 | }; 60 | 61 | function inspectFn(depth: number, options: util.InspectOptions): string { 62 | const newOptions = Object.assign({}, options, { 63 | depth: options.depth == null ? null : options.depth - 1, 64 | }); 65 | if (depth < 0) { 66 | // eslint-disable-next-line no-unused-vars 67 | const { parent, symbol, localSymbol, ...rest } = this; 68 | delete rest[inspect]; 69 | if (rest.kind) { 70 | return `${ts.SyntaxKind[rest.kind]} ${util.inspect(rest, newOptions)}`; 71 | } else { 72 | return util.inspect(rest, newOptions); 73 | } 74 | } 75 | // eslint-disable-next-line no-unused-vars 76 | const { parent, symbol, localSymbol, ...rest } = this; 77 | for (const key in rest) { 78 | if ( 79 | Object.prototype.hasOwnProperty.call(rest, key) && 80 | typeof rest[key] === "object" 81 | ) { 82 | rest[key][inspect] = inspectFn.bind(rest[key]); 83 | } 84 | } 85 | delete rest[inspect]; 86 | if (rest.kind) { 87 | return `${ts.SyntaxKind[rest.kind]} ${util.inspect(rest, newOptions)}`; 88 | } else { 89 | return util.inspect(rest, newOptions); 90 | } 91 | } 92 | 93 | // Traverse a node and strip information we dont care about 94 | // This is mostly to make debugging a bit less verbose 95 | export const stripDetailsFromTree = (root: RawNode): any => { 96 | for (const key in root) { 97 | const val = root[key]; 98 | 99 | if (key === "parent") continue; 100 | if (key === "symbol") continue; 101 | if (key === "localSymbol") continue; 102 | if (typeof val === "function") continue; 103 | if (typeof val !== "object") continue; 104 | 105 | if ( 106 | Object.prototype.hasOwnProperty.call(root, key) && 107 | typeof val === "object" 108 | ) { 109 | if (Array.isArray(val)) { 110 | root[key] = root[key].map(stripDetailsFromTree); 111 | // @ts-expect-error todo(flow->ts) 112 | root[key].pos = val.pos; 113 | // @ts-expect-error todo(flow->ts) 114 | root[key].end = val.end; 115 | root[key].assertHasRealPosition = root.assertHasRealPosition.bind(val); 116 | root[key].getStart = root.getStart.bind(val); 117 | root[key].getEnd = root.getEnd.bind(val); 118 | } else { 119 | root[key][inspect] = inspectFn.bind(val); 120 | } 121 | } 122 | } 123 | 124 | root[inspect] = inspectFn.bind(root); 125 | return root; 126 | }; 127 | 128 | export function getMembersFromNode(node: any): void { 129 | if (node.members) { 130 | return node.members; 131 | } 132 | 133 | if (node.type && node.type.members) { 134 | return node.type.members; 135 | } 136 | 137 | console.log("NO MEMBERS_", ts.SyntaxKind[node.kind], node); 138 | } 139 | -------------------------------------------------------------------------------- /src/parse/index.ts: -------------------------------------------------------------------------------- 1 | import type { RawNode } from "../nodes/node"; 2 | 3 | import * as ts from "typescript"; 4 | 5 | import Node from "../nodes/node"; 6 | import type ModuleNode from "../nodes/module"; 7 | import NodeFactory from "../nodes/factory"; 8 | import type { Factory } from "../nodes/factory"; 9 | import { parseNameFromNode, stripDetailsFromTree } from "./ast"; 10 | import * as logger from "../logger"; 11 | import { checker } from "../checker"; 12 | 13 | const collectNode = (node: RawNode, context: Node, factory: Factory): void => { 14 | stripDetailsFromTree(node); 15 | switch (node.kind) { 16 | case ts.SyntaxKind.ModuleDeclaration: 17 | if ( 18 | node.flags === 4098 || 19 | (node.flags & ts.NodeFlags.Namespace) === ts.NodeFlags.Namespace 20 | ) { 21 | if ( 22 | (node.flags & ts.NodeFlags.GlobalAugmentation) === 23 | ts.NodeFlags.GlobalAugmentation 24 | ) { 25 | logger.error(node, { type: "UnsupportedGlobalAugmentation" }); 26 | const globalAugmentation = factory.createModuleNode( 27 | node, 28 | node.name.text, 29 | ); 30 | context.addChild("module" + node.name.text, globalAugmentation); 31 | traverseNode(node.body, globalAugmentation, factory); 32 | break; 33 | } 34 | const namespace = factory.createNamespaceNode( 35 | node, 36 | node.name.text, 37 | context, 38 | ); 39 | 40 | traverseNode(node.body, namespace, factory); 41 | 42 | context.addChildren("namespace" + node.name.text, namespace); 43 | break; 44 | } else { 45 | const module = factory.createModuleNode(node, node.name.text); 46 | 47 | context.addChild("module" + node.name.text, module); 48 | 49 | traverseNode(node.body, module, factory); 50 | break; 51 | } 52 | 53 | case ts.SyntaxKind.FunctionDeclaration: 54 | // TODO: rewrite this 55 | factory.createFunctionDeclaration(node, parseNameFromNode(node), context); 56 | break; 57 | 58 | case ts.SyntaxKind.InterfaceDeclaration: 59 | context.addChild( 60 | parseNameFromNode(node), 61 | factory.createPropertyNode(node, parseNameFromNode(node), context), 62 | ); 63 | break; 64 | 65 | case ts.SyntaxKind.TypeAliasDeclaration: 66 | context.addChild( 67 | parseNameFromNode(node), 68 | factory.createPropertyNode(node, parseNameFromNode(node), context), 69 | ); 70 | break; 71 | 72 | case ts.SyntaxKind.ClassDeclaration: 73 | context.addChild( 74 | parseNameFromNode(node), 75 | factory.createPropertyNode(node), 76 | ); 77 | break; 78 | 79 | case ts.SyntaxKind.VariableStatement: 80 | context.addChild( 81 | parseNameFromNode(node), 82 | factory.createPropertyNode(node), 83 | ); 84 | break; 85 | 86 | case ts.SyntaxKind.ExportAssignment: 87 | context.addChild( 88 | "exportassign" + parseNameFromNode(node), 89 | factory.createExportNode(node), 90 | ); 91 | break; 92 | 93 | case ts.SyntaxKind.ImportDeclaration: 94 | context.addChild(parseNameFromNode(node), factory.createImportNode(node)); 95 | break; 96 | 97 | case ts.SyntaxKind.ExportDeclaration: 98 | context.addChild( 99 | "exportdecl" + parseNameFromNode(node), 100 | factory.createExportDeclarationNode(node), 101 | ); 102 | break; 103 | 104 | case ts.SyntaxKind.ImportEqualsDeclaration: 105 | // see transformers 106 | break; 107 | 108 | case ts.SyntaxKind.NamespaceExportDeclaration: 109 | // TODO: unimplemented; 110 | break; 111 | 112 | case ts.SyntaxKind.EnumDeclaration: 113 | context.addChild( 114 | parseNameFromNode(node), 115 | factory.createPropertyNode(node), 116 | ); 117 | break; 118 | 119 | case ts.SyntaxKind.EmptyStatement: 120 | // This should be empty 121 | break; 122 | 123 | default: 124 | console.log("Missing node parse", ts.SyntaxKind[node.kind]); 125 | } 126 | }; 127 | 128 | // Walk the AST and extract all the definitions we care about 129 | const traverseNode = (node, context: Node, factory: Factory): void => { 130 | if (!node.statements) { 131 | collectNode(node, context, factory); 132 | } else { 133 | node.statements.forEach(n => collectNode(n, context, factory)); 134 | } 135 | }; 136 | 137 | function findDuplicatedSymbolsAndUsedNames( 138 | node: ts.Node, 139 | ): [ts.Symbol[], string[]] { 140 | if (ts.isIdentifier(node)) { 141 | const s = checker.current.getSymbolAtLocation(node); 142 | if (!s) { 143 | return [[], []]; 144 | } 145 | 146 | if (ts.isTypeAliasDeclaration(node.parent) && s.declarations.length > 1) { 147 | return [[s], [s.getName()]]; 148 | } else { 149 | return [[], [s.getName()]]; 150 | } 151 | } 152 | 153 | const childResult: [ts.Symbol[], string[]] = [[], []]; 154 | ts.forEachChild(node, child => { 155 | ts.visitNode(child, (n: ts.Node) => { 156 | const r = findDuplicatedSymbolsAndUsedNames(n); 157 | const duplicatedSymbols = r[0]; 158 | const names = r[1]; 159 | childResult[0].push(...duplicatedSymbols); 160 | childResult[1].push(...names); 161 | return n; 162 | }); 163 | }); 164 | 165 | return childResult; 166 | } 167 | 168 | function generateUniqueName(name: string, usedNames: Set): string { 169 | if (!usedNames.has(name)) { 170 | return name; 171 | } 172 | let i = 1; 173 | // Just make sure we won't endup with infinite loop 174 | while (i < 10_000) { 175 | const r = `${name}${i}`; 176 | if (!usedNames.has(r)) { 177 | return r; 178 | } 179 | i++; 180 | } 181 | 182 | return name; 183 | } 184 | 185 | function createMakeNameCompatibleWithFlowTransformer( 186 | duplicatedSymbols: Set, 187 | usedNames: Set, 188 | ) { 189 | const renameMap = new Map(); 190 | 191 | function makeNameCompatibleWithFlowTransformer( 192 | context: ts.TransformationContext, 193 | ) { 194 | const { factory } = context; 195 | const visitor = (node: ts.Node): ts.Node | ts.Node[] => { 196 | if (ts.isTypeAliasDeclaration(node)) { 197 | const s = checker.current.getSymbolAtLocation(node.name); 198 | if (duplicatedSymbols.has(s)) { 199 | const id = 200 | renameMap.get(s) ?? 201 | factory.createIdentifier( 202 | generateUniqueName(`${s.getName()}Type`, usedNames), 203 | ); 204 | renameMap.set(s, id); 205 | return factory.createTypeAliasDeclaration( 206 | node.decorators, 207 | node.modifiers, 208 | id, 209 | node.typeParameters, 210 | node.type, 211 | ); 212 | } 213 | } 214 | 215 | if (ts.isTypeReferenceNode(node)) { 216 | if (ts.isIdentifier(node.typeName)) { 217 | const s = checker.current.getSymbolAtLocation(node.typeName); 218 | if (duplicatedSymbols.has(s)) { 219 | const id = 220 | renameMap.get(s) ?? 221 | factory.createIdentifier( 222 | generateUniqueName(`${s.getName()}Type`, usedNames), 223 | ); 224 | renameMap.set(s, id); 225 | return factory.createTypeReferenceNode(id.text, node.typeArguments); 226 | } 227 | } 228 | } 229 | 230 | if (!ts.isIdentifier(node)) { 231 | return ts.visitEachChild(node, visitor, context); 232 | } 233 | 234 | return node; 235 | }; 236 | 237 | return visitor; 238 | } 239 | 240 | return makeNameCompatibleWithFlowTransformer; 241 | } 242 | 243 | // In TypeScript you can have the same name for a variable and a type but not in FlowJs 244 | function makeNameCompatibleWithFlow(ast: any) { 245 | const [duplicatedSymbols, usedNames] = findDuplicatedSymbolsAndUsedNames(ast); 246 | return ts.transform(ast, [ 247 | createMakeNameCompatibleWithFlowTransformer( 248 | new Set(duplicatedSymbols), 249 | new Set(usedNames), 250 | ), 251 | ]).transformed[0]; 252 | } 253 | 254 | export function recursiveWalkTree(ast: any): ModuleNode { 255 | const factory = NodeFactory.create(); 256 | 257 | const root = factory.createModuleNode(null, "root"); 258 | 259 | traverseNode(makeNameCompatibleWithFlow(ast), root, factory); 260 | 261 | return root; 262 | } 263 | -------------------------------------------------------------------------------- /src/printers/basics.ts: -------------------------------------------------------------------------------- 1 | const types: { 2 | [key: string]: string; 3 | } = { 4 | VoidKeyword: "void", 5 | StringKeyword: "string", 6 | BigIntKeyword: "bigint", 7 | AnyKeyword: "any", 8 | NumberKeyword: "number", 9 | BooleanKeyword: "boolean", 10 | NullKeyword: "null", 11 | UndefinedKeyword: "void", 12 | ObjectKeyword: "{[key: string]: any}", 13 | FalseKeyword: "false", 14 | TrueKeyword: "true", 15 | NeverKeyword: "empty", 16 | UnknownKeyword: "mixed", 17 | }; 18 | 19 | export const print = (kind: string): string => { 20 | return types[kind]; 21 | }; 22 | -------------------------------------------------------------------------------- /src/printers/common.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { opts } from "../options"; 3 | import type { RawNode } from "../nodes/node"; 4 | 5 | import * as printers from "./index"; 6 | import { withEnv } from "../env"; 7 | 8 | export const literalType = (node: RawNode): string => { 9 | if (node.literal) { 10 | if (node.literal.kind === ts.SyntaxKind.StringLiteral) { 11 | return printers.node.printType(node.literal); 12 | } else if (node.literal.text) { 13 | return node.literal.text; 14 | } else { 15 | return printers.node.printType(node.literal); 16 | } 17 | } 18 | 19 | if (node.type.typeName) { 20 | return node.type.typeName.text; 21 | } 22 | return printers.node.printType(node.type); 23 | }; 24 | 25 | export const typeParameter = ( 26 | node: ts.TypeParameterDeclaration & { 27 | withoutDefault: boolean; 28 | }, 29 | ): string => { 30 | let defaultType = ""; 31 | let constraint = ""; 32 | if (node.constraint) { 33 | constraint = `: ${printers.node.printType(node.constraint)}`; 34 | } 35 | if (!node.withoutDefault && node.default) { 36 | defaultType = `= ${printers.node.printType(node.default)}`; 37 | } 38 | return `${node.name.text}${constraint}${defaultType}`; 39 | }; 40 | 41 | export const parameter = ( 42 | param: 43 | | ts.ParameterDeclaration 44 | | ts.PropertySignature 45 | | ts.GetAccessorDeclaration 46 | | ts.SetAccessorDeclaration, 47 | ): string => { 48 | let left = ""; 49 | if (param.modifiers) { 50 | for (const modifier of param.modifiers) { 51 | if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) left += "+"; 52 | } 53 | } 54 | if (param.kind === ts.SyntaxKind.SetAccessor) { 55 | left += "-"; 56 | } 57 | let right: string; 58 | 59 | if ( 60 | param.name.kind === ts.SyntaxKind.ObjectBindingPattern || 61 | param.name.kind === ts.SyntaxKind.ArrayBindingPattern 62 | ) { 63 | left = `x`; 64 | } else { 65 | left += printers.node.printType(param.name); 66 | } 67 | 68 | if (!param.type) { 69 | if (param.name.kind === ts.SyntaxKind.ObjectBindingPattern) { 70 | if (param.name.elements.length > 0) { 71 | right = `{${param.name.elements 72 | .map(element => `${printers.node.printType(element)}: any`) 73 | .join(", ")}}`; 74 | } else { 75 | right = "{}"; 76 | } 77 | } else { 78 | right = "any"; 79 | } 80 | } else { 81 | right = printers.node.printType(param.type); 82 | } 83 | 84 | if ( 85 | param.questionToken && 86 | param.name.kind !== ts.SyntaxKind.ComputedPropertyName 87 | ) { 88 | left += "?"; 89 | } 90 | 91 | if ( 92 | param.questionToken && 93 | param.name.kind === ts.SyntaxKind.ComputedPropertyName 94 | ) { 95 | right = `(${right}) | void`; 96 | } 97 | 98 | if (ts.isParameter(param) && param.dotDotDotToken) { 99 | left = "..." + left; 100 | } 101 | 102 | return `${left}: ${right}`; 103 | }; 104 | 105 | export const methodSignature = ( 106 | param: ts.MethodSignature | ts.MethodDeclaration, 107 | ): string => { 108 | let left = ""; 109 | let isMethod = true; 110 | if (param.modifiers) { 111 | for (const modifier of param.modifiers) { 112 | if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) { 113 | left += "+"; 114 | isMethod = false; 115 | } 116 | } 117 | } 118 | left += printers.node.printType(param.name); 119 | let right; 120 | 121 | if ( 122 | param.questionToken && 123 | param.name.kind !== ts.SyntaxKind.ComputedPropertyName 124 | ) { 125 | left += "?"; 126 | isMethod = false; 127 | } 128 | if (param.name.kind === ts.SyntaxKind.ComputedPropertyName) { 129 | isMethod = false; 130 | } 131 | 132 | right = printers.functions.functionType(param, isMethod); 133 | 134 | if ( 135 | param.questionToken && 136 | param.name.kind === ts.SyntaxKind.ComputedPropertyName 137 | ) { 138 | right = `(${right}) | void`; 139 | } 140 | 141 | return `${left}${isMethod ? "" : ": "}${right}`; 142 | }; 143 | 144 | export const generics = (types?: ReadonlyArray | null): string => { 145 | if (types && typeof types.length !== "undefined") { 146 | return `<${types.map(printers.node.printType).join(", ")}>`; 147 | } 148 | return ""; 149 | }; 150 | 151 | export const genericsWithoutDefault = ( 152 | types?: ReadonlyArray | null, 153 | ): string => { 154 | if (types && typeof types.length !== "undefined") { 155 | return `<${types 156 | .map(node => { 157 | node.withoutDefault = true; 158 | return printers.node.printType(node); 159 | }) 160 | .join(", ")}>`; 161 | } 162 | return ""; 163 | }; 164 | 165 | const jsDocPrintTag = (tag): string => { 166 | const typeNameValue = tag.typeExpression && tag.typeExpression.type; 167 | const parameterNameValue = tag.name || tag.preParameterName; 168 | const typeName = typeNameValue 169 | ? ` {${printers.node.printType(typeNameValue)}}` 170 | : ""; 171 | const parameterName = parameterNameValue 172 | ? ` ${ 173 | tag.isBracketed 174 | ? `[${printers.node.printType(parameterNameValue)}]` 175 | : printers.node.printType(parameterNameValue) 176 | }` 177 | : ""; 178 | const comment = tag.comment ? ` ${tag.comment}`.replace(/\n/g, "\n * ") : ""; 179 | if (typeNameValue && typeNameValue.kind === ts.SyntaxKind.JSDocTypeLiteral) { 180 | let output = `\n * @${tag.tagName.text}${typeName}${parameterName}${comment}`; 181 | for (const jsDocPropertyTag of typeNameValue.jsDocPropertyTags) { 182 | output += jsDocPrintTag(jsDocPropertyTag); 183 | } 184 | return output; 185 | } 186 | if (tag && tag.kind === ts.SyntaxKind.JSDocCallbackTag) { 187 | const parameterName = parameterNameValue 188 | ? printers.node.printType(parameterNameValue) 189 | : ""; 190 | let output = `\n * @${tag.tagName.text} ${parameterName}${tag.comment}`; 191 | for (const param of tag.typeExpression.parameters) { 192 | output += jsDocPrintTag(param); 193 | } 194 | if (typeNameValue) output += jsDocPrintTag(typeNameValue); 195 | return output; 196 | } 197 | return `\n * @${tag.tagName.text}${typeName}${parameterName}${comment}`; 198 | }; 199 | 200 | /** The node's jsdoc comments, if any and if the `jsdoc` option is enabled. */ 201 | export const jsdoc = withEnv((env, node): string => { 202 | if (!opts().jsdoc) return ""; 203 | 204 | // @ts-expect-error The jsDoc property is internal, on ts.JSDocContainer. 205 | const jsDoc = node.jsDoc as void | ts.JSDoc[]; 206 | if (!jsDoc) return ""; 207 | 208 | const blocks = jsDoc 209 | .map(doc => { 210 | const comment = (doc.comment ? `\n${doc.comment}` : "").replace( 211 | /\n/g, 212 | "\n * ", 213 | ); 214 | 215 | env.tsdoc = true; 216 | const tags = (doc.tags || []).map(jsDocPrintTag); 217 | env.tsdoc = false; 218 | 219 | return `/**${comment + tags.join("")}\n */\n`; 220 | }) 221 | .join(""); 222 | return `\n${blocks}`; 223 | }); 224 | -------------------------------------------------------------------------------- /src/printers/function.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import type { RawNode } from "../nodes/node"; 3 | import * as printers from "./index"; 4 | 5 | export const functionType = ( 6 | func: 7 | | ts.FunctionTypeNode 8 | | ts.FunctionDeclaration 9 | | ts.MethodDeclaration 10 | | ts.MethodSignature 11 | | ts.ConstructSignatureDeclaration, 12 | dotAsReturn = false, 13 | ): string => { 14 | const params = func.parameters 15 | .filter( 16 | param => !(ts.isIdentifier(param.name) && param.name.text === "this"), 17 | ) 18 | .map(printers.common.parameter); 19 | const generics = printers.common.genericsWithoutDefault(func.typeParameters); 20 | const returns = func.type ? printers.node.printType(func.type) : "void"; 21 | 22 | const firstPass = `${generics}(${params.join(", ")})${ 23 | dotAsReturn ? ":" : " =>" 24 | } ${returns}`; 25 | 26 | // Make sure our functions aren't too wide 27 | if (firstPass.length > 80) { 28 | // break params onto a new line for better formatting 29 | const paramsWithNewlines = `\n${params.join(",\n")}`; 30 | 31 | return `${generics}(${paramsWithNewlines})${ 32 | dotAsReturn ? ":" : " =>" 33 | } ${returns}`; 34 | } 35 | 36 | return firstPass; 37 | }; 38 | 39 | export const functionDeclaration = ( 40 | nodeName: string, 41 | node: RawNode, 42 | ): string => { 43 | const exporter = printers.relationships.exporter(node); 44 | let name = nodeName; 45 | if ( 46 | node.modifiers && 47 | node.modifiers.some( 48 | modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword, 49 | ) 50 | ) { 51 | name = nodeName === "INVALID NAME REF" ? "fn" : nodeName; 52 | } 53 | // each functionDeclaration gets it's own line 54 | const str = `declare ${exporter}function ${name}${functionType( 55 | node, 56 | true, 57 | )}\n`; 58 | 59 | return str; 60 | }; 61 | -------------------------------------------------------------------------------- /src/printers/identifiers.ts: -------------------------------------------------------------------------------- 1 | // Please add only built-in type references 2 | 3 | import * as printers from "./index"; 4 | import { opts } from "../options"; 5 | import { withEnv } from "../env"; 6 | import ts from "typescript"; 7 | 8 | const Record = ([key, value]: [any, any], isInexact = opts().inexact) => { 9 | const valueType = printers.node.printType(value); 10 | 11 | switch (key.kind) { 12 | case ts.SyntaxKind.LiteralType: 13 | return `{ ${printers.node.printType(key)}: ${valueType}${ 14 | isInexact ? ", ..." : "" 15 | }}`; 16 | case ts.SyntaxKind.UnionType: 17 | if (key.types.every(t => t.kind === ts.SyntaxKind.LiteralType)) { 18 | const fields = key.types.reduce((acc, t) => { 19 | acc += `${printers.node.printType(t)}: ${valueType},\n`; 20 | return acc; 21 | }, ""); 22 | return `{ ${fields}${isInexact ? "..." : ""}}`; 23 | } 24 | // Fallthrough 25 | default: 26 | return `{[key: ${printers.node.printType(key)}]: ${valueType}${ 27 | isInexact ? ", ..." : "" 28 | }}`; 29 | } 30 | }; 31 | 32 | type IdentifierResult = string | ((...args: any[]) => any); 33 | 34 | const identifiers: { [name: string]: IdentifierResult } = { 35 | ReadonlyArray: "$ReadOnlyArray", 36 | ReadonlySet: "$ReadOnlySet", 37 | ReadonlyMap: "$ReadOnlyMap", 38 | Readonly: "$ReadOnly", 39 | RegExpMatchArray: "RegExp$matchResult", 40 | NonNullable: "$NonMaybeType", 41 | Partial: ([type]: any[]) => { 42 | const isInexact = opts().inexact; 43 | return `$Rest<${printers.node.printType(type)}, {${ 44 | isInexact ? "..." : "" 45 | }}>`; 46 | }, 47 | ReturnType: (typeArguments: any[]) => { 48 | return `$Call<((...args: any[]) => R) => R, ${printers.node.printType( 49 | typeArguments[0], 50 | )}>`; 51 | }, 52 | Record, 53 | Omit: ([obj, keys]: [any, any]) => { 54 | return `$Diff<${printers.node.printType(obj)},${Record( 55 | [keys, { kind: ts.SyntaxKind.AnyKeyword }], 56 | false, 57 | )}>`; 58 | }, 59 | }; 60 | 61 | export const print = withEnv((env, kind) => { 62 | if (env.classHeritage) return kind; 63 | return Object.prototype.hasOwnProperty.call(identifiers, kind) 64 | ? identifiers[kind] 65 | : kind; 66 | }); 67 | -------------------------------------------------------------------------------- /src/printers/index.ts: -------------------------------------------------------------------------------- 1 | import * as basics from "./basics"; 2 | import * as identifiers from "./identifiers"; 3 | import * as declarations from "./declarations"; 4 | import * as relationships from "./relationships"; 5 | import * as common from "./common"; 6 | import * as node from "./node"; 7 | import * as functions from "./function"; 8 | 9 | export { 10 | basics, 11 | identifiers, 12 | declarations, 13 | common, 14 | functions, 15 | relationships, 16 | node, 17 | }; 18 | -------------------------------------------------------------------------------- /src/printers/relationships.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { opts } from "../options"; 3 | import type { RawNode } from "../nodes/node"; 4 | 5 | import namespaceManager from "../namespace-manager"; 6 | import * as printers from "./index"; 7 | 8 | export const moduleExports = (node: RawNode): string => { 9 | const name = printers.node.printType(node.expression); 10 | if (node.isExportEquals && opts().moduleExports) { 11 | return `declare module.exports: typeof ${name}\n`; 12 | } else { 13 | return `declare export default typeof ${name}\n`; 14 | } 15 | }; 16 | 17 | export const exporter = (node: RawNode): string => { 18 | let str = ""; 19 | 20 | if ( 21 | node.modifiers && 22 | node.modifiers.some( 23 | modifier => modifier.kind === ts.SyntaxKind.ExportKeyword, 24 | ) 25 | ) { 26 | str += "export "; 27 | } 28 | 29 | if ( 30 | node.modifiers && 31 | node.modifiers.some( 32 | modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword, 33 | ) 34 | ) { 35 | str += "default "; 36 | } 37 | 38 | return str; 39 | }; 40 | 41 | export const importExportSpecifier = ( 42 | node: ts.ImportSpecifier | ts.ExportSpecifier, 43 | ): string => { 44 | if (node.propertyName) { 45 | return `${printers.node.printType( 46 | node.propertyName, 47 | )} as ${printers.node.printType(node.name)}`; 48 | } 49 | return printers.node.printType(node.name); 50 | }; 51 | 52 | // TODO: move import here 53 | // export const imports = (node: ImportNode, moduleName: string): string => { 54 | // let str = "import type "; 55 | 56 | // if (node.default) { 57 | // str += node.default; 58 | 59 | // if (node.explicit.length) { 60 | // str += ", "; 61 | // } 62 | // } 63 | 64 | // if (node.explicit.length) { 65 | // str += `{ ${node.explicit.join(", ")} }`; 66 | // } 67 | 68 | // str += ` from '${moduleName}'`; 69 | 70 | // return str; 71 | // }; 72 | 73 | export const namespace = (name: string, hidePunctuation = false): string => { 74 | if (namespaceManager.nsExists(name)) { 75 | return `${name}${hidePunctuation ? "" : "$"}`; 76 | } 77 | 78 | return name + (hidePunctuation ? "" : "."); 79 | }; 80 | 81 | export const namespaceProp = ( 82 | name: string, 83 | hidePunctuation = false, 84 | ): string => { 85 | if (namespaceManager.nsPropExists(name)) { 86 | return `${namespaceManager.getNSForProp(name)}${ 87 | hidePunctuation ? "" : "$" 88 | }${name}`; 89 | } 90 | 91 | return name; 92 | }; 93 | -------------------------------------------------------------------------------- /src/printers/smart-identifiers.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { checker } from "../checker"; 3 | 4 | const setImportedName = ( 5 | name: ts.__String, 6 | type: any, 7 | symbol: ts.Symbol, 8 | decl: ts.Declaration, 9 | ): boolean => { 10 | const specifiers = ["react"]; 11 | const namespaces = ["React"]; 12 | const paths = (name: string) => { 13 | if (name === "react" || name === "React") { 14 | return { 15 | ReactNode: "Node", 16 | ReactElement: "Element", 17 | }; 18 | } 19 | return {}; 20 | }; 21 | // @ts-expect-error todo(flow->ts) 22 | if (namespaces.includes(symbol.parent?.escapedName)) { 23 | // @ts-expect-error todo(flow->ts) 24 | type.escapedText = paths(symbol.parent?.escapedName)[name] || name; 25 | return true; 26 | } else if ( 27 | // @ts-expect-error todo(flow->ts) 28 | specifiers.includes(decl.parent?.parent?.parent?.moduleSpecifier?.text) 29 | ) { 30 | type.escapedText = 31 | // @ts-expect-error todo(flow->ts) 32 | paths(decl.parent.parent.parent.moduleSpecifier.text)[name] || name; 33 | return true; 34 | } 35 | return false; 36 | }; 37 | 38 | const setGlobalName = ( 39 | type: ts.TypeReferenceNode, 40 | _symbol: ts.Symbol, 41 | ): boolean => { 42 | const globals = [ 43 | { 44 | from: ts.createQualifiedName(ts.createIdentifier("JSX"), "Element"), 45 | to: ts.createIdentifier("React$Node"), 46 | }, 47 | ]; 48 | if (checker.current) { 49 | const bools = []; 50 | for (const { from, to } of globals) { 51 | if ( 52 | ts.isQualifiedName(type.typeName) && 53 | compareQualifiedName(type.typeName, from) 54 | ) { 55 | // @ts-expect-error readonly property, but we write to it 56 | type.typeName = to; 57 | bools.push(true); 58 | } 59 | } 60 | return bools.length > 0; 61 | } 62 | return false; 63 | }; 64 | 65 | export function renames( 66 | symbol: ts.Symbol | void, 67 | type: ts.TypeReferenceNode | ts.ImportSpecifier, 68 | ): boolean { 69 | if (!symbol) return false; 70 | if (!symbol.declarations) return false; 71 | const decl = symbol.declarations[0]; 72 | if (ts.isImportSpecifier(type)) { 73 | // @ts-expect-error todo(flow->ts) 74 | setImportedName(decl.name.escapedText, decl.name, symbol, decl); 75 | } else if (type.kind === ts.SyntaxKind.TypeReference) { 76 | const leftMost = getLeftMostEntityName(type.typeName); 77 | if (leftMost && checker.current) { 78 | const leftMostSymbol = checker.current.getSymbolAtLocation(leftMost); 79 | const isGlobal = leftMostSymbol?.parent?.escapedName === "__global"; 80 | if (isGlobal) { 81 | return setGlobalName(type, symbol); 82 | } 83 | } 84 | if (ts.isQualifiedName(type.typeName)) { 85 | return setImportedName( 86 | symbol.escapedName, 87 | type.typeName.right, 88 | symbol, 89 | decl, 90 | ); 91 | } else { 92 | return setImportedName(symbol.escapedName, type.typeName, symbol, decl); 93 | } 94 | } 95 | return false; 96 | } 97 | 98 | export function getLeftMostEntityName(type: ts.EntityName): ts.Identifier { 99 | if (type.kind === ts.SyntaxKind.QualifiedName) { 100 | return type.left.kind === ts.SyntaxKind.Identifier 101 | ? type.left 102 | : getLeftMostEntityName(type.left); 103 | } else if (type.kind === ts.SyntaxKind.Identifier) { 104 | return type; 105 | } 106 | } 107 | 108 | function compareIdentifier(a: ts.Identifier, b: ts.Identifier): boolean { 109 | if (a.kind !== b.kind) return false; 110 | if (a.escapedText === b.escapedText && a.text === b.text) return true; 111 | return false; 112 | } 113 | 114 | function compareEntityName(a: ts.EntityName, b: ts.EntityName): boolean { 115 | if ( 116 | a.kind === ts.SyntaxKind.Identifier && 117 | b.kind === ts.SyntaxKind.Identifier 118 | ) { 119 | return compareIdentifier(a, b); 120 | } 121 | if ( 122 | a.kind === ts.SyntaxKind.QualifiedName && 123 | b.kind === ts.SyntaxKind.QualifiedName 124 | ) { 125 | return compareQualifiedName(a, b); 126 | } 127 | return false; 128 | } 129 | 130 | function compareQualifiedName( 131 | a: ts.QualifiedName, 132 | b: ts.QualifiedName, 133 | ): boolean { 134 | return ( 135 | compareEntityName(a.left, b.left) && compareIdentifier(a.right, b.right) 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /src/printers/unionNode.ts: -------------------------------------------------------------------------------- 1 | import type Node from "../nodes/node"; 2 | 3 | /** 4 | * A way to represent multiple nodes with the same name 5 | * in the same scope. 6 | * 7 | * TypeScript supports declaring the same function/type/interface multiple times, 8 | * which flow does not. This is a representation of that data. 9 | */ 10 | export default class UnionNode { 11 | _nodes: Array; 12 | 13 | constructor(nodes: Node | Node[]) { 14 | this._nodes = []; 15 | 16 | this.add(nodes); 17 | } 18 | 19 | add(nodes: Node | Node[]) { 20 | if (Array.isArray(nodes)) { 21 | nodes.forEach(node => { 22 | this._nodes.push(node); 23 | }); 24 | } else { 25 | this._nodes.push(nodes); 26 | } 27 | } 28 | 29 | get() { 30 | return this._nodes; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test-matchers.ts: -------------------------------------------------------------------------------- 1 | import { execFileSync } from "child_process"; 2 | 3 | import chalk from "chalk"; 4 | import flow from "flow-bin"; 5 | 6 | import { beautify } from "."; 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars 9 | declare global { 10 | // eslint-disable-next-line @typescript-eslint/no-namespace 11 | namespace jest { 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars 13 | interface Matchers { 14 | toBeValidFlowTypeDeclarations(): R; 15 | } 16 | } 17 | } 18 | 19 | expect.extend({ 20 | toBeValidFlowTypeDeclarations(source) { 21 | const beautifiedSource = beautify(source); 22 | try { 23 | execFileSync( 24 | flow, 25 | ["check-contents", "--all", "--color=always", "--timeout=30"], 26 | { 27 | input: beautifiedSource, 28 | stdio: ["pipe", "pipe", "pipe"], 29 | }, 30 | ); 31 | } catch (err) { 32 | return { 33 | message: () => 34 | `expected ${chalk.bold( 35 | beautifiedSource.trimEnd(), 36 | )} to be valid flow:\n${chalk.red(err.stdout)}`, 37 | pass: false, 38 | }; 39 | } 40 | 41 | return { 42 | message: () => 43 | `expected ${chalk.bold( 44 | beautifiedSource.trimEnd(), 45 | )} not to be valid flow`, 46 | pass: true, 47 | }; 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /src/typescript-internals.ts: -------------------------------------------------------------------------------- 1 | import "typescript"; 2 | // declare typescript internals used by flowgen but not directly exported 3 | // eslint-disable-next-line @typescript-eslint/no-namespace 4 | declare module "typescript" { 5 | interface Symbol { 6 | parent?: Symbol; 7 | } 8 | enum SymbolFormatFlags { 9 | DoNotIncludeSymbolChain, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.declaration.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "declaration": true, 6 | "noEmit": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "exclude": [ 10 | "src/**/*.spec.ts", 11 | "**/__tests__/**/*.*", 12 | "src/cli/__tests__/fixtures/**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [ 67 | "src" 68 | ], 69 | "exclude": [ 70 | "src/cli/__tests__/fixtures/*" 71 | ] 72 | } 73 | --------------------------------------------------------------------------------