Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Chore] add initial version of ts-smoosh (#1437)
Co-authored-by: Brian Ford <brian@sequence.studio>
- Loading branch information
1 parent
6b39c43
commit d4698bb
Showing
13 changed files
with
316 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
## TS Smoosh | ||
|
||
Combine type decls with related source files. | ||
|
||
|
||
# Use | ||
|
||
Given a JavaScript file (or list of files) like so: | ||
|
||
``` | ||
$ node ts-smoosh/bin ./src/some-file.js | ||
``` | ||
|
||
Will produce `.jsx` files using nearby `.d.ts` files. | ||
|
||
|
||
# Running Tests | ||
|
||
``` | ||
$ node test | ||
``` | ||
|
||
Test cases live in various directories in `./tests`. Tests run by comparing the output against golden master `.tsx` files. To update the masters, run `node ts-smoosh/bin ./test/0X-dir/somefile.js`. | ||
|
||
# Notes | ||
|
||
Here are articles and docs I that were helpful when writing this script. | ||
|
||
- [Using the Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) | ||
- [Blog post about using TS's parser directly](https://medium.com/allenhwkim/how-to-parse-typescript-from-source-643387971f4e) | ||
- [TypeScript compiler APIs revisited](https://blog.scottlogic.com/2017/05/02/typescript-compiler-api-revisited.html) | ||
- [TypeScript API Playground on Glitch](https://typescript-api-playground.glitch.me/#example=Transformation%203) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const { smoosh } = require("./smoosh"); | ||
|
||
// Given an array of `.js` files, smooshes their .d.ts declarations and | ||
// produces a .tsx file. | ||
|
||
const without = (ending) => (fileName) => | ||
fileName.endsWith(ending) | ||
? fileName.substr(0, fileName.length - ending.length) | ||
: fileName; | ||
|
||
const files = process.argv.slice(2); | ||
|
||
files.map(without(".js")).map(smoosh); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
const ts = require("typescript"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
|
||
/** | ||
* Writes the result of smooshing to a file | ||
*/ | ||
function smoosh(base) { | ||
const smooshedSrc = returnSmooshed(base); | ||
const outputFile = `./${base}.tsx`; | ||
fs.writeFileSync(outputFile, smooshedSrc); | ||
} | ||
|
||
const declDoesntExist = { typeAliases: [], declarations: [], imports: [] }; | ||
|
||
function returnSmooshed(base) { | ||
const dtsFile = `${base}.d.ts`; | ||
// TODO(btford): log a warning here? | ||
const decls = fs.existsSync(dtsFile) ? parseDts(dtsFile) : declDoesntExist; | ||
|
||
const jsFile = `${base}.js`; | ||
const enrichedJsNode = enrichJs(jsFile, decls); | ||
|
||
const outputFile = `${base}.tsx`; | ||
|
||
const resultFile = ts.createSourceFile( | ||
outputFile, | ||
"", | ||
ts.ScriptTarget.Latest, | ||
false, | ||
ts.ScriptKind.TSX | ||
); | ||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); | ||
|
||
const smooshedSrc = printer.printNode( | ||
ts.EmitHint.Unspecified, | ||
enrichedJsNode, | ||
resultFile | ||
); | ||
|
||
return smooshedSrc; | ||
} | ||
|
||
function parseDts(dtsFile) { | ||
const parsed = ts.createSourceFile( | ||
dtsFile, | ||
fs.readFileSync(dtsFile, "utf8"), | ||
ts.ScriptTarget.Latest | ||
); | ||
|
||
// these are going on top | ||
const typeAliases = []; | ||
const declarations = {}; | ||
const imports = []; | ||
|
||
const aggregateDecl = (statement) => { | ||
// console.log(statement); | ||
const kind = ts.SyntaxKind[statement.kind]; | ||
|
||
if (kind === "TypeAliasDeclaration") { | ||
declarations[getIdentifierName(statement)] = statement.type; | ||
typeAliases.push(statement); | ||
return; | ||
} | ||
if (kind === "ImportDeclaration") { | ||
return console.log("import..."); | ||
} | ||
|
||
if (kind === "FirstStatement") { | ||
return statement.declarationList.declarations.map(aggregateDecl); | ||
} | ||
|
||
if (!kind.endsWith("Declaration")) { | ||
const message = `Unexpected statement kind "${kind}" in type definition file "${dtsFile}"`; | ||
return console.warn(message); | ||
} else { | ||
declarations[getIdentifierName(statement)] = statement; | ||
} | ||
}; | ||
|
||
parsed.statements.forEach(aggregateDecl); | ||
|
||
return { typeAliases, declarations, imports }; | ||
} | ||
|
||
function enrichJs(jsFile, dts) { | ||
const parsed = ts.createSourceFile( | ||
jsFile, | ||
fs.readFileSync(jsFile, "utf8"), | ||
ts.ScriptTarget.Latest | ||
); | ||
|
||
const findSource = (node) => { | ||
|
||
let typeSource = null; | ||
|
||
// First, search for a jsdoc tag with the type, like: | ||
// @type {typeof import('./b').Noop} | ||
if (node.jsDoc) { | ||
const typeTag = node.jsDoc[0].tags.find( | ||
(tag) => tag.tagName.escapedText === "type" | ||
); | ||
if (typeTag) { | ||
const fileName = | ||
typeTag.typeExpression.type.argument.literal.text; | ||
const identifier = | ||
typeTag.typeExpression.type.qualifier.escapedText; | ||
const dir = path.dirname(jsFile); | ||
const fullPath = path.resolve(dir, fileName + ".d.ts"); | ||
const importedDts = parseDts(fullPath); | ||
const importedType = importedDts.declarations[identifier]; | ||
if (!importedType) { | ||
console.warn( | ||
`Could not find ${identifier} in ${fullPath} while trying to smoosh ${jsFile}` | ||
); | ||
return node; | ||
} | ||
typeSource = importedType; | ||
} | ||
} | ||
|
||
// Second, use the d.ts file with the same name as this file. | ||
if (!typeSource) { | ||
typeSource = dts.declarations[getIdentifierName(node)]; | ||
} | ||
|
||
return typeSource; | ||
} | ||
|
||
const transformer = (context) => (rootNode) => { | ||
function visit(node) { | ||
const kind = ts.SyntaxKind[node.kind]; | ||
if (kind.endsWith("Declaration")) { | ||
if (kind === "FunctionDeclaration") { | ||
const typeSource = findSource(node); | ||
if (typeSource) { | ||
return ts.factory.updateFunctionDeclaration( | ||
node, | ||
node.decorators, | ||
node.modifiers, | ||
node.asteriskToken, | ||
node.name, | ||
typeSource.typeParameters, | ||
typeSource.parameters, | ||
typeSource.type, | ||
node.body | ||
); | ||
} | ||
return node; | ||
} else if (kind === 'VariableDeclaration') { | ||
const typeSource = findSource(node); | ||
if (typeSource) { | ||
return ts.factory.updateVariableDeclaration( | ||
node, | ||
node.name, | ||
node.exclamationToken, | ||
typeSource.type, | ||
node.initializer | ||
); | ||
} | ||
return node; | ||
} | ||
|
||
return node; | ||
} | ||
|
||
return ts.visitEachChild(node, visit, context); | ||
} | ||
return ts.visitNode(rootNode, visit); | ||
}; | ||
|
||
return ts.transform(parsed, [transformer]).transformed[0]; | ||
} | ||
|
||
function getIdentifierName(node) { | ||
return node.name.escapedText; | ||
} | ||
|
||
module.exports = { | ||
smoosh, | ||
returnSmooshed, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
const fs = require("fs"); | ||
const assert = require("assert").strict; | ||
const path = require("path"); | ||
|
||
const { returnSmooshed } = require("./smoosh"); | ||
|
||
const args = process.argv.slice(2); | ||
|
||
const dirs = args.length > 0 ? args : fs.readdirSync("./tests").map(f => `./tests/${f}`); | ||
|
||
dirs.forEach((dir) => { | ||
const files = fs.readdirSync(dir); | ||
|
||
files | ||
.filter((file) => file.endsWith(".js")) | ||
.map((file) => file.substr(0, file.length - 3)) | ||
.forEach((file) => { | ||
const fullFile = path.resolve(dir, file); | ||
console.log(`Testing ${fullFile}.js`); | ||
const smooshed = returnSmooshed(fullFile); | ||
|
||
const target = fs.readFileSync(fullFile + ".tsx", "utf8"); | ||
assert.equal(smooshed, target); | ||
}); | ||
}); | ||
|
||
console.log("Done"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export function foo( | ||
bar: string, | ||
baz: number | ||
): {boo: number}; | ||
|
||
export function typeParams<A, B>(a: A, b: B): B; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* A function with a JSDoc type import that matches its name | ||
* @type {typeof import('./a').foo} | ||
*/ | ||
export function foo( | ||
bar, | ||
baz | ||
) { | ||
return {boo: baz} | ||
} | ||
|
||
// A function with no type import | ||
export function typeParams(a, b) { | ||
return a + b; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/** | ||
* A function with a JSDoc type import that matches its name | ||
* @type {typeof import('./a').foo} | ||
*/ | ||
export function foo(bar: string, baz: number): { | ||
boo: number; | ||
} { | ||
return { boo: baz }; | ||
} | ||
// A function with no type import | ||
export function typeParams<A, B>(a: A, b: B): B { | ||
return a + b; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
export type MyFn = (a: number, b: number) => number |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
|
||
/** | ||
* A function with a JSDoc type import that is different from its name | ||
* @type {typeof import('./b').MyFn} | ||
*/ | ||
function c(a, b) { | ||
console.log('haha hi') | ||
return a + b; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** | ||
* A function with a JSDoc type import that is different from its name | ||
* @type {typeof import('./b').MyFn} | ||
*/ | ||
function c(a: number, b: number): number { | ||
console.log("haha hi"); | ||
return a + b; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// hi | ||
export const x: number; | ||
|
||
export const add: (a: number, b: number) => number; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const x = 3; | ||
|
||
export const add = (a, b) => a + b; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const x: number = 3; | ||
export const add: (a: number, b: number) => number = (a, b) => a + b; |