Skip to content

Commit 5da0681

Browse files
committed
fix #86; typescript expressions
1 parent 7196e32 commit 5da0681

File tree

1 file changed

+105
-4
lines changed

1 file changed

+105
-4
lines changed

src/javascript/typescript.ts

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,110 @@
1+
import {Token, tokenizer as Tokenizer, tokTypes} from "acorn";
2+
import type {SourceFile} from "typescript";
13
import {ModuleKind, ScriptTarget, transpile} from "typescript";
4+
import {createProgram, createSourceFile} from "typescript";
5+
import {isClassExpression, isFunctionExpression, isParenthesizedExpression} from "typescript";
6+
import {isExpressionStatement} from "typescript";
7+
8+
const tokenizerOptions = {
9+
ecmaVersion: "latest"
10+
} as const;
11+
12+
const compilerOptions = {
13+
target: ScriptTarget.ESNext,
14+
module: ModuleKind.Preserve,
15+
verbatimModuleSyntax: true
16+
} as const;
217

318
export function transpileTypeScript(input: string): string {
4-
return transpile(input, {
5-
target: ScriptTarget.ESNext,
6-
module: ModuleKind.Preserve,
7-
verbatimModuleSyntax: true
19+
const expr = asExpression(input);
20+
if (expr) return trimTrailingSemicolon(transpile(expr, compilerOptions));
21+
parseTypeScript(input); // enforce valid syntax
22+
return transpile(input, compilerOptions);
23+
}
24+
25+
/** If the given is an expression (not a statement), returns it with parens. */
26+
function asExpression(input: string): string | undefined {
27+
if (hasUnmatchedParens(input)) return; // disallow funny business
28+
const expr = `(${trim(input)})`;
29+
if (!isSolitaryExpression(expr)) return;
30+
return expr;
31+
}
32+
33+
/** Parses the specified TypeScript input, returning the AST or throwing a SyntaxError. */
34+
function parseTypeScript(input: string): SourceFile {
35+
const file = createSourceFile("input.ts", input, compilerOptions.target);
36+
const program = createProgram(["input.ts"], compilerOptions, {
37+
getSourceFile: (path) => (path === "input.ts" ? file : undefined),
38+
getDefaultLibFileName: () => "lib.d.ts",
39+
writeFile: () => {},
40+
getCurrentDirectory: () => "/",
41+
getDirectories: () => [],
42+
getCanonicalFileName: (path) => path,
43+
useCaseSensitiveFileNames: () => true,
44+
getNewLine: () => "\n",
45+
fileExists: (path) => path === "input.ts",
46+
readFile: (path) => (path === "input.ts" ? input : undefined)
847
});
48+
const diagnostics = program.getSyntacticDiagnostics(file);
49+
if (diagnostics.length > 0) {
50+
const [diagnostic] = diagnostics;
51+
throw new SyntaxError(String(diagnostic.messageText));
52+
}
53+
return file;
54+
}
55+
56+
/** Returns true if the specified input is exactly one parenthesized expression statement. */
57+
function isSolitaryExpression(input: string): boolean {
58+
let file;
59+
try {
60+
file = parseTypeScript(input);
61+
} catch {
62+
return false;
63+
}
64+
if (file.statements.length !== 1) return false;
65+
const statement = file.statements[0];
66+
if (!isExpressionStatement(statement)) return false;
67+
const expression = statement.expression;
68+
if (!isParenthesizedExpression(expression)) return false;
69+
const subexpression = expression.expression;
70+
if (isClassExpression(subexpression) && subexpression.name) return false;
71+
if (isFunctionExpression(subexpression) && subexpression.name) return false;
72+
return true;
73+
}
74+
75+
function* tokenize(input: string): Generator<Token> {
76+
const tokenizer = Tokenizer(input, tokenizerOptions);
77+
while (true) {
78+
const t = tokenizer.getToken();
79+
if (t.type === tokTypes.eof) break;
80+
yield t;
81+
}
82+
}
83+
84+
/** Returns true if the specified input has mismatched parens. */
85+
function hasUnmatchedParens(input: string): boolean {
86+
let depth = 0;
87+
for (const t of tokenize(input)) {
88+
if (t.type === tokTypes.parenL) ++depth;
89+
else if (t.type === tokTypes.parenR && --depth < 0) return true;
90+
}
91+
return false;
92+
}
93+
94+
/** Removes leading and trailing whitespace around the specified input. */
95+
function trim(input: string): string {
96+
let start;
97+
let end;
98+
for (const t of tokenize(input)) {
99+
start ??= t;
100+
end = t;
101+
}
102+
return input.slice(start?.start, end?.end);
103+
}
104+
105+
/** Removes a trailing semicolon, if present. */
106+
function trimTrailingSemicolon(input: string): string {
107+
let end;
108+
for (const t of tokenize(input)) end = t;
109+
return end?.type === tokTypes.semi ? input.slice(0, end.start) : input;
9110
}

0 commit comments

Comments
 (0)