Skip to content

Commit 0703412

Browse files
authored
Support variable statements in TypeScript goody module declarations (#419)
Should help unblock #417 by not requiring everything inside a `declare global` block to be an interface.
1 parent dd729b2 commit 0703412

File tree

5 files changed

+111
-46
lines changed

5 files changed

+111
-46
lines changed

workspaces/adventure-pack/src/app/mergeCode.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { mergeJavaCode } from "./mergeJavaCode";
1111
import type { JavaGoody } from "./zod-types/javaGoodyZodType";
1212
import { sortTypeScriptModuleAndInterfaceDeclarations } from "./sortTypeScriptModuleAndInterfaceDeclarations";
1313
import { stringifyTypeScriptModuleDeclarations } from "./stringifyTypeScriptModuleDeclarations";
14+
import type { GoodyModuleDeclaration } from "../scripts/package-goodies/typescript/extractModuleDeclarations";
1415

1516
function topo({
1617
equippedGoodies,
@@ -116,21 +117,29 @@ export function mergeCode({
116117
return "";
117118
}
118119

119-
const mergedDeclarations: Record<string, Record<string, string[]>> = {};
120+
const mergedDeclarations: Record<string, GoodyModuleDeclaration> = {};
120121
for (const goody of orderedGoodies) {
121122
invariant(
122123
goody.language === "typescript",
123124
"Goody language must match language!",
124125
);
125126

126-
for (const [moduleName, interfaceDeclarations] of Object.entries(
127-
goody.moduleDeclarations,
128-
)) {
127+
for (const [
128+
moduleName,
129+
{ interfaces = {}, variables = [] },
130+
] of Object.entries(goody.moduleDeclarations)) {
129131
for (const [interfaceName, codeSections] of Object.entries(
130-
interfaceDeclarations,
132+
interfaces,
131133
)) {
132-
((mergedDeclarations[moduleName] ??= {})[interfaceName] ??=
133-
[]).push(...codeSections);
134+
(((mergedDeclarations[moduleName] ??= {}).interfaces ??= {})[
135+
interfaceName
136+
] ??= []).push(...codeSections);
137+
}
138+
139+
if (variables.length > 0) {
140+
((mergedDeclarations[moduleName] ??= {}).variables ??= []).push(
141+
...variables,
142+
);
134143
}
135144
}
136145
}

workspaces/adventure-pack/src/app/sortTypeScriptModuleAndInterfaceDeclarations.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,33 @@ import { compareStringsCaseInsensitive } from "@code-chronicles/util/compareStri
44
import { mapObjectValues } from "@code-chronicles/util/mapObjectValues";
55
import { sortObjectKeysRecursive } from "@code-chronicles/util/sortObjectKeysRecursive";
66

7+
import type { GoodyModuleDeclaration } from "../scripts/package-goodies/typescript/extractModuleDeclarations";
8+
79
export function sortTypeScriptModuleAndInterfaceDeclarations(
8-
moduleDeclarations: ReadonlyDeep<Record<string, Record<string, string[]>>>,
9-
): Record<string, Record<string, string[]>> {
10+
moduleDeclarations: ReadonlyDeep<Record<string, GoodyModuleDeclaration>>,
11+
): Record<string, GoodyModuleDeclaration> {
1012
return sortObjectKeysRecursive(
11-
mapObjectValues(moduleDeclarations, (interfaceDeclarations) =>
12-
mapObjectValues(interfaceDeclarations, (codeSections) =>
13-
[...codeSections].sort(compareStringsCaseInsensitive),
14-
),
13+
mapObjectValues(
14+
moduleDeclarations,
15+
(
16+
moduleDeclaration: ReadonlyDeep<GoodyModuleDeclaration>,
17+
): GoodyModuleDeclaration => ({
18+
...(moduleDeclaration.interfaces && {
19+
interfaces: mapObjectValues(
20+
moduleDeclaration.interfaces,
21+
(codeSections) =>
22+
[...codeSections].sort(compareStringsCaseInsensitive),
23+
),
24+
}),
25+
26+
...(moduleDeclaration.variables && {
27+
variables: [...moduleDeclaration.variables].sort(
28+
compareStringsCaseInsensitive,
29+
),
30+
}),
31+
}),
1532
),
1633
compareStringsCaseInsensitive,
17-
2,
34+
3,
1835
);
1936
}

workspaces/adventure-pack/src/app/stringifyTypeScriptModuleDeclarations.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,26 @@ import type { ReadonlyDeep } from "type-fest";
22

33
import { stringifyTypeScriptInterfaceDeclarations } from "./stringifyTypeScriptInterfaceDeclarations";
44

5+
import type { GoodyModuleDeclaration } from "../scripts/package-goodies/typescript/extractModuleDeclarations";
6+
import { isStringEmptyOrWhitespaceOnly } from "@code-chronicles/util/isStringEmptyOrWhitespaceOnly";
7+
8+
const INDENT = " ";
9+
510
export function stringifyTypeScriptModuleDeclarations(
6-
moduleDeclarations: ReadonlyDeep<Record<string, Record<string, string[]>>>,
11+
moduleDeclarations: ReadonlyDeep<Record<string, GoodyModuleDeclaration>>,
712
): string {
813
return Object.entries(moduleDeclarations)
9-
.map(
10-
([moduleName, interfaceDeclarations]) =>
11-
`declare ${moduleName} {\n${stringifyTypeScriptInterfaceDeclarations(interfaceDeclarations, { indent: " " })}\n}`,
12-
)
14+
.map(([moduleName, { interfaces = {}, variables = [] }]) => {
15+
const body = [
16+
stringifyTypeScriptInterfaceDeclarations(interfaces, {
17+
indent: INDENT,
18+
}),
19+
...variables.map((variable) => INDENT + variable),
20+
]
21+
.filter((s) => !isStringEmptyOrWhitespaceOnly(s))
22+
.join("\n\n");
23+
24+
return `declare ${moduleName} {\n${body}\n}`;
25+
})
1326
.join("\n\n");
1427
}

workspaces/adventure-pack/src/app/zod-types/typeScriptGoodyZodType.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ export const typeScriptGoodyZodType = goodyBaseZodType
1111
code: nonBlankStringZodType,
1212
moduleDeclarations: z.record(
1313
z.string(),
14-
z.record(z.string(), z.array(nonBlankStringZodType)),
14+
z.strictObject({
15+
interfaces: z
16+
.record(z.string(), z.array(nonBlankStringZodType))
17+
.optional(),
18+
variables: z.array(nonBlankStringZodType).optional(),
19+
}),
1520
),
1621
language: z.literal("typescript"),
1722
})

workspaces/adventure-pack/src/scripts/package-goodies/typescript/extractModuleDeclarations.ts

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,68 @@
11
import { type SourceFile as TSSourceFile, SyntaxKind } from "ts-morph";
22

3+
import { invariantViolation } from "@code-chronicles/util/invariantViolation";
34
import { sortTypeScriptModuleAndInterfaceDeclarations } from "../../../app/sortTypeScriptModuleAndInterfaceDeclarations";
45
import { removeNode } from "./removeNode";
56

7+
export type GoodyModuleDeclaration = {
8+
interfaces?: Record<string, string[]>;
9+
variables?: string[];
10+
};
11+
612
export function extractModuleDeclarations(
713
sourceFile: TSSourceFile,
8-
): Record<string, Record<string, string[]>> {
9-
const res: Record<string, Record<string, string[]>> = {};
14+
): Record<string, GoodyModuleDeclaration> {
15+
const res: Record<string, GoodyModuleDeclaration> = {};
1016

1117
sourceFile.getModules().forEach((mod) => {
1218
mod
1319
.getBodyOrThrow()
1420
.getChildSyntaxListOrThrow()
1521
.getChildren()
1622
.forEach((child) => {
17-
if (child.getKind() === SyntaxKind.SingleLineCommentTrivia) {
18-
return;
23+
switch (child.getKind()) {
24+
case SyntaxKind.SingleLineCommentTrivia: {
25+
return;
26+
}
27+
case SyntaxKind.InterfaceDeclaration: {
28+
const interfaceDecl = child.asKindOrThrow(
29+
SyntaxKind.InterfaceDeclaration,
30+
);
31+
32+
const interfaceName = interfaceDecl.getName();
33+
const typeParameters = interfaceDecl
34+
.getFirstChildByKind(SyntaxKind.TypeParameter)
35+
?.getParentSyntaxListOrThrow()
36+
.getFullText();
37+
38+
const interfaceKey =
39+
typeParameters == null
40+
? interfaceName
41+
: `${interfaceName}<${typeParameters}>`;
42+
43+
(((res[mod.getName()] ??= {}).interfaces ??= {})[interfaceKey] ??=
44+
[]).push(
45+
interfaceDecl
46+
.getChildSyntaxListOrThrow()
47+
.getFullText()
48+
.replace(/^\n+/, "")
49+
.replace(/\n+$/, ""),
50+
);
51+
52+
return;
53+
}
54+
case SyntaxKind.VariableStatement: {
55+
((res[mod.getName()] ??= {}).variables ??= []).push(
56+
child.getFullText().trim(),
57+
);
58+
return;
59+
}
1960
}
2061

21-
const interfaceDecl = child.asKindOrThrow(
22-
SyntaxKind.InterfaceDeclaration,
23-
"Only interface declarations are currently supported in module declarations, got: " +
62+
invariantViolation(
63+
"Only interface declarations or variable statements are currently supported in module declarations, got: " +
2464
child.getKindName(),
2565
);
26-
27-
const interfaceName = interfaceDecl.getName();
28-
const typeParameters = interfaceDecl
29-
.getFirstChildByKind(SyntaxKind.TypeParameter)
30-
?.getParentSyntaxListOrThrow()
31-
.getFullText();
32-
33-
const interfaceKey =
34-
typeParameters == null
35-
? interfaceName
36-
: `${interfaceName}<${typeParameters}>`;
37-
38-
((res[mod.getName()] ??= {})[interfaceKey] ??= []).push(
39-
interfaceDecl
40-
.getChildSyntaxListOrThrow()
41-
.getFullText()
42-
.replace(/^\n+/, "")
43-
.replace(/\n+$/, ""),
44-
);
4566
});
4667

4768
removeNode(mod);

0 commit comments

Comments
 (0)