Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f1b42da

Browse files
committed
docs(ast-spec): add script to generates code strings for the doc files
I'm not sure if we will want to actually use this or not. This was just an idea I had to allow us to embed the relevant code samples directly in a `.md` file saving consumers from having to switch between the .ts file and the .md file. We could ofc just embed the file itself, but that's not as elegant as there's the extra cruft of `export` and `import`s. May end up just ditching this... but it was worth putting it up for posterity. As an� example - this script generates the following output for `ClassProperty.ts`: ```ts interface ClassPropertyComputedName extends BaseNode { type: 'ClassProperty'; key: Expression; computed: true; value: Expression | null; static: boolean; declare: boolean; readonly?: boolean; decorators?: Decorator[]; accessibility?: "private" | "protected" | "public"; optional?: boolean; definite?: boolean; typeAnnotation?: TSTypeAnnotation; } interface ClassPropertyNonComputedName extends BaseNode { type: 'ClassProperty'; key: Identifier | NumberLiteral | StringLiteral; computed: false; value: Expression | null; static: boolean; declare: boolean; readonly?: boolean; decorators?: Decorator[]; accessibility?: "private" | "protected" | "public"; optional?: boolean; definite?: boolean; typeAnnotation?: TSTypeAnnotation; } type ClassProperty = | ClassPropertyComputedName | ClassPropertyNonComputedName; ```
1 parent 07860e0 commit f1b42da

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed

