From c2f42c95d3882f72f9c105515f5c30efd07d6f28 Mon Sep 17 00:00:00 2001 From: Miorel-Lucian Palii Date: Mon, 9 Sep 2024 22:53:46 -0700 Subject: [PATCH] Support variable statements in TypeScript goody module declarations --- .../adventure-pack/src/app/mergeCode.ts | 23 ++++-- ...ypeScriptModuleAndInterfaceDeclarations.ts | 31 ++++++-- .../stringifyTypeScriptModuleDeclarations.ts | 23 ++++-- .../app/zod-types/typeScriptGoodyZodType.ts | 7 +- .../typescript/extractModuleDeclarations.ts | 73 ++++++++++++------- 5 files changed, 111 insertions(+), 46 deletions(-) diff --git a/workspaces/adventure-pack/src/app/mergeCode.ts b/workspaces/adventure-pack/src/app/mergeCode.ts index 496c80f9..56aab73e 100644 --- a/workspaces/adventure-pack/src/app/mergeCode.ts +++ b/workspaces/adventure-pack/src/app/mergeCode.ts @@ -11,6 +11,7 @@ import { mergeJavaCode } from "./mergeJavaCode"; import type { JavaGoody } from "./zod-types/javaGoodyZodType"; import { sortTypeScriptModuleAndInterfaceDeclarations } from "./sortTypeScriptModuleAndInterfaceDeclarations"; import { stringifyTypeScriptModuleDeclarations } from "./stringifyTypeScriptModuleDeclarations"; +import type { GoodyModuleDeclaration } from "../scripts/package-goodies/typescript/extractModuleDeclarations"; function topo({ equippedGoodies, @@ -116,21 +117,29 @@ export function mergeCode({ return ""; } - const mergedDeclarations: Record> = {}; + const mergedDeclarations: Record = {}; for (const goody of orderedGoodies) { invariant( goody.language === "typescript", "Goody language must match language!", ); - for (const [moduleName, interfaceDeclarations] of Object.entries( - goody.moduleDeclarations, - )) { + for (const [ + moduleName, + { interfaces = {}, variables = [] }, + ] of Object.entries(goody.moduleDeclarations)) { for (const [interfaceName, codeSections] of Object.entries( - interfaceDeclarations, + interfaces, )) { - ((mergedDeclarations[moduleName] ??= {})[interfaceName] ??= - []).push(...codeSections); + (((mergedDeclarations[moduleName] ??= {}).interfaces ??= {})[ + interfaceName + ] ??= []).push(...codeSections); + } + + if (variables.length > 0) { + ((mergedDeclarations[moduleName] ??= {}).variables ??= []).push( + ...variables, + ); } } } diff --git a/workspaces/adventure-pack/src/app/sortTypeScriptModuleAndInterfaceDeclarations.ts b/workspaces/adventure-pack/src/app/sortTypeScriptModuleAndInterfaceDeclarations.ts index 1941f630..43641ee1 100644 --- a/workspaces/adventure-pack/src/app/sortTypeScriptModuleAndInterfaceDeclarations.ts +++ b/workspaces/adventure-pack/src/app/sortTypeScriptModuleAndInterfaceDeclarations.ts @@ -4,16 +4,33 @@ import { compareStringsCaseInsensitive } from "@code-chronicles/util/compareStri import { mapObjectValues } from "@code-chronicles/util/mapObjectValues"; import { sortObjectKeysRecursive } from "@code-chronicles/util/sortObjectKeysRecursive"; +import type { GoodyModuleDeclaration } from "../scripts/package-goodies/typescript/extractModuleDeclarations"; + export function sortTypeScriptModuleAndInterfaceDeclarations( - moduleDeclarations: ReadonlyDeep>>, -): Record> { + moduleDeclarations: ReadonlyDeep>, +): Record { return sortObjectKeysRecursive( - mapObjectValues(moduleDeclarations, (interfaceDeclarations) => - mapObjectValues(interfaceDeclarations, (codeSections) => - [...codeSections].sort(compareStringsCaseInsensitive), - ), + mapObjectValues( + moduleDeclarations, + ( + moduleDeclaration: ReadonlyDeep, + ): GoodyModuleDeclaration => ({ + ...(moduleDeclaration.interfaces && { + interfaces: mapObjectValues( + moduleDeclaration.interfaces, + (codeSections) => + [...codeSections].sort(compareStringsCaseInsensitive), + ), + }), + + ...(moduleDeclaration.variables && { + variables: [...moduleDeclaration.variables].sort( + compareStringsCaseInsensitive, + ), + }), + }), ), compareStringsCaseInsensitive, - 2, + 3, ); } diff --git a/workspaces/adventure-pack/src/app/stringifyTypeScriptModuleDeclarations.ts b/workspaces/adventure-pack/src/app/stringifyTypeScriptModuleDeclarations.ts index a9edeb8c..1b483134 100644 --- a/workspaces/adventure-pack/src/app/stringifyTypeScriptModuleDeclarations.ts +++ b/workspaces/adventure-pack/src/app/stringifyTypeScriptModuleDeclarations.ts @@ -2,13 +2,26 @@ import type { ReadonlyDeep } from "type-fest"; import { stringifyTypeScriptInterfaceDeclarations } from "./stringifyTypeScriptInterfaceDeclarations"; +import type { GoodyModuleDeclaration } from "../scripts/package-goodies/typescript/extractModuleDeclarations"; +import { isStringEmptyOrWhitespaceOnly } from "@code-chronicles/util/isStringEmptyOrWhitespaceOnly"; + +const INDENT = " "; + export function stringifyTypeScriptModuleDeclarations( - moduleDeclarations: ReadonlyDeep>>, + moduleDeclarations: ReadonlyDeep>, ): string { return Object.entries(moduleDeclarations) - .map( - ([moduleName, interfaceDeclarations]) => - `declare ${moduleName} {\n${stringifyTypeScriptInterfaceDeclarations(interfaceDeclarations, { indent: " " })}\n}`, - ) + .map(([moduleName, { interfaces = {}, variables = [] }]) => { + const body = [ + stringifyTypeScriptInterfaceDeclarations(interfaces, { + indent: INDENT, + }), + ...variables.map((variable) => INDENT + variable), + ] + .filter((s) => !isStringEmptyOrWhitespaceOnly(s)) + .join("\n\n"); + + return `declare ${moduleName} {\n${body}\n}`; + }) .join("\n\n"); } diff --git a/workspaces/adventure-pack/src/app/zod-types/typeScriptGoodyZodType.ts b/workspaces/adventure-pack/src/app/zod-types/typeScriptGoodyZodType.ts index 8a69450f..09c42c01 100644 --- a/workspaces/adventure-pack/src/app/zod-types/typeScriptGoodyZodType.ts +++ b/workspaces/adventure-pack/src/app/zod-types/typeScriptGoodyZodType.ts @@ -11,7 +11,12 @@ export const typeScriptGoodyZodType = goodyBaseZodType code: nonBlankStringZodType, moduleDeclarations: z.record( z.string(), - z.record(z.string(), z.array(nonBlankStringZodType)), + z.strictObject({ + interfaces: z + .record(z.string(), z.array(nonBlankStringZodType)) + .optional(), + variables: z.array(nonBlankStringZodType).optional(), + }), ), language: z.literal("typescript"), }) diff --git a/workspaces/adventure-pack/src/scripts/package-goodies/typescript/extractModuleDeclarations.ts b/workspaces/adventure-pack/src/scripts/package-goodies/typescript/extractModuleDeclarations.ts index a018e35e..52cb698c 100644 --- a/workspaces/adventure-pack/src/scripts/package-goodies/typescript/extractModuleDeclarations.ts +++ b/workspaces/adventure-pack/src/scripts/package-goodies/typescript/extractModuleDeclarations.ts @@ -1,12 +1,18 @@ import { type SourceFile as TSSourceFile, SyntaxKind } from "ts-morph"; +import { invariantViolation } from "@code-chronicles/util/invariantViolation"; import { sortTypeScriptModuleAndInterfaceDeclarations } from "../../../app/sortTypeScriptModuleAndInterfaceDeclarations"; import { removeNode } from "./removeNode"; +export type GoodyModuleDeclaration = { + interfaces?: Record; + variables?: string[]; +}; + export function extractModuleDeclarations( sourceFile: TSSourceFile, -): Record> { - const res: Record> = {}; +): Record { + const res: Record = {}; sourceFile.getModules().forEach((mod) => { mod @@ -14,34 +20,49 @@ export function extractModuleDeclarations( .getChildSyntaxListOrThrow() .getChildren() .forEach((child) => { - if (child.getKind() === SyntaxKind.SingleLineCommentTrivia) { - return; + switch (child.getKind()) { + case SyntaxKind.SingleLineCommentTrivia: { + return; + } + case SyntaxKind.InterfaceDeclaration: { + const interfaceDecl = child.asKindOrThrow( + SyntaxKind.InterfaceDeclaration, + ); + + const interfaceName = interfaceDecl.getName(); + const typeParameters = interfaceDecl + .getFirstChildByKind(SyntaxKind.TypeParameter) + ?.getParentSyntaxListOrThrow() + .getFullText(); + + const interfaceKey = + typeParameters == null + ? interfaceName + : `${interfaceName}<${typeParameters}>`; + + (((res[mod.getName()] ??= {}).interfaces ??= {})[interfaceKey] ??= + []).push( + interfaceDecl + .getChildSyntaxListOrThrow() + .getFullText() + .replace(/^\n+/, "") + .replace(/\n+$/, ""), + ); + + return; + } + case SyntaxKind.VariableStatement: { + ((res[mod.getName()] ??= {}).variables ??= []).push( + child.getFullText().trim(), + ); + return; + } } - const interfaceDecl = child.asKindOrThrow( - SyntaxKind.InterfaceDeclaration, - "Only interface declarations are currently supported in module declarations, got: " + + invariantViolation( + "Only interface declarations or variable statements are currently supported in module declarations, got: " + child.getKindName(), ); - - const interfaceName = interfaceDecl.getName(); - const typeParameters = interfaceDecl - .getFirstChildByKind(SyntaxKind.TypeParameter) - ?.getParentSyntaxListOrThrow() - .getFullText(); - - const interfaceKey = - typeParameters == null - ? interfaceName - : `${interfaceName}<${typeParameters}>`; - - ((res[mod.getName()] ??= {})[interfaceKey] ??= []).push( - interfaceDecl - .getChildSyntaxListOrThrow() - .getFullText() - .replace(/^\n+/, "") - .replace(/\n+$/, ""), - ); }); removeNode(mod);