From c59e8a8cc75ee579d48ea9675986955311f30d6d Mon Sep 17 00:00:00 2001 From: Tom Chauveau Date: Wed, 30 Oct 2024 18:04:22 +0100 Subject: [PATCH] feat: typescript SDK resolve value by reference. Fixes https://github.com/dagger/dagger/issues/8676. Refactor typedef resolution to be resolve by reference. The introspection starts from the entrypoint (which is the class named the same as the module) and recursively resolve every references. Refactor the TS compiler API to be abstract with a AST that handle lookups, typedef resolutions and default value resolution. Use `imports` to resolve default value that refers to an exported variable. Add custom introspection error. Improve error handling to point to the line that couldn't be introspected. Add supports for native `enum` keyword and `type` native keyword. Simplify overall code abstractions too minimal. Avoid long abstractions functions. Signed-off-by: Tom Chauveau --- .../modules/typescript/minimal/index.ts | 4 +- .../common/errors/IntrospectionError.ts | 11 + sdk/typescript/common/errors/errors-codes.ts | 5 + sdk/typescript/entrypoint/entrypoint.ts | 2 +- sdk/typescript/entrypoint/invoke.ts | 12 +- sdk/typescript/entrypoint/load.ts | 39 +- sdk/typescript/entrypoint/register.ts | 18 +- .../scanner/abtractions/argument.ts | 272 ------------- .../scanner/abtractions/constructor.ts | 58 --- .../introspector/scanner/abtractions/enum.ts | 118 ------ .../scanner/abtractions/enumValue.ts | 76 ---- .../scanner/abtractions/method.ts | 180 --------- .../scanner/abtractions/module.ts | 141 ------- .../scanner/abtractions/object.ts | 172 -------- .../scanner/abtractions/property.ts | 156 -------- .../scanner/abtractions/typeToTypedef.ts | 110 ------ .../introspector/scanner/case_convertor.ts | 21 + .../scanner/dagger_module/argument.ts | 147 +++++++ .../scanner/dagger_module/constructor.ts | 53 +++ .../scanner/dagger_module/decorator.ts | 20 + .../scanner/dagger_module/enum.ts | 73 ++++ .../scanner/dagger_module/enumBase.ts | 15 + .../scanner/dagger_module/enumClass.ts | 79 ++++ .../scanner/dagger_module/function.ts | 127 ++++++ .../scanner/dagger_module/module.ts | 365 +++++++++++++++++ .../scanner/dagger_module/object.ts | 120 ++++++ .../scanner/dagger_module/objectBase.ts | 31 ++ .../scanner/dagger_module/property.ts | 120 ++++++ .../scanner/dagger_module/reference.ts | 58 +++ .../scanner/dagger_module/typeObject.ts | 76 ++++ .../dagger_module/typeObjectProperty.ts | 84 ++++ sdk/typescript/introspector/scanner/scan.ts | 34 +- .../scanner/{typeDefs.ts => typedef.ts} | 0 .../scanner/typescript_module/ast.ts | 367 ++++++++++++++++++ .../scanner/typescript_module/declarations.ts | 26 ++ .../scanner/typescript_module/explorer.ts | 23 ++ .../typescript_module/typedef_utils.ts | 41 ++ sdk/typescript/introspector/scanner/utils.ts | 131 ------- .../introspector/test/case_convertor.spec.ts | 37 ++ .../introspector/test/invoke.spec.ts | 26 +- sdk/typescript/introspector/test/scan.spec.ts | 116 +++--- .../test/testdata/alias/expected.json | 124 +++--- .../test/testdata/constructor/expected.json | 13 +- .../test/testdata/constructor/index.ts | 4 +- .../test/testdata/context/index.ts | 9 +- .../test/testdata/coreEnums/index.ts | 3 +- .../introspector/test/testdata/enums/index.ts | 9 +- .../test/testdata/list/expected.json | 2 +- .../multipleObjectsAsFields/expected.json | 2 +- .../test/testdata/objectParam/expected.json | 2 +- .../testdata/optionalParameter/expected.json | 20 +- .../test/testdata/optionalParameter/index.ts | 2 +- .../test/testdata/primitives/index.ts | 2 +- .../test/testdata/references/expected.json | 183 +++++++++ .../test/testdata/references/index.ts | 57 +++ .../test/testdata/references/types.ts | 29 ++ .../test/testdata/scalar/index.ts | 20 +- sdk/typescript/introspector/utils/files.ts | 2 +- 58 files changed, 2419 insertions(+), 1628 deletions(-) create mode 100644 sdk/typescript/common/errors/IntrospectionError.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/argument.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/constructor.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/enum.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/enumValue.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/method.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/module.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/object.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/property.ts delete mode 100644 sdk/typescript/introspector/scanner/abtractions/typeToTypedef.ts create mode 100644 sdk/typescript/introspector/scanner/case_convertor.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/argument.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/constructor.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/decorator.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/enum.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/enumBase.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/enumClass.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/function.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/module.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/object.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/objectBase.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/property.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/reference.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/typeObject.ts create mode 100644 sdk/typescript/introspector/scanner/dagger_module/typeObjectProperty.ts rename sdk/typescript/introspector/scanner/{typeDefs.ts => typedef.ts} (100%) create mode 100644 sdk/typescript/introspector/scanner/typescript_module/ast.ts create mode 100644 sdk/typescript/introspector/scanner/typescript_module/declarations.ts create mode 100644 sdk/typescript/introspector/scanner/typescript_module/explorer.ts create mode 100644 sdk/typescript/introspector/scanner/typescript_module/typedef_utils.ts delete mode 100644 sdk/typescript/introspector/scanner/utils.ts create mode 100644 sdk/typescript/introspector/test/case_convertor.spec.ts create mode 100644 sdk/typescript/introspector/test/testdata/references/expected.json create mode 100644 sdk/typescript/introspector/test/testdata/references/index.ts create mode 100644 sdk/typescript/introspector/test/testdata/references/types.ts diff --git a/core/integration/testdata/modules/typescript/minimal/index.ts b/core/integration/testdata/modules/typescript/minimal/index.ts index ca67ba8baf8..dd14702e7a9 100644 --- a/core/integration/testdata/modules/typescript/minimal/index.ts +++ b/core/integration/testdata/modules/typescript/minimal/index.ts @@ -49,12 +49,12 @@ export class Minimal { } @func() - echoOptional(msg = "default"): string { + echoOptional(msg: string = "default"): string { return this.echo(msg); } @func() - echoOptionalSlice(msg = ["foobar"]): string { + echoOptionalSlice(msg: string[] = ["foobar"]): string { return this.echo(msg.join("+")); } diff --git a/sdk/typescript/common/errors/IntrospectionError.ts b/sdk/typescript/common/errors/IntrospectionError.ts new file mode 100644 index 00000000000..0963d191c5a --- /dev/null +++ b/sdk/typescript/common/errors/IntrospectionError.ts @@ -0,0 +1,11 @@ +import { DaggerSDKError, DaggerSDKErrorOptions } from "./DaggerSDKError.js" +import { ERROR_CODES, ERROR_NAMES } from "./errors-codes.js" + +export class IntrospectionError extends DaggerSDKError { + name = ERROR_NAMES.IntrospectionError + code = ERROR_CODES.IntrospectionError + + constructor(message: string, options?: DaggerSDKErrorOptions) { + super(message, options) + } +} diff --git a/sdk/typescript/common/errors/errors-codes.ts b/sdk/typescript/common/errors/errors-codes.ts index 5683017264f..2a575992545 100644 --- a/sdk/typescript/common/errors/errors-codes.ts +++ b/sdk/typescript/common/errors/errors-codes.ts @@ -48,6 +48,11 @@ export const ERROR_CODES = { * (@link ExecError} */ ExecError: "D109", + + /** + * {@link IntrospectionError} + */ + IntrospectionError: "D110", } as const type ErrorCodesType = typeof ERROR_CODES diff --git a/sdk/typescript/entrypoint/entrypoint.ts b/sdk/typescript/entrypoint/entrypoint.ts index e112ed92a2a..a42a85b20e5 100644 --- a/sdk/typescript/entrypoint/entrypoint.ts +++ b/sdk/typescript/entrypoint/entrypoint.ts @@ -28,7 +28,7 @@ export async function entrypoint() { const moduleName = await dag.currentModule().name() const parentName = await fnCall.parentName() - const scanResult = scan(files, moduleName) + const scanResult = await scan(files, moduleName) // Pre allocate the result, we got one in both case. // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/sdk/typescript/entrypoint/invoke.ts b/sdk/typescript/entrypoint/invoke.ts index 714e0618edf..325228208a4 100644 --- a/sdk/typescript/entrypoint/invoke.ts +++ b/sdk/typescript/entrypoint/invoke.ts @@ -1,11 +1,11 @@ import { FunctionNotFound } from "../common/errors/FunctionNotFound.js" import { Executor } from "../introspector/executor/executor.js" import { registry } from "../introspector/registry/registry.js" -import { Constructor } from "../introspector/scanner/abtractions/constructor.js" -import { DaggerEnum } from "../introspector/scanner/abtractions/enum.js" -import { Method } from "../introspector/scanner/abtractions/method.js" -import { DaggerModule } from "../introspector/scanner/abtractions/module.js" -import { DaggerObject } from "../introspector/scanner/abtractions/object.js" +import { DaggerConstructor as Constructor } from "../introspector/scanner/dagger_module/constructor.js" +import { DaggerEnumBase } from "../introspector/scanner/dagger_module/enumBase.js" +import { DaggerFunction as Method } from "../introspector/scanner/dagger_module/function.js" +import { DaggerModule } from "../introspector/scanner/dagger_module/module.js" +import { DaggerObjectBase } from "../introspector/scanner/dagger_module/objectBase.js" import { InvokeCtx } from "./context.js" import { loadResult, @@ -74,7 +74,7 @@ export async function invoke( } if (result) { - let returnType: DaggerObject | DaggerEnum + let returnType: DaggerObjectBase | DaggerEnumBase // Handle alias serialization by getting the return type to load // if the function called isn't a constructor. diff --git a/sdk/typescript/entrypoint/load.ts b/sdk/typescript/entrypoint/load.ts index a3b4aa9c874..7952073b4eb 100644 --- a/sdk/typescript/entrypoint/load.ts +++ b/sdk/typescript/entrypoint/load.ts @@ -4,12 +4,14 @@ import Module from "node:module" import { dag, TypeDefKind } from "../api/client.gen.js" import { Executor } from "../introspector/executor/executor.js" import { Args } from "../introspector/executor/executor.js" -import { Constructor } from "../introspector/scanner/abtractions/constructor.js" -import { DaggerEnum } from "../introspector/scanner/abtractions/enum.js" -import { Method } from "../introspector/scanner/abtractions/method.js" -import { DaggerModule } from "../introspector/scanner/abtractions/module.js" -import { DaggerObject } from "../introspector/scanner/abtractions/object.js" -import { TypeDef } from "../introspector/scanner/typeDefs.js" +import { DaggerConstructor as Constructor } from "../introspector/scanner/dagger_module/constructor.js" +import { DaggerEnum } from "../introspector/scanner/dagger_module/enum.js" +import { DaggerEnumBase } from "../introspector/scanner/dagger_module/enumBase.js" +import { DaggerFunction as Method } from "../introspector/scanner/dagger_module/function.js" +import { DaggerModule } from "../introspector/scanner/dagger_module/module.js" +import { DaggerObject } from "../introspector/scanner/dagger_module/object.js" +import { DaggerObjectBase } from "../introspector/scanner/dagger_module/objectBase.js" +import { TypeDef } from "../introspector/scanner/typedef.js" import { InvokeCtx } from "./context.js" /** @@ -32,7 +34,7 @@ export function loadInvokedObject( module: DaggerModule, parentName: string, ): DaggerObject { - return module.objects[parentName] + return module.objects[parentName] as DaggerObject } export function loadInvokedMethod( @@ -60,12 +62,16 @@ export async function loadArgs( const args: Args = {} // Load arguments - for (const argName of method.getArgOrder()) { + for (const argName of method.getArgsOrder()) { const argument = method.arguments[argName] if (!argument) { throw new Error(`could not find argument ${argName}`) } + if (!argument.type) { + throw new Error(`could not find type for argument ${argName}`) + } + const loadedArg = await loadValue( executor, ctx.fnArgs[argName], @@ -118,6 +124,10 @@ export async function loadParentState( throw new Error(`could not find parent property ${key}`) } + if (!property.type) { + throw new Error(`could not find type for parent property ${key}`) + } + parentState[property.name] = await loadValue(executor, value, property.type) } @@ -192,8 +202,11 @@ export function loadObjectReturnType( module: DaggerModule, object: DaggerObject, method: Method, -): DaggerObject | DaggerEnum { +): DaggerObjectBase | DaggerEnumBase { const retType = method.returnType + if (!retType) { + throw new Error(`could not find return type for ${method.name}`) + } switch (retType.kind) { case TypeDefKind.ListKind: { @@ -218,7 +231,7 @@ export function loadObjectReturnType( export async function loadResult( result: any, module: DaggerModule, - object: DaggerObject | DaggerEnum, + object: DaggerObjectBase | DaggerEnumBase, ): Promise { // Handle IDable objects if (result && typeof result?.id === "function") { @@ -246,7 +259,11 @@ export async function loadResult( throw new Error(`could not find result property ${key}`) } - let referencedObject: DaggerObject | undefined = undefined + if (!property.type) { + throw new Error(`could not find type for result property ${key}`) + } + + let referencedObject: DaggerObjectBase | undefined = undefined // Handle nested objects if (property.type.kind === TypeDefKind.ObjectKind) { diff --git a/sdk/typescript/entrypoint/register.ts b/sdk/typescript/entrypoint/register.ts index 372b01e4547..386caecd93d 100644 --- a/sdk/typescript/entrypoint/register.ts +++ b/sdk/typescript/entrypoint/register.ts @@ -6,17 +6,17 @@ import { TypeDef, TypeDefKind, } from "../api/client.gen.js" -import { Arguments } from "../introspector/scanner/abtractions/argument.js" -import { Constructor } from "../introspector/scanner/abtractions/constructor.js" -import { Method } from "../introspector/scanner/abtractions/method.js" -import { DaggerModule } from "../introspector/scanner/abtractions/module.js" +import { DaggerArguments as Arguments } from "../introspector/scanner/dagger_module/argument.js" +import { DaggerConstructor as Constructor } from "../introspector/scanner/dagger_module/constructor.js" +import { DaggerFunction as Method } from "../introspector/scanner/dagger_module/function.js" +import { DaggerModule } from "../introspector/scanner/dagger_module/module.js" import { EnumTypeDef, ListTypeDef, ObjectTypeDef, ScalarTypeDef, TypeDef as ScannerTypeDef, -} from "../introspector/scanner/typeDefs.js" +} from "../introspector/scanner/typedef.js" /** * Register the module files and returns its ID @@ -50,7 +50,7 @@ export async function register( if (field.isExposed) { typeDef = typeDef.withField( field.alias ?? field.name, - addTypeDef(field.type), + addTypeDef(field.type!), { description: field.description, }, @@ -99,7 +99,7 @@ function addConstructor(constructor: Constructor, owner: TypeDef): Function_ { */ function addFunction(fct: Method): Function_ { return dag - .function_(fct.alias ?? fct.name, addTypeDef(fct.returnType)) + .function_(fct.alias ?? fct.name, addTypeDef(fct.returnType!)) .withDescription(fct.description) .with(addArg(fct.arguments)) } @@ -114,7 +114,7 @@ function addArg(args: Arguments): (fct: Function_) => Function_ { description: arg.description, } - let typeDef = addTypeDef(arg.type) + let typeDef = addTypeDef(arg.type!) if (arg.isOptional) { typeDef = typeDef.withOptional(true) } @@ -132,7 +132,7 @@ function addArg(args: Arguments): (fct: Function_) => Function_ { // to workaround the fact that the API isn't aware of the default value and will // expect it to be set as required input. if (arg.defaultValue) { - if (isPrimitiveType(arg.type)) { + if (isPrimitiveType(arg.type!)) { opts.defaultValue = arg.defaultValue as string & { __JSON: never } } else { typeDef = typeDef.withOptional(true) diff --git a/sdk/typescript/introspector/scanner/abtractions/argument.ts b/sdk/typescript/introspector/scanner/abtractions/argument.ts deleted file mode 100644 index 985eb74a51b..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/argument.ts +++ /dev/null @@ -1,272 +0,0 @@ -import ts from "typescript" - -import { TypeDefKind } from "../../../api/client.gen.js" -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" -import { argument } from "../../decorators/decorators.js" -import { ArgumentOptions } from "../../registry/registry.js" -import { TypeDef } from "../typeDefs.js" -import { typeToTypedef } from "./typeToTypedef.js" - -export const ARGUMENT_DECORATOR = argument.name - -export type Arguments = { [name: string]: Argument } - -/** - * Argument is an abstraction of a function argument. - * - * This aims to simplify and adds clarity to how we analyse the code and using - * clear accessor. - */ -export class Argument { - private symbol: ts.Symbol - - private checker: ts.TypeChecker - - private param: ts.ParameterDeclaration - - // Preloaded values. - private _name: string - private _description: string - private _type: TypeDef - private _defaultValue: string | undefined - private _isOptional: boolean - private _isNullable: boolean - private _isVariadic: boolean - private _defaultPath?: string - private _ignore?: string[] - - /** - * Create a new Argument instance. - * - * @param checker Checker to use to introspect the type of the argument. - * @param param The symbol of the argument to introspect. - * - * @throws UnknownDaggerError If the symbol doesn't have any declaration. - * @throws UnknownDaggerError If the declaration of the symbol isn't a parameter. - */ - constructor(checker: ts.TypeChecker, param: ts.Symbol) { - this.symbol = param - this.checker = checker - - const declarations = this.symbol.getDeclarations() - if (!declarations || declarations.length < 0) { - throw new UnknownDaggerError( - `could not find param declarations of symbol ${this.symbol.name}`, - {}, - ) - } - - const parameterDeclaration = declarations[0] - if (!ts.isParameter(parameterDeclaration)) { - throw new UnknownDaggerError( - `the declaration of symbol ${this.symbol.name} isn't a parameter`, - {}, - ) - } - - this.param = parameterDeclaration - - // Preload to optimize the introspection. - this._name = this.loadName() - this._description = this.loadDescription() - this._type = this.loadType() - this._defaultValue = this.loadDefaultValue() - this._isNullable = this.loadIsNullable() - this._isVariadic = this.loadIsVariadic() - this._isOptional = this.loadIsOptional() - this.loadMetadata() - } - - get name(): string { - return this._name - } - - get description(): string { - return this._description - } - - /** - * Return the type of the argument in a Dagger TypeDef format. - */ - get type(): TypeDef { - return this._type - } - - get defaultValue(): string | undefined { - return this._defaultValue - } - - /** - * Return true if the parameter is optional. - * - * A parameter is considered optional if he fits one of the following: - * - It has a question token (e.g. `foo?: `). - * - It's variadic (e.g. `...foo: []`). - * - It's nullable (e.g. `foo: | null`). - */ - get isOptional(): boolean { - return this._isOptional - } - - /** - * Return true if the parameter is nullable. - * - * A parameter is considered nullable if itstype is a union type with `null` - * on the list of types. - * Example: `foo: string | null`. - */ - get isNullable(): boolean { - return this._isNullable - } - - get isVariadic(): boolean { - return this._isVariadic - } - - get defaultPath(): string | undefined { - return this._defaultPath - } - - get ignore(): string[] | undefined { - return this._ignore - } - - toJSON() { - return { - name: this.name, - description: this.description, - type: this.type, - isVariadic: this.isVariadic, - isNullable: this.isNullable, - isOptional: this.isOptional, - defaultValue: this.defaultValue, - defaultPath: this.defaultPath, - ignore: this.ignore, - } - } - - private loadName(): string { - return this.symbol.getName() - } - - private loadDescription(): string { - return ts.displayPartsToString( - this.symbol.getDocumentationComment(this.checker), - ) - } - - private loadType(): TypeDef { - if (!this.symbol.valueDeclaration) { - throw new UnknownDaggerError( - "could not find symbol value declaration", - {}, - ) - } - - const type = this.checker.getTypeOfSymbolAtLocation( - this.symbol, - this.symbol.valueDeclaration, - ) - - return typeToTypedef(this.checker, type) - } - - private loadDefaultValue(): string | undefined { - if (this.param.initializer === undefined) { - return undefined - } - - return this.formatDefaultValue(this.param.initializer.getText()) - } - - /** - * Return true if the parameter is optional. - * - * A parameter is considered optional if: - * - It has a question token (e.g. `foo?: `). - * - It's variadic (e.g. `...foo: []`). - * - It's nullable (e.g. `foo: | null`). - */ - private loadIsOptional(): boolean { - return ( - this.param.questionToken !== undefined || - this.isVariadic || - this.isNullable - ) - } - - private loadIsNullable(): boolean { - if (!this.param.type) { - return false - } - - if (ts.isUnionTypeNode(this.param.type)) { - for (const _type of this.param.type.types) { - if (_type.getText() === "null") { - return true - } - } - } - - return false - } - - private loadIsVariadic(): boolean { - return this.param.dotDotDotToken !== undefined - } - - /** - * The TypeScript Compiler API returns the raw default value as it is written - * by the user. - * However, some notations are not supported by GraphQL so this function - * formats the default value to be compatible with the GraphQL syntax. - * - * Formatting rules: - * - Single quote strings are converted to double quote strings. - * - * @param value The value to format. - */ - private formatDefaultValue(value: string): string { - const isSingleQuoteString = (): boolean => - value.startsWith("'") && value.endsWith("'") - - if (isSingleQuoteString()) { - return `"${value.slice(1, value.length - 1)}"` - } - - return value - } - - private loadMetadata(): void { - const contextDecorator = ts.getDecorators(this.param)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === ARGUMENT_DECORATOR - } - - return false - }) - - if (!contextDecorator) { - return undefined - } - - const expression = contextDecorator.expression as ts.CallExpression - const arg = expression.arguments[0] - if (!arg) { - return - } - - // We eval the string to convert it to a JavaScript object, because parsing it - // with JSON.parse will fails if the properties are not quoted. - const parsedArg = eval(`(${arg.getText()})`) as ArgumentOptions - if (parsedArg.defaultPath) { - this._defaultPath = parsedArg.defaultPath - } - - if (parsedArg.ignore) { - this._ignore = parsedArg.ignore - } - - return - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/constructor.ts b/sdk/typescript/introspector/scanner/abtractions/constructor.ts deleted file mode 100644 index 1c4642c67db..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/constructor.ts +++ /dev/null @@ -1,58 +0,0 @@ -import ts from "typescript" - -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" -import { Argument, Arguments } from "./argument.js" - -export class Constructor { - private checker: ts.TypeChecker - - private declaration: ts.ConstructorDeclaration - - // Preloaded values. - private _name: string = "" - private _arguments: Arguments - - constructor(checker: ts.TypeChecker, declaration: ts.ConstructorDeclaration) { - this.checker = checker - this.declaration = declaration - - // Preload values. - this._arguments = this.loadArguments() - } - - get name(): string { - return this._name - } - - get arguments(): Arguments { - return this._arguments - } - - toJSON() { - return { - args: this.arguments, - } - } - - getArgOrder(): string[] { - return Object.keys(this.arguments) - } - - private loadArguments(): Arguments { - return this.declaration.parameters.reduce((acc: Arguments, param) => { - const symbol = this.checker.getSymbolAtLocation(param.name) - if (!symbol) { - throw new UnknownDaggerError( - `could not get constructor param: ${param.name.getText()}`, - {}, - ) - } - - const argument = new Argument(this.checker, symbol) - - acc[argument.name] = argument - - return acc - }, {}) - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/enum.ts b/sdk/typescript/introspector/scanner/abtractions/enum.ts deleted file mode 100644 index de78279d3ad..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/enum.ts +++ /dev/null @@ -1,118 +0,0 @@ -import ts from "typescript" - -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" -import { enumType } from "../../decorators/decorators.js" -import { DaggerEnumValue, DaggerEnumValues } from "./enumValue.js" - -export const ENUM_DECORATOR = enumType.name - -/** - * Return true if the given class declaration has the decorator @enum() on - * top of its declaration. - */ -export function isEnumDecorated(object: ts.ClassDeclaration): boolean { - return ( - ts.getDecorators(object)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === ENUM_DECORATOR - } - - return false - }) !== undefined - ) -} - -export type DaggerEnums = { [name: string]: DaggerEnum } - -export class DaggerEnum { - private checker: ts.TypeChecker - - private enumClass: ts.ClassDeclaration - - private symbol: ts.Symbol - - private file: ts.SourceFile - - private _name: string - - private _description: string - - private _values: DaggerEnumValues - - constructor( - checker: ts.TypeChecker, - file: ts.SourceFile, - enumClassDeclaration: ts.ClassDeclaration, - ) { - this.checker = checker - this.enumClass = enumClassDeclaration - this.file = file - - if (!enumClassDeclaration.name) { - throw new UnknownDaggerError( - `could not introspect enum class: ${enumClassDeclaration}`, - {}, - ) - } - - const enumClassSymbol = checker.getSymbolAtLocation( - enumClassDeclaration.name, - ) - if (!enumClassSymbol) { - throw new UnknownDaggerError( - `could not get enum class symbol: ${enumClassDeclaration.name.getText()}`, - {}, - ) - } - - this.symbol = enumClassSymbol - - // Preload definition to optimize the introspection. - this._name = this.loadName() - this._description = this.loadDescription() - this._values = this.loadEnumValues() - } - - get name(): string { - return this._name - } - - get description(): string { - return this._description - } - - get values(): DaggerEnumValues { - return this._values - } - - toJSON() { - return { - name: this.name, - description: this.description, - values: this._values, - } - } - - private loadName(): string { - return this.symbol.getName() - } - - private loadDescription(): string { - return ts.displayPartsToString( - this.symbol.getDocumentationComment(this.checker), - ) - } - - private loadEnumValues(): DaggerEnumValues { - return this.enumClass.members - .filter((member) => ts.isPropertyDeclaration(member)) - .reduce((acc, member) => { - const value = new DaggerEnumValue( - this.checker, - member as ts.PropertyDeclaration, - ) - - return { ...acc, [value.name]: value } - }, {}) - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/enumValue.ts b/sdk/typescript/introspector/scanner/abtractions/enumValue.ts deleted file mode 100644 index aaeba946a70..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/enumValue.ts +++ /dev/null @@ -1,76 +0,0 @@ -import ts from "typescript" - -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" - -export type DaggerEnumValues = { [name: string]: DaggerEnumValue } - -export class DaggerEnumValue { - private checker: ts.TypeChecker - - private property: ts.PropertyDeclaration - - private symbol: ts.Symbol - - private _name: string - - private _value: string - - private _description: string - - constructor(checker: ts.TypeChecker, property: ts.PropertyDeclaration) { - this.checker = checker - this.property = property - - const propertySymbol = checker.getSymbolAtLocation(property.name) - if (!propertySymbol) { - throw new UnknownDaggerError( - `could not get property symbol: ${property.name.getText()}`, - {}, - ) - } - - this.symbol = propertySymbol - this._name = this.loadName() - this._value = this.loadValue() - this._description = this.loadDescription() - } - - get name(): string { - return this._name - } - - get value(): string { - return this._value - } - - get description(): string { - return this._description - } - - toJSON() { - return { - name: this.value, - description: this.description, - } - } - - private loadName(): string { - return this.symbol.getName() - } - - // Load the value of the enum value from the property initializer. - // If the initializer is not set, it will throw an error. - private loadValue(): string { - if (!this.property.initializer) { - throw new Error("Dagger enum value has no value set") - } - - return JSON.parse(this.property.initializer.getText()) - } - - private loadDescription(): string { - return ts.displayPartsToString( - this.symbol.getDocumentationComment(this.checker), - ) - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/method.ts b/sdk/typescript/introspector/scanner/abtractions/method.ts deleted file mode 100644 index b7cad60f372..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/method.ts +++ /dev/null @@ -1,180 +0,0 @@ -import ts from "typescript" - -import { TypeDefKind } from "../../../api/client.gen.js" -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" -import { func } from "../../decorators/decorators.js" -import { TypeDef } from "../typeDefs.js" -import { Argument, Arguments } from "./argument.js" -import { typeToTypedef } from "./typeToTypedef.js" - -export const METHOD_DECORATOR = func.name - -/** - * Return true if the given method has the decorator @fct() on top - * of its declaration. - * - * @param method The method to check - */ -export function isMethodDecorated(method: ts.MethodDeclaration): boolean { - return ( - ts.getDecorators(method)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === METHOD_DECORATOR - } - - return false - }) !== undefined - ) -} - -export type Methods = { [name: string]: Method } - -/** - * Method is an abstraction of a function or method. - * - * This aims to simplify and adds clarity to how we analyse the code and using - * clear accessor. - */ -export class Method { - private checker: ts.TypeChecker - - private symbol: ts.Symbol - - private signature: ts.Signature - - private decorator: ts.Decorator | undefined - - // Preloaded values. - private _name: string - private _description: string - private _alias: string | undefined - private _arguments: Arguments - private _returnType: TypeDef - - /** - * Create a new Method instance. - * - * @param checker Checker to use to introspect the method. - * @param method The method to introspect. - * - * @throws UnknownDaggerError If the method doesn't have any symbol. - * @throws UnknownDaggerError If the method doesn't have any signature. - */ - constructor(checker: ts.TypeChecker, method: ts.MethodDeclaration) { - this.checker = checker - - const methodSymbol = checker.getSymbolAtLocation(method.name) - if (!methodSymbol) { - throw new UnknownDaggerError( - `could not get method symbol: ${method.name.getText()}`, - {}, - ) - } - - this.symbol = methodSymbol - - const signature = checker.getSignatureFromDeclaration(method) - if (!signature) { - throw new UnknownDaggerError( - `could not get method signature: ${method.name.getText()}`, - {}, - ) - } - - this.signature = signature - - this.decorator = ts.getDecorators(method)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === METHOD_DECORATOR - } - - return false - }) - - // Preload to optimize the introspection. - this._name = this.loadName() - this._description = this.loadDescription() - this._alias = this.loadAlias() - this._arguments = this.loadArguments() - this._returnType = this.loadReturnType() - } - - get name(): string { - return this._name - } - - get description(): string { - return this._description - } - - /** - * Return the alias of the method if it has one. - */ - get alias(): string | undefined { - return this._alias - } - - get arguments(): Arguments { - return this._arguments - } - - /** - * Return the type of the return value in a Dagger TypeDef format. - */ - get returnType(): TypeDef { - return this._returnType - } - - toJSON() { - return { - name: this.name, - description: this.description, - alias: this.alias, - arguments: this.arguments, - returnType: this.returnType, - } - } - - getArgOrder(): string[] { - return Object.keys(this.arguments) - } - - private loadName(): string { - return this.symbol.getName() - } - - private loadDescription(): string { - return ts.displayPartsToString( - this.symbol.getDocumentationComment(this.checker), - ) - } - - private loadAlias(): string | undefined { - if (!this.decorator) { - return undefined - } - - const expression = this.decorator.expression as ts.CallExpression - const aliasArg = expression.arguments[0] - - if (!aliasArg) { - return undefined - } - - return JSON.parse(aliasArg.getText().replace(/'/g, '"')) - } - - private loadArguments(): Arguments { - return this.signature.parameters.reduce((acc: Arguments, param) => { - const argument = new Argument(this.checker, param) - - acc[argument.name] = argument - - return acc - }, {}) - } - - private loadReturnType(): TypeDef { - return typeToTypedef(this.checker, this.signature.getReturnType()) - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/module.ts b/sdk/typescript/introspector/scanner/abtractions/module.ts deleted file mode 100644 index 4758cdb2be0..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/module.ts +++ /dev/null @@ -1,141 +0,0 @@ -import ts from "typescript" - -import { DaggerEnum, DaggerEnums, isEnumDecorated } from "./enum.js" -import { DaggerObject, DaggerObjects, isObjectDecorated } from "./object.js" - -export class DaggerModule { - private checker: ts.TypeChecker - - private readonly files: ts.SourceFile[] - - public name: string - - // Preloaded values. - private _description: string | undefined - private _objects: DaggerObjects - private _enums: DaggerEnums - - constructor( - checker: ts.TypeChecker, - name = "", - files: readonly ts.SourceFile[], - ) { - this.checker = checker - this.files = files.filter((file) => !file.isDeclarationFile) - this.name = this.toPascalCase(name) - - // Preload values to optimize introspection. - this._objects = this.loadObjects() - this._enums = this.loadEnums() - this._description = this.loadDescription() - } - - get objects(): DaggerObjects { - return this._objects - } - - get enums(): DaggerEnums { - return this._enums - } - - get description(): string | undefined { - return this._description - } - - toJSON() { - return { - name: this.name, - description: this.description, - objects: this._objects, - enums: this._enums, - } - } - - private loadObjects(): DaggerObjects { - const objects: DaggerObjects = {} - - for (const file of this.files) { - ts.forEachChild(file, (node) => { - if (ts.isClassDeclaration(node) && isObjectDecorated(node)) { - const object = new DaggerObject(this.checker, file, node) - - objects[object.name] = object - } - }) - } - - return objects - } - - private loadEnums(): DaggerEnums { - const daggerEnums: DaggerEnums = {} - - for (const file of this.files) { - ts.forEachChild(file, (node) => { - if (ts.isClassDeclaration(node) && isEnumDecorated(node)) { - const daggerEnum = new DaggerEnum(this.checker, file, node) - - daggerEnums[daggerEnum.name] = daggerEnum - } - }) - } - - return daggerEnums - } - - private loadDescription(): string | undefined { - const mainObject = Object.values(this.objects).find( - (object) => object.name === this.name, - ) - if (!mainObject) { - return undefined - } - - const file = mainObject.file - const topLevelStatement = file.statements[0] - if (!topLevelStatement) { - return undefined - } - - // Get the range of the top level comment - const topLevelCommentRanges = ts.getLeadingCommentRanges( - file.getFullText(), - topLevelStatement.pos, - ) - if (!topLevelCommentRanges || topLevelCommentRanges.length === 0) { - return undefined - } - - const topLevelCommentRange = topLevelCommentRanges[0] - - return file - .getFullText() - .substring(topLevelCommentRange.pos, topLevelCommentRange.end) - .split("\n") - .slice(1, -1) // Remove start and ending comments characters `/** */` - .map((line) => line.replace("*", "").trim()) // Remove leading * and spaces - .join("\n") - } - - private toPascalCase(input: string): string { - const words = input - .replace(/[^a-zA-Z0-9]/g, " ") // Replace non-alphanumeric characters with spaces - .split(/\s+/) - .filter((word) => word.length > 0) - - if (words.length === 0) { - return "" // No valid words found - } - - // It's an edge case when moduleName is already in PascalCase or camelCase - if (words.length === 1) { - return words[0].charAt(0).toUpperCase() + words[0].slice(1) - } - - const pascalCase = words - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join("") - - return pascalCase - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/object.ts b/sdk/typescript/introspector/scanner/abtractions/object.ts deleted file mode 100644 index 9c091ca7d6c..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/object.ts +++ /dev/null @@ -1,172 +0,0 @@ -import ts from "typescript" - -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" -import { object } from "../../decorators/decorators.js" -import { Constructor } from "./constructor.js" -import { Method, Methods, isMethodDecorated } from "./method.js" -import { Properties, Property } from "./property.js" - -export const OBJECT_DECORATOR = object.name - -/** - * Return true if the given class declaration has the decorator @obj() on - * top of its declaration. - * @param object - */ -export function isObjectDecorated(object: ts.ClassDeclaration): boolean { - return ( - ts.getDecorators(object)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === OBJECT_DECORATOR - } - - return false - }) !== undefined - ) -} - -export type DaggerObjects = { [name: string]: DaggerObject } - -export class DaggerObject { - private checker: ts.TypeChecker - - private class: ts.ClassDeclaration - - private symbol: ts.Symbol - - public file: ts.SourceFile - - // Preloaded values. - private _name: string - private _description: string - private _classConstructor: Constructor | undefined - private _methods: Methods - private _properties: Properties - - /** - * - * @param checker The checker to use to introspect the class. - * @param classDeclaration The class to introspect. - * - * @throws UnknownDaggerError If the class doesn't have a name. - * @throws UnknownDaggerError If the class doesn't have a symbol. - */ - constructor( - checker: ts.TypeChecker, - file: ts.SourceFile, - classDeclaration: ts.ClassDeclaration, - ) { - this.checker = checker - this.class = classDeclaration - this.file = file - - if (!classDeclaration.name) { - throw new UnknownDaggerError( - `could not introspect class: ${classDeclaration}`, - {}, - ) - } - - const classSymbol = checker.getSymbolAtLocation(classDeclaration.name) - if (!classSymbol) { - throw new UnknownDaggerError( - `could not get class symbol: ${classDeclaration.name.getText()}`, - {}, - ) - } - - this.symbol = classSymbol - - // Preload values to optimize introspection. - this._name = this.loadName() - this._description = this.loadDescription() - this._classConstructor = this.loadConstructor() - this._methods = this.loadMethods() - this._properties = this.loadProperties() - } - - get name(): string { - return this._name - } - - get description(): string { - return this._description - } - - get _constructor(): Constructor | undefined { - return this._classConstructor - } - - get methods(): Methods { - return this._methods - } - - get properties(): Properties { - return this._properties - } - - toJSON() { - return { - name: this.name, - description: this.description, - constructor: this._constructor, - methods: this.methods, - properties: this.properties, - } - } - - private loadName(): string { - return this.symbol.getName() - } - - private loadDescription(): string { - return ts.displayPartsToString( - this.symbol.getDocumentationComment(this.checker), - ) - } - - private loadConstructor(): Constructor | undefined { - const constructor = this.class.members.find((member) => { - if (ts.isConstructorDeclaration(member)) { - return true - } - }) - - if (!constructor) { - return undefined - } - - return new Constructor( - this.checker, - constructor as ts.ConstructorDeclaration, - ) - } - - private loadMethods(): Methods { - return this.class.members - .filter( - (member) => ts.isMethodDeclaration(member) && isMethodDecorated(member), - ) - .reduce((acc: Methods, member) => { - const method = new Method(this.checker, member as ts.MethodDeclaration) - - acc[method.alias ?? method.name] = method - - return acc - }, {}) - } - - private loadProperties(): Properties { - return this.class.members - .filter((member) => ts.isPropertyDeclaration(member)) - .reduce((acc: Properties, member) => { - const property = new Property( - this.checker, - member as ts.PropertyDeclaration, - ) - - acc[property.alias ?? property.name] = property - return acc - }, {}) - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/property.ts b/sdk/typescript/introspector/scanner/abtractions/property.ts deleted file mode 100644 index 380574f9b5b..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/property.ts +++ /dev/null @@ -1,156 +0,0 @@ -import ts from "typescript" - -import { TypeDefKind } from "../../../api/client.gen.js" -import { UnknownDaggerError } from "../../../common/errors/UnknownDaggerError.js" -import { field, func } from "../../decorators/decorators.js" -import { TypeDef } from "../typeDefs.js" -import { typeToTypedef } from "./typeToTypedef.js" - -const DEPRECATED_PROPERTY_DECORATOR = field.name -const PROPERTY_DECORATOR = func.name - -export type Properties = { [name: string]: Property } - -/** - * Property is an abstraction of a class property. - * - * This aims to simplify and adds clarity to how we analyse the code and using - * clear accessor. - */ -export class Property { - private symbol: ts.Symbol - - private checker: ts.TypeChecker - - private property: ts.PropertyDeclaration - - private decorator: ts.Decorator | undefined - - // Preloaded values. - private _name: string - private _description: string - private _alias: string | undefined - private _type: TypeDef - private _isExposed: boolean - - /** - * - * @param checker Checker to use to introspect the property. - * @param property The property to introspect. - * - * @throws UnknownDaggerError If the property doesn't have any symbol. - */ - constructor(checker: ts.TypeChecker, property: ts.PropertyDeclaration) { - this.checker = checker - this.property = property - - const propertySymbol = checker.getSymbolAtLocation(property.name) - if (!propertySymbol) { - throw new UnknownDaggerError( - `could not get property symbol: ${property.name.getText()}`, - {}, - ) - } - - this.symbol = propertySymbol - - this.decorator = ts.getDecorators(property)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return ( - d.expression.expression.getText() === PROPERTY_DECORATOR || - d.expression.expression.getText() === DEPRECATED_PROPERTY_DECORATOR - ) - } - - return false - }) - - // Preload values to optimize introspection - this._name = this.loadName() - this._description = this.loadDescription() - this._alias = this.loadAlias() - this._type = this.loadType() - this._isExposed = this.loadIsExposed() - } - - get name(): string { - return this._name - } - - get description(): string { - return this._description - } - - /** - * Return the alias of the property if it has one. - */ - get alias(): string | undefined { - return this._alias - } - - /** - * Return the type of the property in a Dagger TypeDef format. - */ - get type(): TypeDef { - return this._type - } - - get isExposed(): boolean { - return this._isExposed - } - - toJSON() { - return { - name: this.name, - description: this.description, - alias: this.alias, - type: this.type, - isExposed: this.isExposed, - } - } - - private loadName(): string { - return this.symbol.getName() - } - - private loadDescription(): string { - return ts.displayPartsToString( - this.symbol.getDocumentationComment(this.checker), - ) - } - - private loadAlias(): string | undefined { - if (!this.decorator) { - return undefined - } - - const expression = this.decorator.expression as ts.CallExpression - const aliasArg = expression.arguments[0] - - if (!aliasArg) { - return undefined - } - - return JSON.parse(aliasArg.getText().replace(/'/g, '"')) - } - - private loadType(): TypeDef { - if (!this.symbol.valueDeclaration) { - throw new UnknownDaggerError( - "could not find symbol value declaration", - {}, - ) - } - - const type = this.checker.getTypeOfSymbolAtLocation( - this.symbol, - this.symbol.valueDeclaration, - ) - - return typeToTypedef(this.checker, type) - } - - private loadIsExposed(): boolean { - return this.decorator !== undefined - } -} diff --git a/sdk/typescript/introspector/scanner/abtractions/typeToTypedef.ts b/sdk/typescript/introspector/scanner/abtractions/typeToTypedef.ts deleted file mode 100644 index b154a3cd049..00000000000 --- a/sdk/typescript/introspector/scanner/abtractions/typeToTypedef.ts +++ /dev/null @@ -1,110 +0,0 @@ -import ts from "typescript" - -import { TypeDefKind } from "../../../api/client.gen.js" -import { TypeDef } from "../typeDefs.js" -import { isEnumDecorated } from "./enum.js" - -/** - * Convert a type into a Dagger Typedef using dynamic typing. - */ -export function typeToTypedef( - checker: ts.TypeChecker, - type: ts.Type, -): TypeDef { - const symbol = type.getSymbol() - const symbolName = symbol?.name - const symbolDeclaration = symbol?.valueDeclaration - - if (symbolName === "Promise") { - const typeArgs = checker.getTypeArguments(type as ts.TypeReference) - if (typeArgs.length > 0) { - return typeToTypedef(checker, typeArgs[0]) - } - } - - if (symbolName === "Array") { - const typeArgs = checker.getTypeArguments(type as ts.TypeReference) - if (typeArgs.length === 0) { - throw new Error("Generic array not supported") - } - return { - kind: TypeDefKind.ListKind, - typeDef: typeToTypedef(checker, typeArgs[0]), - } - } - - const strType = checker.typeToString(type) - - switch (strType) { - case "string": - return { kind: TypeDefKind.StringKind } - case "number": - return { kind: TypeDefKind.IntegerKind } - case "boolean": - return { kind: TypeDefKind.BooleanKind } - case "void": - return { kind: TypeDefKind.VoidKind } - // Intercept primitive types and throw error in this case - case "String": - throw new Error( - "Use of primitive String type detected, did you mean string?", - ) - case "Number": - throw new Error( - "Use of primitive Number type detected, did you mean number?", - ) - case "Boolean": - throw new Error( - "Use of primitive Boolean type detected, did you mean boolean?", - ) - default: - if ( - symbolName && - type.isClassOrInterface() && - symbolDeclaration && - ts.isClassDeclaration(symbolDeclaration) - ) { - if (isEnumDecorated(symbolDeclaration)) { - return { - kind: TypeDefKind.EnumKind, - name: symbolName, - } - } - - return { - kind: TypeDefKind.ObjectKind, - name: symbolName, - } - } - - if ( - symbol?.getFlags() !== undefined && - (symbol.getFlags() & ts.SymbolFlags.Enum) !== 0 - ) { - return { - kind: TypeDefKind.EnumKind, - name: strType, - } - } - - // If it's a union, then it's a scalar type - if (type.isUnionOrIntersection()) { - return { - kind: TypeDefKind.ScalarKind, - name: strType, - } - } - - // If we cannot resolve the symbol, we check for the alias symbol. - // This should mostly lead to a failure since external types are not supported by - // dagger yet. - if (type.aliasSymbol && type.aliasSymbol.flags & ts.TypeFlags.Object) { - return { - kind: TypeDefKind.ObjectKind, - name: type.aliasSymbol.escapedName.toString(), - } - } - - throw new Error(`Unsupported type ${strType}`) - } -} diff --git a/sdk/typescript/introspector/scanner/case_convertor.ts b/sdk/typescript/introspector/scanner/case_convertor.ts new file mode 100644 index 00000000000..4e0d57f7e3f --- /dev/null +++ b/sdk/typescript/introspector/scanner/case_convertor.ts @@ -0,0 +1,21 @@ +export function convertToPascalCase(input: string): string { + const words = input + .replace(/[^a-zA-Z0-9]/g, " ") // Replace non-alphanumeric characters with spaces + .split(/\s+/) + .filter((word) => word.length > 0) + + if (words.length === 0) { + return "" // No valid words found + } + + // It's an edge case when moduleName is already in PascalCase or camelCase + if (words.length === 1) { + return words[0].charAt(0).toUpperCase() + words[0].slice(1) + } + + const pascalCase = words + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join("") + + return pascalCase +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/argument.ts b/sdk/typescript/introspector/scanner/dagger_module/argument.ts new file mode 100644 index 00000000000..a77c2858e4f --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/argument.ts @@ -0,0 +1,147 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import ts from "typescript" + +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { ArgumentOptions } from "../../registry/registry.js" +import { TypeDef } from "../typedef.js" +import { AST } from "../typescript_module/ast.js" +import { + isTypeDefResolved, + resolveTypeDef, +} from "../typescript_module/typedef_utils.js" +import { ARGUMENT_DECORATOR } from "./decorator.js" +import { References } from "./reference.js" + +export type DaggerArguments = { [name: string]: DaggerArgument } + +export class DaggerArgument { + public name: string + public description: string + private _typeRef?: string + public type?: TypeDef + public isVariadic: boolean + public isNullable: boolean + public isOptional: boolean + public defaultPath?: string + public ignore?: string[] + public defaultValue?: any + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.ParameterDeclaration, + private readonly ast: AST, + ) { + this.symbol = this.ast.getSymbolOrThrow(node.name) + this.name = this.node.name.getText() + this.description = this.ast.getDocFromSymbol(this.symbol) + this.defaultValue = this.getDefaultValue() + this.isVariadic = this.node.dotDotDotToken !== undefined + this.isNullable = this.getIsNullable() + this.isOptional = + this.isVariadic || + this.defaultValue !== undefined || + this.isNullable || + this.node.questionToken !== undefined + + const decoratorArguments = this.ast.getDecoratorArgument( + this.node, + ARGUMENT_DECORATOR, + "object", + ) + + if (decoratorArguments) { + this.ignore = decoratorArguments.ignore + this.defaultPath = decoratorArguments.defaultPath + } + + this.type = this.getType() + } + + /** + * Get the type of the parameter. + * + * If for it's a complex type that cannot be + * resolve yet, we save its string representation for further reference. + */ + private getType(): TypeDef | undefined { + const type = this.ast.checker.getTypeAtLocation(this.node) + + const typedef = this.ast.tsTypeToTypeDef(this.node, type) + if (typedef === undefined || !isTypeDefResolved(typedef)) { + this._typeRef = this.ast.typeToStringType(type) + } + + return typedef + } + + private getIsNullable(): boolean { + if (!this.node.type) { + return false + } + + if (ts.isUnionTypeNode(this.node.type)) { + for (const _type of this.node.type.types) { + if (_type.getText() === "null") { + return true + } + } + } + + return false + } + + private getDefaultValue(): any { + const initializer = this.node.initializer + if (!initializer) { + return undefined + } + + return this.ast.resolveParameterDefaultValue(initializer) + } + + public getReference(): string | undefined { + if ( + this._typeRef && + (this.type === undefined || !isTypeDefResolved(this.type)) + ) { + return this._typeRef + } + + return undefined + } + + public propagateReferences(references: References) { + if (!this._typeRef) { + return + } + + if (this.type && isTypeDefResolved(this.type)) { + return + } + + const typeDef = references[this._typeRef] + if (!typeDef) { + throw new IntrospectionError( + `could not find type reference for ${this._typeRef} at ${AST.getNodePosition(this.node)}.`, + ) + } + + this.type = resolveTypeDef(this.type, typeDef) + } + + toJSON() { + return { + name: this.name, + description: this.description, + type: this.type, + isVariadic: this.isVariadic, + isNullable: this.isNullable, + isOptional: this.isOptional, + defaultValue: this.defaultValue, + defaultPath: this.defaultPath, + ignore: this.ignore, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/constructor.ts b/sdk/typescript/introspector/scanner/dagger_module/constructor.ts new file mode 100644 index 00000000000..0a925659cde --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/constructor.ts @@ -0,0 +1,53 @@ +import ts from "typescript" + +import { AST } from "../typescript_module/ast.js" +import { DaggerArgument, DaggerArguments } from "./argument.js" +import { References } from "./reference.js" + +export class DaggerConstructor { + public name: string = "" + public arguments: DaggerArguments = {} + + constructor( + private readonly node: ts.ConstructorDeclaration, + private readonly ast: AST, + ) { + const parameters = this.node.parameters + + for (const parameter of parameters) { + this.arguments[parameter.name.getText()] = new DaggerArgument( + parameter, + this.ast, + ) + } + } + + public getArgsOrder(): string[] { + return Object.keys(this.arguments) + } + + public getReferences(): string[] { + const references: string[] = [] + + for (const argument of Object.values(this.arguments)) { + const ref = argument.getReference() + if (ref) { + references.push(ref) + } + } + + return references + } + + public propagateReferences(references: References) { + for (const argument of Object.values(this.arguments)) { + argument.propagateReferences(references) + } + } + + toJSON() { + return { + arguments: this.arguments, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/decorator.ts b/sdk/typescript/introspector/scanner/dagger_module/decorator.ts new file mode 100644 index 00000000000..720999b862b --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/decorator.ts @@ -0,0 +1,20 @@ +import { + argument, + func, + object, + enumType, + field, +} from "../../decorators/decorators.js" + +export type DaggerDecorators = + | "object" + | "func" + | "argument" + | "enumType" + | "field" + +export const OBJECT_DECORATOR = object.name as DaggerDecorators +export const FUNCTION_DECORATOR = func.name as DaggerDecorators +export const FIELD_DECORATOR = field.name as DaggerDecorators +export const ARGUMENT_DECORATOR = argument.name as DaggerDecorators +export const ENUM_DECORATOR = enumType.name as DaggerDecorators diff --git a/sdk/typescript/introspector/scanner/dagger_module/enum.ts b/sdk/typescript/introspector/scanner/dagger_module/enum.ts new file mode 100644 index 00000000000..11742c12b1e --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/enum.ts @@ -0,0 +1,73 @@ +import ts from "typescript" + +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { AST } from "../typescript_module/ast.js" +import { DaggerEnumBase, DaggerEnumBaseValue } from "./enumBase.js" + +export type DaggerEnums = { [name: string]: DaggerEnum } +export type DaggerEnumValues = { [name: string]: DaggerEnumValue } + +export class DaggerEnumValue implements DaggerEnumBaseValue { + public name: string + public value: string + public description: string + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.EnumMember, + private readonly ast: AST, + ) { + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.name = this.node.name.getText() + this.description = this.ast.getDocFromSymbol(this.symbol) + + const initializer = this.node.initializer + if (!initializer) { + throw new IntrospectionError( + `enum ${this.name} at ${AST.getNodePosition(this.node)} has no value set to its member.`, + ) + } + + this.value = this.ast.resolveParameterDefaultValue(initializer) + } + + toJSON() { + return { + name: this.value, + description: this.description, + } + } +} + +export class DaggerEnum implements DaggerEnumBase { + public name: string + public description: string + public values: DaggerEnumValues = {} + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.EnumDeclaration, + private readonly ast: AST, + ) { + this.name = this.node.name.getText() + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.description = this.ast.getDocFromSymbol(this.symbol) + + const members = this.node.members + for (const member of members) { + const value = new DaggerEnumValue(member, this.ast) + + this.values[value.name] = value + } + } + + toJSON() { + return { + name: this.name, + description: this.description, + values: this.values, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/enumBase.ts b/sdk/typescript/introspector/scanner/dagger_module/enumBase.ts new file mode 100644 index 00000000000..b52cb1a33fe --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/enumBase.ts @@ -0,0 +1,15 @@ +export interface DaggerEnumBaseValue { + name: string + value: string + description: string +} + +export type DaggerEnumBaseValues = { [name: string]: DaggerEnumBaseValue } + +export interface DaggerEnumBase { + name: string + description: string + values: DaggerEnumBaseValues +} + +export type DaggerEnumsBase = { [name: string]: DaggerEnumBase } diff --git a/sdk/typescript/introspector/scanner/dagger_module/enumClass.ts b/sdk/typescript/introspector/scanner/dagger_module/enumClass.ts new file mode 100644 index 00000000000..9eef224c569 --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/enumClass.ts @@ -0,0 +1,79 @@ +import ts from "typescript" + +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { AST } from "../typescript_module/ast.js" +import { DaggerEnumBase, DaggerEnumBaseValue } from "./enumBase.js" + +export type DaggerEnumClasses = { [name: string]: DaggerEnumClass } + +export type DaggerEnumValues = { [name: string]: DaggerEnumClassValue } + +export class DaggerEnumClassValue implements DaggerEnumBaseValue { + public name: string + public value: string + public description: string + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.PropertyDeclaration, + private readonly ast: AST, + ) { + this.name = this.node.name.getText() + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.description = this.ast.getDocFromSymbol(this.symbol) + + const initializer = this.node.initializer + if (!initializer) { + throw new Error("Dagger enum value has no value set") + } + + this.value = this.ast.resolveParameterDefaultValue(initializer) + } + + toJSON() { + return { + name: this.value, + description: this.description, + } + } +} + +export class DaggerEnumClass implements DaggerEnumBase { + public name: string + public description: string + public values: DaggerEnumValues = {} + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.ClassDeclaration, + private readonly ast: AST, + ) { + if (!this.node.name) { + throw new IntrospectionError( + `could not resolve name of enum at ${AST.getNodePosition(node)}.`, + ) + } + this.name = this.node.name.getText() + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.description = this.ast.getDocFromSymbol(this.symbol) + + const properties = this.node.members + for (const property of properties) { + if (ts.isPropertyDeclaration(property)) { + const value = new DaggerEnumClassValue(property, this.ast) + + this.values[value.name] = value + } + } + } + + toJSON() { + return { + name: this.name, + description: this.description, + values: this.values, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/function.ts b/sdk/typescript/introspector/scanner/dagger_module/function.ts new file mode 100644 index 00000000000..b155e027ece --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/function.ts @@ -0,0 +1,127 @@ +import ts from "typescript" + +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { TypeDef } from "../typedef.js" +import { AST } from "../typescript_module/ast.js" +import { + isTypeDefResolved, + resolveTypeDef, +} from "../typescript_module/typedef_utils.js" +import { DaggerArgument, DaggerArguments } from "./argument.js" +import { FUNCTION_DECORATOR } from "./decorator.js" +import { References } from "./reference.js" + +export type DaggerFunctions = { [name: string]: DaggerFunction } + +export class DaggerFunction { + public name: string + public description: string + private _returnTypeRef?: string + public returnType?: TypeDef + public alias: string | undefined + public arguments: DaggerArguments = {} + + private signature: ts.Signature + private symbol: ts.Symbol + + constructor( + private readonly node: ts.MethodDeclaration, + private readonly ast: AST, + ) { + this.symbol = this.ast.getSymbolOrThrow(node.name) + this.signature = this.ast.getSignatureFromFunctionOrThrow(node) + this.name = this.node.name.getText() + this.description = this.ast.getDocFromSymbol(this.symbol) + + for (const parameter of this.node.parameters) { + this.arguments[parameter.name.getText()] = new DaggerArgument( + parameter, + this.ast, + ) + } + this.returnType = this.getReturnType() + this.alias = this.getAlias() + } + + private getReturnType(): TypeDef | undefined { + const type = this.signature.getReturnType() + + const typedef = this.ast.tsTypeToTypeDef(this.node, type) + if (typedef === undefined || !isTypeDefResolved(typedef)) { + this._returnTypeRef = this.ast.typeToStringType(type) + } + + return typedef + } + + private getAlias(): string | undefined { + const alias = this.ast.getDecoratorArgument( + this.node, + FUNCTION_DECORATOR, + "string", + ) + if (!alias) { + return + } + + return JSON.parse(alias.replace(/'/g, '"')) + } + + public getArgsOrder(): string[] { + return Object.keys(this.arguments) + } + + public getReferences(): string[] { + const references: string[] = [] + + if ( + this._returnTypeRef && + (this.returnType === undefined || !isTypeDefResolved(this.returnType)) + ) { + references.push(this._returnTypeRef) + } + + for (const argument of Object.values(this.arguments)) { + const reference = argument.getReference() + if (reference) { + references.push(reference) + } + } + + return references + } + + public propagateReferences(references: References) { + for (const argument of Object.values(this.arguments)) { + argument.propagateReferences(references) + } + + if (!this._returnTypeRef) { + return + } + + if (this.returnType && isTypeDefResolved(this.returnType)) { + return + } + + const typeDef = references[this._returnTypeRef] + if (!typeDef) { + throw new IntrospectionError( + `could not find type reference for ${this._returnTypeRef} at ${AST.getNodePosition(this.node)}.`, + ) + } + + this.returnType = resolveTypeDef(this.returnType, typeDef) + } + + public toJSON() { + return { + name: this.name, + description: this.description, + alias: this.alias, + arguments: this.arguments, + returnType: this.returnType, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/module.ts b/sdk/typescript/introspector/scanner/dagger_module/module.ts new file mode 100644 index 00000000000..c4a66f458ed --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/module.ts @@ -0,0 +1,365 @@ +import Module from "node:module" +import ts from "typescript" + +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { + AST, + CLIENT_GEN_FILE, + ResolvedNodeWithSymbol, +} from "../typescript_module/ast.js" +import { findModuleByExportedName } from "../typescript_module/explorer.js" +import { ENUM_DECORATOR, OBJECT_DECORATOR } from "./decorator.js" +import { DaggerEnum } from "./enum.js" +import { DaggerEnumsBase } from "./enumBase.js" +import { DaggerEnumClass } from "./enumClass.js" +import { DaggerObject } from "./object.js" +import { DaggerObjectsBase } from "./objectBase.js" +import { References } from "./reference.js" +import { DaggerTypeObject } from "./typeObject.js" + +/** + * DaggerModule represents a TypeScript module with a set of files + * with Dagger object conversion and notation. + * + * It starts from the module entrypoint (the class named the same as the module) and + * then recursively resolve every references to other declared identifiers. + * After resolution, it propagates all the references to the modules declarations + * and finally it generates the final Dagger module representation. + */ +export class DaggerModule { + /** + * An object is either a decorated class or a type alias object. + * Type alias objects cannot be decorated so they are resolved if referenced in the module. + * + * @example + * ```ts + * @object() + * export class MyObject { + * @func() + * public name: string + * } + * ``` + * + * @example + * ```ts + * export type Example = { + * name: string + * } + * ``` + */ + public objects: DaggerObjectsBase = {} + + /** + * An enum is either a decorated class or a native enum. + * Native enum cannot be decorated so they are resolved if referenced in the module. + * + * @example + * ```ts + * @enumType() + * export class Example { + * static A: string = "a" + * static B: string = "b" + * } + * ``` + * + * or + * + * @example + * ```ts + * export enum Example { + * A = "a", + * B = "b", + * } + * ``` + */ + public enums: DaggerEnumsBase = {} + public description: string | undefined + + private references: References = {} + + constructor( + public name: string, + private userModule: Module[], + private ast: AST, + ) { + const mainModule = findModuleByExportedName(this.name, this.userModule) + if (!mainModule) { + console.warn( + `could not find module entrypoint: class ${this.name} from import. Class should be exported to benefit from all features.`, + ) + } + + const mainObjectNode = this.ast.findResolvedNodeByName( + this.name, + ts.SyntaxKind.ClassDeclaration, + ) + if (!mainObjectNode) { + throw new IntrospectionError( + `could not find main object symbol or node ${this.name} in module ${JSON.stringify(mainModule, null, 2)} located at ${this.ast.files}`, + ) + } + + const mainFileContent = mainObjectNode.file.getFullText() + this.description = this.getDescription(mainFileContent) + + const mainDaggerObject = new DaggerObject(mainObjectNode.node, this.ast) + + this.objects[this.name] = mainDaggerObject + this.references[this.name] = { + kind: TypeDefKind.ObjectKind, + name: this.name, + } + + this.resolveReferences(mainDaggerObject.getReferences()) + this.propagateReferences() + } + + /** + * Find the reference of the module and register it to the module references. + * + * To do so, we check the user module to find a corresponding symbol (name) for each of + * typedef we support. + * This only applies for: + * - classes + * - enums + * - scalars + * + * If the reference is an object or a class, recursively find the references of the object. + * + * *Note*: If a class is referenced but not exported and not decorated with `@object()`, we throw an error + * because we aim to be explicit. (TomChv: Should we change this behaviour?) + */ + private resolveReferences(references: string[]) { + if (references.length === 0) { + return + } + + for (const reference of references) { + // If we already know that reference, we don't need to explore it again. + if (this.references[reference]) { + continue + } + + const classRef = this.ast.findResolvedNodeByName( + reference, + ts.SyntaxKind.ClassDeclaration, + ) + if (classRef) { + if (classRef.file.fileName.endsWith(CLIENT_GEN_FILE)) { + this.references[reference] = { + kind: TypeDefKind.ObjectKind, + name: reference, + } + continue + } + + if (this.ast.isNodeDecoratedWith(classRef.node, OBJECT_DECORATOR)) { + const daggerObject = new DaggerObject(classRef.node, this.ast) + this.objects[daggerObject.name] = daggerObject + this.references[daggerObject.name] = { + kind: TypeDefKind.ObjectKind, + name: daggerObject.name, + } + + this.resolveReferences(daggerObject.getReferences()) + + continue + } + + if (this.ast.isNodeDecoratedWith(classRef.node, ENUM_DECORATOR)) { + const daggerEnum = new DaggerEnumClass(classRef.node, this.ast) + this.enums[daggerEnum.name] = daggerEnum + this.references[daggerEnum.name] = { + kind: TypeDefKind.EnumKind, + name: daggerEnum.name, + } + + // There should be no references in enums. + continue + } + + throw new IntrospectionError( + `class ${reference} in ${AST.getNodePosition(classRef.node)} is used by the module but not exposed with a dagger decorator.`, + ) + } + + const enumRef = this.ast.findResolvedNodeByName( + reference, + ts.SyntaxKind.EnumDeclaration, + ) + if (enumRef) { + if (enumRef.file.fileName.endsWith(CLIENT_GEN_FILE)) { + this.references[reference] = { + kind: TypeDefKind.EnumKind, + name: reference, + } + continue + } + + // Typescript enum declaration cannot be decorated, so we don't check it. + const daggerEnum = new DaggerEnum(enumRef.node, this.ast) + this.enums[daggerEnum.name] = daggerEnum + this.references[daggerEnum.name] = { + kind: TypeDefKind.EnumKind, + name: daggerEnum.name, + } + + // There should be no reference in enums. + continue + } + + const typeAliasRef = this.ast.findResolvedNodeByName( + reference, + ts.SyntaxKind.TypeAliasDeclaration, + ) + if (typeAliasRef) { + // The resolution is to big so we split it in a sub function. + this.resolveTypeAlias(reference, typeAliasRef) + + continue + } + + // Handle primitives here + if (reference === "String") { + throw new IntrospectionError( + `String is a reserved word, please use "string" instead.`, + ) + } + + if (reference === "Boolean") { + throw new IntrospectionError( + `Boolean is a reserved word, please use "boolean" instead.`, + ) + } + + if (reference === "Number") { + throw new IntrospectionError( + `Number is a reserved word, please use "number" instead.`, + ) + } + + throw new IntrospectionError( + `could not resolve type reference for ${reference}.`, + ) + } + } + + /** + * Resolve type alias to the corresponding TypeDef. + * A type might refer to anything typeable in TypeScript but right now we supports: + * - `type Example = string` + * - `type Example = { prop: string}` + * - `type Example = number` + * - `type Example = boolean` + * - `type Example = void` + * + * If the reference is an object, we recursively resolve its references. + * If the type cannot be resolved or is not supported, we throw an error. + */ + private resolveTypeAlias( + reference: string, + typeAlias: ResolvedNodeWithSymbol, + ) { + const type = this.ast.getTypeFromTypeAlias(typeAlias.node) + + if (type.flags & ts.TypeFlags.String) { + this.references[reference] = { kind: TypeDefKind.StringKind } + + return + } + + if (type.flags & ts.TypeFlags.Number) { + this.references[reference] = { kind: TypeDefKind.IntegerKind } + + return + } + + if (type.flags & ts.TypeFlags.Boolean) { + this.references[reference] = { kind: TypeDefKind.BooleanKind } + + return + } + + if (type.flags & ts.TypeFlags.Void) { + this.references[reference] = { kind: TypeDefKind.VoidKind } + + return + } + + if ( + type.flags & ts.TypeFlags.Intersection || + type.flags & ts.TypeFlags.Union + ) { + this.references[reference] = { + kind: TypeDefKind.ScalarKind, + name: reference, + } + + return + } + + if (type.flags & ts.TypeFlags.Object) { + if (typeAlias.file.fileName.endsWith(CLIENT_GEN_FILE)) { + this.references[reference] = { + kind: TypeDefKind.ObjectKind, + name: reference, + } + + return + } + + const daggerObject = new DaggerTypeObject(typeAlias.node, this.ast) + this.objects[daggerObject.name] = daggerObject + this.references[daggerObject.name] = { + kind: TypeDefKind.ObjectKind, + name: daggerObject.name, + } + + this.resolveReferences(daggerObject.getReferences()) + + return + } + + throw new IntrospectionError( + `could not resolve type reference for ${reference} at ${AST.getNodePosition(typeAlias.node)}`, + ) + } + + /** + * Recursively propagate references to all objects properties and functions. + */ + private propagateReferences() { + for (const object of Object.values(this.objects)) { + object.propagateReferences(this.references) + } + } + + /** + * Get the top level comment of the file that contains the module entrypoint. + */ + private getDescription(sourceFileContent: string): string | undefined { + const regex = /^(?!.*import)[\s]*\/\*\*([\s\S]*?)\*\// + const match = sourceFileContent.match(regex) + + if (!match) { + return undefined + } + + const comment = match[1] + .split("\n") + .map((line) => line.replace(/^\s*\*\s?/, "")) + .join("\n") + + return comment.trim() + } + + toJSON() { + return { + name: this.name, + description: this.description, + objects: this.objects, + enums: this.enums, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/object.ts b/sdk/typescript/introspector/scanner/dagger_module/object.ts new file mode 100644 index 00000000000..7944e8bf278 --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/object.ts @@ -0,0 +1,120 @@ +import ts from "typescript" + +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { AST } from "../typescript_module/ast.js" +import { DaggerConstructor } from "./constructor.js" +import { FUNCTION_DECORATOR, OBJECT_DECORATOR } from "./decorator.js" +import { DaggerFunction, DaggerFunctions } from "./function.js" +import { DaggerObjectBase } from "./objectBase.js" +import { DaggerProperties, DaggerProperty } from "./property.js" +import { References } from "./reference.js" + +export class DaggerObject implements DaggerObjectBase { + public name: string + public description: string + public _constructor: DaggerConstructor | undefined = undefined + public methods: DaggerFunctions = {} + public properties: DaggerProperties = {} + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.ClassDeclaration, + private readonly ast: AST, + ) { + if (!this.node.name) { + throw new IntrospectionError( + `could not resolve name of class at ${AST.getNodePosition(node)}.`, + ) + } + this.name = this.node.name.getText() + + if (!this.ast.isNodeDecoratedWith(node, OBJECT_DECORATOR)) { + throw new IntrospectionError( + `class ${this.name} in ${AST.getNodePosition(node)} is used by the module but not exposed with a dagger decorator.`, + ) + } + + const modifiers = ts.getCombinedModifierFlags(this.node) + + if (!(modifiers & ts.ModifierFlags.Export)) { + console.warn( + `missing export in class ${this.name} at ${AST.getNodePosition(node)} but it's used by the module.`, + ) + } + + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.description = this.ast.getDocFromSymbol(this.symbol) + + for (const member of this.node.members) { + if (ts.isPropertyDeclaration(member)) { + const property = new DaggerProperty(member, this.ast) + this.properties[property.alias ?? property.name] = property + + continue + } + + if (ts.isConstructorDeclaration(member)) { + this._constructor = new DaggerConstructor(member, this.ast) + + continue + } + + if ( + ts.isMethodDeclaration(member) && + this.ast.isNodeDecoratedWith(member, FUNCTION_DECORATOR) + ) { + const daggerFunction = new DaggerFunction(member, this.ast) + this.methods[daggerFunction.alias ?? daggerFunction.name] = + daggerFunction + + continue + } + } + } + + public getReferences(): string[] { + const references: string[] = [] + + if (this._constructor) { + references.push(...this._constructor.getReferences()) + } + + for (const property of Object.values(this.properties)) { + const ref = property.getReference() + if (ref) { + references.push(ref) + } + } + + for (const fn of Object.values(this.methods)) { + references.push(...fn.getReferences()) + } + + return references.filter((v, i, arr) => arr.indexOf(v) === i) + } + + public propagateReferences(references: References): void { + if (this._constructor) { + this._constructor.propagateReferences(references) + } + + for (const property of Object.values(this.properties)) { + property.propagateReferences(references) + } + + for (const fn of Object.values(this.methods)) { + fn.propagateReferences(references) + } + } + + public toJSON() { + return { + name: this.name, + description: this.description, + constructor: this._constructor, + methods: this.methods, + properties: this.properties, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/objectBase.ts b/sdk/typescript/introspector/scanner/dagger_module/objectBase.ts new file mode 100644 index 00000000000..abb97d75220 --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/objectBase.ts @@ -0,0 +1,31 @@ +import { TypeDefKind } from "../../../api/client.gen.js" +import { TypeDef } from "../typedef.js" +import { DaggerConstructor } from "./constructor.js" +import { DaggerFunctions } from "./function.js" +import { References } from "./reference.js" + +export interface DaggerObjectPropertyBase { + name: string + description: string + alias?: string + isExposed: boolean + type?: TypeDef + + propagateReferences(references: References): void +} + +export type DaggerObjectPropertiesBase = { + [name: string]: DaggerObjectPropertyBase +} + +export interface DaggerObjectBase { + name: string + description: string + _constructor: DaggerConstructor | undefined + methods: DaggerFunctions + properties: DaggerObjectPropertiesBase + + propagateReferences(references: References): void +} + +export type DaggerObjectsBase = { [name: string]: DaggerObjectBase } diff --git a/sdk/typescript/introspector/scanner/dagger_module/property.ts b/sdk/typescript/introspector/scanner/dagger_module/property.ts new file mode 100644 index 00000000000..bd2d956ab5b --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/property.ts @@ -0,0 +1,120 @@ +import ts from "typescript" + +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { TypeDef } from "../typedef.js" +import { AST } from "../typescript_module/ast.js" +import { + isTypeDefResolved, + resolveTypeDef, +} from "../typescript_module/typedef_utils.js" +import { FIELD_DECORATOR, FUNCTION_DECORATOR } from "./decorator.js" +import { DaggerObjectPropertyBase } from "./objectBase.js" +import { References } from "./reference.js" + +export type DaggerProperties = { [name: string]: DaggerProperty } + +export class DaggerProperty implements DaggerObjectPropertyBase { + public name: string + public description: string + public alias: string | undefined + public isExposed: boolean + + private symbol: ts.Symbol + private _typeRef?: string + public type?: TypeDef + + constructor( + private readonly node: ts.PropertyDeclaration, + private readonly ast: AST, + ) { + if (!this.node.name) { + throw new IntrospectionError( + `could not resolve name of class at ${AST.getNodePosition(node)}.`, + ) + } + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.name = this.node.name.getText() + + this.isExposed = + this.ast.isNodeDecoratedWith(this.node, FUNCTION_DECORATOR) || + this.ast.isNodeDecoratedWith(this.node, FIELD_DECORATOR) + + this.description = this.ast.getDocFromSymbol(this.symbol) + this.alias = this.getAlias() + this.type = this.getType() + } + + private getAlias(): string | undefined { + let alias = this.ast.getDecoratorArgument( + this.node, + FUNCTION_DECORATOR, + "string", + ) + + if (alias) { + return JSON.parse(alias.replace(/'/g, '"')) + } + + alias = this.ast.getDecoratorArgument( + this.node, + FIELD_DECORATOR, + "string", + ) + + if (alias) { + return JSON.parse(alias.replace(/'/g, '"')) + } + } + + private getType(): TypeDef | undefined { + const type = this.ast.checker.getTypeAtLocation(this.node) + + const typedef = this.ast.tsTypeToTypeDef(this.node, type) + if (typedef === undefined || !isTypeDefResolved(typedef)) { + this._typeRef = this.ast.typeToStringType(type) + } + + return typedef + } + + public getReference(): string | undefined { + if ( + this._typeRef && + (this.type === undefined || !isTypeDefResolved(this.type)) + ) { + return this._typeRef + } + + return undefined + } + + public propagateReferences(references: References): void { + if (!this._typeRef) { + return + } + + if (this.type && isTypeDefResolved(this.type)) { + return + } + + const typeDef = references[this._typeRef] + if (!typeDef) { + throw new IntrospectionError( + `could not find type reference for ${this._typeRef} at ${AST.getNodePosition(this.node)}.`, + ) + } + + this.type = resolveTypeDef(this.type, typeDef) + } + + public toJSON() { + return { + name: this.name, + description: this.description, + alias: this.alias, + type: this.type, + isExposed: this.isExposed, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/reference.ts b/sdk/typescript/introspector/scanner/dagger_module/reference.ts new file mode 100644 index 00000000000..2fcc1843796 --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/reference.ts @@ -0,0 +1,58 @@ +import { TypeDefKind } from "../../../api/client.gen.js" +import { TypeDef } from "../typedef.js" + +export type References = { [name: string]: TypeDef } + +export type ReferencableType = + | TypeDef + | TypeDef + | TypeDef + +export function isKindArray( + type: TypeDef, +): type is TypeDef { + return type.kind === TypeDefKind.ListKind +} + +export function isKindObject( + type: TypeDef, +): type is TypeDef { + return type.kind === TypeDefKind.ObjectKind +} + +export function isKindEnum( + type: TypeDef, +): type is TypeDef { + return type.kind === TypeDefKind.EnumKind +} + +export function isKindScalar( + type: TypeDef, +): type is TypeDef { + return type.kind === TypeDefKind.ScalarKind +} + +export function isReferencableTypeDef(type: TypeDef): boolean { + switch (type.kind) { + case TypeDefKind.ObjectKind: + return true + case TypeDefKind.EnumKind: + return true + case TypeDefKind.ScalarKind: + return true + case TypeDefKind.ListKind: + return isReferencableTypeDef(getTypeDefArrayBaseType(type)) + default: + return false + } +} + +export function getTypeDefArrayBaseType( + type: TypeDef, +): TypeDef { + if (isKindArray(type)) { + return getTypeDefArrayBaseType(type.typeDef) + } + + return type +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/typeObject.ts b/sdk/typescript/introspector/scanner/dagger_module/typeObject.ts new file mode 100644 index 00000000000..0db1a8cc168 --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/typeObject.ts @@ -0,0 +1,76 @@ +import ts from "typescript" + +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { AST } from "../typescript_module/ast.js" +import { DaggerObjectBase } from "./objectBase.js" +import { References } from "./reference.js" +import { + DaggerObjectTypeProperties, + DaggerObjectTypeProperty, +} from "./typeObjectProperty.js" + +export class DaggerTypeObject implements DaggerObjectBase { + public name: string + public description: string + public _constructor = undefined + public methods = {} + public properties: DaggerObjectTypeProperties = {} + + private symbol: ts.Symbol + + constructor( + private readonly node: ts.TypeAliasDeclaration, + private readonly ast: AST, + ) { + if (!this.node.name) { + throw new IntrospectionError( + `could not resolve name of enum at ${AST.getNodePosition(node)}.`, + ) + } + this.name = this.node.name.getText() + this.symbol = this.ast.getSymbolOrThrow(this.node.name) + this.description = this.ast.getDocFromSymbol(this.symbol) + + const type = this.ast.getTypeFromTypeAlias(this.node) + + if (type.flags & ts.TypeFlags.Object) { + const objectType = type as ts.ObjectType + const properties = objectType.getProperties() + for (const property of properties) { + const daggerProperty = new DaggerObjectTypeProperty( + this.node, + property, + this.ast, + ) + this.properties[daggerProperty.name] = daggerProperty + } + } + } + + public getReferences(): string[] { + const references: string[] = [] + + for (const property of Object.values(this.properties)) { + const ref = property.getReference() + if (ref) { + references.push(ref) + } + } + + return references.filter((v, i, arr) => arr.indexOf(v) === i) + } + + public propagateReferences(references: References): void { + for (const property of Object.values(this.properties)) { + property.propagateReferences(references) + } + } + + toJSON() { + return { + name: this.name, + description: this.description, + properties: this.properties, + } + } +} diff --git a/sdk/typescript/introspector/scanner/dagger_module/typeObjectProperty.ts b/sdk/typescript/introspector/scanner/dagger_module/typeObjectProperty.ts new file mode 100644 index 00000000000..221470df53c --- /dev/null +++ b/sdk/typescript/introspector/scanner/dagger_module/typeObjectProperty.ts @@ -0,0 +1,84 @@ +import ts from "typescript" + +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { TypeDef } from "../typedef.js" +import { AST } from "../typescript_module/ast.js" +import { + isTypeDefResolved, + resolveTypeDef, +} from "../typescript_module/typedef_utils.js" +import { DaggerObjectPropertyBase } from "./objectBase.js" +import { References } from "./reference.js" + +export type DaggerObjectTypeProperties = { + [name: string]: DaggerObjectTypeProperty +} + +export class DaggerObjectTypeProperty implements DaggerObjectPropertyBase { + public name: string + public description: string + public alias = undefined + public isExposed: boolean = true + + private _typeRef?: string + public type?: TypeDef + + constructor( + private readonly node: ts.TypeAliasDeclaration, + private readonly symbol: ts.Symbol, + private readonly ast: AST, + ) { + this.name = symbol.name + this.description = this.ast.getDocFromSymbol(this.symbol) + + const type = this.ast.checker.getTypeOfSymbolAtLocation( + this.symbol, + this.node, + ) + this.type = this.ast.tsTypeToTypeDef(this.node, type) + if (this.type === undefined || !isTypeDefResolved(this.type)) { + this._typeRef = this.ast.typeToStringType(type) + } + } + + public getReference(): string | undefined { + if ( + this._typeRef && + (this.type === undefined || !isTypeDefResolved(this.type)) + ) { + return this._typeRef + } + + return undefined + } + + public propagateReferences(references: References): void { + if (!this._typeRef) { + return + } + + if (this.type && isTypeDefResolved(this.type)) { + return + } + + const typeDef = references[this._typeRef] + if (!typeDef) { + throw new IntrospectionError( + `could not find type reference for ${this._typeRef}.`, + ) + } + + this.type = resolveTypeDef(this.type, typeDef) + } + + public toJSON() { + return { + name: this.name, + description: this.description, + alias: this.alias, + type: this.type, + isExposed: this.isExposed, + } + } +} diff --git a/sdk/typescript/introspector/scanner/scan.ts b/sdk/typescript/introspector/scanner/scan.ts index 05f78f00748..09136818b5d 100644 --- a/sdk/typescript/introspector/scanner/scan.ts +++ b/sdk/typescript/introspector/scanner/scan.ts @@ -1,31 +1,21 @@ -import ts from "typescript" +import { IntrospectionError } from "../../common/errors/IntrospectionError.js" +import { load } from "../../entrypoint/load.js" +import { convertToPascalCase } from "./case_convertor.js" +import { DaggerModule } from "./dagger_module/module.js" +import { AST } from "./typescript_module/ast.js" -import { DaggerModule } from "./abtractions/module.js" - -/** - * Scan the list of TypeScript File using the TypeScript compiler API. - * - * This function introspect files and returns metadata of their class and - * functions that should be exposed to the Dagger API. - * - * WARNING(28/11/23): This does NOT include arrow style function. - * - * @param files List of TypeScript files to introspect. - * @param moduleName The name of the module to introspect. - */ -export function scan(files: string[], moduleName = ""): DaggerModule { +export async function scan(files: string[], moduleName = "") { if (files.length === 0) { - throw new Error("no files to introspect found") + throw new IntrospectionError("no files to introspect found") } + const formattedModuleName = convertToPascalCase(moduleName) + const userModule = await load(files) + // Interpret the given typescript source files. - const program = ts.createProgram(files, { experimentalDecorators: true }) - const checker = program.getTypeChecker() + const ast = new AST(files, userModule) - const module = new DaggerModule(checker, moduleName, program.getSourceFiles()) - if (Object.keys(module.objects).length === 0) { - throw new Error("no objects found in the module") - } + const module = new DaggerModule(formattedModuleName, userModule, ast) return module } diff --git a/sdk/typescript/introspector/scanner/typeDefs.ts b/sdk/typescript/introspector/scanner/typedef.ts similarity index 100% rename from sdk/typescript/introspector/scanner/typeDefs.ts rename to sdk/typescript/introspector/scanner/typedef.ts diff --git a/sdk/typescript/introspector/scanner/typescript_module/ast.ts b/sdk/typescript/introspector/scanner/typescript_module/ast.ts new file mode 100644 index 00000000000..5b59a29f8ba --- /dev/null +++ b/sdk/typescript/introspector/scanner/typescript_module/ast.ts @@ -0,0 +1,367 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import Module from "node:module" +import ts from "typescript" + +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { DaggerDecorators } from "../dagger_module/decorator.js" +import { TypeDef } from "../typedef.js" +import { DeclarationsMap, isDeclarationOf } from "./declarations.js" +import { getValueByExportedName } from "./explorer.js" + +export const CLIENT_GEN_FILE = "client.gen.ts" + +export type ResolvedNodeWithSymbol = { + type: T + node: DeclarationsMap[T] + symbol: ts.Symbol + file: ts.SourceFile +} + +export class AST { + public checker: ts.TypeChecker + + private readonly sourceFiles: ts.SourceFile[] + + constructor( + public readonly files: string[], + private readonly userModule: Module[], + ) { + const program = ts.createProgram(files, { + experimentalDecorators: true, + moduleResolution: ts.ModuleResolutionKind.Node10, + target: ts.ScriptTarget.ES2022, + }) + this.checker = program.getTypeChecker() + this.sourceFiles = program + .getSourceFiles() + .filter((file) => !file.isDeclarationFile) + } + + public findResolvedNodeByName( + name: string, + + /** + * Optionally look for a specific node kind if we already know + * what we're looking for. + */ + kind: T, + ): ResolvedNodeWithSymbol | undefined { + let result: ResolvedNodeWithSymbol | undefined + + for (const sourceFile of this.sourceFiles) { + ts.forEachChild(sourceFile, (node) => { + if (result !== undefined) return + + // Skip if it's not from the client gen nor the user module + if ( + !sourceFile.fileName.endsWith(CLIENT_GEN_FILE) && + !this.files.includes(sourceFile.fileName) + ) { + return + } + + if (kind !== undefined && node.kind === kind) { + const isDeclarationValid = isDeclarationOf[kind](node) + if (!isDeclarationValid) return + + const convertedNode = node as DeclarationsMap[typeof kind] + if (!convertedNode.name || convertedNode.name.getText() !== name) { + return + } + + const symbol = this.checker.getSymbolAtLocation(convertedNode.name) + if (!symbol) { + console.debug( + `missing symbol for ${name} at ${sourceFile.fileName}:${node.pos}`, + ) + return + } + + result = { + type: kind, + node: convertedNode, + symbol: symbol, + file: sourceFile, + } + } + }) + } + + return result + } + + public getTypeFromTypeAlias(typeAlias: ts.TypeAliasDeclaration): ts.Type { + const symbol = this.getSymbolOrThrow(typeAlias.name) + + return this.checker.getDeclaredTypeOfSymbol(symbol) + } + + public static getNodePosition(node: ts.Node): string { + const sourceFile = node.getSourceFile() + + const position = ts.getLineAndCharacterOfPosition( + sourceFile, + node.getStart(), + ) + + return `${sourceFile.fileName}:${position.line}:${position.character}` + } + + public getDocFromSymbol(symbol: ts.Symbol): string { + return ts.displayPartsToString(symbol.getDocumentationComment(this.checker)) + } + + public getSymbolOrThrow(node: ts.Node): ts.Symbol { + const symbol = this.getSymbol(node) + if (!symbol) { + throw new IntrospectionError( + `could not find symbol at ${AST.getNodePosition(node)}`, + ) + } + + return symbol + } + + public getSignatureFromFunctionOrThrow( + node: ts.SignatureDeclaration, + ): ts.Signature { + const signature = this.checker.getSignatureFromDeclaration(node) + if (!signature) { + throw new IntrospectionError( + `could not find signature at ${AST.getNodePosition(node)}`, + ) + } + + return signature + } + + public getSymbol(node: ts.Node): ts.Symbol | undefined { + return this.checker.getSymbolAtLocation(node) + } + + public isNodeDecoratedWith( + node: ts.HasDecorators, + daggerDecorator: DaggerDecorators, + ): boolean { + const decorators = ts.getDecorators(node) + if (!decorators) { + return false + } + + const decorator = decorators.find((d) => + d.expression.getText().startsWith(daggerDecorator), + ) + if (!decorator) { + return false + } + + if (!ts.isCallExpression(decorator.expression)) { + throw new IntrospectionError( + `decorator at ${AST.getNodePosition(node)} should be a call expression, please use ${daggerDecorator}() instead.`, + ) + } + + return true + } + + public getDecoratorArgument( + node: ts.HasDecorators, + daggerDecorator: DaggerDecorators, + type: "string" | "object", + position = 0, + ): T | undefined { + const decorators = ts.getDecorators(node) + if (!decorators) { + return undefined + } + + const decorator = decorators.find((d) => + d.expression.getText().startsWith(daggerDecorator), + ) + if (!decorator) { + return undefined + } + + const argument = (decorator.expression as ts.CallExpression).arguments[ + position + ] + if (!argument) { + return undefined + } + + switch (type) { + case "string": + return argument.getText() as T + case "object": + return eval(`(${argument.getText()})`) + } + } + + public unwrapTypeStringFromPromise(type: string): string { + if (type.startsWith("Promise<")) { + return type.slice("Promise<".length, -">".length) + } + + if (type.startsWith("Awaited<")) { + return type.slice("Awaited<".length, -">".length) + } + + return type + } + + public unwrapTypeStringFromArray(type: string): string { + if (type.endsWith("[]")) { + return type.replace("[]", "") + } + + if (type.startsWith("Array<")) { + return type.slice("Array<".length, -">".length) + } + + return type + } + + public stringTypeToUnwrappedType(type: string): string { + type = this.unwrapTypeStringFromPromise(type) + + // If they are difference, that means we upwrapped the array. + const extractedTypeFromArray = this.unwrapTypeStringFromArray(type) + if (extractedTypeFromArray !== type) { + return this.stringTypeToUnwrappedType(extractedTypeFromArray) + } + + return type + } + + public typeToStringType(type: ts.Type): string { + const stringType = this.checker.typeToString(type) + + return this.stringTypeToUnwrappedType(stringType) + } + + public tsTypeToTypeDef( + node: ts.Node, + type: ts.Type, + ): TypeDef | undefined { + if (type.flags & ts.TypeFlags.String) + return { kind: TypeDefKind.StringKind } + if (type.flags & ts.TypeFlags.Number) + return { kind: TypeDefKind.IntegerKind } + if (type.flags & ts.TypeFlags.Boolean) + return { kind: TypeDefKind.BooleanKind } + if (type.flags & ts.TypeFlags.Void) return { kind: TypeDefKind.VoidKind } + + // If a type has a flag Object, is can basically be anything. + // We firstly wants to see if it's a promise or an array so we can unwrap the + // actual type. + if (type.flags & ts.TypeFlags.Object) { + const objectType = type as ts.ObjectType + + // If it's a reference, that means it's a generic type like + // `Promise` or `number[]` or `Array`. + if (objectType.objectFlags & ts.ObjectFlags.Reference) { + const typeArguments = this.checker.getTypeArguments( + type as ts.TypeReference, + ) + + switch (typeArguments.length) { + case 0: + // Might change after to support more complex type + break + case 1: { + const typeArgument = typeArguments[0] + + if (type.symbol.getName() === "Promise") { + return this.tsTypeToTypeDef(node, typeArgument) + } + + if (type.symbol.getName() === "Array") { + return { + kind: TypeDefKind.ListKind, + typeDef: this.tsTypeToTypeDef(node, typeArgument), + } + } + + return undefined + } + default: { + throw new IntrospectionError( + `could not resolve type ${type.symbol.getName()} at ${AST.getNodePosition(node)}, dagger does not support generics with argument yet.`, + ) + } + } + } + } + } + + private resolveParameterDefaultValueTypeReference( + expression: ts.Expression, + value: any, + ): any { + const type = typeof value + + switch (type) { + case "string": + case "number": + case "bigint": + case "boolean": + case "object": + // Value will be jsonified on registration so it must be a string. + return `${value}` + default: + // If we cannot resolve the value, we skip it and let the value be resolved automatically by the runtime + return undefined + } + } + + public resolveParameterDefaultValue(expression: ts.Expression): any { + const kind = expression.kind + + switch (kind) { + case ts.SyntaxKind.StringLiteral: + return expression.getText() + case ts.SyntaxKind.NumericLiteral: + return `${parseInt(expression.getText())}` + case ts.SyntaxKind.TrueKeyword: + return `${true}` + case ts.SyntaxKind.FalseKeyword: + return `${false}` + case ts.SyntaxKind.NullKeyword: + return "null" + case ts.SyntaxKind.ArrayLiteralExpression: + return eval(expression.getText()) + case ts.SyntaxKind.Identifier: { + // If the parameter is a reference to a variable, we try to resolve it using + // exported modules value. + const value = getValueByExportedName( + expression.getText(), + this.userModule, + ) + + if (value === undefined) { + throw new IntrospectionError( + `could not resolve default value reference to the variable: '${expression.getText()}' from ${AST.getNodePosition(expression)}. Is it exported by the module?`, + ) + } + + return this.resolveParameterDefaultValueTypeReference(expression, value) + } + case ts.SyntaxKind.PropertyAccessExpression: { + const accessors = expression.getText().split(".") + + let value = getValueByExportedName(accessors[0], this.userModule) + for (let i = 1; i < accessors.length; i++) { + value = value[accessors[i]] + } + + return this.resolveParameterDefaultValueTypeReference(expression, value) + } + default: { + throw new IntrospectionError( + `default value '${expression.getText()}' at ${AST.getNodePosition(expression)} cannot be resolved, dagger does not support object or function as default value.`, + ) + } + } + } +} diff --git a/sdk/typescript/introspector/scanner/typescript_module/declarations.ts b/sdk/typescript/introspector/scanner/typescript_module/declarations.ts new file mode 100644 index 00000000000..711a35187ff --- /dev/null +++ b/sdk/typescript/introspector/scanner/typescript_module/declarations.ts @@ -0,0 +1,26 @@ +import ts from "typescript" + +export type DeclarationsMap = { + [ts.SyntaxKind.ClassDeclaration]: ts.ClassDeclaration + [ts.SyntaxKind.MethodDeclaration]: ts.MethodDeclaration + [ts.SyntaxKind.PropertyDeclaration]: ts.PropertyDeclaration + [ts.SyntaxKind.FunctionDeclaration]: ts.FunctionDeclaration + [ts.SyntaxKind.EnumDeclaration]: ts.EnumDeclaration + [ts.SyntaxKind.InterfaceDeclaration]: ts.InterfaceDeclaration + [ts.SyntaxKind.TypeAliasDeclaration]: ts.TypeAliasDeclaration +} + +export type Declarations = + T extends keyof DeclarationsMap ? DeclarationsMap[T] : ts.Node + +export const isDeclarationOf: { + [K in keyof DeclarationsMap]: (node: ts.Node) => node is DeclarationsMap[K] +} = { + [ts.SyntaxKind.ClassDeclaration]: ts.isClassDeclaration, + [ts.SyntaxKind.MethodDeclaration]: ts.isMethodDeclaration, + [ts.SyntaxKind.PropertyDeclaration]: ts.isPropertyDeclaration, + [ts.SyntaxKind.FunctionDeclaration]: ts.isFunctionDeclaration, + [ts.SyntaxKind.EnumDeclaration]: ts.isEnumDeclaration, + [ts.SyntaxKind.InterfaceDeclaration]: ts.isInterfaceDeclaration, + [ts.SyntaxKind.TypeAliasDeclaration]: ts.isTypeAliasDeclaration, +} diff --git a/sdk/typescript/introspector/scanner/typescript_module/explorer.ts b/sdk/typescript/introspector/scanner/typescript_module/explorer.ts new file mode 100644 index 00000000000..160f04df084 --- /dev/null +++ b/sdk/typescript/introspector/scanner/typescript_module/explorer.ts @@ -0,0 +1,23 @@ +import Module from "node:module" + +export function findModuleByExportedName( + name: string, + modules: Module[], +): Module | undefined { + for (const module of modules) { + if (module[name as keyof typeof module]) { + return module + } + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getValueByExportedName(name: string, modules: Module[]): any { + for (const module of modules) { + if (module[name as keyof typeof module]) { + return module[name as keyof typeof module] + } + } + + return undefined +} diff --git a/sdk/typescript/introspector/scanner/typescript_module/typedef_utils.ts b/sdk/typescript/introspector/scanner/typescript_module/typedef_utils.ts new file mode 100644 index 00000000000..5930d380ed8 --- /dev/null +++ b/sdk/typescript/introspector/scanner/typescript_module/typedef_utils.ts @@ -0,0 +1,41 @@ +import { TypeDefKind } from "../../../api/client.gen.js" +import { IntrospectionError } from "../../../common/errors/IntrospectionError.js" +import { TypeDef } from "../typedef.js" + +export function isTypeDefResolved(typeDef: TypeDef): boolean { + if (typeDef.kind !== TypeDefKind.ListKind) { + return true + } + + const arrayTypeDef = typeDef as TypeDef + + if (arrayTypeDef.typeDef === undefined) { + return false + } + + if (arrayTypeDef.typeDef.kind === TypeDefKind.ListKind) { + return isTypeDefResolved(arrayTypeDef.typeDef) + } + + return true +} + +export function resolveTypeDef( + typeDef: TypeDef | undefined, + reference: TypeDef, +): TypeDef { + if (typeDef === undefined) { + return reference + } + + if (typeDef.kind === TypeDefKind.ListKind) { + const listTypeDef = typeDef as TypeDef + + listTypeDef.typeDef = resolveTypeDef(listTypeDef.typeDef, reference) + return listTypeDef + } + + throw new IntrospectionError( + `type ${JSON.stringify(typeDef)} has already been resolved, it should not be overwritten ; reference: ${JSON.stringify(reference)}`, + ) +} diff --git a/sdk/typescript/introspector/scanner/utils.ts b/sdk/typescript/introspector/scanner/utils.ts deleted file mode 100644 index a23c2e855a1..00000000000 --- a/sdk/typescript/introspector/scanner/utils.ts +++ /dev/null @@ -1,131 +0,0 @@ -import ts from "typescript" - -import { TypeDefKind } from "../../api/client.gen.js" -import { TypeDef } from "./typeDefs.js" - -/** - * Return true if the given class declaration has the decorator @obj() on - * top of its declaration. - * @param object - */ -export function isObject(object: ts.ClassDeclaration): boolean { - return ( - ts.getDecorators(object)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === "object" - } - - return false - }) !== undefined - ) -} - -export function toPascalCase(input: string): string { - const words = input - .replace(/[^a-zA-Z0-9]/g, " ") // Replace non-alphanumeric characters with spaces - .split(/\s+/) - .filter((word) => word.length > 0) - - if (words.length === 0) { - return "" // No valid words found - } - - // It's an edge case when moduleName is already in PascalCase or camelCase - if (words.length === 1) { - return words[0].charAt(0).toUpperCase() + words[0].slice(1) - } - - const pascalCase = words - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join("") - - return pascalCase -} - -/** - * Return true if the given method has the decorator @func() on top - * of its declaration. - * - * @param method The method to check - */ -export function isFunction(method: ts.MethodDeclaration): boolean { - return ( - ts.getDecorators(method)?.find((d) => { - if (ts.isCallExpression(d.expression)) { - return d.expression.expression.getText() === "func" - } - - return false - }) !== undefined - ) -} - -/** - * Convert a type into a Dagger Typedef using dynamic typing. - */ -export function typeToTypedef( - checker: ts.TypeChecker, - type: ts.Type, -): TypeDef { - if (type.symbol?.name === "Promise") { - const typeArgs = checker.getTypeArguments(type as ts.TypeReference) - if (typeArgs.length > 0) { - return typeToTypedef(checker, typeArgs[0]) - } - } - - if (type.symbol?.name === "Array") { - const typeArgs = checker.getTypeArguments(type as ts.TypeReference) - if (typeArgs.length === 0) { - throw new Error("Generic array not supported") - } - return { - kind: TypeDefKind.ListKind, - typeDef: typeToTypedef(checker, typeArgs[0]), - } - } - - const strType = checker.typeToString(type) - - switch (strType) { - case "string": - return { kind: TypeDefKind.StringKind } - case "number": - return { kind: TypeDefKind.IntegerKind } - case "boolean": - return { kind: TypeDefKind.BooleanKind } - case "void": - return { kind: TypeDefKind.VoidKind } - // Intercept primitive types and throw error in this case - case "String": - throw new Error( - "Use of primitive String type detected, did you mean string?", - ) - case "Number": - throw new Error( - "Use of primitive Number type detected, did you mean number?", - ) - case "Boolean": - throw new Error( - "Use of primitive Boolean type detected, did you mean boolean?", - ) - default: - // If it's a class or interface, it's an object - if (type.symbol?.name && type.isClassOrInterface()) { - return { - kind: TypeDefKind.ObjectKind, - name: strType, - } - } - - // If it's a union, then it's a scalar type - if (type.isUnionOrIntersection()) { - return { - kind: TypeDefKind.ScalarKind, - name: strType, - } - } - - throw new Error(`Unsupported type ${strType}`) - } -} diff --git a/sdk/typescript/introspector/test/case_convertor.spec.ts b/sdk/typescript/introspector/test/case_convertor.spec.ts new file mode 100644 index 00000000000..a68d0457e44 --- /dev/null +++ b/sdk/typescript/introspector/test/case_convertor.spec.ts @@ -0,0 +1,37 @@ +import assert from "assert" + +import { convertToPascalCase } from "../scanner/case_convertor.js" + +describe("case convertor", function () { + describe("convertToPascalCase", function () { + it("should convert kebab-case to pascal case", function () { + const result = convertToPascalCase("hello-world") + assert.equal(result, "HelloWorld") + }) + + it("should convert snake-case to pascal case", function () { + const result = convertToPascalCase("hello_world") + assert.equal(result, "HelloWorld") + }) + + it("should convert camel case to pascal case", function () { + const result = convertToPascalCase("helloWorld") + assert.equal(result, "HelloWorld") + }) + + it("should convert single word to pascal case", function () { + const result = convertToPascalCase("hello") + assert.equal(result, "Hello") + }) + + it("should convert empty string to empty string", function () { + const result = convertToPascalCase("") + assert.equal(result, "") + }) + + it("should keep pascal case if already valid", function () { + const result = convertToPascalCase("HelloWorld") + assert.equal(result, "HelloWorld") + }) + }) +}) diff --git a/sdk/typescript/introspector/test/invoke.spec.ts b/sdk/typescript/introspector/test/invoke.spec.ts index 6c3682cf72e..f491afb13e5 100644 --- a/sdk/typescript/introspector/test/invoke.spec.ts +++ b/sdk/typescript/introspector/test/invoke.spec.ts @@ -32,7 +32,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "helloWorld") // Mocking the fetch from the dagger API const input = { @@ -57,7 +57,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "multipleObjects") // Mocking the fetch from the dagger API const input = { @@ -88,7 +88,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "multiArgs") // Mocking the fetch from the dagger API const input = { @@ -120,7 +120,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "state") // We wrap the execution into a Dagger connection await connection( @@ -196,7 +196,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "multipleObjectsAsFields") const constructorInput = { parentName: "MultipleObjectsAsFields", @@ -302,7 +302,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "variadic") // We wrap the execution into a Dagger connection await connection(async () => { @@ -326,7 +326,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "alias") // We wrap the execution into a Dagger connection await connection(async () => { @@ -370,7 +370,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "alias") await connection(async () => { const constructorInput = { parentName: "Alias", // class name @@ -414,7 +414,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "optionalParameter") // Mocking the fetch from the dagger API const input = { @@ -439,7 +439,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "optionalParameter") // Mocking the fetch from the dagger API const input = { @@ -471,7 +471,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "objectParam") const inputUpper = { parentName: "ObjectParam", @@ -519,7 +519,7 @@ describe("Invoke typescript function", function () { const modules = await load(files) const executor = new Executor(modules) - const scanResult = scan(files) + const scanResult = await scan(files, "list") const input = { parentName: "List", @@ -550,7 +550,7 @@ describe("Invoke typescript function", function () { } const executor = new Executor(modules) - const module = scan(files) + const module = await scan(files, "enums") const inputDefault = { parentName: "Enums", diff --git a/sdk/typescript/introspector/test/scan.spec.ts b/sdk/typescript/introspector/test/scan.spec.ts index 893fb15b4ed..05f0d4be900 100644 --- a/sdk/typescript/introspector/test/scan.spec.ts +++ b/sdk/typescript/introspector/test/scan.spec.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import assert from "assert" import * as fs from "fs" -import { describe, it } from "mocha" -import * as path from "path" +import path from "path" import { fileURLToPath } from "url" import { scan } from "../scanner/scan.js" @@ -17,89 +16,104 @@ type TestCase = { directory: string } -describe("scan static TypeScript", function () { +describe("scan by reference TypeScript", function () { const testCases: TestCase[] = [ { name: "Should correctly scan a basic class with one method", directory: "helloWorld", }, { - name: "Should correctly scan multiple arguments", - directory: "multiArgs", + name: "Should correctly resolve references", + directory: "references", }, { - name: "Should supports multiple files and classes that returns classes", - directory: "multipleObjects", - }, - { - name: "Should not expose private methods from a class", - directory: "privateMethod", + name: "Should correctly handle optional parameters", + directory: "optionalParameter", }, { - name: "Should scan classes' properties to keep a state", - directory: "state", + name: "Should correctly handle context", + directory: "context", }, { - name: "Should detect optional parameters of a method", - directory: "optionalParameter", + name: "Should correctly scan scalar", + directory: "scalar", }, { - name: "Should correctly handle function with void return", - directory: "voidReturn", + name: "Should correctly scan enums", + directory: "enums", }, { - name: "Should introspect constructor", - directory: "constructor", + name: "Should correctly scan list", + directory: "list", }, { - name: "Should correctly scan variadic arguments", + name: "Should correctly scan variadic", directory: "variadic", }, { - name: "Should correctly scan alias", - directory: "alias", + name: "Should correctly scan void return", + directory: "voidReturn", }, { - name: "Should correctly serialize object param", - directory: "objectParam", + name: "Should correctly scan state", + directory: "state", }, { - name: "Should correctly scan multiple objects as fields", - directory: "multipleObjectsAsFields", + name: "Should correctly scan private method", + directory: "privateMethod", }, { - name: "Should correctly scan scalar arguments", - directory: "scalar", + name: "Should correctly scan object param", + directory: "objectParam", }, { - name: "Should correctly scan list of objects", - directory: "list", + name: "Should correctly scan multiple objects as fields", + directory: "multipleObjectsAsFields", }, { - name: "Should correctly scan enums", - directory: "enums", + name: "Should correctly scan multiple objects", + directory: "multipleObjects", }, { name: "Should correctly scan core enums", directory: "coreEnums", }, { - name: "Should correctly scan contextual arguments", - directory: "context", + name: "Should correctly scan constructor", + directory: "constructor", + }, + { + name: "Should correctly scan alias", + directory: "alias", }, ] for (const test of testCases) { it(test.name, async function () { - const files = await listFiles(`${rootDirectory}/${test.directory}`) - const result = scan(files, test.directory) - const jsonResult = JSON.stringify(result, null, 2) - const expected = fs.readFileSync( - `${rootDirectory}/${test.directory}/expected.json`, - "utf-8", - ) + this.timeout(60000) + + try { + const files = await listFiles(`${rootDirectory}/${test.directory}`) + const result = await scan(files, test.directory) + const jsonResult = JSON.stringify(result, null, 2) + const expected = fs.readFileSync( + `${rootDirectory}/${test.directory}/expected.json`, + "utf-8", + ) - assert.deepEqual(JSON.parse(jsonResult), JSON.parse(expected)) + assert.deepStrictEqual( + JSON.parse(jsonResult), + JSON.parse(expected), + ` +Expected: +${expected} +Got: +${jsonResult} + `, + ) + } catch (e) { + assert.fail(e as Error) + } }) } @@ -117,10 +131,13 @@ describe("scan static TypeScript", function () { try { const files = await listFiles(`${rootDirectory}/invalid`) - scan(files, "invalid") + await scan(files, "invalid") assert.fail("Should throw an error") } catch (e: any) { - assert.equal(e.message, "no objects found in the module") + assert.equal( + e.message, + "could not find module entrypoint: class Invalid. Please export it.", + ) } }) @@ -128,10 +145,13 @@ describe("scan static TypeScript", function () { try { const files = await listFiles(`${rootDirectory}/noDecorators`) - scan(files, "noDecorators") + await scan(files, "noDecorators") assert.fail("Should throw an error") } catch (e: any) { - assert.equal(e.message, "no objects found in the module") + assert.match( + e.message, + /is used by the module but not exposed with a dagger decorator/, + ) } }) @@ -139,7 +159,7 @@ describe("scan static TypeScript", function () { try { const files = await listFiles(`${rootDirectory}/primitives`) - const f = scan(files, "primitives") + const f = await scan(files, "primitives") // Trigger the module resolution with a strigify JSON.stringify(f, null, 2) @@ -147,7 +167,7 @@ describe("scan static TypeScript", function () { } catch (e: any) { assert.equal( e.message, - "Use of primitive String type detected, did you mean string?", + 'String is a reserved word, please use "string" instead.', ) } }) diff --git a/sdk/typescript/introspector/test/testdata/alias/expected.json b/sdk/typescript/introspector/test/testdata/alias/expected.json index 30d4c00ea5b..61e3dd37669 100644 --- a/sdk/typescript/introspector/test/testdata/alias/expected.json +++ b/sdk/typescript/introspector/test/testdata/alias/expected.json @@ -1,72 +1,11 @@ { "name": "Alias", "objects": { - "Bar": { - "name": "Bar", - "description": "", - "constructor": { - "args": { - "baz": { - "name": "baz", - "description": "", - "type": { - "kind": "STRING_KIND" - }, - "isVariadic": false, - "isNullable": false, - "isOptional": false, - "defaultValue": "\"baz\"" - }, - "foo": { - "name": "foo", - "description": "", - "type": { - "kind": "INTEGER_KIND" - }, - "isVariadic": false, - "isNullable": false, - "isOptional": false, - "defaultValue": "4" - } - } - }, - "methods": { - "zoo": { - "name": "za", - "description": "", - "alias": "zoo", - "arguments": {}, - "returnType": { - "kind": "STRING_KIND" - } - } - }, - "properties": { - "bar": { - "name": "baz", - "description": "", - "alias": "bar", - "type": { - "kind": "STRING_KIND" - }, - "isExposed": true - }, - "oof": { - "name": "foo", - "description": "", - "alias": "oof", - "type": { - "kind": "INTEGER_KIND" - }, - "isExposed": true - } - } - }, "Alias": { "name": "Alias", "description": "", "constructor": { - "args": { + "arguments": { "ctr": { "name": "ctr", "description": "", @@ -193,6 +132,67 @@ "isExposed": true } } + }, + "Bar": { + "name": "Bar", + "description": "", + "constructor": { + "arguments": { + "baz": { + "name": "baz", + "description": "", + "type": { + "kind": "STRING_KIND" + }, + "isVariadic": false, + "isNullable": false, + "isOptional": true, + "defaultValue": "baz" + }, + "foo": { + "name": "foo", + "description": "", + "type": { + "kind": "INTEGER_KIND" + }, + "isVariadic": false, + "isNullable": false, + "isOptional": true, + "defaultValue": "4" + } + } + }, + "methods": { + "zoo": { + "name": "za", + "description": "", + "alias": "zoo", + "arguments": {}, + "returnType": { + "kind": "STRING_KIND" + } + } + }, + "properties": { + "bar": { + "name": "baz", + "description": "", + "alias": "bar", + "type": { + "kind": "STRING_KIND" + }, + "isExposed": true + }, + "oof": { + "name": "foo", + "description": "", + "alias": "oof", + "type": { + "kind": "INTEGER_KIND" + }, + "isExposed": true + } + } } }, "enums": {} diff --git a/sdk/typescript/introspector/test/testdata/constructor/expected.json b/sdk/typescript/introspector/test/testdata/constructor/expected.json index 6cea512687d..3391361c06a 100644 --- a/sdk/typescript/introspector/test/testdata/constructor/expected.json +++ b/sdk/typescript/introspector/test/testdata/constructor/expected.json @@ -1,11 +1,12 @@ { "name": "Constructor", + "description": "Constructor module", "objects": { - "HelloWorld": { - "name": "HelloWorld", - "description": "HelloWorld class", + "Constructor": { + "name": "Constructor", + "description": "Constructor class", "constructor": { - "args": { + "arguments": { "name": { "name": "name", "description": "", @@ -14,8 +15,8 @@ }, "isVariadic": false, "isNullable": false, - "isOptional": false, - "defaultValue": "\"world\"" + "isOptional": true, + "defaultValue": "world" } } }, diff --git a/sdk/typescript/introspector/test/testdata/constructor/index.ts b/sdk/typescript/introspector/test/testdata/constructor/index.ts index ceeb512f5b6..d33e1ff3197 100644 --- a/sdk/typescript/introspector/test/testdata/constructor/index.ts +++ b/sdk/typescript/introspector/test/testdata/constructor/index.ts @@ -4,10 +4,10 @@ import { func, object } from "../../../decorators/decorators.js" /** - * HelloWorld class + * Constructor class */ @object() -export class HelloWorld { +export class Constructor { name: string constructor(name: string = "world") { diff --git a/sdk/typescript/introspector/test/testdata/context/index.ts b/sdk/typescript/introspector/test/testdata/context/index.ts index 2437b31087e..99c75bba751 100644 --- a/sdk/typescript/introspector/test/testdata/context/index.ts +++ b/sdk/typescript/introspector/test/testdata/context/index.ts @@ -1,12 +1,11 @@ -import { func, object, argument } from "../../../decorators/decorators.js" import { Directory } from "../../../../api/client.gen.js" -import { dag } from "../../../../api/client.gen.js" +import { func, object, argument } from "../../../decorators/decorators.js" @object() export class Context { @func() helloWorld( - @argument({ defaultPath: "." }) + @argument({ defaultPath: "." }) dir: Directory, ): string { return `hello ${name}` @@ -14,9 +13,9 @@ export class Context { @func() helloWorldIgnored( - @argument({ defaultPath: ".", ignore: ["dir"] }) + @argument({ defaultPath: ".", ignore: ["dir"] }) dir: Directory, ): string { return `hello ${name}` } -} \ No newline at end of file +} diff --git a/sdk/typescript/introspector/test/testdata/coreEnums/index.ts b/sdk/typescript/introspector/test/testdata/coreEnums/index.ts index e0a13f9801c..d7c2abe5384 100644 --- a/sdk/typescript/introspector/test/testdata/coreEnums/index.ts +++ b/sdk/typescript/introspector/test/testdata/coreEnums/index.ts @@ -1,7 +1,6 @@ -import { ImageLayerCompression } from '../../../../api/client.gen.js' +import { ImageLayerCompression } from "../../../../api/client.gen.js" import { func, object } from "../../../decorators/decorators.js" - @object() export class CoreEnums { @func() diff --git a/sdk/typescript/introspector/test/testdata/enums/index.ts b/sdk/typescript/introspector/test/testdata/enums/index.ts index 47e7275a92b..417d69a67bd 100644 --- a/sdk/typescript/introspector/test/testdata/enums/index.ts +++ b/sdk/typescript/introspector/test/testdata/enums/index.ts @@ -1,10 +1,15 @@ -import { func, object, field, enumType } from "../../../decorators/decorators.js" +import { + enumType, + field, + func, + object, +} from "../../../decorators/decorators.js" /** * Enum for Status */ @enumType() -class Status { +export class Status { /** * Active status */ diff --git a/sdk/typescript/introspector/test/testdata/list/expected.json b/sdk/typescript/introspector/test/testdata/list/expected.json index 58de02735f9..03befd14133 100644 --- a/sdk/typescript/introspector/test/testdata/list/expected.json +++ b/sdk/typescript/introspector/test/testdata/list/expected.json @@ -5,7 +5,7 @@ "name": "Integer", "description": "", "constructor": { - "args": { + "arguments": { "value": { "name": "value", "description": "", diff --git a/sdk/typescript/introspector/test/testdata/multipleObjectsAsFields/expected.json b/sdk/typescript/introspector/test/testdata/multipleObjectsAsFields/expected.json index 04221ce3a0f..0388f20e5a8 100644 --- a/sdk/typescript/introspector/test/testdata/multipleObjectsAsFields/expected.json +++ b/sdk/typescript/introspector/test/testdata/multipleObjectsAsFields/expected.json @@ -35,7 +35,7 @@ "name": "MultipleObjectsAsFields", "description": "", "constructor": { - "args": {} + "arguments": {} }, "methods": {}, "properties": { diff --git a/sdk/typescript/introspector/test/testdata/objectParam/expected.json b/sdk/typescript/introspector/test/testdata/objectParam/expected.json index 5ed0c887e4f..38a6cc57243 100644 --- a/sdk/typescript/introspector/test/testdata/objectParam/expected.json +++ b/sdk/typescript/introspector/test/testdata/objectParam/expected.json @@ -5,7 +5,7 @@ "name": "Message", "description": "", "constructor": { - "args": { + "arguments": { "content": { "name": "content", "description": "", diff --git a/sdk/typescript/introspector/test/testdata/optionalParameter/expected.json b/sdk/typescript/introspector/test/testdata/optionalParameter/expected.json index 6587b0fa995..8c8715f64a4 100644 --- a/sdk/typescript/introspector/test/testdata/optionalParameter/expected.json +++ b/sdk/typescript/introspector/test/testdata/optionalParameter/expected.json @@ -55,7 +55,7 @@ }, "isVariadic": false, "isNullable": false, - "isOptional": false, + "isOptional": true, "defaultValue": "0" }, "b": { @@ -66,7 +66,7 @@ }, "isVariadic": false, "isNullable": false, - "isOptional": false, + "isOptional": true, "defaultValue": "0" } }, @@ -86,7 +86,7 @@ }, "isVariadic": false, "isNullable": false, - "isOptional": false, + "isOptional": true, "defaultValue": "false" } }, @@ -136,8 +136,8 @@ }, "isVariadic": false, "isNullable": false, - "isOptional": false, - "defaultValue": "\"foo\"" + "isOptional": true, + "defaultValue": "foo" }, "e": { "name": "e", @@ -159,7 +159,7 @@ "isVariadic": false, "isNullable": true, "isOptional": true, - "defaultValue": "\"bar\"" + "defaultValue": "bar" } }, "returnType": { @@ -181,7 +181,13 @@ }, "isVariadic": false, "isNullable": false, - "isOptional": false + "isOptional": true, + "defaultValue": [ + "a", + "b", + "c", + "d" + ] }, "b": { "name": "b", diff --git a/sdk/typescript/introspector/test/testdata/optionalParameter/index.ts b/sdk/typescript/introspector/test/testdata/optionalParameter/index.ts index af8b292b92d..ec0696c986d 100644 --- a/sdk/typescript/introspector/test/testdata/optionalParameter/index.ts +++ b/sdk/typescript/introspector/test/testdata/optionalParameter/index.ts @@ -39,7 +39,7 @@ export class OptionalParameter { @func() array( - a: string[], + a: string[] = ["a", "b", "c", "d"], b: (string | null)[], c: (string | null)[] | null, ): string { diff --git a/sdk/typescript/introspector/test/testdata/primitives/index.ts b/sdk/typescript/introspector/test/testdata/primitives/index.ts index 46c2f3a6fb3..25e9244e31e 100644 --- a/sdk/typescript/introspector/test/testdata/primitives/index.ts +++ b/sdk/typescript/introspector/test/testdata/primitives/index.ts @@ -16,4 +16,4 @@ export class Primitives { integer(v: Number): Number { return v } -} \ No newline at end of file +} diff --git a/sdk/typescript/introspector/test/testdata/references/expected.json b/sdk/typescript/introspector/test/testdata/references/expected.json new file mode 100644 index 00000000000..382d8d19c36 --- /dev/null +++ b/sdk/typescript/introspector/test/testdata/references/expected.json @@ -0,0 +1,183 @@ +{ + "name": "References", + "objects": { + "References": { + "name": "References", + "description": "References class\n\ntest", + "constructor": { + "arguments": { + "data": { + "name": "data", + "description": "", + "type": { + "kind": "LIST_KIND", + "typeDef": { + "kind": "OBJECT_KIND", + "name": "Data" + } + }, + "isVariadic": false, + "isNullable": false, + "isOptional": true, + "defaultValue": [] + } + } + }, + "methods": { + "appendData": { + "name": "addData", + "description": "", + "alias": "appendData", + "arguments": { + "data": { + "name": "data", + "description": "", + "type": { + "kind": "LIST_KIND", + "typeDef": { + "kind": "OBJECT_KIND", + "name": "Data" + } + }, + "isVariadic": false, + "isNullable": false, + "isOptional": false + } + }, + "returnType": { + "kind": "OBJECT_KIND", + "name": "References" + } + }, + "dumpDatas": { + "name": "dumpDatas", + "description": "", + "arguments": {}, + "returnType": { + "kind": "LIST_KIND", + "typeDef": { + "kind": "OBJECT_KIND", + "name": "Data" + } + } + }, + "testEnum": { + "name": "testEnum", + "description": "", + "arguments": { + "test": { + "name": "test", + "description": "", + "type": { + "kind": "ENUM_KIND", + "name": "TestEnum" + }, + "isVariadic": false, + "isNullable": false, + "isOptional": true, + "defaultValue": "b" + } + }, + "returnType": { + "kind": "ENUM_KIND", + "name": "TestEnum" + } + }, + "testEnumStatic": { + "name": "testEnumStatic", + "description": "", + "arguments": { + "test": { + "name": "test", + "description": "", + "type": { + "kind": "ENUM_KIND", + "name": "TestEnum" + }, + "isVariadic": false, + "isNullable": false, + "isOptional": true, + "defaultValue": "a" + } + }, + "returnType": { + "kind": "ENUM_KIND", + "name": "TestEnum" + } + }, + "testDefaultValue": { + "name": "testDefaultValue", + "description": "Doc\n\nfoo", + "arguments": { + "foo": { + "name": "foo", + "description": "Doc\n\ntest", + "type": { + "kind": "STRING_KIND" + }, + "isVariadic": false, + "isNullable": false, + "isOptional": true, + "defaultValue": "a" + } + }, + "returnType": { + "kind": "STRING_KIND" + } + } + }, + "properties": { + "data": { + "name": "data", + "description": "", + "type": { + "kind": "LIST_KIND", + "typeDef": { + "kind": "OBJECT_KIND", + "name": "Data" + } + }, + "isExposed": true + } + } + }, + "Data": { + "name": "Data", + "description": "Data", + "properties": { + "item1": { + "name": "item1", + "description": "Item 1", + "type": { + "kind": "STRING_KIND" + }, + "isExposed": true + }, + "item2": { + "name": "item2", + "description": "", + "type": { + "kind": "INTEGER_KIND" + }, + "isExposed": true + } + } + } + }, + "enums": { + "TestEnum": { + "name": "TestEnum", + "description": "Test Enum", + "values": { + "A": { + "name": "a", + "description": "Field A" + }, + "B": { + "name": "b", + "description": "Field B" + } + } + } + } +} \ No newline at end of file diff --git a/sdk/typescript/introspector/test/testdata/references/index.ts b/sdk/typescript/introspector/test/testdata/references/index.ts new file mode 100644 index 00000000000..364473702fc --- /dev/null +++ b/sdk/typescript/introspector/test/testdata/references/index.ts @@ -0,0 +1,57 @@ +import { func, object } from "../../../decorators/decorators.js" +import { defaultEnum, TestEnum } from "./types.js" +import type { STR, Data } from "./types.js" + +/** + * References class + * + * test + */ +@object() +export class References { + @func() + data: Data[] + + constructor(data: Data[] = []) { + this.data = data + } + + @func("appendData") + addData(data: Data[]): References { + this.data.push(...data) + + return this + } + + @func() + dumpDatas(): Data[] { + return this.data + } + + @func() + testEnum(test: TestEnum = defaultEnum): TestEnum { + return test + } + + @func() + testEnumStatic(test: TestEnum = TestEnum.A): TestEnum { + return test + } + + /** + * Doc + * + * foo + */ + @func() + async testDefaultValue( + /** + * Doc + * + * test + */ + foo: STR = "a", + ): Promise { + return foo + } +} diff --git a/sdk/typescript/introspector/test/testdata/references/types.ts b/sdk/typescript/introspector/test/testdata/references/types.ts new file mode 100644 index 00000000000..884338f8703 --- /dev/null +++ b/sdk/typescript/introspector/test/testdata/references/types.ts @@ -0,0 +1,29 @@ +/** + * Data + */ +export type Data = { + /** + * Item 1 + */ + item1: string + item2: number +} + +/** + * Test Enum + */ +export enum TestEnum { + /** + * Field A + */ + A = "a", + + /** + * Field B + */ + B = "b", +} + +export const defaultEnum = TestEnum.B + +export type STR = string \ No newline at end of file diff --git a/sdk/typescript/introspector/test/testdata/scalar/index.ts b/sdk/typescript/introspector/test/testdata/scalar/index.ts index ad2ad48a34d..1aff7eb8f1b 100644 --- a/sdk/typescript/introspector/test/testdata/scalar/index.ts +++ b/sdk/typescript/introspector/test/testdata/scalar/index.ts @@ -1,15 +1,15 @@ -import { func, object } from '../../../decorators/decorators.js' -import { Platform } from '../../../../api/client.gen.js' +import type { Platform } from "../../../../api/client.gen.js" +import { func, object } from "../../../decorators/decorators.js" @object() export class Scalar { - @func() - fromPlatform(platform: Platform): string { - return platform as string - } + @func() + fromPlatform(platform: Platform): string { + return platform as string + } - @func() - fromPlatforms(platforms: Platform[]): string[] { - return platforms.map(p => p as string) - } + @func() + fromPlatforms(platforms: Platform[]): string[] { + return platforms.map((p) => p as string) + } } diff --git a/sdk/typescript/introspector/utils/files.ts b/sdk/typescript/introspector/utils/files.ts index 4c9acd839d5..1e8f9d7bc56 100644 --- a/sdk/typescript/introspector/utils/files.ts +++ b/sdk/typescript/introspector/utils/files.ts @@ -27,7 +27,7 @@ export async function listFiles(dir = "."): Promise { const ext = path.extname(filepath) if (allowedExtensions.find((allowedExt) => allowedExt === ext)) { - return [`${dir}/${file}`] + return [path.join(dir, file)] } return []