-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathextract-hooks.ts
132 lines (109 loc) Β· 4.03 KB
/
extract-hooks.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import {Cli, Command, Option, UsageError} from 'clipanion';
import fs from 'fs';
import path from 'path';
import ts from 'typescript';
async function parseFile(p: string) {
const content = await fs.promises.readFile(p, `utf8`);
return ts.createSourceFile(
p,
content,
ts.ScriptTarget.ES2015,
/* setParentNodes */ true,
);
}
type HookDefinition = {
name: string;
definition: string;
file?: string;
comment?: string;
};
async function processFile(file: ts.SourceFile) {
const hooks: Array<HookDefinition> = [];
let isInsideInterface = false;
const processNode = (node: ts.Node) => {
switch (node.kind) {
case ts.SyntaxKind.TypeAliasDeclaration: {
const typeAliasNode = node as ts.TypeAliasDeclaration;
if (typeAliasNode.name.getText() === `Hooks`) {
throw new UsageError(`Hooks should only be declared using interfaces, not type aliases (in ${file.fileName})`);
}
} break;
case ts.SyntaxKind.InterfaceDeclaration: {
const interfaceNode = node as ts.InterfaceDeclaration;
if (interfaceNode.name.getText() === `Hooks`) {
isInsideInterface = true;
ts.forEachChild(node, processNode);
isInsideInterface = false;
}
} break;
case ts.SyntaxKind.PropertySignature: {
const propertySignatureNode = node as ts.PropertySignature;
if (isInsideInterface) {
const type = propertySignatureNode.type;
if (type?.kind === ts.SyntaxKind.FunctionType) {
const {character} = file.getLineAndCharacterOfPosition(propertySignatureNode.getStart());
const removeIndent = (code: string) => code.replace(new RegExp(`^ {${character}}`, `gm`), ``);
// undocumented
const jsDoc = (propertySignatureNode as any).jsDoc;
const comment = jsDoc && jsDoc.length > 0 ? jsDoc[jsDoc.length - 1].comment : undefined;
hooks.push({
name: propertySignatureNode.name.getText(),
definition: removeIndent(propertySignatureNode.getText()),
comment,
file: file.fileName,
});
}
}
} break;
default: {
ts.forEachChild(node, processNode);
} break;
}
};
processNode(file);
return hooks;
}
export async function execute(files: Array<string>) {
const allHooks = new Map<string, HookDefinition>();
for (const relativePath of files) {
const absolutePath = path.resolve(relativePath);
const file = await parseFile(absolutePath);
const fileHooks = await processFile(file);
for (const hook of fileHooks) {
let existingDefinition = allHooks.get(hook.name);
if (typeof existingDefinition === `undefined`)
allHooks.set(hook.name, existingDefinition = {...hook, file: undefined});
if (existingDefinition.definition !== hook.definition)
throw new UsageError(`Mismatched hook definitions for ${hook.name}`);
if (typeof hook.comment !== `undefined`) {
if (typeof existingDefinition.file !== `undefined`)
if (existingDefinition.file !== hook.file)
throw new UsageError(`Duplicate hook documentation; only document it in one file`);
existingDefinition.comment = hook.comment;
existingDefinition.file = hook.file;
}
}
}
const allHooksArray = [...allHooks.values()];
allHooksArray.sort((a, b) => {
return a.name < b.name ? -1 : a.name > b.name ? +1 : 0;
});
for (const hook of allHooksArray)
if (typeof hook.comment === `undefined`)
throw new UsageError(`Undocumented hook ${hook.name}`);
return allHooksArray;
}
if (require.main === module) {
Cli.from([
class extends Command {
files = Option.Rest();
async execute() {
const allHooks = await execute(this.files);
this.context.stdout.write(`${JSON.stringify(allHooks, null, 2)}\n`);
}
},
]).runExit(
process.argv.slice(2),
Cli.defaultContext,
);
}