diff --git a/denogen/deno.jsonc b/denogen/deno.jsonc index 3987ce7..17f188f 100644 --- a/denogen/deno.jsonc +++ b/denogen/deno.jsonc @@ -1,8 +1,9 @@ { "imports": { - "ts_morph/": "https://deno.land/x/ts_morph/" + "typescript": "npm:typescript@4.9.4", + "std/": "https://deno.land/std@0.178.0/" }, "tasks": { - "dev": "deno run --watch main.ts" + "dev": "deno run --allow-read --watch mod.ts" } } diff --git a/denogen/deno.lock b/denogen/deno.lock index 9508ab1..2544ea9 100644 --- a/denogen/deno.lock +++ b/denogen/deno.lock @@ -16,6 +16,30 @@ "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.178.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.178.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.178.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32", + "https://deno.land/std@0.178.0/fs/copy.ts": "14214efd94fc3aa6db1e4af2b4b9578e50f7362b7f3725d5a14ad259a5df26c8", + "https://deno.land/std@0.178.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", + "https://deno.land/std@0.178.0/fs/ensure_dir.ts": "724209875497a6b4628dfb256116e5651c4f7816741368d6c44aab2531a1e603", + "https://deno.land/std@0.178.0/fs/ensure_file.ts": "c38602670bfaf259d86ca824a94e6cb9e5eb73757fefa4ebf43a90dd017d53d9", + "https://deno.land/std@0.178.0/fs/ensure_link.ts": "c0f5b2f0ec094ed52b9128eccb1ee23362a617457aa0f699b145d4883f5b2fb4", + "https://deno.land/std@0.178.0/fs/ensure_symlink.ts": "2955cc8332aeca9bdfefd05d8d3976b94e282b0f353392a71684808ed2ffdd41", + "https://deno.land/std@0.178.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", + "https://deno.land/std@0.178.0/fs/exists.ts": "b8c8a457b71e9d7f29b9d2f87aad8dba2739cbe637e8926d6ba6e92567875f8e", + "https://deno.land/std@0.178.0/fs/expand_glob.ts": "45d17e89796a24bd6002e4354eda67b4301bb8ba67d2cac8453cdabccf1d9ab0", + "https://deno.land/std@0.178.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", + "https://deno.land/std@0.178.0/fs/move.ts": "4cb47f880e3f0582c55e71c9f8b1e5e8cfaacb5e84f7390781dd563b7298ec19", + "https://deno.land/std@0.178.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32", + "https://deno.land/std@0.178.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.178.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.178.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.178.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.178.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.178.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232", + "https://deno.land/std@0.178.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.178.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.178.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", "https://deno.land/x/code_block_writer@11.0.3/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", "https://deno.land/x/code_block_writer@11.0.3/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", "https://deno.land/x/ts_morph@17.0.1/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9", @@ -30,5 +54,16 @@ "https://deno.land/x/ts_morph@17.0.1/mod.ts": "adba9b82f24865d15d2c78ef6074b9a7457011719056c9928c800f130a617c93", "https://deno.land/x/ts_morph@17.0.1/ts_morph.d.ts": "a54b0c51b06d84defedf5fdd59c773d803808ae7c9678f7165f7a1a6dfa7f6a3", "https://deno.land/x/ts_morph@17.0.1/ts_morph.js": "1bb80284b9e31a4c5c2078cd533fe9b12b4b2d710267055cb655225aa88fb2df" + }, + "npm": { + "specifiers": { + "typescript@4.9.4": "typescript@4.9.4" + }, + "packages": { + "typescript@4.9.4": { + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "dependencies": {} + } + } } } diff --git a/denogen/mod.ts b/denogen/mod.ts index 2f2fae2..5bfa165 100644 --- a/denogen/mod.ts +++ b/denogen/mod.ts @@ -1,23 +1,5 @@ -import { Project, ResolutionHosts, ts } from "ts_morph/mod.ts"; - -export function generate(path: string) { - const project = new Project({ - resolutionHost: ResolutionHosts.deno, - }); - - const file = project.addSourceFileAtPath(path); - - const classes = file.getClasses(); - - const props = classes[0].getConstructors(); - - const ps = props[0].getParameters()[0]; - - console.log(classes[0].getProperties()[0].hasQuestionToken()); - - console.log(ps.isParameterProperty()); -} +import { Parser } from "./parsing/mod.ts"; if (import.meta.main) { - generate("../myapp/todo.ts"); + console.log(await new Parser().parse("../myapp/**/*.ts")); } diff --git a/denogen/parsing/mod.ts b/denogen/parsing/mod.ts new file mode 100644 index 0000000..857d45a --- /dev/null +++ b/denogen/parsing/mod.ts @@ -0,0 +1 @@ +export * from "./parser.ts"; diff --git a/denogen/parsing/nodes/class.ts b/denogen/parsing/nodes/class.ts new file mode 100644 index 0000000..5a01e72 --- /dev/null +++ b/denogen/parsing/nodes/class.ts @@ -0,0 +1,109 @@ +import ts from "typescript"; +import { TypeRegistry } from "../registry.ts"; +import { ParameterNode } from "./function.ts"; +import { TypeReference } from "./reference.ts"; + +export class ClassNode { + private constructor( + public readonly name: string, + public readonly fields: FieldNode[], + public readonly methods: MethodNode[] + ) {} + + public static create( + registry: TypeRegistry, + node: ts.ClassDeclaration, + symbol?: ts.Symbol + ) { + const fields: FieldNode[] = []; + const methods: MethodNode[] = []; + + symbol?.members?.forEach((value) => { + const decl = value.declarations?.[0]; + + if (!decl) { + return; + } + + if (ts.isPropertyDeclaration(decl) || ts.isParameter(decl)) { + fields.push(FieldNode.create(registry, decl)); + } else if ( + ts.isConstructorDeclaration(decl) || + ts.isMethodDeclaration(decl) + ) { + methods.push(MethodNode.create(registry, decl)); + } + }); + + return new ClassNode( + symbol?.escapedName ?? node?.name?.escapedText ?? "", + fields, + methods + ); + } +} + +export type MemberVisibility = "public" | "private" | "protected"; + +/** Retrieve the visibility of a member based on the modifier flags */ +function visibilityFrom(decl: ts.Declaration): MemberVisibility { + const modifiers = ts.getCombinedModifierFlags(decl); + + if ((ts.ModifierFlags.Private & modifiers) === ts.ModifierFlags.Private) { + return "private"; + } + + if ((ts.ModifierFlags.Protected & modifiers) === ts.ModifierFlags.Protected) { + return "protected"; + } + + return "public"; +} + +export class FieldNode { + private constructor( + public readonly name: string, + public readonly type: TypeReference, + public readonly visibility: MemberVisibility, + public readonly optional: boolean + ) {} + + public static create( + registry: TypeRegistry, + decl: ts.PropertyDeclaration | ts.ParameterDeclaration + ) { + return new FieldNode( + decl.name.getText(), + registry.getOrCreate(decl.type)!, + visibilityFrom(decl), + !!decl.questionToken // FIXME: or union with undefined + ); + } +} + +export class MethodNode { + static readonly ctorName = "__constructor"; + + public readonly isConstructor: boolean; + + private constructor( + public readonly name: string, + public readonly visibility: MemberVisibility, + public readonly parameters: ParameterNode[], + public readonly returnType?: TypeReference + ) { + this.isConstructor = name === MethodNode.ctorName; + } + + public static create( + registry: TypeRegistry, + decl: ts.MethodDeclaration | ts.ConstructorDeclaration + ) { + return new MethodNode( + decl?.name?.getText() ?? MethodNode.ctorName, + visibilityFrom(decl), + [], + registry.getOrCreate(decl.type) + ); + } +} diff --git a/denogen/parsing/nodes/function.ts b/denogen/parsing/nodes/function.ts new file mode 100644 index 0000000..5fd980c --- /dev/null +++ b/denogen/parsing/nodes/function.ts @@ -0,0 +1,31 @@ +import ts from "typescript"; +import { TypeRegistry } from "../registry.ts"; +import { TypeReference } from "./reference.ts"; + +export class FunctionNode { + private constructor( + public readonly name: string, + public readonly parameters: ParameterNode[], + public readonly returnType?: TypeReference + ) {} + + public static create( + registry: TypeRegistry, + node: ts.FunctionDeclaration, + symbol?: ts.Symbol + ) { + return new FunctionNode( + symbol?.escapedName ?? node?.name?.escapedText ?? "", + [], + registry.getOrCreate(node.type) + ); + } +} + +export class ParameterNode { + constructor( + public readonly name: string, + public readonly type: TypeReference, + public readonly optional: boolean + ) {} +} diff --git a/denogen/parsing/nodes/mod.ts b/denogen/parsing/nodes/mod.ts new file mode 100644 index 0000000..dbc950b --- /dev/null +++ b/denogen/parsing/nodes/mod.ts @@ -0,0 +1,7 @@ +import { ClassNode } from "./class.ts"; +import { FunctionNode } from "./function.ts"; + +export { ClassNode } from "./class.ts"; +export { FunctionNode } from "./function.ts"; + +export type Node = FunctionNode | ClassNode; diff --git a/denogen/parsing/nodes/reference.ts b/denogen/parsing/nodes/reference.ts new file mode 100644 index 0000000..b0bf8ab --- /dev/null +++ b/denogen/parsing/nodes/reference.ts @@ -0,0 +1,3 @@ +export class TypeReference { + constructor(public readonly name: string) {} +} diff --git a/denogen/parsing/parser.ts b/denogen/parsing/parser.ts new file mode 100644 index 0000000..a318732 --- /dev/null +++ b/denogen/parsing/parser.ts @@ -0,0 +1,84 @@ +import ts from "typescript"; +import { expandGlob, WalkEntry } from "std/fs/mod.ts"; +import { ClassNode, FunctionNode, Node } from "./nodes/mod.ts"; +import { TypeRegistry } from "./registry.ts"; + +/** + * Parser to retrieve a friendlier structure of the AST from the typescript compiler API + * to work with. + * + * Reference documentation: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker + */ +export class Parser { + private readonly _registry = new TypeRegistry(); + + /** + * Parse file matching the given glob pattern. + */ + public async parse(pattern: string): Promise { + const files = await filesMatching(pattern); + return this.parseSourceFiles(files); + } + + /** + * Parse all source files using the typescript compiler API and build our nodes. + */ + private parseSourceFiles(files: WalkEntry[]): Node[] { + const program = ts.createProgram( + files.map((f) => f.path), + {} + ); + + const typeChecker = program.getTypeChecker(); + + return program.getSourceFiles().reduce((nodes, file) => { + // That's not interesting for us. + if (file.isDeclarationFile) { + return nodes; + } + + // file.locals is undocumented but may contain all we need... + + file.forEachChild((node) => { + if (ts.isFunctionDeclaration(node)) { + nodes.push( + FunctionNode.create( + this._registry, + node, + // @ts-expect-error Wrong getSymbolAtLocation types + typeChecker.getSymbolAtLocation(node?.name) + ) + ); + } else if (ts.isClassDeclaration(node)) { + nodes.push( + ClassNode.create( + this._registry, + node, + // @ts-expect-error Wrong getSymbolAtLocation types + typeChecker.getSymbolAtLocation(node?.name) + ) + ); + } + }); + + return nodes; + }, []); + } +} + +/** + * Retrieve all files matching the given glob pattern. + */ +async function filesMatching(pattern: string): Promise { + const files: WalkEntry[] = []; + + // Let's find all files + for await (const entry of expandGlob(pattern)) { + if (!entry.isFile) { + continue; + } + files.push(entry); + } + + return files; +} diff --git a/denogen/parsing/registry.ts b/denogen/parsing/registry.ts new file mode 100644 index 0000000..7be9c2a --- /dev/null +++ b/denogen/parsing/registry.ts @@ -0,0 +1,37 @@ +import ts from "typescript"; +import { TypeReference } from "./nodes/reference.ts"; + +/** + * Retrieve and manage types referenced when parsing. + */ +export class TypeRegistry { + private readonly _types: Record = {}; + + public getOrCreate(node?: ts.TypeNode): TypeReference | undefined { + if (!node) { + return undefined; + } + + switch (node.kind) { + case ts.SyntaxKind.TypeReference: + { + const a = node as ts.TypeReferenceNode; + + console.log(a.typeName.getText()); + } + break; + case ts.SyntaxKind.ArrayType: + { + const b = node as ts.ArrayTypeNode; + + console.log("array", b.elementType?.getText()); + } + break; + case ts.SyntaxKind.StringKeyword: + console.log("string!"); + break; + } + + return undefined; + } +} diff --git a/myapp/deno.lock b/myapp/deno.lock new file mode 100644 index 0000000..64ac918 --- /dev/null +++ b/myapp/deno.lock @@ -0,0 +1,12 @@ +{ + "version": "2", + "remote": { + "https://deno.land/std@0.178.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.178.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", + "https://deno.land/std@0.178.0/uuid/_common.ts": "cb1441f4df460571fc0919e1c5c217f3e7006189b703caf946604b3f791ae34d", + "https://deno.land/std@0.178.0/uuid/mod.ts": "cd4da71beaa7ebfaa575ecb42e24e792559b65d8927e612c13b6ac5d1306f8bb", + "https://deno.land/std@0.178.0/uuid/v1.ts": "fe36009afce7ced96e1b5928565e12c5a8eb0df1a2b5063c0a72bda6b75c0de5", + "https://deno.land/std@0.178.0/uuid/v4.ts": "0f081880c156fd59b9e44e2f84ea0f94a3627e89c224eaf6cc982b53d849f37e", + "https://deno.land/std@0.178.0/uuid/v5.ts": "10558a9c09a06b86fef9e61205180b9585ec4fe3fed7d696e675b8e118f74e8e" + } +} diff --git a/myapp/gen.ts b/myapp/gen.ts index 4b0bece..4649a68 100644 --- a/myapp/gen.ts +++ b/myapp/gen.ts @@ -1,5 +1,5 @@ -import { generate } from "denogen/mod.ts"; +import { parse } from "denogen/mod.ts"; if (import.meta.main) { - generate("todo.ts"); + parse("."); } diff --git a/myapp/src/user.ts b/myapp/src/user.ts new file mode 100644 index 0000000..4f82c14 --- /dev/null +++ b/myapp/src/user.ts @@ -0,0 +1 @@ +export class User {} diff --git a/myapp/todo.ts b/myapp/todo.ts index d68006e..544964c 100644 --- a/myapp/todo.ts +++ b/myapp/todo.ts @@ -1,17 +1,29 @@ export class Todo { - something?: boolean; + public readonly id: string; + protected content: string; - constructor(private readonly id: string, private content: string) {} - - // Update the todo's content. - setContent(content: string) { + public constructor(content: string, private completed?: boolean) { + this.id = crypto.randomUUID(); this.content = content; } + + // public markAsCompleted() { + // this.completed = true; + // } + + // protected setContent(content: string) { + // this.content = content; + // } } -/** - * @post /api/todos - */ +const todos: Todo[] = []; + export function createTodo(content: string): Todo { - return new Todo(new Date().toISOString(), content); + const newTodo = new Todo(content); + todos.push(newTodo); + return newTodo; +} + +export function getTodos(): Todo[] { + return todos; }