diff --git a/.changeset/class-facade-static-interfaces.md b/.changeset/class-facade-static-interfaces.md new file mode 100644 index 00000000..cd4c1a66 --- /dev/null +++ b/.changeset/class-facade-static-interfaces.md @@ -0,0 +1,20 @@ +--- +"@effect/tsgo": patch +--- + +declarations: facet bare-imported class schemas and emit static class namespace interfaces + +Two facade-emit gaps under newer effect Schema types: + +- Heritage detection only matched namespaced constructors (`S.Class(...)` / + `Schema.Opaque(...)`); a bare named import (`import { Class } from "effect-app/Schema"` + → `class X extends Class(...)`) was not recognized, so the class base kept its + fully-inlined `EnhancedClass>` instead of the compact + `OpaqueClassFacade`. Now both heritage shapes are recognized. + +- The class static-property resolver (`CreateTypeOfClassStaticProperty`) serialized + `Type`/`Encoded`/`Make`/`Fields` via the node builder, which under effect #2442 keeps + the deferred mapped helper (`Struct.ReadonlySide<…>`) as a reference — regressing the + generated namespace from static interfaces to mapped-type aliases. Now object-typed + properties are expanded to static `interface`s (matching the struct-facade path); + callable members (`never` services, the base `mapFields`/`copy`) keep their signatures. diff --git a/.changeset/struct-facade-companion-type.md b/.changeset/struct-facade-companion-type.md new file mode 100644 index 00000000..b4846741 --- /dev/null +++ b/.changeset/struct-facade-companion-type.md @@ -0,0 +1,18 @@ +--- +"@effect/tsgo": patch +--- + +declarations: keep a companion type for faceted struct schemas instead of dropping it + +The effect-schema struct facade transform dropped the `export type X = typeof X.Type` +companion of a `const X = S.Struct(...)` model, but only emitted the replacement +`interface X` when the decoded `Type` materialized as an object literal. Under newer +effect Schema types `.Type` can resolve to a mapped type (e.g. +`Struct.ReadonlySide<…, "Type">`), so the facade builder bailed, the const stayed +un-faceted, and the companion alias was dropped with no replacement — leaving `X` +value-only and breaking cross-module `import { type X }` usage (TS2749) under faceted +resolution. + +Now the builder emits the decoded `Type` as a `type X = …` alias when it is not an +object literal (keeping the `interface X` form when it is), and the companion alias is +only dropped when the struct was actually faceted; otherwise the original alias is kept. diff --git a/_patches/029-transformers-declarations-effect-schema.patch b/_patches/029-transformers-declarations-effect-schema.patch new file mode 100644 index 00000000..4d866541 --- /dev/null +++ b/_patches/029-transformers-declarations-effect-schema.patch @@ -0,0 +1,1184 @@ +diff --git a/internal/transformers/declarations/transform.go b/internal/transformers/declarations/transform.go +index b6596e0d7..be48e2eb8 100644 +--- a/internal/transformers/declarations/transform.go ++++ b/internal/transformers/declarations/transform.go +@@ -351,6 +351,7 @@ func (tx *DeclarationTransformer) transformSourceFile(node *ast.SourceFile) *ast + statements := tx.Visitor().VisitNodes(node.Statements) + combinedStatements = tx.transformAndReplaceLatePaintedStatements(statements) + combinedStatements = tx.appendCjsExports(combinedStatements) ++ combinedStatements = tx.createEffectSchemaSourceFileDeclarations(combinedStatements) + combinedStatements.Loc = statements.Loc // setTextRange + if ast.IsExternalOrCommonJSModule(node) { + if ast.IsInJSFile(node.AsNode()) { +@@ -2277,6 +2278,1171 @@ func (tx *DeclarationTransformer) transformEnumDeclaration(input *ast.EnumDeclar + ) + } + ++type effectSchemaRequestBaseInfo struct { ++ modelName string ++ brand *ast.Node ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaSourceFileDeclarations(statements *ast.StatementList) *ast.StatementList { ++ if statements == nil || tx.state.currentSourceFile == nil { ++ return statements ++ } ++ ++ modelNames := map[string]bool{} ++ existingNamespaces := map[string]bool{} ++ classes := map[string]*ast.Node{} ++ schemaClasses := tx.getEffectSchemaOriginalClasses() ++ ++ for _, statement := range statements.Nodes { ++ if className := getEffectSchemaClassName(statement); className != "" && ast.IsClassDeclaration(statement) { ++ classes[className] = statement ++ } ++ } ++ ++ for _, statement := range statements.Nodes { ++ if isEffectSchemaModelNamespace(statement) || isEffectSchemaMaterializedModelNamespace(statement, classes) { ++ name := moduleDeclarationIdentifierName(statement) ++ if name != "" && schemaClasses[name] != nil { ++ modelNames[name] = true ++ existingNamespaces[name] = true ++ } ++ } ++ } ++ ++ requestBaseInfos := map[string]effectSchemaRequestBaseInfo{} ++ for _, statement := range statements.Nodes { ++ if info, ok := getEffectSchemaRequestBaseInfo(statement); ok && classes[info.modelName] != nil && schemaClasses[info.modelName] == nil { ++ requestBaseInfos[info.modelName] = info ++ } ++ } ++ ++ for className, classDeclaration := range schemaClasses { ++ if !existingNamespaces[className] && tx.canCreateEffectSchemaGeneratedNamespace(classDeclaration) { ++ modelNames[className] = true ++ } ++ } ++ ++ // Top-level `const X = S.Struct(...)` / `S.TaggedStruct(...)` schema values. Faceted on ++ // the const itself (it is a value, not a class): the giant `S.Struct<{...}>` annotation ++ // becomes a compact `StructFacade<...>` plus a generated `interface X` (decoded Self) and ++ // a type-only `declare namespace X`. ++ structModelNames := map[string]bool{} ++ for name := range tx.getEffectSchemaOriginalStructs() { ++ if modelNames[name] { ++ continue ++ } ++ if _, ok := requestBaseInfos[name]; ok { ++ continue ++ } ++ if hasTopLevelInterface(statements, name) || hasTopLevelNamespace(statements, name) { ++ continue ++ } ++ if tx.canCreateEffectSchemaGeneratedStructNamespace(name) { ++ structModelNames[name] = true ++ } ++ } ++ ++ if len(modelNames) == 0 && len(requestBaseInfos) == 0 && len(structModelNames) == 0 { ++ return statements ++ } ++ ++ changed := false ++ facetedStructNames := map[string]bool{} ++ next := make([]*ast.Node, 0, len(statements.Nodes)) ++ for _, statement := range statements.Nodes { ++ baseModelName := getEffectSchemaBaseModelName(statement) ++ if baseModelName != "" && modelNames[baseModelName] { ++ classDeclaration := schemaClasses[baseModelName] ++ if classDeclaration == nil { ++ classDeclaration = classes[baseModelName] ++ } ++ if classDeclaration != nil { ++ if updated := tx.updateEffectSchemaBaseDeclaration(statement, baseModelName, classDeclaration, needsEffectSchemaIntermediateClass(classDeclaration)); updated != nil { ++ changed = true ++ next = append(next, updated) ++ continue ++ } ++ } ++ } ++ ++ if requestBaseInfo, ok := requestBaseInfos[baseModelName]; ok { ++ if updated := tx.updateEffectSchemaRequestBaseDeclaration(statement, requestBaseInfo); updated != nil { ++ changed = true ++ next = append(next, updated) ++ continue ++ } ++ } ++ ++ if isEffectSchemaModelNamespace(statement) || isEffectSchemaMaterializedModelNamespace(statement, classes) { ++ name := moduleDeclarationIdentifierName(statement) ++ classDeclaration := schemaClasses[name] ++ if classDeclaration == nil { ++ classDeclaration = classes[name] ++ } ++ if classDeclaration != nil { ++ if updated := tx.updateEffectSchemaNamespaceDeclaration(statement, classDeclaration); updated != nil { ++ changed = true ++ next = append(next, updated) ++ continue ++ } ++ } ++ } ++ ++ className := getEffectSchemaClassName(statement) ++ if requestBaseInfos[className].modelName != "" { ++ classDeclaration := statement ++ var typeInterface *ast.Node ++ if !hasTopLevelInterface(statements, className) { ++ typeInterface = tx.createEffectSchemaTypeInterface(classDeclaration) ++ } ++ var namespace *ast.Node ++ if !hasTopLevelNamespace(statements, className) { ++ namespace = tx.createEffectSchemaGeneratedNamespaceDeclaration(className, classDeclaration) ++ } ++ if typeInterface != nil || namespace != nil { ++ changed = true ++ next = append(next, statement) ++ if typeInterface != nil { ++ next = append(next, typeInterface) ++ } ++ if namespace != nil { ++ next = append(next, namespace) ++ } ++ continue ++ } ++ } ++ ++ if className != "" && modelNames[className] { ++ classDeclaration := schemaClasses[className] ++ if classDeclaration == nil { ++ classDeclaration = statement ++ } ++ usesIntermediate := needsEffectSchemaIntermediateClass(classDeclaration) ++ updatedClass := statement ++ if usesIntermediate { ++ updatedClass = tx.updateEffectSchemaClassDeclaration(statement, className) ++ } ++ var typeInterface *ast.Node ++ if !hasTopLevelInterface(statements, className) { ++ typeInterface = tx.createEffectSchemaTypeInterface(classDeclaration) ++ } ++ var namespace *ast.Node ++ if !existingNamespaces[className] { ++ namespace = tx.createEffectSchemaGeneratedNamespaceDeclaration(className, classDeclaration) ++ } ++ if updatedClass != statement || typeInterface != nil || namespace != nil { ++ changed = true ++ if usesIntermediate { ++ next = append(next, tx.createEffectSchemaIntermediateClass(className)) ++ } ++ next = append(next, updatedClass) ++ if typeInterface != nil { ++ next = append(next, typeInterface) ++ } ++ if namespace != nil { ++ existingNamespaces[className] = true ++ next = append(next, namespace) ++ } ++ continue ++ } ++ } ++ ++ if structName := getEffectSchemaStructVariableName(statement); structName != "" && structModelNames[structName] { ++ if declarations := tx.createEffectSchemaStructDeclarations(statement, structName); declarations != nil { ++ facetedStructNames[structName] = true ++ changed = true ++ next = append(next, declarations...) ++ continue ++ } ++ } ++ ++ if isEffectSchemaStructCompanionTypeAlias(statement, structModelNames) { ++ if facetedStructNames[statement.Name().Text()] { ++ // Dropped — replaced by the generated `interface X` (or `type X`). ++ changed = true ++ continue ++ } ++ // Faceting bailed for this struct (e.g. the decoded `Type` did not ++ // materialize): keep the original `type X = typeof X.Type` so `X` stays a ++ // usable type. Dropping it without a replacement would leave `X` value-only ++ // and break cross-module type usage under faceted resolution. ++ } ++ ++ next = append(next, statement) ++ } ++ ++ if !changed { ++ return statements ++ } ++ return tx.Factory().NewNodeList(next) ++} ++ ++func (tx *DeclarationTransformer) getEffectSchemaOriginalClasses() map[string]*ast.Node { ++ classes := map[string]*ast.Node{} ++ for _, statement := range tx.state.currentSourceFile.Statements.Nodes { ++ if ast.IsClassDeclaration(statement) && statement.Name() != nil && hasEffectSchemaOpaqueHeritage(statement) { ++ classes[statement.Name().Text()] = statement ++ } ++ } ++ return classes ++} ++ ++// --- Struct/TaggedStruct const faceting --- ++ ++func (tx *DeclarationTransformer) getEffectSchemaOriginalStructs() map[string]bool { ++ structs := map[string]bool{} ++ for _, statement := range tx.state.currentSourceFile.Statements.Nodes { ++ name := getEffectSchemaStructVariableName(statement) ++ if name == "" { ++ continue ++ } ++ decl := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0] ++ if decl.AsVariableDeclaration().Initializer != nil && isEffectSchemaStructInitializer(decl.AsVariableDeclaration().Initializer) { ++ structs[name] = true ++ } ++ } ++ return structs ++} ++ ++func isEffectSchemaStructInitializer(expression *ast.Node) bool { ++ if !ast.IsCallExpression(expression) { ++ return false ++ } ++ callee := expression.AsCallExpression().Expression ++ if !ast.IsPropertyAccessExpression(callee) || callee.Name() == nil { ++ return false ++ } ++ name := callee.Name().Text() ++ if name != "Struct" && name != "TaggedStruct" { ++ return false ++ } ++ left := callee.Expression() ++ return left != nil && ast.IsIdentifier(left) && (left.Text() == "S" || left.Text() == "Schema") ++} ++ ++func getEffectSchemaStructVariableName(statement *ast.Node) string { ++ if !ast.IsVariableStatement(statement) || statement.AsVariableStatement().DeclarationList == nil { ++ return "" ++ } ++ declarations := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes ++ if len(declarations) != 1 || declarations[0].Name() == nil || !ast.IsIdentifier(declarations[0].Name()) { ++ return "" ++ } ++ return declarations[0].Name().Text() ++} ++ ++func (tx *DeclarationTransformer) getEffectSchemaSourceStructDeclaration(modelName string) *ast.Node { ++ for _, statement := range tx.state.currentSourceFile.Statements.Nodes { ++ if getEffectSchemaStructVariableName(statement) != modelName { ++ continue ++ } ++ decl := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0] ++ if decl.AsVariableDeclaration().Initializer != nil && isEffectSchemaStructInitializer(decl.AsVariableDeclaration().Initializer) { ++ return decl ++ } ++ } ++ return nil ++} ++ ++// Reads a property (Encoded / Type / ~type.make.in / fields / services) off the source ++// struct value's type and serializes it; `never` services stay `never`. ++func (tx *DeclarationTransformer) materializeEffectSchemaStructProperty(modelName string, propertyName string) *ast.Node { ++ declaration := tx.getEffectSchemaSourceStructDeclaration(modelName) ++ if declaration == nil { ++ return nil ++ } ++ return tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeOfStructSchemaProperty(tx.EmitContext(), declaration, propertyName, tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++} ++ ++func (tx *DeclarationTransformer) canCreateEffectSchemaGeneratedStructNamespace(modelName string) bool { ++ return tx.materializeEffectSchemaStructProperty(modelName, "Encoded") != nil ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaStructInterfaceFromProperty(modelName string, propertyName string, declaredName string) *ast.Node { ++ typeNode := tx.materializeEffectSchemaStructProperty(modelName, propertyName) ++ if typeNode == nil { ++ return nil ++ } ++ if ast.IsTypeLiteralNode(typeNode) { ++ return tx.Factory().NewInterfaceDeclaration(nil, tx.Factory().NewIdentifier(declaredName), nil, nil, tx.Factory().NewNodeList(typeNode.AsTypeLiteralNode().Members.Nodes)) ++ } ++ return tx.Factory().NewTypeAliasDeclaration(nil, tx.Factory().NewIdentifier(declaredName), nil, typeNode) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaStructServiceDeclaration(modelName string, name string) *ast.Node { ++ resolved := tx.materializeEffectSchemaStructProperty(modelName, name) ++ serviceType := resolved ++ if resolved == nil || resolved.Kind == ast.KindAnyKeyword { ++ serviceType = tx.Factory().NewKeywordTypeNode(ast.KindNeverKeyword) ++ } ++ return tx.Factory().NewTypeAliasDeclaration(nil, tx.Factory().NewIdentifier(name), nil, serviceType) ++} ++ ++func structHasExportModifier(statement *ast.Node) bool { ++ return ast.HasSyntacticModifier(statement, ast.ModifierFlagsExport) ++} ++ ++func (tx *DeclarationTransformer) effectSchemaStructModifiers(exported bool, includeDeclare bool) *ast.ModifierList { ++ modifiers := []*ast.Node{} ++ if exported { ++ modifiers = append(modifiers, tx.Factory().NewModifier(ast.KindExportKeyword)) ++ } ++ if includeDeclare { ++ modifiers = append(modifiers, tx.Factory().NewModifier(ast.KindDeclareKeyword)) ++ } ++ if len(modifiers) == 0 { ++ return nil ++ } ++ return tx.Factory().NewModifierList(modifiers) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaGeneratedStructNamespace(modelName string, exported bool) *ast.Node { ++ fields := tx.createEffectSchemaStructInterfaceFromProperty(modelName, "fields", "Fields") ++ if fields == nil { ++ return nil ++ } ++ encoded := tx.createEffectSchemaStructInterfaceFromProperty(modelName, "Encoded", "Encoded") ++ if encoded == nil { ++ return nil ++ } ++ statements := []*ast.Node{fields, encoded} ++ if makeDeclaration := tx.createEffectSchemaStructInterfaceFromProperty(modelName, "~type.make.in", "Make"); makeDeclaration != nil { ++ statements = append(statements, makeDeclaration) ++ } ++ statements = append(statements, tx.createEffectSchemaStructServiceDeclaration(modelName, "DecodingServices")) ++ statements = append(statements, tx.createEffectSchemaStructServiceDeclaration(modelName, "EncodingServices")) ++ return tx.Factory().NewModuleDeclaration( ++ tx.effectSchemaStructModifiers(exported, true), ++ ast.KindNamespaceKeyword, ++ tx.Factory().NewIdentifier(modelName), ++ tx.Factory().NewModuleBlock(tx.Factory().NewNodeList(statements)), ++ ) ++} ++ ++// `S.StructFacade` — ++// `StructFacade` is exported from effect-app (>= 4.0.0-beta.279), so it resolves through the ++// file's own `S` (effect-app/Schema) import, exactly like `S.OpaqueFacade`. It extends ++// `S.Struct`, so the faceted value stays Workflow-compatible. ++func (tx *DeclarationTransformer) createEffectSchemaStructFacadeType(modelName string) *ast.Node { ++ member := func(name string) *ast.Node { ++ return tx.Factory().NewTypeReferenceNode(tx.Factory().NewQualifiedName(tx.Factory().NewIdentifier(modelName), tx.Factory().NewIdentifier(name)), nil) ++ } ++ typeArguments := tx.Factory().NewNodeList([]*ast.Node{ ++ tx.Factory().NewTypeReferenceNode(tx.Factory().NewIdentifier(modelName), nil), ++ member("Encoded"), ++ member("Make"), ++ member("DecodingServices"), ++ member("EncodingServices"), ++ member("Fields"), ++ }) ++ return tx.Factory().NewTypeReferenceNode( ++ tx.Factory().NewQualifiedName(tx.Factory().NewIdentifier("S"), tx.Factory().NewIdentifier("StructFacade")), ++ typeArguments, ++ ) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaStructDeclarations(statement *ast.Node, modelName string) []*ast.Node { ++ typeNode := tx.materializeEffectSchemaStructProperty(modelName, "Type") ++ if typeNode == nil || !ast.IsTypeLiteralNode(typeNode) { ++ return nil ++ } ++ namespace := tx.createEffectSchemaGeneratedStructNamespace(modelName, structHasExportModifier(statement)) ++ if namespace == nil { ++ return nil ++ } ++ exported := structHasExportModifier(statement) ++ declaration := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0] ++ updatedDeclaration := tx.Factory().UpdateVariableDeclaration( ++ declaration.AsVariableDeclaration(), ++ declaration.Name(), ++ declaration.AsVariableDeclaration().ExclamationToken, ++ tx.createEffectSchemaStructFacadeType(modelName), ++ declaration.AsVariableDeclaration().Initializer, ++ ) ++ declarations := tx.Factory().NewNodeList([]*ast.Node{updatedDeclaration}) ++ declarationList := tx.Factory().UpdateVariableDeclarationList(statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList(), declarations, statement.AsVariableStatement().DeclarationList.Flags) ++ retypedConst := tx.Factory().UpdateVariableStatement(statement.AsVariableStatement(), statement.Modifiers(), declarationList) ++ typeInterface := tx.Factory().NewInterfaceDeclaration(tx.effectSchemaStructModifiers(exported, false), tx.Factory().NewIdentifier(modelName), nil, nil, tx.Factory().NewNodeList(typeNode.AsTypeLiteralNode().Members.Nodes)) ++ return []*ast.Node{retypedConst, typeInterface, namespace} ++} ++ ++type generatedTypeImport struct { ++ importedName string ++ moduleSpecifier string ++} ++ ++func (tx *DeclarationTransformer) generatedTypeNamedImports() map[string]generatedTypeImport { ++ imports := map[string]generatedTypeImport{} ++ for _, statement := range tx.state.currentSourceFile.Statements.Nodes { ++ if !ast.IsImportDeclaration(statement) { ++ continue ++ } ++ decl := statement.AsImportDeclaration() ++ if decl.ModuleSpecifier == nil || !ast.IsStringLiteralLike(decl.ModuleSpecifier) || decl.ImportClause == nil || decl.ImportClause.AsImportClause().NamedBindings == nil { ++ continue ++ } ++ moduleSpecifier := decl.ModuleSpecifier.Text() ++ namedBindings := decl.ImportClause.AsImportClause().NamedBindings ++ if !ast.IsNamedImports(namedBindings) { ++ continue ++ } ++ for _, specifier := range namedBindings.AsNamedImports().Elements.Nodes { ++ importedName := specifier.Name().Text() ++ if specifier.AsImportSpecifier().PropertyName != nil { ++ importedName = specifier.AsImportSpecifier().PropertyName.Text() ++ } ++ imports[specifier.Name().Text()] = generatedTypeImport{importedName: importedName, moduleSpecifier: moduleSpecifier} ++ } ++ } ++ return imports ++} ++ ++func (tx *DeclarationTransformer) generatedTypeNamespaceImports() map[string]string { ++ imports := map[string]string{} ++ for _, statement := range tx.state.currentSourceFile.Statements.Nodes { ++ if !ast.IsImportDeclaration(statement) { ++ continue ++ } ++ decl := statement.AsImportDeclaration() ++ if decl.ModuleSpecifier == nil || !ast.IsStringLiteralLike(decl.ModuleSpecifier) || decl.ImportClause == nil || decl.ImportClause.AsImportClause().NamedBindings == nil { ++ continue ++ } ++ namedBindings := decl.ImportClause.AsImportClause().NamedBindings ++ if ast.IsNamespaceImport(namedBindings) { ++ imports[decl.ModuleSpecifier.Text()] = namedBindings.Name().Text() ++ } ++ } ++ return imports ++} ++ ++func (tx *DeclarationTransformer) normalizeGeneratedImportedTypes(typeNode *ast.Node) *ast.Node { ++ if typeNode == nil { ++ return nil ++ } ++ namedImports := tx.generatedTypeNamedImports() ++ if len(namedImports) == 0 { ++ return typeNode ++ } ++ namespaceImports := tx.generatedTypeNamespaceImports() ++ var visitor *ast.NodeVisitor ++ visitor = tx.EmitContext().NewNodeVisitor(func(node *ast.Node) *ast.Node { ++ if node != nil && node.Kind == ast.KindTypeReference { ++ typeReference := node.AsTypeReferenceNode() ++ if typeReference.TypeName != nil && typeReference.TypeName.Kind == ast.KindIdentifier { ++ imported := namedImports[typeReference.TypeName.Text()] ++ if imported.importedName == "" || imported.moduleSpecifier == "" { ++ return visitor.VisitEachChild(node) ++ } ++ typeArguments := visitor.VisitNodes(typeReference.TypeArguments) ++ if namespaceName := namespaceImports[imported.moduleSpecifier]; namespaceName != "" { ++ return tx.Factory().NewTypeReferenceNode( ++ tx.Factory().NewQualifiedName(tx.Factory().NewIdentifier(namespaceName), tx.Factory().NewIdentifier(imported.importedName)), ++ typeArguments, ++ ) ++ } ++ return tx.Factory().NewImportTypeNode( ++ false, ++ tx.Factory().NewLiteralTypeNode(tx.Factory().NewStringLiteral(imported.moduleSpecifier, ast.TokenFlagsNone)), ++ nil, ++ tx.Factory().NewIdentifier(imported.importedName), ++ typeArguments, ++ ) ++ } ++ } ++ return visitor.VisitEachChild(node) ++ }) ++ return visitor.VisitNode(typeNode) ++} ++ ++func isEffectSchemaStructCompanionTypeAlias(statement *ast.Node, structModelNames map[string]bool) bool { ++ return ast.IsTypeAliasDeclaration(statement) && statement.Name() != nil && structModelNames[statement.Name().Text()] ++} ++ ++func hasEffectSchemaOpaqueHeritage(classDeclaration *ast.Node) bool { ++ heritageType := getFirstHeritageType(classDeclaration) ++ if heritageType == nil { ++ return false ++ } ++ expression := heritageType.AsExpressionWithTypeArguments().Expression ++ if expression == nil { ++ return false ++ } ++ if ast.IsCallExpression(expression) { ++ expression = expression.AsCallExpression().Expression ++ } ++ if ast.IsCallExpression(expression) { ++ expression = expression.AsCallExpression().Expression ++ } ++ return isEffectSchemaOpaqueExpression(expression) ++} ++ ++func getFirstHeritageType(classDeclaration *ast.Node) *ast.Node { ++ if classDeclaration == nil || !ast.IsClassDeclaration(classDeclaration) || classDeclaration.AsClassDeclaration().HeritageClauses == nil { ++ return nil ++ } ++ clauses := classDeclaration.AsClassDeclaration().HeritageClauses.Nodes ++ if len(clauses) == 0 || clauses[0].AsHeritageClause().Types == nil || len(clauses[0].AsHeritageClause().Types.Nodes) == 0 { ++ return nil ++ } ++ return clauses[0].AsHeritageClause().Types.Nodes[0] ++} ++ ++func isEffectSchemaOpaqueExpression(expression *ast.Node) bool { ++ return getEffectSchemaCtorFacadeName(expression) != "" ++} ++ ++// Maps a schema-model heritage constructor (S.Opaque(...), S.Class(...), S.ErrorClass(...), ...) ++// to the effect-app facade type its base should be rewritten to. Returns "" for non-model ++// constructors. Opaque family (incl. requests) -> OpaqueFacade; class family -> OpaqueClassFacade; ++// error family -> OpaqueErrorFacadeClass. ++func getEffectSchemaCtorFacadeName(expression *ast.Node) string { ++ // The heritage constructor is either a namespaced access (`S.Class(...)` / ++ // `Schema.Opaque(...)`) or a bare named import (`import { Class } from ++ // "effect-app/Schema"` → `class X extends Class(...)`). Both forms name the ++ // same effect-app schema constructors; match on the constructor name in either ++ // shape (consistent with the namespace form trusting the `S`/`Schema` alias). ++ var ctorName string ++ switch { ++ case ast.IsPropertyAccessExpression(expression) && expression.Name() != nil && expression.Expression() != nil: ++ left := expression.Expression() ++ if !ast.IsIdentifier(left) || (left.Text() != "S" && left.Text() != "Schema") { ++ return "" ++ } ++ ctorName = expression.Name().Text() ++ case ast.IsIdentifier(expression): ++ ctorName = expression.Text() ++ default: ++ return "" ++ } ++ switch ctorName { ++ case "Opaque", "OpaqueFacade": ++ return "OpaqueFacade" ++ case "Class", "TaggedClass": ++ return "OpaqueClassFacade" ++ case "ErrorClass", "TaggedErrorClass": ++ return "OpaqueErrorFacadeClass" ++ default: ++ return "" ++ } ++} ++ ++func getEffectSchemaClassFacadeName(classDeclaration *ast.Node) string { ++ heritageType := getFirstHeritageType(classDeclaration) ++ if heritageType == nil { ++ return "OpaqueFacade" ++ } ++ expression := heritageType.AsExpressionWithTypeArguments().Expression ++ for expression != nil && ast.IsCallExpression(expression) { ++ expression = expression.AsCallExpression().Expression ++ } ++ if expression != nil { ++ if name := getEffectSchemaCtorFacadeName(expression); name != "" { ++ return name ++ } ++ } ++ return "OpaqueFacade" ++} ++ ++func isEffectSchemaModelNamespace(statement *ast.Node) bool { ++ if !ast.IsModuleDeclaration(statement) || statement.Name() == nil || !ast.IsIdentifier(statement.Name()) || statement.AsModuleDeclaration().Body == nil || statement.AsModuleDeclaration().Body.Kind != ast.KindModuleBlock { ++ return false ++ } ++ return core.Some(statement.AsModuleDeclaration().Body.AsModuleBlock().Statements.Nodes, isEffectSchemaStructNestedEncodedInterface) ++} ++ ++func isEffectSchemaMaterializedModelNamespace(statement *ast.Node, classes map[string]*ast.Node) bool { ++ name := moduleDeclarationIdentifierName(statement) ++ if name == "" || classes[name] == nil || statement.AsModuleDeclaration().Body == nil || statement.AsModuleDeclaration().Body.Kind != ast.KindModuleBlock { ++ return false ++ } ++ return core.Some(statement.AsModuleDeclaration().Body.AsModuleBlock().Statements.Nodes, isEffectSchemaEncodedInterface) ++} ++ ++func moduleDeclarationIdentifierName(statement *ast.Node) string { ++ if !ast.IsModuleDeclaration(statement) || statement.Name() == nil || !ast.IsIdentifier(statement.Name()) { ++ return "" ++ } ++ return statement.Name().Text() ++} ++ ++func isEffectSchemaStructNestedEncodedInterface(statement *ast.Node) bool { ++ if !ast.IsInterfaceDeclaration(statement) || statement.Name() == nil || statement.Name().Text() != "Encoded" || statement.AsInterfaceDeclaration().HeritageClauses == nil { ++ return false ++ } ++ clauses := statement.AsInterfaceDeclaration().HeritageClauses.Nodes ++ if len(clauses) != 1 || clauses[0].AsHeritageClause().Types == nil || len(clauses[0].AsHeritageClause().Types.Nodes) != 1 { ++ return false ++ } ++ heritageType := clauses[0].AsHeritageClause().Types.Nodes[0] ++ expression := heritageType.AsExpressionWithTypeArguments().Expression ++ typeArguments := heritageType.AsExpressionWithTypeArguments().TypeArguments ++ return ast.IsPropertyAccessExpression(expression) && ++ expression.Name() != nil && ++ expression.Name().Text() == "StructNestedEncoded" && ++ typeArguments != nil && ++ len(typeArguments.Nodes) == 1 && ++ typeArguments.Nodes[0].Kind == ast.KindTypeQuery ++} ++ ++func isEffectSchemaStructNestedEncodedInterfaceForModel(statement *ast.Node, modelName string) bool { ++ if !isEffectSchemaStructNestedEncodedInterface(statement) { ++ return false ++ } ++ typeQuery := statement.AsInterfaceDeclaration().HeritageClauses.Nodes[0].AsHeritageClause().Types.Nodes[0].AsExpressionWithTypeArguments().TypeArguments.Nodes[0] ++ exprName := typeQuery.AsTypeQueryNode().ExprName ++ return exprName != nil && ast.IsIdentifier(exprName) && exprName.Text() == modelName ++} ++ ++func (tx *DeclarationTransformer) materializeSchemaNestedEncoded(encoded *ast.Node) *ast.Node { ++ heritageType := encoded.AsInterfaceDeclaration().HeritageClauses.Nodes[0].AsHeritageClause().Types.Nodes[0] ++ return tx.resolver.CreateTypeLiteralOfTypeNode(tx.EmitContext(), heritageType, tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaMaterializedEncodedDeclaration(encoded *ast.Node, classDeclaration *ast.Node) *ast.Node { ++ encodedType := tx.materializeSchemaNestedEncoded(encoded) ++ if encodedType == nil || !ast.IsTypeLiteralNode(encodedType) { ++ return nil ++ } ++ return tx.Factory().NewInterfaceDeclaration(encoded.Modifiers(), encoded.Name(), encoded.AsInterfaceDeclaration().TypeParameters, nil, tx.Factory().NewNodeList(encodedType.AsTypeLiteralNode().Members.Nodes)) ++} ++ ++func isEffectSchemaEncodedInterface(statement *ast.Node) bool { ++ return ast.IsInterfaceDeclaration(statement) && statement.Name() != nil && statement.Name().Text() == "Encoded" ++} ++ ++func (tx *DeclarationTransformer) canCreateEffectSchemaGeneratedNamespace(classDeclaration *ast.Node) bool { ++ return tx.createEffectSchemaEncodedDeclaration(classDeclaration) != nil ++} ++ ++func getEffectSchemaClassName(statement *ast.Node) string { ++ if ast.IsClassDeclaration(statement) && statement.Name() != nil { ++ return statement.Name().Text() ++ } ++ return "" ++} ++ ++func hasTopLevelInterface(statements *ast.StatementList, name string) bool { ++ return core.Some(statements.Nodes, func(statement *ast.Node) bool { ++ return ast.IsInterfaceDeclaration(statement) && statement.Name() != nil && statement.Name().Text() == name ++ }) ++} ++ ++func hasTopLevelNamespace(statements *ast.StatementList, name string) bool { ++ return core.Some(statements.Nodes, func(statement *ast.Node) bool { ++ return moduleDeclarationIdentifierName(statement) == name ++ }) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaTypeInterface(classDeclaration *ast.Node) *ast.Node { ++ if classDeclaration == nil || classDeclaration.Name() == nil { ++ return nil ++ } ++ literal := tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeLiteralOfClassDeclaration(tx.EmitContext(), classDeclaration, tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ if literal == nil || !ast.IsTypeLiteralNode(literal) { ++ return nil ++ } ++ return tx.Factory().NewInterfaceDeclaration( ++ tx.createEffectSchemaNamespaceModifiers(classDeclaration, false), ++ tx.Factory().NewIdentifier(classDeclaration.Name().Text()), ++ nil, ++ nil, ++ tx.Factory().NewNodeList(literal.AsTypeLiteralNode().Members.Nodes), ++ ) ++} ++ ++func (tx *DeclarationTransformer) updateEffectSchemaNamespaceDeclaration(namespace *ast.Node, classDeclaration *ast.Node) *ast.Node { ++ body := namespace.AsModuleDeclaration().Body ++ if body == nil || body.Kind != ast.KindModuleBlock { ++ return nil ++ } ++ replacedEncoded := false ++ existing := map[string]bool{} ++ kept := make([]*ast.Node, 0, len(body.AsModuleBlock().Statements.Nodes)) ++ for _, statement := range body.AsModuleBlock().Statements.Nodes { ++ if (ast.IsInterfaceDeclaration(statement) || ast.IsTypeAliasDeclaration(statement)) && statement.Name() != nil { ++ name := statement.Name().Text() ++ if name == "Encoded" { ++ var encodedDeclaration *ast.Node ++ if isEffectSchemaStructNestedEncodedInterfaceForModel(statement, moduleDeclarationIdentifierName(namespace)) { ++ encodedDeclaration = tx.createEffectSchemaMaterializedEncodedDeclaration(statement, classDeclaration) ++ } ++ if encodedDeclaration != nil { ++ kept = append(kept, encodedDeclaration) ++ replacedEncoded = true ++ continue ++ } ++ } else { ++ existing[name] = true ++ if name == "Make" || name == "DecodingServices" || name == "EncodingServices" { ++ continue ++ } ++ } ++ } ++ kept = append(kept, statement) ++ } ++ ++ additions := []*ast.Node{} ++ if makeDeclaration := tx.createEffectSchemaMakeDeclaration(classDeclaration); makeDeclaration != nil { ++ additions = append(additions, makeDeclaration) ++ } ++ if decodingServices := tx.createEffectSchemaServiceDeclaration(classDeclaration, "DecodingServices"); decodingServices != nil { ++ additions = append(additions, decodingServices) ++ } ++ if encodingServices := tx.createEffectSchemaServiceDeclaration(classDeclaration, "EncodingServices"); encodingServices != nil { ++ additions = append(additions, encodingServices) ++ } ++ if !existing["Fields"] { ++ if fieldsDeclaration := tx.createEffectSchemaFieldsDeclaration(classDeclaration); fieldsDeclaration != nil { ++ additions = append([]*ast.Node{fieldsDeclaration}, additions...) ++ } ++ } ++ if !replacedEncoded && len(additions) == 0 && len(existing) == 0 { ++ return nil ++ } ++ ++ statements := tx.Factory().NewNodeList(append(kept, additions...)) ++ moduleBlock := tx.Factory().UpdateModuleBlock(body.AsModuleBlock(), statements) ++ return tx.Factory().UpdateModuleDeclaration(namespace.AsModuleDeclaration(), namespace.Modifiers(), namespace.AsModuleDeclaration().Keyword, namespace.Name(), moduleBlock) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaGeneratedNamespaceDeclaration(modelName string, classDeclaration *ast.Node) *ast.Node { ++ encoded := tx.createEffectSchemaEncodedDeclaration(classDeclaration) ++ if encoded == nil { ++ return nil ++ } ++ statements := []*ast.Node{encoded} ++ if fieldsDeclaration := tx.createEffectSchemaFieldsDeclaration(classDeclaration); fieldsDeclaration != nil { ++ statements = append(statements, fieldsDeclaration) ++ } ++ if makeDeclaration := tx.createEffectSchemaMakeDeclaration(classDeclaration); makeDeclaration != nil { ++ statements = append(statements, makeDeclaration) ++ } ++ if decodingServices := tx.createEffectSchemaServiceDeclaration(classDeclaration, "DecodingServices"); decodingServices != nil { ++ statements = append(statements, decodingServices) ++ } ++ if encodingServices := tx.createEffectSchemaServiceDeclaration(classDeclaration, "EncodingServices"); encodingServices != nil { ++ statements = append(statements, encodingServices) ++ } ++ return tx.Factory().NewModuleDeclaration( ++ tx.createEffectSchemaNamespaceModifiers(classDeclaration, true), ++ ast.KindNamespaceKeyword, ++ tx.Factory().NewIdentifier(modelName), ++ tx.Factory().NewModuleBlock(tx.Factory().NewNodeList(statements)), ++ ) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaNamespaceModifiers(classDeclaration *ast.Node, includeDeclare bool) *ast.ModifierList { ++ modifiers := []*ast.Node{} ++ if ast.HasSyntacticModifier(classDeclaration, ast.ModifierFlagsExport) { ++ modifiers = append(modifiers, tx.Factory().NewModifier(ast.KindExportKeyword)) ++ } ++ if includeDeclare { ++ modifiers = append(modifiers, tx.Factory().NewModifier(ast.KindDeclareKeyword)) ++ } ++ if len(modifiers) == 0 { ++ return nil ++ } ++ return tx.Factory().NewModifierList(modifiers) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaFieldsDeclaration(classDeclaration *ast.Node) *ast.Node { ++ fieldsType := tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeOfClassStaticProperty(tx.EmitContext(), classDeclaration, "fields", tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ if fieldsType == nil || fieldsType.Kind == ast.KindAnyKeyword { ++ return nil ++ } ++ if ast.IsTypeLiteralNode(fieldsType) { ++ return tx.Factory().NewInterfaceDeclaration(nil, tx.Factory().NewIdentifier("Fields"), nil, nil, tx.Factory().NewNodeList(fieldsType.AsTypeLiteralNode().Members.Nodes)) ++ } ++ return tx.Factory().NewTypeAliasDeclaration(nil, tx.Factory().NewIdentifier("Fields"), nil, fieldsType) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaEncodedDeclaration(classDeclaration *ast.Node) *ast.Node { ++ encodedType := tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeOfClassStaticProperty(tx.EmitContext(), classDeclaration, "Encoded", tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ if encodedType == nil { ++ return nil ++ } ++ if ast.IsTypeLiteralNode(encodedType) { ++ return tx.Factory().NewInterfaceDeclaration(nil, tx.Factory().NewIdentifier("Encoded"), nil, nil, tx.Factory().NewNodeList(encodedType.AsTypeLiteralNode().Members.Nodes)) ++ } ++ return tx.Factory().NewTypeAliasDeclaration(nil, tx.Factory().NewIdentifier("Encoded"), nil, encodedType) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaMakeDeclaration(classDeclaration *ast.Node) *ast.Node { ++ makeType := tx.normalizeGeneratedImportedTypes(tx.resolver.CreateMakeTypeOfClassDeclaration(tx.EmitContext(), classDeclaration, tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ if makeType == nil { ++ makeType = tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeOfClassStaticProperty(tx.EmitContext(), classDeclaration, "~type.make.in", tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ } ++ if makeType == nil { ++ return nil ++ } ++ if ast.IsTypeLiteralNode(makeType) { ++ return tx.Factory().NewInterfaceDeclaration(nil, tx.Factory().NewIdentifier("Make"), nil, nil, tx.Factory().NewNodeList(makeType.AsTypeLiteralNode().Members.Nodes)) ++ } ++ return tx.Factory().NewTypeAliasDeclaration(nil, tx.Factory().NewIdentifier("Make"), nil, makeType) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaServiceDeclaration(classDeclaration *ast.Node, name string) *ast.Node { ++ resolved := tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeOfClassStaticProperty(tx.EmitContext(), classDeclaration, name, tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ if resolved == nil { ++ return nil ++ } ++ serviceType := resolved ++ if resolved.Kind == ast.KindAnyKeyword { ++ serviceType = tx.Factory().NewKeywordTypeNode(ast.KindNeverKeyword) ++ } ++ return tx.Factory().NewTypeAliasDeclaration(nil, tx.Factory().NewIdentifier(name), nil, serviceType) ++} ++ ++func getEffectSchemaBaseModelName(statement *ast.Node) string { ++ if !ast.IsVariableStatement(statement) || statement.AsVariableStatement().DeclarationList == nil { ++ return "" ++ } ++ declarations := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes ++ if len(declarations) != 1 || declarations[0].Name() == nil || !ast.IsIdentifier(declarations[0].Name()) { ++ return "" ++ } ++ name := declarations[0].Name().Text() ++ if !strings.HasSuffix(name, "_base") { ++ return "" ++ } ++ baseName := strings.TrimSuffix(name, "_base") ++ return strings.TrimPrefix(baseName, "__") ++} ++ ++func (tx *DeclarationTransformer) updateEffectSchemaBaseDeclaration(statement *ast.Node, modelName string, classDeclaration *ast.Node, usesIntermediateClass bool) *ast.Node { ++ declaration := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0] ++ name := declaration.Name() ++ if usesIntermediateClass { ++ name = tx.Factory().NewIdentifier("__" + modelName + "_base") ++ } ++ updatedDeclaration := tx.Factory().UpdateVariableDeclaration( ++ declaration.AsVariableDeclaration(), ++ name, ++ declaration.AsVariableDeclaration().ExclamationToken, ++ tx.createEffectSchemaFacadeBaseType(modelName, classDeclaration, declaration.AsVariableDeclaration().Type), ++ declaration.AsVariableDeclaration().Initializer, ++ ) ++ declarations := tx.Factory().NewNodeList([]*ast.Node{updatedDeclaration}) ++ declarationList := tx.Factory().UpdateVariableDeclarationList(statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList(), declarations, statement.AsVariableStatement().DeclarationList.Flags) ++ return tx.Factory().UpdateVariableStatement(statement.AsVariableStatement(), statement.Modifiers(), declarationList) ++} ++ ++func getEffectSchemaRequestBaseInfo(statement *ast.Node) (effectSchemaRequestBaseInfo, bool) { ++ if !ast.IsVariableStatement(statement) || statement.AsVariableStatement().DeclarationList == nil { ++ return effectSchemaRequestBaseInfo{}, false ++ } ++ declarations := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes ++ if len(declarations) != 1 || declarations[0].Name() == nil || !ast.IsIdentifier(declarations[0].Name()) || declarations[0].Type() == nil { ++ return effectSchemaRequestBaseInfo{}, false ++ } ++ modelName := getEffectSchemaBaseModelName(statement) ++ if modelName == "" { ++ return effectSchemaRequestBaseInfo{}, false ++ } ++ opaqueType := getEffectSchemaRequestOpaqueType(declarations[0].Type(), modelName) ++ if opaqueType == nil || opaqueType.AsTypeReferenceNode().TypeArguments == nil || len(opaqueType.AsTypeReferenceNode().TypeArguments.Nodes) < 4 { ++ return effectSchemaRequestBaseInfo{}, false ++ } ++ return effectSchemaRequestBaseInfo{modelName: modelName, brand: opaqueType.AsTypeReferenceNode().TypeArguments.Nodes[3]}, true ++} ++ ++func getEffectSchemaRequestOpaqueType(typeNode *ast.Node, modelName string) *ast.Node { ++ if isEffectSchemaRequestOpaqueType(typeNode, modelName) { ++ return typeNode ++ } ++ if typeNode == nil || typeNode.Kind != ast.KindIntersectionType { ++ return nil ++ } ++ for _, part := range typeNode.AsIntersectionTypeNode().Types.Nodes { ++ if isEffectSchemaRequestOpaqueType(part, modelName) { ++ return part ++ } ++ } ++ return nil ++} ++ ++func isEffectSchemaRequestOpaqueType(typeNode *ast.Node, modelName string) bool { ++ if typeNode == nil || typeNode.Kind != ast.KindTypeReference { ++ return false ++ } ++ typeName := typeNode.AsTypeReferenceNode().TypeName ++ if typeName == nil || typeName.Kind != ast.KindQualifiedName { ++ return false ++ } ++ qualifiedName := typeName.AsQualifiedName() ++ if qualifiedName.Right.Text() != "Opaque" || qualifiedName.Left == nil || qualifiedName.Left.Kind != ast.KindIdentifier || qualifiedName.Left.Text() != "S" { ++ return false ++ } ++ typeArguments := typeNode.AsTypeReferenceNode().TypeArguments ++ if typeArguments == nil || len(typeArguments.Nodes) < 4 { ++ return false ++ } ++ return isEffectSchemaNamedTypeReference(typeArguments.Nodes[0], modelName) && isEffectSchemaExtendedSchemaNoEncodedType(typeArguments.Nodes[1]) ++} ++ ++func isEffectSchemaNamedTypeReference(typeNode *ast.Node, name string) bool { ++ return typeNode != nil && typeNode.Kind == ast.KindTypeReference && typeNode.AsTypeReferenceNode().TypeName.Kind == ast.KindIdentifier && typeNode.AsTypeReferenceNode().TypeName.Text() == name ++} ++ ++func isEffectSchemaExtendedSchemaNoEncodedType(typeNode *ast.Node) bool { ++ if typeNode == nil || typeNode.Kind != ast.KindTypeQuery { ++ return false ++ } ++ exprName := typeNode.AsTypeQueryNode().ExprName ++ if exprName == nil || exprName.Kind != ast.KindQualifiedName { ++ return false ++ } ++ qualifiedName := exprName.AsQualifiedName() ++ return qualifiedName.Right.Text() == "ExtendedSchemaNoEncoded" && qualifiedName.Left != nil && qualifiedName.Left.Kind == ast.KindIdentifier && qualifiedName.Left.Text() == "S" ++} ++ ++func (tx *DeclarationTransformer) updateEffectSchemaRequestBaseDeclaration(statement *ast.Node, info effectSchemaRequestBaseInfo) *ast.Node { ++ declaration := statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList().Declarations.Nodes[0] ++ updatedType := tx.updateEffectSchemaRequestBaseType(declaration.Type(), info) ++ if updatedType == nil { ++ return nil ++ } ++ updatedDeclaration := tx.Factory().UpdateVariableDeclaration( ++ declaration.AsVariableDeclaration(), ++ declaration.Name(), ++ declaration.AsVariableDeclaration().ExclamationToken, ++ updatedType, ++ declaration.AsVariableDeclaration().Initializer, ++ ) ++ declarations := tx.Factory().NewNodeList([]*ast.Node{updatedDeclaration}) ++ declarationList := tx.Factory().UpdateVariableDeclarationList(statement.AsVariableStatement().DeclarationList.AsVariableDeclarationList(), declarations, statement.AsVariableStatement().DeclarationList.Flags) ++ return tx.Factory().UpdateVariableStatement(statement.AsVariableStatement(), statement.Modifiers(), declarationList) ++} ++ ++func (tx *DeclarationTransformer) updateEffectSchemaRequestBaseType(typeNode *ast.Node, info effectSchemaRequestBaseInfo) *ast.Node { ++ if isEffectSchemaRequestOpaqueType(typeNode, info.modelName) { ++ return tx.createEffectSchemaFacadeTypeReference(info.modelName, info.brand, "OpaqueFacade") ++ } ++ if typeNode == nil || typeNode.Kind != ast.KindIntersectionType { ++ return nil ++ } ++ changed := false ++ types := make([]*ast.Node, 0, len(typeNode.AsIntersectionTypeNode().Types.Nodes)) ++ for _, part := range typeNode.AsIntersectionTypeNode().Types.Nodes { ++ if isEffectSchemaRequestOpaqueType(part, info.modelName) { ++ changed = true ++ types = append(types, tx.createEffectSchemaFacadeTypeReference(info.modelName, info.brand, "OpaqueFacade")) ++ } else { ++ types = append(types, part) ++ } ++ } ++ if !changed { ++ return nil ++ } ++ return tx.Factory().UpdateIntersectionTypeNode(typeNode.AsIntersectionTypeNode(), tx.Factory().NewNodeList(types)) ++} ++ ++func needsEffectSchemaIntermediateClass(classDeclaration *ast.Node) bool { ++ return classDeclaration != nil && classDeclaration.ClassLikeData() != nil && len(classDeclaration.ClassLikeData().Members.Nodes) > 0 ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaIntermediateClass(modelName string) *ast.Node { ++ return tx.Factory().NewClassDeclaration( ++ tx.Factory().NewModifierList([]*ast.Node{tx.Factory().NewModifier(ast.KindDeclareKeyword)}), ++ tx.Factory().NewIdentifier("__"+modelName), ++ nil, ++ tx.Factory().NewNodeList([]*ast.Node{ ++ tx.Factory().NewHeritageClause(ast.KindExtendsKeyword, tx.Factory().NewNodeList([]*ast.Node{ ++ tx.Factory().NewExpressionWithTypeArguments(tx.Factory().NewIdentifier("__"+modelName+"_base"), nil), ++ })), ++ }), ++ tx.Factory().NewNodeList([]*ast.Node{}), ++ ) ++} ++ ++func (tx *DeclarationTransformer) updateEffectSchemaClassDeclaration(classDeclaration *ast.Node, modelName string) *ast.Node { ++ return tx.Factory().UpdateClassDeclaration( ++ classDeclaration.AsClassDeclaration(), ++ classDeclaration.Modifiers(), ++ classDeclaration.Name(), ++ classDeclaration.AsClassDeclaration().TypeParameters, ++ tx.Factory().NewNodeList([]*ast.Node{ ++ tx.Factory().NewHeritageClause(ast.KindExtendsKeyword, tx.Factory().NewNodeList([]*ast.Node{ ++ tx.Factory().NewExpressionWithTypeArguments(tx.Factory().NewIdentifier("__"+modelName), nil), ++ })), ++ }), ++ classDeclaration.AsClassDeclaration().Members, ++ ) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaFacadeBaseType(modelName string, classDeclaration *ast.Node, baseType *ast.Node) *ast.Node { ++ facadeName := getEffectSchemaClassFacadeName(classDeclaration) ++ brandType := tx.getEffectSchemaFacadeBrandType(baseType, facadeName) ++ return tx.Factory().NewIntersectionTypeNode(tx.Factory().NewNodeList([]*ast.Node{ ++ tx.createEffectSchemaFacadeTypeReference(modelName, brandType, facadeName), ++ tx.Factory().NewTypeLiteralNode(tx.Factory().NewNodeList(tx.createEffectSchemaStaticMembers(classDeclaration, modelName))), ++ })) ++} ++ ++// The facade's Brand (last type arg). For the class/error families the source base is ++// S.EnhancedClass — the 3rd arg is the brand (e.g. Cause.YieldableError ++// for errors); preserve it. The Opaque family carries no brand on the base, so use {}. ++func (tx *DeclarationTransformer) getEffectSchemaFacadeBrandType(baseType *ast.Node, facadeName string) *ast.Node { ++ empty := tx.Factory().NewTypeLiteralNode(tx.Factory().NewNodeList([]*ast.Node{})) ++ if facadeName == "OpaqueFacade" || baseType == nil { ++ return empty ++ } ++ typeNode := baseType ++ if ast.IsIntersectionTypeNode(typeNode) { ++ nodes := typeNode.AsIntersectionTypeNode().Types.Nodes ++ if len(nodes) == 0 { ++ return empty ++ } ++ typeNode = nodes[0] ++ } ++ if typeNode.Kind != ast.KindTypeReference || typeNode.AsTypeReferenceNode().TypeArguments == nil { ++ return empty ++ } ++ args := typeNode.AsTypeReferenceNode().TypeArguments.Nodes ++ if len(args) >= 3 { ++ return args[2] ++ } ++ return empty ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaFacadeTypeReference(modelName string, brandType *ast.Node, facadeName string) *ast.Node { ++ model := tx.Factory().NewIdentifier(modelName) ++ return tx.Factory().NewTypeReferenceNode( ++ tx.Factory().NewQualifiedName(tx.Factory().NewIdentifier("S"), tx.Factory().NewIdentifier(facadeName)), ++ tx.Factory().NewNodeList([]*ast.Node{ ++ tx.Factory().NewTypeReferenceNode(model, nil), ++ tx.Factory().NewTypeReferenceNode(tx.Factory().NewQualifiedName(model, tx.Factory().NewIdentifier("Encoded")), nil), ++ tx.Factory().NewTypeReferenceNode(tx.Factory().NewQualifiedName(model, tx.Factory().NewIdentifier("Make")), nil), ++ tx.Factory().NewTypeReferenceNode(tx.Factory().NewQualifiedName(model, tx.Factory().NewIdentifier("DecodingServices")), nil), ++ tx.Factory().NewTypeReferenceNode(tx.Factory().NewQualifiedName(model, tx.Factory().NewIdentifier("EncodingServices")), nil), ++ brandType, ++ }), ++ ) ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaStaticMembers(classDeclaration *ast.Node, modelName string) []*ast.Node { ++ members := []*ast.Node{} ++ hasFields := tx.createEffectSchemaFieldsDeclaration(classDeclaration) != nil ++ // NOTE: `identifier` (generic `string`) is intentionally NOT emitted here — it lives on the ++ // facade interfaces (OpaqueFacade/OpaqueClassFacade/OpaqueErrorFacadeClass) in effect-app. ++ // Only per-model, precisely-typed statics belong here. ++ tx.addSchemaStaticMember(&members, classDeclaration, "fields", true, core.IfElse(hasFields, modelName, "")) ++ tx.addSchemaStaticMember(&members, classDeclaration, "mapFields", false, core.IfElse(hasFields, modelName, "")) ++ tx.addSchemaStaticMember(&members, classDeclaration, "to", true, "") ++ tx.addSchemaStaticMember(&members, classDeclaration, "from", true, "") ++ tx.addSchemaStaticMember(&members, classDeclaration, "copy", true, modelName) ++ return members ++} ++ ++func (tx *DeclarationTransformer) createEffectSchemaNamedMemberReference(modelName string, memberName string) *ast.Node { ++ return tx.Factory().NewTypeReferenceNode(tx.Factory().NewQualifiedName(tx.Factory().NewIdentifier(modelName), tx.Factory().NewIdentifier(memberName)), nil) ++} ++ ++func (tx *DeclarationTransformer) rewriteEffectSchemaStaticMemberType(typeNode *ast.Node, name string, modelName string) *ast.Node { ++ switch name { ++ case "fields": ++ return tx.createEffectSchemaNamedMemberReference(modelName, "Fields") ++ case "mapFields": ++ return tx.rewriteEffectSchemaMapFieldsType(typeNode, modelName) ++ case "copy": ++ return tx.rewriteEffectSchemaCopyType(typeNode, modelName) ++ default: ++ return typeNode ++ } ++} ++ ++func (tx *DeclarationTransformer) rewriteEffectSchemaMapFieldsType(typeNode *ast.Node, modelName string) *ast.Node { ++ if typeNode == nil || typeNode.Kind != ast.KindFunctionType { ++ return typeNode ++ } ++ mapFieldsType := typeNode.AsFunctionTypeNode() ++ if mapFieldsType.Parameters == nil || len(mapFieldsType.Parameters.Nodes) == 0 { ++ return typeNode ++ } ++ callbackParameter := mapFieldsType.Parameters.Nodes[0].AsParameterDeclaration() ++ callbackType := callbackParameter.Type ++ if callbackType == nil || callbackType.Kind != ast.KindFunctionType { ++ return typeNode ++ } ++ callbackFunctionType := callbackType.AsFunctionTypeNode() ++ if callbackFunctionType.Parameters == nil || len(callbackFunctionType.Parameters.Nodes) == 0 { ++ return typeNode ++ } ++ fieldsParameter := callbackFunctionType.Parameters.Nodes[0].AsParameterDeclaration() ++ updatedFieldsParameter := tx.Factory().UpdateParameterDeclaration( ++ fieldsParameter, ++ fieldsParameter.Modifiers(), ++ fieldsParameter.DotDotDotToken, ++ fieldsParameter.Name(), ++ fieldsParameter.QuestionToken, ++ tx.createEffectSchemaNamedMemberReference(modelName, "Fields"), ++ fieldsParameter.Initializer, ++ ) ++ updatedCallbackParameters := append([]*ast.Node{updatedFieldsParameter}, callbackFunctionType.Parameters.Nodes[1:]...) ++ updatedCallbackType := tx.Factory().UpdateFunctionTypeNode( ++ callbackFunctionType, ++ callbackFunctionType.TypeParameters, ++ tx.Factory().NewNodeList(updatedCallbackParameters), ++ callbackFunctionType.Type, ++ ) ++ updatedCallbackParameter := tx.Factory().UpdateParameterDeclaration( ++ callbackParameter, ++ callbackParameter.Modifiers(), ++ callbackParameter.DotDotDotToken, ++ callbackParameter.Name(), ++ callbackParameter.QuestionToken, ++ updatedCallbackType, ++ callbackParameter.Initializer, ++ ) ++ updatedMapFieldsParameters := append([]*ast.Node{updatedCallbackParameter}, mapFieldsType.Parameters.Nodes[1:]...) ++ return tx.Factory().UpdateFunctionTypeNode( ++ mapFieldsType, ++ mapFieldsType.TypeParameters, ++ tx.Factory().NewNodeList(updatedMapFieldsParameters), ++ mapFieldsType.Type, ++ ) ++} ++ ++func (tx *DeclarationTransformer) rewriteEffectSchemaCopyType(typeNode *ast.Node, modelName string) *ast.Node { ++ model := tx.Factory().NewTypeReferenceNode(tx.Factory().NewIdentifier(modelName), nil) ++ if ast.IsImportTypeNode(typeNode) { ++ importType := typeNode.AsImportTypeNode() ++ if importType.Qualifier != nil && rightmostEntityNameText(importType.Qualifier) == "StructuralCopyOrigin" { ++ return tx.Factory().UpdateImportTypeNode(importType, importType.IsTypeOf, importType.Argument, importType.Attributes, importType.Qualifier, tx.Factory().NewNodeList([]*ast.Node{model})) ++ } ++ return typeNode ++ } ++ if ast.IsTypeReferenceNode(typeNode) && rightmostEntityNameText(typeNode.AsTypeReferenceNode().TypeName) == "StructuralCopyOrigin" { ++ return tx.Factory().UpdateTypeReferenceNode(typeNode.AsTypeReferenceNode(), typeNode.AsTypeReferenceNode().TypeName, tx.Factory().NewNodeList([]*ast.Node{model})) ++ } ++ return typeNode ++} ++ ++func rightmostEntityNameText(name *ast.Node) string { ++ if ast.IsIdentifier(name) { ++ return name.Text() ++ } ++ if ast.IsQualifiedName(name) { ++ return name.AsQualifiedName().Right.Text() ++ } ++ return "" ++} ++ ++func (tx *DeclarationTransformer) addSchemaStaticMember(members *[]*ast.Node, classDeclaration *ast.Node, name string, readonly bool, modelName string) { ++ typeNode := tx.normalizeGeneratedImportedTypes(tx.resolver.CreateTypeOfClassStaticProperty(tx.EmitContext(), classDeclaration, name, tx.enclosingDeclaration, declarationEmitNodeBuilderFlags, declarationEmitInternalNodeBuilderFlags, tx.tracker)) ++ if typeNode == nil || typeNode.Kind == ast.KindAnyKeyword { ++ return ++ } ++ var modifiers *ast.ModifierList ++ if readonly { ++ modifiers = tx.Factory().NewModifierList([]*ast.Node{tx.Factory().NewModifier(ast.KindReadonlyKeyword)}) ++ } ++ if modelName != "" { ++ typeNode = tx.rewriteEffectSchemaStaticMemberType(typeNode, name, modelName) ++ } ++ *members = append(*members, tx.Factory().NewPropertySignatureDeclaration(modifiers, tx.Factory().NewIdentifier(name), nil, typeNode, nil)) ++} ++ + func (tx *DeclarationTransformer) ensureModifiers(node *ast.Node) *ast.ModifierList { + currentFlags := ast.GetCombinedModifierFlags(tx.EmitContext().ParseNode(node)) & ast.ModifierFlagsAll + newFlags := tx.ensureModifierFlags(node) diff --git a/_patches/030-checker-emitresolver-effect-schema.patch b/_patches/030-checker-emitresolver-effect-schema.patch new file mode 100644 index 00000000..4901936d --- /dev/null +++ b/_patches/030-checker-emitresolver-effect-schema.patch @@ -0,0 +1,283 @@ +diff --git a/internal/checker/emitresolver.go b/internal/checker/emitresolver.go +index 7bf3719e5..b4c50f58c 100644 +--- a/internal/checker/emitresolver.go ++++ b/internal/checker/emitresolver.go +@@ -12,6 +12,7 @@ import ( + "github.com/microsoft/typescript-go/internal/jsnum" + "github.com/microsoft/typescript-go/internal/nodebuilder" + "github.com/microsoft/typescript-go/internal/printer" ++ "github.com/microsoft/typescript-go/internal/scanner" + ) + + var _ printer.EmitResolver = (*EmitResolver)(nil) +@@ -1043,6 +1044,270 @@ func (r *EmitResolver) CreateTypeOfExpression(emitContext *printer.EmitContext, + return requestNodeBuilder.SerializeTypeForExpression(expression, enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals, internalFlags, tracker) + } + ++func (r *EmitResolver) CreateTypeOfTypeNode(emitContext *printer.EmitContext, typeNode *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ typeNode = emitContext.ParseNode(typeNode) ++ if typeNode == nil { ++ return emitContext.Factory.NewKeywordTypeNode(ast.KindAnyKeyword) ++ } ++ ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ requestNodeBuilder := NewNodeBuilder(r.checker, emitContext) ++ return requestNodeBuilder.TypeToTypeNode(r.checker.getTypeFromTypeNode(typeNode), enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals, internalFlags, tracker) ++} ++ ++func (r *EmitResolver) CreateTypeLiteralOfTypeNode(emitContext *printer.EmitContext, typeNode *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ typeNode = emitContext.ParseNode(typeNode) ++ if typeNode == nil { ++ return nil ++ } ++ ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ return r.createTypeLiteralOfType(emitContext, r.checker.getTypeFromTypeNode(typeNode), enclosingDeclaration, flags, internalFlags, tracker) ++} ++ ++func (r *EmitResolver) CreateTypeLiteralOfClassDeclaration(emitContext *printer.EmitContext, declaration *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ declaration = emitContext.ParseNode(declaration) ++ if declaration == nil { ++ return nil ++ } ++ ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ if schemaType := r.getTypeOfClassSchemaProperty(declaration, "Type"); schemaType != nil { ++ return r.createTypeLiteralOfType(emitContext, schemaType, enclosingDeclaration, flags, internalFlags, tracker) ++ } ++ symbol := r.checker.getSymbolOfDeclaration(declaration) ++ if symbol == nil { ++ return nil ++ } ++ return r.createTypeLiteralOfType(emitContext, r.checker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, flags, internalFlags, tracker) ++} ++ ++func (r *EmitResolver) CreateTypeLiteralOfClassStaticProperty(emitContext *printer.EmitContext, declaration *ast.Node, propertyName string, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ declaration = emitContext.ParseNode(declaration) ++ if declaration == nil { ++ return nil ++ } ++ ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ propertyType := r.getTypeOfClassSchemaProperty(declaration, propertyName) ++ if propertyType == nil { ++ return nil ++ } ++ return r.createTypeLiteralOfType(emitContext, propertyType, enclosingDeclaration, flags, internalFlags, tracker) ++} ++ ++func (r *EmitResolver) CreateMakeTypeOfClassDeclaration(emitContext *printer.EmitContext, declaration *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ declaration = emitContext.ParseNode(declaration) ++ if declaration == nil { ++ return nil ++ } ++ ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ makeType := r.getTypeOfClassSchemaProperty(declaration, "~type.make.in") ++ typeType := r.getTypeOfClassSchemaProperty(declaration, "Type") ++ if makeType == nil || typeType == nil { ++ return nil ++ } ++ return r.createMakeTypeOfTypes(emitContext, makeType, typeType, enclosingDeclaration, flags, internalFlags, tracker) ++} ++ ++func (r *EmitResolver) CreateTypeOfClassStaticProperty(emitContext *printer.EmitContext, declaration *ast.Node, propertyName string, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ declaration = emitContext.ParseNode(declaration) ++ if declaration == nil { ++ return nil ++ } ++ ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ propertyType := r.getTypeOfClassSchemaProperty(declaration, propertyName) ++ if propertyType == nil { ++ propertyType = r.getTypeOfClassStaticProperty(declaration, propertyName) ++ } ++ if propertyType == nil { ++ return nil ++ } ++ // Emit the decoded property (Type/Encoded/Make/Fields) as a fully-expanded static ++ // type literal instead of the deferred mapped helper (e.g. `Struct.ReadonlySide<…, ++ // "Encoded">`) the node builder would otherwise reference — matching the struct-facade ++ // path. Keeps the generated namespace interfaces static and avoids re-instantiating ++ // the mapped helper at every use site. Non-object properties (service channels resolve ++ // to `never`, Make can be a void union) keep their direct node form. ++ // Expand only plain data objects. A callable type (e.g. the base's `mapFields` ++ // method, or `copy`) is also `TypeFlagsObject` but has no enumerable properties — ++ // expanding it would collapse the call signatures to `{}`, so leave those to the ++ // node builder which preserves the signature. ++ if propertyType.flags&TypeFlagsObject != 0 && len(r.checker.getPropertiesOfType(propertyType)) > 0 { ++ return r.createTypeLiteralOfType(emitContext, propertyType, enclosingDeclaration, flags, internalFlags, tracker) ++ } ++ requestNodeBuilder := NewNodeBuilder(r.checker, emitContext) ++ return requestNodeBuilder.TypeToTypeNode(propertyType, enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals|nodebuilder.FlagsUseFullyQualifiedType, internalFlags, tracker) ++} ++ ++func (r *EmitResolver) getTypeOfClassSchemaProperty(declaration *ast.Node, propertyName string) *Type { ++ schemaExpression := getClassSchemaExpression(declaration) ++ if schemaExpression == nil { ++ return nil ++ } ++ return r.getTypeOfSchemaExpressionProperty(schemaExpression, propertyName) ++} ++ ++func (r *EmitResolver) getTypeOfSchemaExpressionProperty(schemaExpression *ast.Node, propertyName string) *Type { ++ schemaType := r.checker.getTypeOfExpression(schemaExpression) ++ property := r.checker.getPropertyOfType(schemaType, propertyName) ++ if property == nil { ++ return nil ++ } ++ return r.checker.GetTypeOfSymbolAtLocation(property, schemaExpression) ++} ++ ++// Like CreateTypeOfClassStaticProperty, but for a `const X = S.Struct(...)` schema value: ++// reads propertyName (Encoded / Type / ~type.make.in / DecodingServices / ...) off the type ++// of the const's initializer and serializes the resolved type. Serializing the resolved type ++// keeps `never` as `never` and never synthesizes references that could fail to resolve. ++func (r *EmitResolver) CreateTypeOfStructSchemaProperty(emitContext *printer.EmitContext, declaration *ast.Node, propertyName string, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ declaration = emitContext.ParseNode(declaration) ++ if declaration == nil || !ast.IsVariableDeclaration(declaration) || declaration.AsVariableDeclaration().Initializer == nil { ++ return nil ++ } ++ r.checkerMu.Lock() ++ defer r.checkerMu.Unlock() ++ propertyType := r.getTypeOfSchemaExpressionProperty(declaration.AsVariableDeclaration().Initializer, propertyName) ++ if propertyType == nil { ++ return nil ++ } ++ // Emit the decoded property (Type/Encoded/Make/Fields) as a fully-expanded static ++ // type literal — the same shape the class/request facades produce — instead of the ++ // deferred mapped type (e.g. `Struct.ReadonlySide<…, "Type">`) the node builder would ++ // otherwise reference. This keeps the generated `interface X { … }` static and avoids ++ // re-instantiating the mapped helper at every use site. Non-object properties (the ++ // service channels resolve to `never`) keep their direct node form. ++ // Expand only plain data objects. A callable type (e.g. the base's `mapFields` ++ // method, or `copy`) is also `TypeFlagsObject` but has no enumerable properties — ++ // expanding it would collapse the call signatures to `{}`, so leave those to the ++ // node builder which preserves the signature. ++ if propertyType.flags&TypeFlagsObject != 0 && len(r.checker.getPropertiesOfType(propertyType)) > 0 { ++ return r.createTypeLiteralOfType(emitContext, propertyType, enclosingDeclaration, flags, internalFlags, tracker) ++ } ++ structNodeBuilder := NewNodeBuilder(r.checker, emitContext) ++ return structNodeBuilder.TypeToTypeNode(propertyType, enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals|nodebuilder.FlagsUseFullyQualifiedType, internalFlags, tracker) ++} ++ ++func getClassSchemaExpression(declaration *ast.Node) *ast.Node { ++ if declaration == nil || !ast.IsClassDeclaration(declaration) || declaration.AsClassDeclaration().HeritageClauses == nil || len(declaration.AsClassDeclaration().HeritageClauses.Nodes) == 0 { ++ return nil ++ } ++ heritageClause := declaration.AsClassDeclaration().HeritageClauses.Nodes[0] ++ if heritageClause == nil || len(heritageClause.AsHeritageClause().Types.Nodes) == 0 { ++ return nil ++ } ++ expression := heritageClause.AsHeritageClause().Types.Nodes[0].AsExpressionWithTypeArguments().Expression ++ if expression == nil || !ast.IsCallExpression(expression) || expression.AsCallExpression().Arguments == nil || len(expression.AsCallExpression().Arguments.Nodes) == 0 { ++ return nil ++ } ++ return expression.AsCallExpression().Arguments.Nodes[0] ++} ++ ++func (r *EmitResolver) getTypeOfClassStaticProperty(declaration *ast.Node, propertyName string) *Type { ++ symbol := r.checker.getSymbolOfDeclaration(declaration) ++ if symbol == nil { ++ return nil ++ } ++ staticType := r.checker.getTypeOfSymbol(symbol) ++ property := r.checker.getPropertyOfType(staticType, propertyName) ++ if property == nil { ++ return nil ++ } ++ return r.checker.GetTypeOfSymbolAtLocation(property, declaration) ++} ++ ++func (r *EmitResolver) createTypeLiteralOfType(emitContext *printer.EmitContext, typ *Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ requestNodeBuilder := NewNodeBuilder(r.checker, emitContext) ++ members := core.Map(r.checker.getPropertiesOfType(typ), func(property *ast.Symbol) *ast.Node { ++ propertyTypeNode := requestNodeBuilder.TypeToTypeNode(r.checker.getTypeOfSymbol(property), enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals|nodebuilder.FlagsUseFullyQualifiedType, internalFlags, tracker) ++ if propertyTypeNode == nil { ++ propertyTypeNode = emitContext.Factory.NewKeywordTypeNode(ast.KindAnyKeyword) ++ } ++ var optionalToken *ast.Node ++ if property.Flags&ast.SymbolFlagsOptional != 0 { ++ optionalToken = emitContext.Factory.NewToken(ast.KindQuestionToken) ++ } ++ return emitContext.Factory.NewPropertySignatureDeclaration( ++ emitContext.Factory.NewModifierList([]*ast.Node{emitContext.Factory.NewModifier(ast.KindReadonlyKeyword)}), ++ createPropertyName(emitContext, property.Name), ++ optionalToken, ++ propertyTypeNode, ++ nil, ++ ) ++ }) ++ return emitContext.Factory.NewTypeLiteralNode(emitContext.Factory.NewNodeList(members)) ++} ++ ++func (r *EmitResolver) createMakeTypeOfTypes(emitContext *printer.EmitContext, makeType *Type, typeType *Type, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node { ++ isVoidish := func(typ *Type) bool { return typ.flags&(TypeFlagsVoid|TypeFlagsUndefined) != 0 } ++ var makeTypes []*Type ++ if makeType.flags&TypeFlagsUnion != 0 { ++ makeTypes = makeType.AsUnionType().types ++ } ++ hasVoid := core.Some(makeTypes, isVoidish) ++ objectMakeType := makeType ++ if makeTypes != nil { ++ for _, typ := range makeTypes { ++ if len(r.checker.getPropertiesOfType(typ)) > 0 { ++ objectMakeType = typ ++ break ++ } ++ } ++ } ++ makeProperties := r.checker.getPropertiesOfType(objectMakeType) ++ if len(makeProperties) == 0 { ++ requestNodeBuilder := NewNodeBuilder(r.checker, emitContext) ++ return requestNodeBuilder.TypeToTypeNode(makeType, enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals|nodebuilder.FlagsUseFullyQualifiedType, internalFlags, tracker) ++ } ++ typeProperties := make(map[string]*ast.Symbol) ++ for _, property := range r.checker.getPropertiesOfType(typeType) { ++ typeProperties[property.Name] = property ++ } ++ requestNodeBuilder := NewNodeBuilder(r.checker, emitContext) ++ members := core.Map(makeProperties, func(property *ast.Symbol) *ast.Node { ++ source := property ++ if typeProperty := typeProperties[property.Name]; typeProperty != nil { ++ source = typeProperty ++ } ++ propertyTypeNode := requestNodeBuilder.TypeToTypeNode(r.checker.getTypeOfSymbol(source), enclosingDeclaration, flags|nodebuilder.FlagsMultilineObjectLiterals|nodebuilder.FlagsUseFullyQualifiedType, internalFlags, tracker) ++ if propertyTypeNode == nil { ++ propertyTypeNode = emitContext.Factory.NewKeywordTypeNode(ast.KindAnyKeyword) ++ } ++ var optionalToken *ast.Node ++ if property.Flags&ast.SymbolFlagsOptional != 0 { ++ optionalToken = emitContext.Factory.NewToken(ast.KindQuestionToken) ++ } ++ return emitContext.Factory.NewPropertySignatureDeclaration( ++ emitContext.Factory.NewModifierList([]*ast.Node{emitContext.Factory.NewModifier(ast.KindReadonlyKeyword)}), ++ createPropertyName(emitContext, property.Name), ++ optionalToken, ++ propertyTypeNode, ++ nil, ++ ) ++ }) ++ literal := emitContext.Factory.NewTypeLiteralNode(emitContext.Factory.NewNodeList(members)) ++ if hasVoid { ++ return emitContext.Factory.NewUnionTypeNode(emitContext.Factory.NewNodeList([]*ast.Node{literal, emitContext.Factory.NewKeywordTypeNode(ast.KindVoidKeyword)})) ++ } ++ return literal ++} ++ ++func createPropertyName(emitContext *printer.EmitContext, name string) *ast.Node { ++ if scanner.IsIdentifierText(name, core.LanguageVariantStandard) { ++ return emitContext.Factory.NewIdentifier(name) ++ } ++ return emitContext.Factory.NewStringLiteral(name, ast.TokenFlagsNone) ++} ++ + func (r *EmitResolver) CreateLateBoundIndexSignatures(emitContext *printer.EmitContext, container *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) []*ast.Node { + container = emitContext.ParseNode(container) + r.checkerMu.Lock() diff --git a/_patches/031-printer-emitresolver-effect-schema.patch b/_patches/031-printer-emitresolver-effect-schema.patch new file mode 100644 index 00000000..1921fb54 --- /dev/null +++ b/_patches/031-printer-emitresolver-effect-schema.patch @@ -0,0 +1,18 @@ +diff --git a/internal/printer/emitresolver.go b/internal/printer/emitresolver.go +index 189fe88cc..e617e1171 100644 +--- a/internal/printer/emitresolver.go ++++ b/internal/printer/emitresolver.go +@@ -123,6 +123,13 @@ type EmitResolver interface { + CreateTypeParametersOfSignatureDeclaration(emitContext *EmitContext, signatureDeclaration *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) []*ast.Node + CreateLiteralConstValue(emitContext *EmitContext, node *ast.Node, tracker nodebuilder.SymbolTracker) *ast.Node + CreateTypeOfExpression(emitContext *EmitContext, expression *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateTypeOfTypeNode(emitContext *EmitContext, typeNode *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateTypeLiteralOfTypeNode(emitContext *EmitContext, typeNode *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateTypeLiteralOfClassDeclaration(emitContext *EmitContext, declaration *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateTypeLiteralOfClassStaticProperty(emitContext *EmitContext, declaration *ast.Node, propertyName string, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateMakeTypeOfClassDeclaration(emitContext *EmitContext, declaration *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateTypeOfClassStaticProperty(emitContext *EmitContext, declaration *ast.Node, propertyName string, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node ++ CreateTypeOfStructSchemaProperty(emitContext *EmitContext, declaration *ast.Node, propertyName string, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node + CreateLateBoundIndexSignatures(emitContext *EmitContext, container *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) []*ast.Node + TryJSTypeNodeToTypeNode(emitContext *EmitContext, typeNode *ast.Node, enclosingDeclaration *ast.Node, flags nodebuilder.Flags, internalFlags nodebuilder.InternalFlags, tracker nodebuilder.SymbolTracker) *ast.Node + } diff --git a/_tools/cut-release.sh b/_tools/cut-release.sh new file mode 100755 index 00000000..3cd86848 --- /dev/null +++ b/_tools/cut-release.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +# cut-release.sh — build every platform `tsgo` binary, package each as a gzip, and +# create a GitHub release on effect-app/tsgo carrying them plus a SHA256SUMS file. +# +# This is the manual release path for the effect-app fork: the upstream `release.yml` +# only runs for `Effect-TS`-owned repos on a changeset merge, so fork binaries are cut +# from here. The produced assets are exactly what consumers download via +# scripts/install-patched-compilers.mjs: +# tsgo--.gz (e.g. tsgo-linux-x64.gz) +# tsgo-win32-.exe.gz (windows) +# SHA256SUMS (` ` per line) +# +# The version is derived from source so the tag, release title and the binary's own +# `--version` can never drift: +# core base : typescript-go/internal/core/version.go -> 7.0.0-dev +# suffix : etscheckerhooks/init.go SetVersionSuffix -> effect-app.6 +# => title 'tsgo 7.0.0-dev+effect-app.6' tag 'v7.0.0-dev-effect-app.6' +# Bump the suffix in etscheckerhooks/init.go before cutting a new release. +# +# Usage: +# cut-release.sh [--repo ] [--skip-setup] [--dry-run] [--notes ] +# --repo Target repository (default: effect-app/tsgo) +# --skip-setup Skip `pnpm setup-repo` (reuse the already-patched submodule tree) +# --dry-run Build + package, print the release command, but do not publish +# --notes Override the release notes body + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +REPO="effect-app/tsgo" +DRY_RUN=false +SKIP_SETUP=false +NOTES="" +while [ $# -gt 0 ]; do + case "$1" in + --repo) REPO="$2"; shift 2 ;; + --skip-setup) SKIP_SETUP=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --notes) NOTES="$2"; shift 2 ;; + *) echo "ERROR: unknown flag '$1'"; exit 1 ;; + esac +done + +# ── Derive version from source ──────────────────────────────────────────────── +core_base="$(sed -nE 's/.*var version = "([^"]+)".*/\1/p' typescript-go/internal/core/version.go | head -1)" +suffix="$(sed -nE 's/.*SetVersionSuffix\("\+(effect-app\.[0-9]+)"\).*/\1/p' etscheckerhooks/init.go | head -1)" +if [ -z "$core_base" ] || [ -z "$suffix" ]; then + echo "ERROR: could not derive version (core_base='$core_base' suffix='$suffix')" + exit 1 +fi +full_version="${core_base}+${suffix}" +tag="v${core_base}-${suffix}" +title="tsgo ${full_version}" +echo "==> Cutting ${title} (tag ${tag}) on ${REPO}" + +# npm platform-arch identifiers, matching install-patched-compilers.mjs asset names. +TARGETS=(darwin-arm64 darwin-x64 win32-x64 win32-arm64 linux-x64 linux-arm64 linux-arm) + +# ── Build ───────────────────────────────────────────────────────────────────── +if [ "$SKIP_SETUP" != true ]; then + echo "==> Applying patches (pnpm setup-repo --ci)" + pnpm setup-repo --ci +fi +echo "==> Cross-compiling ${#TARGETS[@]} targets" +pnpm release:prepare --skip-cli + +# ── Package gz assets + checksums ───────────────────────────────────────────── +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT +assets=() +for t in "${TARGETS[@]}"; do + bin="tsgo"; gz="tsgo-${t}.gz" + case "$t" in win32-*) bin="tsgo.exe"; gz="tsgo-${t}.exe.gz" ;; esac + src="_packages/tsgo-${t}/lib/${bin}" + [ -s "$src" ] || { echo "ERROR: missing built binary $src"; exit 1; } + gzip -c -9 "$src" > "$STAGE/$gz" + assets+=("$STAGE/$gz") +done +( cd "$STAGE" && sha256sum tsgo-*.gz > SHA256SUMS ) +assets+=("$STAGE/SHA256SUMS") +echo "==> Packaged assets:" +( cd "$STAGE" && ls -la && echo "--- SHA256SUMS ---" && cat SHA256SUMS ) + +# ── Sanity check: the host-platform binary reports the derived version ───────── +host_os="$(go env GOOS)"; host_arch="$(go env GOARCH)" +case "${host_os}-${host_arch}" in + darwin-arm64) host_id=darwin-arm64 ;; darwin-amd64) host_id=darwin-x64 ;; + linux-amd64) host_id=linux-x64 ;; linux-arm64) host_id=linux-arm64 ;; + *) host_id="" ;; +esac +if [ -n "$host_id" ]; then + reported="$("_packages/tsgo-${host_id}/lib/tsgo" --version 2>/dev/null | awk '{print $2}')" + if [ "$reported" != "$full_version" ]; then + echo "ERROR: built binary reports '$reported', expected '$full_version' (bump etscheckerhooks/init.go?)" + exit 1 + fi + echo "==> Host binary version OK: $reported" +fi + +if [ -z "$NOTES" ]; then + NOTES="Effect language-service tsgo + .d.ts schema-facade emit (${full_version}). Built from PR #1." +fi + +if [ "$DRY_RUN" = true ]; then + echo "==> DRY RUN — would run:" + echo " gh release create $tag --repo $REPO --title \"$title\" ${assets[*]}" + exit 0 +fi + +# ── Publish ─────────────────────────────────────────────────────────────────── +if gh release view "$tag" --repo "$REPO" >/dev/null 2>&1; then + echo "==> Release $tag exists — uploading/overwriting assets" + gh release upload "$tag" --repo "$REPO" --clobber "${assets[@]}" +else + gh release create "$tag" --repo "$REPO" --title "$title" --notes "$NOTES" "${assets[@]}" +fi +echo "==> Released: https://github.com/${REPO}/releases/tag/${tag}" diff --git a/etscheckerhooks/init.go b/etscheckerhooks/init.go index 255b70e9..644b1216 100644 --- a/etscheckerhooks/init.go +++ b/etscheckerhooks/init.go @@ -16,8 +16,10 @@ import ( // init registers the Effect diagnostics callbacks with TypeScript-Go. func init() { - // Set the version suffix so that core.Version() includes the Effect version - core.SetVersionSuffix("+effect-tsgo." + etscore.EffectVersion) + // Set the version suffix so that core.Version() reports the effect-app build + // as semver build metadata on the upstream 7.x base, e.g. "7.0.0-dev+effect-app.5". + // Bump the build counter on each effect-app binary release (gz on effect-app/tsgo). + core.SetVersionSuffix("+effect-app.8") effectconfigraw.Register() // Register the after check source file callback checker.RegisterAfterCheckSourceFileCallback(afterCheckSourceFile) diff --git a/package.json b/package.json index 0c14d05b..d248acca 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "build": "pnpm run build:go && pnpm run build:cli", "build:go": "CGO_ENABLED=0 go build -o tsgo ./typescript-go/cmd/tsgo", "build:cli": "pnpm --filter @effect/tsgo build", - "release:prepare": "bash _tools/release-prepare.sh" + "release:prepare": "bash _tools/release-prepare.sh", + "release:cut": "bash _tools/cut-release.sh" }, "devDependencies": { "@changesets/cli": "^2.30.0"