packages/ast-spec/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
"lint": "eslint . --ext .js,.ts --ignore-path='../../.eslintignore'",
3737
"typecheck": "tsc -p tsconfig.json --noEmit"
3838
},
39+
"devDependencies": {
40+
"@typescript-eslint/typescript-estree": "4.20.0",
41+
"globby": "*",
42+
"typescript": "*"
43+
},
3944
"funding": {
4045
"type": "opencollective",
4146
"url": "https://opencollective.com/typescript-eslint"
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import * as fs from 'fs';
2+
import { sync as globSync } from 'globby';
3+
import * as path from 'path';
4+
import { parseAndGenerateServices } from '@typescript-eslint/typescript-estree';
5+
import * as ts from 'typescript';
6+
7+
declare module 'typescript' {
8+
// private APIs we want to consume in this script
9+
interface TypeChecker {
10+
getUnionType(types: ts.Type[]): ts.Type;
11+
}
12+
}
13+
14+
const IGNORED = [
15+
'base',
16+
'unions',
17+
'./spec.ts',
18+
'*/spec.ts',
19+
path.join('expression', 'literal', 'spec.ts'),
20+
];
21+
22+
const ROOT = path.resolve(__dirname, '..');
23+
const SRC_ROOT = path.join(ROOT, 'src');
24+
const TSCONFIG = path.join(ROOT, 'tsconfig.json');
25+
26+
function getTypeName(type: ts.Type): string {
27+
return (type.getSymbol() ?? type.aliasSymbol)!.getName();
28+
}
29+
30+
function isUndefinedType(type: ts.Type): boolean {
31+
return (type.flags & ts.TypeFlags.Undefined) != 0;
32+
}
33+
34+
function maybeRemoveUndefinedFromUnion(
35+
checker: ts.TypeChecker,
36+
type: ts.Type,
37+
isOptional: boolean,
38+
): ts.Type {
39+
if (isOptional && type.isUnion()) {
40+
const hasUndefined = type.types.find(isUndefinedType);
41+
if (hasUndefined) {
42+
// clone the union and remove `undefined` from it
43+
return checker.getUnionType(type.types.filter(t => !isUndefinedType(t)));
44+
}
45+
}
46+
47+
return type;
48+
}
49+
50+
function inlinePropertyNameComputed(
51+
checker: ts.TypeChecker,
52+
type: ts.Type,
53+
): ts.Type {
54+
if (getTypeName(type) === 'PropertyNameNonComputed') {
55+
// clone the type to remove the symbol so the type printer will explicitly expand it
56+
return checker.getUnionType((type as ts.UnionType).types);
57+
}
58+
59+
return type;
60+
}
61+
62+
function printInterface(
63+
checker: ts.TypeChecker,
64+
type: ts.Type,
65+
nodeName: string,
66+
typeName = nodeName,
67+
): void {
68+
const IGNORED_PROPS = new Set(['type', 'loc', 'range']);
69+
const properties = checker
70+
.getPropertiesOfType(type)
71+
.filter(p => !IGNORED_PROPS.has(p.getName()));
72+
73+
console.log(`
74+
interface ${typeName} extends BaseNode {
75+
type: '${nodeName}';
76+
${properties
77+
.map(p => {
78+
const declaration = p.getDeclarations() ?? [];
79+
const isOptional = declaration.some(
80+
d => ts.isPropertySignature(d) && d.questionToken != null,
81+
);
82+
const typeString = declaration
83+
.map(decl => checker.getTypeAtLocation(decl))
84+
.map(originalType => {
85+
let type = originalType;
86+
type = maybeRemoveUndefinedFromUnion(
87+
checker,
88+
originalType,
89+
isOptional,
90+
);
91+
if (p.getName() === 'key') {
92+
type = inlinePropertyNameComputed(checker, type);
93+
}
94+
95+
return checker.typeToString(
96+
type,
97+
undefined,
98+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
99+
);
100+
})
101+
.join(' | ');
102+
103+
return `${p.getName()}${isOptional ? '?' : ''}: ${typeString};`;
104+
})
105+
.join('\n ')}
106+
}`);
107+
}
108+
109+
function printUnion(type: ts.UnionType, nodeName: string): void {
110+
console.log(`
111+
type ${nodeName} =
112+
| ${type.types.map(t => getTypeName(t)).join('\n | ')};
113+
`);
114+
}
115+
116+
function main(): void {
117+
const files = globSync('**/spec.ts', {
118+
cwd: SRC_ROOT,
119+
ignore: IGNORED,
120+
}).map(f => path.join(SRC_ROOT, f));
121+
122+
for (const filePath of files) {
123+
console.log(filePath);
124+
const code = fs.readFileSync(filePath, 'utf8');
125+
const result = parseAndGenerateServices(code, {
126+
project: TSCONFIG,
127+
filePath,
128+
});
129+
130+
const checker = result.services.program.getTypeChecker();
131+
// const sourceFile = result.services.program.getSourceFile(filePath);
132+
const program = result.services.esTreeNodeToTSNodeMap.get(result.ast);
133+
const symbol = checker.getSymbolAtLocation(program);
134+
if (symbol == null) {
135+
throw new Error(`${filePath} did not have a module symbol`);
136+
}
137+
const exports = checker.getExportsOfModule(symbol);
138+
139+
const nodeName = path.parse(path.dirname(filePath)).name;
140+
const exportedSymbol = exports.find(ex => ex.getEscapedName() === nodeName);
141+
if (exportedSymbol == null) {
142+
throw new Error(`${filePath} does not export a type called ${nodeName}`);
143+
}
144+
145+
const exportedType = checker.getDeclaredTypeOfSymbol(exportedSymbol);
146+
147+
if (exportedType.isUnion()) {
148+
for (const childType of exportedType.types) {
149+
printInterface(checker, childType, nodeName, getTypeName(childType));
150+
}
151+
152+
printUnion(exportedType, nodeName);
153+
154+
// eslint-disable-next-line no-process-exit -- TODO - remove this once the script is complete
155+
process.exit(0);
156+
} else {
157+
printInterface(checker, exportedType, nodeName);
158+
}
159+
}
160+
}
161+
162+
main();

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4498,7 +4498,7 @@ globals@^12.1.0:
44984498
dependencies:
44994499
type-fest "^0.8.1"
45004500

4501-
globby@^11.0.1:
4501+
globby@*, globby@^11.0.1:
45024502
version "11.0.3"
45034503
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb"
45044504
integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==

0 commit comments

Comments
 (0)