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 []