// Copyright 2016-2020, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the // goconst linter's warning. // // nolint: lll, goconst package nodejs import ( "bytes" "encoding/json" "fmt" "io" "path" "path/filepath" "reflect" "sort" "strconv" "strings" "unicode" "github.com/pkg/errors" "github.com/pulumi/pulumi/pkg/v3/codegen" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) type typeDetails struct { outputType bool inputType bool argsType bool plainType bool } func title(s string) string { if s == "" { return "" } runes := []rune(s) return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)) } func camel(s string) string { if s == "" { return "" } runes := []rune(s) res := make([]rune, 0, len(runes)) for i, r := range runes { if unicode.IsLower(r) { res = append(res, runes[i:]...) break } res = append(res, unicode.ToLower(r)) } return string(res) } type modContext struct { pkg *schema.Package mod string types []*schema.ObjectType enums []*schema.EnumType resources []*schema.Resource functions []*schema.Function typeDetails map[*schema.ObjectType]*typeDetails children []*modContext extraSourceFiles []string tool string // Name overrides set in NodeJSInfo modToPkg map[string]string // Module name -> package name compatibility string // Toggle compatibility mode for a specified target. disableUnionOutputTypes bool // Disable unions in output types. } func (mod *modContext) String() string { return mod.mod } func (mod *modContext) details(t *schema.ObjectType) *typeDetails { details, ok := mod.typeDetails[t] if !ok { details = &typeDetails{} if mod.typeDetails == nil { mod.typeDetails = map[*schema.ObjectType]*typeDetails{} } mod.typeDetails[t] = details } return details } func (mod *modContext) tokenToModName(tok string) string { components := strings.Split(tok, ":") contract.Assertf(len(components) == 3, "malformed token %v", tok) modName := mod.pkg.TokenToModule(tok) if override, ok := mod.modToPkg[modName]; ok { modName = override } if modName != "" { modName = strings.Replace(modName, "/", ".", -1) + "." } return modName } func (mod *modContext) namingContext(pkg *schema.Package) (namingCtx *modContext, pkgName string, external bool) { namingCtx = mod if pkg != nil && pkg != mod.pkg { external = true pkgName = pkg.Name + "." var info NodePackageInfo contract.AssertNoError(pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer})) if v, ok := pkg.Language["nodejs"].(NodePackageInfo); ok { info = v } namingCtx = &modContext{ pkg: pkg, modToPkg: info.ModuleToPackage, compatibility: info.Compatibility, } } return } func (mod *modContext) objectType(pkg *schema.Package, tok string, input, args, enum bool) string { root := "outputs." if input { root = "inputs." } namingCtx, pkgName, external := mod.namingContext(pkg) if external { root = "types.output." if input { root = "types.input." } } modName, name := namingCtx.tokenToModName(tok), tokenToName(tok) if enum { return "enums." + modName + title(name) } if args && mod.compatibility != tfbridge20 && mod.compatibility != kubernetes20 { name += "Args" } return pkgName + root + modName + title(name) } func (mod *modContext) resourceType(r *schema.ResourceType) string { if strings.HasPrefix(r.Token, "pulumi:providers:") { pkgName := strings.TrimPrefix(r.Token, "pulumi:providers:") return fmt.Sprintf("%s.Provider", pkgName) } pkg := mod.pkg if r.Resource != nil { pkg = r.Resource.Package } namingCtx, pkgName, _ := mod.namingContext(pkg) modName, name := namingCtx.tokenToModName(r.Token), tokenToName(r.Token) return pkgName + modName + title(name) } func tokenToName(tok string) string { components := strings.Split(tok, ":") contract.Assertf(len(components) == 3, "malformed token %v", tok) return title(components[2]) } func resourceName(r *schema.Resource) string { if r.IsProvider { return "Provider" } return tokenToName(r.Token) } func (mod *modContext) resourceFileName(r *schema.Resource) string { fileName := camel(resourceName(r)) + ".ts" if mod.isReservedSourceFileName(fileName) { fileName = camel(resourceName(r)) + "_.ts" } return fileName } func tokenToFunctionName(tok string) string { return camel(tokenToName(tok)) } func (mod *modContext) typeString(t schema.Type, input, wrapInput, args, optional bool, constValue interface{}) string { var typ string switch t := t.(type) { case *schema.EnumType: typ = mod.objectType(nil, t.Token, input, args, true) case *schema.ArrayType: typ = mod.typeString(t.ElementType, input, wrapInput, args, false, constValue) + "[]" case *schema.MapType: typ = fmt.Sprintf("{[key: string]: %v}", mod.typeString(t.ElementType, input, wrapInput, args, false, constValue)) case *schema.ObjectType: typ = mod.objectType(t.Package, t.Token, input, args, false) case *schema.ResourceType: typ = mod.resourceType(t) case *schema.TokenType: typ = tokenToName(t.Token) case *schema.UnionType: if !input && mod.disableUnionOutputTypes { if t.DefaultType != nil { return mod.typeString(t.DefaultType, input, wrapInput, args, optional, constValue) } typ = "any" } else { var elements []string for _, e := range t.ElementTypes { t := mod.typeString(e, input, wrapInput, args, false, constValue) if args && strings.HasPrefix(t, "pulumi.Input<") { contract.Assert(t[len(t)-1] == '>') // Strip off the leading `pulumi.Input<` and the trailing `>` t = t[len("pulumi.Input<") : len(t)-1] } elements = append(elements, t) } typ = strings.Join(elements, " | ") } default: switch t { case schema.BoolType: typ = "boolean" case schema.IntType, schema.NumberType: typ = "number" case schema.StringType: typ = "string" case schema.ArchiveType: typ = "pulumi.asset.Archive" case schema.AssetType: typ = "pulumi.asset.Asset | pulumi.asset.Archive" case schema.JSONType: fallthrough case schema.AnyType: typ = "any" } } if constValue != nil && typ == "string" { typ = fmt.Sprintf("%q", constValue.(string)) } if wrapInput && typ != "any" { typ = fmt.Sprintf("pulumi.Input<%s>", typ) } if optional { return typ + " | undefined" } return typ } func isStringType(t schema.Type) bool { for tt, ok := t.(*schema.TokenType); ok; tt, ok = t.(*schema.TokenType) { t = tt.UnderlyingType } return t == schema.StringType } func sanitizeComment(str string) string { return strings.Replace(str, "*/", "*/", -1) } func printComment(w io.Writer, comment, deprecationMessage, indent string) { if comment == "" && deprecationMessage == "" { return } lines := strings.Split(sanitizeComment(comment), "\n") for len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] } fmt.Fprintf(w, "%s/**\n", indent) for _, l := range lines { if l == "" { fmt.Fprintf(w, "%s *\n", indent) } else { fmt.Fprintf(w, "%s * %s\n", indent, l) } } if deprecationMessage != "" { if len(lines) > 0 { fmt.Fprintf(w, "%s *\n", indent) } fmt.Fprintf(w, "%s * @deprecated %s\n", indent, deprecationMessage) } fmt.Fprintf(w, "%s */\n", indent) } func (mod *modContext) genPlainType(w io.Writer, name, comment string, properties []*schema.Property, input, arg, readonly bool, level int) { indent := strings.Repeat(" ", level) printComment(w, comment, "", indent) fmt.Fprintf(w, "%sexport interface %s {\n", indent, name) for _, p := range properties { printComment(w, p.Comment, p.DeprecationMessage, indent+" ") prefix := "" if readonly { prefix = "readonly " } sigil := "" if !p.IsRequired { sigil = "?" } typ := mod.typeString(p.Type, input, arg && !p.IsPlain, arg && !p.IsPlain, false, p.ConstValue) fmt.Fprintf(w, "%s %s%s%s: %s;\n", indent, prefix, p.Name, sigil, typ) } fmt.Fprintf(w, "%s}\n", indent) } func tsPrimitiveValue(value interface{}) (string, error) { v := reflect.ValueOf(value) if v.Kind() == reflect.Interface { v = v.Elem() } switch v.Kind() { case reflect.Bool: if v.Bool() { return "true", nil } return "false", nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: return strconv.FormatInt(v.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Float32, reflect.Float64: return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil case reflect.String: return fmt.Sprintf("%q", v.String()), nil default: return "", errors.Errorf("unsupported default value of type %T", value) } } func (mod *modContext) getConstValue(cv interface{}) (string, error) { if cv == nil { return "", nil } return tsPrimitiveValue(cv) } func (mod *modContext) getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) { var val string if dv.Value != nil { v, err := tsPrimitiveValue(dv.Value) if err != nil { return "", err } val = v } if len(dv.Environment) != 0 { getType := "" switch t { case schema.BoolType: getType = "Boolean" case schema.IntType, schema.NumberType: getType = "Number" } envVars := fmt.Sprintf("%q", dv.Environment[0]) for _, e := range dv.Environment[1:] { envVars += fmt.Sprintf(", %q", e) } cast := "" if t != schema.StringType { cast = "" } getEnv := fmt.Sprintf("%sutilities.getEnv%s(%s)", cast, getType, envVars) if val != "" { val = fmt.Sprintf("(%s || %s)", getEnv, val) } else { val = getEnv } } return val, nil } func (mod *modContext) genAlias(w io.Writer, alias *schema.Alias) { fmt.Fprintf(w, "{ ") var parts []string if alias.Name != nil { parts = append(parts, fmt.Sprintf("name: \"%v\"", *alias.Name)) } if alias.Project != nil { parts = append(parts, fmt.Sprintf("project: \"%v\"", *alias.Project)) } if alias.Type != nil { parts = append(parts, fmt.Sprintf("type: \"%v\"", *alias.Type)) } for i, part := range parts { if i > 0 { fmt.Fprintf(w, ", ") } fmt.Fprintf(w, "%s", part) } fmt.Fprintf(w, " }") } func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error { // Create a resource module file into which all of this resource's types will go. name := resourceName(r) // Write the TypeDoc/JSDoc for the resource class printComment(w, codegen.FilterExamples(r.Comment, "typescript"), r.DeprecationMessage, "") var baseType, optionsType string switch { case r.IsComponent: baseType, optionsType = "ComponentResource", "ComponentResourceOptions" case r.IsProvider: baseType, optionsType = "ProviderResource", "ResourceOptions" default: baseType, optionsType = "CustomResource", "CustomResourceOptions" } // Begin defining the class. fmt.Fprintf(w, "export class %s extends pulumi.%s {\n", name, baseType) // Emit a static factory to read instances of this resource unless this is a provider resource or ComponentResource. stateType := name + "State" if !r.IsProvider && !r.IsComponent { fmt.Fprintf(w, " /**\n") fmt.Fprintf(w, " * Get an existing %s resource's state with the given name, ID, and optional extra\n", name) fmt.Fprintf(w, " * properties used to qualify the lookup.\n") fmt.Fprintf(w, " *\n") fmt.Fprintf(w, " * @param name The _unique_ name of the resulting resource.\n") fmt.Fprintf(w, " * @param id The _unique_ provider ID of the resource to lookup.\n") // TODO: Document id format: https://github.com/pulumi/pulumi/issues/4754 if r.StateInputs != nil { fmt.Fprintf(w, " * @param state Any extra arguments used during the lookup.\n") } fmt.Fprintf(w, " * @param opts Optional settings to control the behavior of the CustomResource.\n") fmt.Fprintf(w, " */\n") stateParam, stateRef := "", "undefined as any, " if r.StateInputs != nil { stateParam, stateRef = fmt.Sprintf("state?: %s, ", stateType), "state, " } fmt.Fprintf(w, " public static get(name: string, id: pulumi.Input, %sopts?: pulumi.%s): %s {\n", stateParam, optionsType, name) if r.DeprecationMessage != "" && mod.compatibility != kubernetes20 { fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, r.DeprecationMessage) } fmt.Fprintf(w, " return new %s(name, %s{ ...opts, id: id });\n", name, stateRef) fmt.Fprintf(w, " }\n") fmt.Fprintf(w, "\n") } pulumiType := r.Token if r.IsProvider { pulumiType = mod.pkg.Name } fmt.Fprintf(w, " /** @internal */\n") fmt.Fprintf(w, " public static readonly __pulumiType = '%s';\n", pulumiType) fmt.Fprintf(w, "\n") fmt.Fprintf(w, " /**\n") fmt.Fprintf(w, " * Returns true if the given object is an instance of %s. This is designed to work even\n", name) fmt.Fprintf(w, " * when multiple copies of the Pulumi SDK have been loaded into the same process.\n") fmt.Fprintf(w, " */\n") fmt.Fprintf(w, " public static isInstance(obj: any): obj is %s {\n", name) fmt.Fprintf(w, " if (obj === undefined || obj === null) {\n") fmt.Fprintf(w, " return false;\n") fmt.Fprintf(w, " }\n") fmt.Fprintf(w, " return obj['__pulumiType'] === %s.__pulumiType;\n", name) fmt.Fprintf(w, " }\n") fmt.Fprintf(w, "\n") // Emit all properties (using their output types). // TODO[pulumi/pulumi#397]: represent sensitive types using a Secret type. ins := codegen.NewStringSet() allOptionalInputs := true for _, prop := range r.InputProperties { ins.Add(prop.Name) allOptionalInputs = allOptionalInputs && !prop.IsRequired } for _, prop := range r.Properties { printComment(w, prop.Comment, prop.DeprecationMessage, " ") // Make a little comment in the code so it's easy to pick out output properties. var outcomment string if !ins.Has(prop.Name) { outcomment = "/*out*/ " } required := prop.IsRequired if mod.compatibility == kubernetes20 { required = true } fmt.Fprintf(w, " public %sreadonly %s!: pulumi.Output<%s>;\n", outcomment, prop.Name, mod.typeString(prop.Type, false, false, false, !required, prop.ConstValue)) } fmt.Fprintf(w, "\n") // Now create a constructor that chains supercalls and stores into properties. fmt.Fprintf(w, " /**\n") fmt.Fprintf(w, " * Create a %s resource with the given unique name, arguments, and options.\n", name) fmt.Fprintf(w, " *\n") fmt.Fprintf(w, " * @param name The _unique_ name of the resource.\n") fmt.Fprintf(w, " * @param args The arguments to use to populate this resource's properties.\n") fmt.Fprintf(w, " * @param opts A bag of options that control this resource's behavior.\n") fmt.Fprintf(w, " */\n") // k8s provider "get" methods don't require args, so make args optional. if mod.compatibility == kubernetes20 { allOptionalInputs = true } // Write out callable constructor: We only emit a single public constructor, even though we use a private signature // as well as part of the implementation of `.get`. This is complicated slightly by the fact that, if there is no // args type, we will emit a constructor lacking that parameter. var argsFlags string if allOptionalInputs { // If the number of required input properties was zero, we can make the args object optional. argsFlags = "?" } argsType := name + "Args" var trailingBrace string switch { case r.IsProvider, r.StateInputs == nil: trailingBrace = " {" default: trailingBrace = "" } if r.DeprecationMessage != "" { fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage) } fmt.Fprintf(w, " constructor(name: string, args%s: %s, opts?: pulumi.%s)%s\n", argsFlags, argsType, optionsType, trailingBrace) genInputProps := func() error { for _, prop := range r.InputProperties { if prop.IsRequired { fmt.Fprintf(w, " if ((!args || args.%s === undefined) && !opts.urn) {\n", prop.Name) fmt.Fprintf(w, " throw new Error(\"Missing required property '%s'\");\n", prop.Name) fmt.Fprintf(w, " }\n") } } for _, prop := range r.InputProperties { var arg string if prop.Secret { arg = fmt.Sprintf("args?.%[1]s ? pulumi.secret(args.%[1]s) : undefined", prop.Name) } else { arg = fmt.Sprintf("args ? args.%[1]s : undefined", prop.Name) } prefix := " " if prop.ConstValue != nil { cv, err := mod.getConstValue(prop.ConstValue) if err != nil { return err } arg = cv } else { if prop.DefaultValue != nil { dv, err := mod.getDefaultValue(prop.DefaultValue, prop.Type) if err != nil { return err } arg = fmt.Sprintf("(%s) ?? %s", arg, dv) } // provider properties must be marshaled as JSON strings. if r.IsProvider && !isStringType(prop.Type) { arg = fmt.Sprintf("pulumi.output(%s).apply(JSON.stringify)", arg) } } fmt.Fprintf(w, "%sinputs[\"%s\"] = %s;\n", prefix, prop.Name, arg) } for _, prop := range r.Properties { prefix := " " if !ins.Has(prop.Name) { fmt.Fprintf(w, "%sinputs[\"%s\"] = undefined /*out*/;\n", prefix, prop.Name) } } return nil } if !r.IsProvider { if r.StateInputs != nil { if r.DeprecationMessage != "" { fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage) } // Now write out a general purpose constructor implementation that can handle the public signature as well as the // signature to support construction via `.get`. And then emit the body preamble which will pluck out the // conditional state into sensible variables using dynamic type tests. fmt.Fprintf(w, " constructor(name: string, argsOrState?: %s | %s, opts?: pulumi.%s) {\n", argsType, stateType, optionsType) } if r.DeprecationMessage != "" && mod.compatibility != kubernetes20 { fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, r.DeprecationMessage) } fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n") fmt.Fprintf(w, " opts = opts || {};\n") if r.StateInputs != nil { // The lookup case: fmt.Fprintf(w, " if (opts.id) {\n") fmt.Fprintf(w, " const state = argsOrState as %[1]s | undefined;\n", stateType) for _, prop := range r.StateInputs.Properties { fmt.Fprintf(w, " inputs[\"%[1]s\"] = state ? state.%[1]s : undefined;\n", prop.Name) } // The creation case (with args): fmt.Fprintf(w, " } else {\n") fmt.Fprintf(w, " const args = argsOrState as %s | undefined;\n", argsType) err := genInputProps() if err != nil { return err } } else { // The creation case: fmt.Fprintf(w, " if (!opts.id) {\n") err := genInputProps() if err != nil { return err } // The get case: fmt.Fprintf(w, " } else {\n") for _, prop := range r.Properties { fmt.Fprintf(w, " inputs[\"%[1]s\"] = undefined /*out*/;\n", prop.Name) } } } else { fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n") fmt.Fprintf(w, " opts = opts || {};\n") fmt.Fprintf(w, " {\n") err := genInputProps() if err != nil { return err } } var secretProps []string for _, prop := range r.Properties { if prop.Secret { secretProps = append(secretProps, prop.Name) } } fmt.Fprintf(w, " }\n") // If the caller didn't request a specific version, supply one using the version of this library. fmt.Fprintf(w, " if (!opts.version) {\n") fmt.Fprintf(w, " opts = pulumi.mergeOptions(opts, { version: utilities.getVersion()});\n") fmt.Fprintf(w, " }\n") // Now invoke the super constructor with the type, name, and a property map. if len(r.Aliases) > 0 { fmt.Fprintf(w, " const aliasOpts = { aliases: [") for i, alias := range r.Aliases { if i > 0 { fmt.Fprintf(w, ", ") } mod.genAlias(w, alias) } fmt.Fprintf(w, "] };\n") fmt.Fprintf(w, " opts = pulumi.mergeOptions(opts, aliasOpts);\n") } if len(secretProps) > 0 { fmt.Fprintf(w, ` const secretOpts = { additionalSecretOutputs: ["%s"] };`, strings.Join(secretProps, `", "`)) fmt.Fprintf(w, "\n opts = pulumi.mergeOptions(opts, secretOpts);\n") } // If it's a ComponentResource, set the remote option. if r.IsComponent { fmt.Fprintf(w, " super(%s.__pulumiType, name, inputs, opts, true /*remote*/);\n", name) } else { fmt.Fprintf(w, " super(%s.__pulumiType, name, inputs, opts);\n", name) } // Finish the class. fmt.Fprintf(w, " }\n") fmt.Fprintf(w, "}\n") // Emit the state type for get methods. if r.StateInputs != nil { fmt.Fprintf(w, "\n") mod.genPlainType(w, stateType, r.StateInputs.Comment, r.StateInputs.Properties, true, true, false, 0) } // Emit the argument type for construction. fmt.Fprintf(w, "\n") argsComment := fmt.Sprintf("The set of arguments for constructing a %s resource.", name) mod.genPlainType(w, argsType, argsComment, r.InputProperties, true, true, false, 0) return nil } func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) { name := tokenToFunctionName(fun.Token) // Write the TypeDoc/JSDoc for the data source function. printComment(w, codegen.FilterExamples(fun.Comment, "typescript"), "", "") if fun.DeprecationMessage != "" { fmt.Fprintf(w, "/** @deprecated %s */\n", fun.DeprecationMessage) } // Now, emit the function signature. var argsig string argsOptional := true if fun.Inputs != nil { for _, p := range fun.Inputs.Properties { if p.IsRequired { argsOptional = false break } } optFlag := "" if argsOptional { optFlag = "?" } argsig = fmt.Sprintf("args%s: %sArgs, ", optFlag, title(name)) } var retty string if fun.Outputs == nil { retty = "void" } else { retty = title(name) + "Result" } fmt.Fprintf(w, "export function %s(%sopts?: pulumi.InvokeOptions): Promise<%s> {\n", name, argsig, retty) if fun.DeprecationMessage != "" && mod.compatibility != kubernetes20 { fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, fun.DeprecationMessage) } // Zero initialize the args if empty and necessary. if fun.Inputs != nil && argsOptional { fmt.Fprintf(w, " args = args || {};\n") } // If the caller didn't request a specific version, supply one using the version of this library. fmt.Fprintf(w, " if (!opts) {\n") fmt.Fprintf(w, " opts = {}\n") fmt.Fprintf(w, " }\n") fmt.Fprintf(w, "\n") fmt.Fprintf(w, " if (!opts.version) {\n") fmt.Fprintf(w, " opts.version = utilities.getVersion();\n") fmt.Fprintf(w, " }\n") // Now simply invoke the runtime function with the arguments, returning the results. fmt.Fprintf(w, " return pulumi.runtime.invoke(\"%s\", {\n", fun.Token) if fun.Inputs != nil { for _, p := range fun.Inputs.Properties { // Pass the argument to the invocation. fmt.Fprintf(w, " \"%[1]s\": args.%[1]s,\n", p.Name) } } fmt.Fprintf(w, " }, opts);\n") fmt.Fprintf(w, "}\n") // If there are argument and/or return types, emit them. if fun.Inputs != nil { fmt.Fprintf(w, "\n") mod.genPlainType(w, title(name)+"Args", fun.Inputs.Comment, fun.Inputs.Properties, true, false, false, 0) } if fun.Outputs != nil { fmt.Fprintf(w, "\n") mod.genPlainType(w, title(name)+"Result", fun.Outputs.Comment, fun.Outputs.Properties, false, false, true, 0) } } func visitObjectTypes(properties []*schema.Property, visitor func(*schema.ObjectType, bool)) { codegen.VisitTypeClosure(properties, func(t codegen.Type) { if o, ok := t.Type.(*schema.ObjectType); ok { visitor(o, t.Plain) } }) } func (mod *modContext) genType(w io.Writer, obj *schema.ObjectType, input bool, level int) { properties := obj.Properties info, hasInfo := obj.Language["nodejs"] if hasInfo { var requiredProperties []string if input { requiredProperties = info.(NodeObjectInfo).RequiredInputs } else { requiredProperties = info.(NodeObjectInfo).RequiredOutputs } if requiredProperties != nil { required := codegen.StringSet{} for _, name := range requiredProperties { required.Add(name) } properties = make([]*schema.Property, len(obj.Properties)) for i, p := range obj.Properties { copy := *p properties[i] = © properties[i].IsRequired = required.Has(p.Name) } } } name := tokenToName(obj.Token) if mod.compatibility == tfbridge20 || mod.compatibility == kubernetes20 { wrapInput := input && !mod.details(obj).plainType mod.genPlainType(w, name, obj.Comment, properties, input, wrapInput, false, level) return } if input { if mod.details(obj).plainType { mod.genPlainType(w, name, obj.Comment, properties, true, false, false, level) } if mod.details(obj).argsType { mod.genPlainType(w, name+"Args", obj.Comment, properties, true, true, false, level) } return } mod.genPlainType(w, name, obj.Comment, properties, false, false, false, level) } func (mod *modContext) getTypeImports(t schema.Type, recurse bool, externalImports codegen.StringSet, imports map[string]codegen.StringSet, seen codegen.Set) bool { if seen.Has(t) { return false } seen.Add(t) resourceOrTokenImport := func(tok string) bool { modName, name, modPath := mod.pkg.TokenToModule(tok), tokenToName(tok), "./index" if override, ok := mod.modToPkg[modName]; ok { modName = override } if modName != mod.mod { mp, err := filepath.Rel(mod.mod, modName) contract.Assert(err == nil) if path.Base(mp) == "." { mp = path.Dir(mp) } modPath = filepath.ToSlash(mp) } if imports[modPath] == nil { imports[modPath] = codegen.NewStringSet() } imports[modPath].Add(name) return false } switch t := t.(type) { case *schema.ArrayType: return mod.getTypeImports(t.ElementType, recurse, externalImports, imports, seen) case *schema.MapType: return mod.getTypeImports(t.ElementType, recurse, externalImports, imports, seen) case *schema.EnumType: return true case *schema.ObjectType: // If it's from another package, add an import for the external package. if t.Package != nil && t.Package != mod.pkg { pkg := t.Package.Name externalImports.Add(fmt.Sprintf("import * as %[1]s from \"@pulumi/%[1]s\";", pkg)) return false } for _, p := range t.Properties { mod.getTypeImports(p.Type, recurse, externalImports, imports, seen) } return true case *schema.ResourceType: // If it's from another package, add an import for the external package. if t.Resource != nil && t.Resource.Package != mod.pkg { pkg := t.Resource.Package.Name externalImports.Add(fmt.Sprintf("import * as %[1]s from \"@pulumi/%[1]s\";", pkg)) return false } return resourceOrTokenImport(t.Token) case *schema.TokenType: return resourceOrTokenImport(t.Token) case *schema.UnionType: needsTypes := false for _, e := range t.ElementTypes { needsTypes = mod.getTypeImports(e, recurse, externalImports, imports, seen) || needsTypes } return needsTypes default: return false } } func (mod *modContext) getImports(member interface{}, externalImports codegen.StringSet, imports map[string]codegen.StringSet) bool { seen := codegen.Set{} switch member := member.(type) { case *schema.ObjectType: needsTypes := false for _, p := range member.Properties { needsTypes = mod.getTypeImports(p.Type, true, externalImports, imports, seen) || needsTypes } return needsTypes case *schema.ResourceType: mod.getTypeImports(member, true, externalImports, imports, seen) return false case *schema.Resource: needsTypes := false for _, p := range member.Properties { needsTypes = mod.getTypeImports(p.Type, false, externalImports, imports, seen) || needsTypes } for _, p := range member.InputProperties { needsTypes = mod.getTypeImports(p.Type, false, externalImports, imports, seen) || needsTypes } return needsTypes case *schema.Function: needsTypes := false if member.Inputs != nil { needsTypes = mod.getTypeImports(member.Inputs, false, externalImports, imports, seen) || needsTypes } if member.Outputs != nil { needsTypes = mod.getTypeImports(member.Outputs, false, externalImports, imports, seen) || needsTypes } return needsTypes case []*schema.Property: needsTypes := false for _, p := range member { needsTypes = mod.getTypeImports(p.Type, false, externalImports, imports, seen) || needsTypes } return needsTypes default: return false } } func (mod *modContext) genHeader(w io.Writer, imports []string, externalImports codegen.StringSet, importedTypes map[string]codegen.StringSet) { fmt.Fprintf(w, "// *** WARNING: this file was generated by %v. ***\n", mod.tool) fmt.Fprintf(w, "// *** Do not edit by hand unless you're certain you know what you are doing! ***\n\n") if len(imports) > 0 { for _, i := range imports { fmt.Fprintf(w, "%s\n", i) } fmt.Fprintf(w, "\n") } if externalImports.Any() { for _, i := range externalImports.SortedValues() { fmt.Fprintf(w, "%s\n", i) } fmt.Fprintf(w, "\n") } if len(importedTypes) > 0 { var modules []string for module := range importedTypes { modules = append(modules, module) } sort.Strings(modules) for _, module := range modules { fmt.Fprintf(w, "import {") for i, name := range importedTypes[module].SortedValues() { if i > 0 { fmt.Fprint(w, ", ") } fmt.Fprint(w, name) } fmt.Fprintf(w, "} from \"%s\";\n", module) } fmt.Fprintf(w, "\n") } } // configGetter returns the name of the config.get* method used for a configuration variable and the cast necessary // for the result of the call, if any. func (mod *modContext) configGetter(v *schema.Property) (string, string) { if v.Type == schema.StringType { return "get", "" } if tok, ok := v.Type.(*schema.TokenType); ok && tok.UnderlyingType == schema.StringType { return "get", fmt.Sprintf("<%s>", mod.typeString(v.Type, false, false, false, false, nil)) } // Only try to parse a JSON object if the config isn't a straight string. return fmt.Sprintf("getObject<%s>", mod.typeString(v.Type, false, false, false, false, nil)), "" } func (mod *modContext) genConfig(w io.Writer, variables []*schema.Property) error { externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{} referencesNestedTypes := mod.getImports(variables, externalImports, imports) mod.genHeader(w, mod.sdkImports(referencesNestedTypes, true), externalImports, imports) // Create a config bag for the variables to pull from. fmt.Fprintf(w, "let __config = new pulumi.Config(\"%v\");\n", mod.pkg.Name) fmt.Fprintf(w, "\n") // Emit an entry for all config variables. for _, p := range variables { getfunc, cast := mod.configGetter(p) printComment(w, p.Comment, "", "") configFetch := fmt.Sprintf("%s__config.%s(\"%s\")", cast, getfunc, p.Name) // TODO: handle ConstValues https://github.com/pulumi/pulumi/issues/4755 if p.DefaultValue != nil { v, err := mod.getDefaultValue(p.DefaultValue, p.Type) if err != nil { return err } // Note: this logic isn't quite correct, but already exists in all of the TF-based providers. // Specifically, this doesn't work right if the first value is set to false but the default value // is true. configFetch += " || " + v } fmt.Fprintf(w, "export let %s: %s = %s;\n", p.Name, mod.typeString(p.Type, false, false, false, true, nil), configFetch) } return nil } func (mod *modContext) getRelativePath() string { rel, err := filepath.Rel(mod.mod, "") contract.Assert(err == nil) return path.Dir(filepath.ToSlash(rel)) } func (mod *modContext) sdkImports(nested, utilities bool) []string { imports := []string{"import * as pulumi from \"@pulumi/pulumi\";"} relRoot := mod.getRelativePath() if nested { enumsImport := "" containsEnums := mod.pkg.Language["nodejs"].(NodePackageInfo).ContainsEnums if containsEnums { enumsImport = ", enums" } imports = append(imports, fmt.Sprintf(`import { input as inputs, output as outputs%s } from "%s/types";`, enumsImport, relRoot)) } if utilities { imports = append(imports, fmt.Sprintf("import * as utilities from \"%s/utilities\";", relRoot)) } return imports } func (mod *modContext) genTypes() (string, string) { externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{} for _, t := range mod.types { mod.getImports(t, externalImports, imports) } inputs, outputs := &bytes.Buffer{}, &bytes.Buffer{} mod.genHeader(inputs, mod.sdkImports(true, false), externalImports, imports) mod.genHeader(outputs, mod.sdkImports(true, false), externalImports, imports) // Build a namespace tree out of the types, then emit them. namespaces := mod.getNamespaces() mod.genNamespace(inputs, namespaces[""], true, 0) mod.genNamespace(outputs, namespaces[""], false, 0) return inputs.String(), outputs.String() } type namespace struct { name string types []*schema.ObjectType enums []*schema.EnumType children []*namespace } func (mod *modContext) getNamespaces() map[string]*namespace { namespaces := map[string]*namespace{} var getNamespace func(string) *namespace getNamespace = func(mod string) *namespace { ns, ok := namespaces[mod] if !ok { name := mod if mod != "" { name = path.Base(mod) } ns = &namespace{name: name} if mod != "" { parentMod := path.Dir(mod) if parentMod == "." { parentMod = "" } parent := getNamespace(parentMod) parent.children = append(parent.children, ns) } namespaces[mod] = ns } return ns } for _, t := range mod.types { modName := mod.pkg.TokenToModule(t.Token) if override, ok := mod.modToPkg[modName]; ok { modName = override } ns := getNamespace(modName) ns.types = append(ns.types, t) } return namespaces } func (mod *modContext) genNamespace(w io.Writer, ns *namespace, input bool, level int) { indent := strings.Repeat(" ", level) sort.Slice(ns.types, func(i, j int) bool { return tokenToName(ns.types[i].Token) < tokenToName(ns.types[j].Token) }) sort.Slice(ns.enums, func(i, j int) bool { return tokenToName(ns.enums[i].Token) < tokenToName(ns.enums[j].Token) }) for i, t := range ns.types { if input && mod.details(t).inputType || !input && mod.details(t).outputType { mod.genType(w, t, input, level) if i != len(ns.types)-1 { fmt.Fprintf(w, "\n") } } } sort.Slice(ns.children, func(i, j int) bool { return ns.children[i].name < ns.children[j].name }) for i, child := range ns.children { fmt.Fprintf(w, "%sexport namespace %s {\n", indent, child.name) mod.genNamespace(w, child, input, level+1) fmt.Fprintf(w, "%s}\n", indent) if i != len(ns.children)-1 { fmt.Fprintf(w, "\n") } } } func (mod *modContext) genEnum(w io.Writer, enum *schema.EnumType) error { indent := " " enumName := tokenToName(enum.Token) fmt.Fprintf(w, "export const %s = {\n", enumName) for _, e := range enum.Elements { // If the enum doesn't have a name, set the value as the name. if e.Name == "" { e.Name = fmt.Sprintf("%v", e.Value) } safeName, err := makeSafeEnumName(e.Name, enumName) if err != nil { return err } e.Name = safeName printComment(w, e.Comment, e.DeprecationMessage, indent) fmt.Fprintf(w, "%s%s: ", indent, e.Name) if val, ok := e.Value.(string); ok { fmt.Fprintf(w, "%q,\n", val) } else { fmt.Fprintf(w, "%v,\n", e.Value) } } fmt.Fprintf(w, "} as const;\n") fmt.Fprintf(w, "\n") printComment(w, enum.Comment, "", "") fmt.Fprintf(w, "export type %[1]s = (typeof %[1]s)[keyof typeof %[1]s];\n", enumName) return nil } type fs map[string][]byte func (fs fs) add(path string, contents []byte) { _, has := fs[path] contract.Assertf(!has, "duplicate file: %s", path) fs[path] = contents } func (mod *modContext) isReservedSourceFileName(name string) bool { switch name { case "index.ts": return true case "input.ts", "output.ts": return len(mod.types) != 0 case "utilities.ts": return mod.mod == "" case "vars.ts": return len(mod.pkg.Config) > 0 default: return false } } func (mod *modContext) gen(fs fs) error { files := append([]string(nil), mod.extraSourceFiles...) modDir := strings.ToLower(mod.mod) addFile := func(name, contents string) { p := path.Join(modDir, name) files = append(files, p) fs.add(p, []byte(contents)) } // Utilities, config, readme switch mod.mod { case "": buffer := &bytes.Buffer{} mod.genHeader(buffer, nil, nil, nil) fmt.Fprintf(buffer, "%s", utilitiesFile) fs.add(path.Join(modDir, "utilities.ts"), buffer.Bytes()) // Ensure that the top-level (provider) module directory contains a README.md file. readme := mod.pkg.Language["nodejs"].(NodePackageInfo).Readme if readme == "" { readme = mod.pkg.Description if readme != "" && readme[len(readme)-1] != '\n' { readme += "\n" } if mod.pkg.Attribution != "" { if len(readme) != 0 { readme += "\n" } readme += mod.pkg.Attribution } } if readme != "" && readme[len(readme)-1] != '\n' { readme += "\n" } fs.add(path.Join(modDir, "README.md"), []byte(readme)) case "config": if len(mod.pkg.Config) > 0 { buffer := &bytes.Buffer{} if err := mod.genConfig(buffer, mod.pkg.Config); err != nil { return err } addFile("vars.ts", buffer.String()) } } // Resources for _, r := range mod.resources { externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{} referencesNestedTypes := mod.getImports(r, externalImports, imports) buffer := &bytes.Buffer{} mod.genHeader(buffer, mod.sdkImports(referencesNestedTypes, true), externalImports, imports) if err := mod.genResource(buffer, r); err != nil { return err } fileName := mod.resourceFileName(r) addFile(fileName, buffer.String()) } // Functions for _, f := range mod.functions { externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{} referencesNestedTypes := mod.getImports(f, externalImports, imports) buffer := &bytes.Buffer{} mod.genHeader(buffer, mod.sdkImports(referencesNestedTypes, true), externalImports, imports) mod.genFunction(buffer, f) fileName := camel(tokenToName(f.Token)) + ".ts" if mod.isReservedSourceFileName(fileName) { fileName = camel(tokenToName(f.Token)) + "_.ts" } addFile(fileName, buffer.String()) } if mod.hasEnums() { buffer := &bytes.Buffer{} mod.genHeader(buffer, []string{}, nil, nil) err := mod.genEnums(buffer, mod.enums) if err != nil { return err } var fileName string if modDir == "" { fileName = "index.ts" } else { fileName = path.Join(modDir, "index.ts") } fileName = path.Join("types", "enums", fileName) fs.add(fileName, buffer.Bytes()) } // Nested types if len(mod.types) > 0 { input, output := mod.genTypes() fs.add(path.Join(modDir, "input.ts"), []byte(input)) fs.add(path.Join(modDir, "output.ts"), []byte(output)) } // Index fs.add(path.Join(modDir, "index.ts"), []byte(mod.genIndex(files))) return nil } func getChildMod(modName string) string { child := strings.ToLower(modName) // Extract version suffix from child modules. Nested versions will have their own index.ts file. // Example: apps/v1beta1 -> v1beta1 parts := strings.SplitN(child, "/", 2) if len(parts) == 2 { child = parts[1] } return child } // genIndex emits an index module, optionally re-exporting other members or submodules. func (mod *modContext) genIndex(exports []string) string { w := &bytes.Buffer{} var imports []string // Include the SDK import if we'll be registering module resources. if len(mod.resources) != 0 { imports = mod.sdkImports(false /*nested*/, true /*utilities*/) } mod.genHeader(w, imports, nil, nil) // Export anything flatly that is a direct export rather than sub-module. if len(exports) > 0 { modDir := strings.ToLower(mod.mod) fmt.Fprintf(w, "// Export members:\n") sort.Strings(exports) for _, exp := range exports { rel, err := filepath.Rel(modDir, exp) contract.Assert(err == nil) if path.Base(rel) == "." { rel = path.Dir(rel) } fmt.Fprintf(w, "export * from \"./%s\";\n", strings.TrimSuffix(rel, ".ts")) } } children := codegen.NewStringSet() for _, mod := range mod.children { child := getChildMod(mod.mod) children.Add(child) } if len(mod.types) > 0 { children.Add("input") children.Add("output") } info, _ := mod.pkg.Language["nodejs"].(NodePackageInfo) if info.ContainsEnums { if mod.mod == "types" { children.Add("enums") } else if len(mod.enums) > 0 { fmt.Fprintf(w, "\n") fmt.Fprintf(w, "// Export enums:\n") rel := mod.getRelativePath() var filePath string if mod.mod == "" { filePath = "" } else { filePath = fmt.Sprintf("/%s", mod.mod) } fmt.Fprintf(w, "export * from \"%s/types/enums%s\";\n", rel, filePath) } } // If there are submodules, export them. if len(children) > 0 { if len(exports) > 0 { fmt.Fprintf(w, "\n") } fmt.Fprintf(w, "// Export sub-modules:\n") sorted := children.SortedValues() for _, mod := range sorted { fmt.Fprintf(w, "import * as %[1]s from \"./%[1]s\";\n", mod) } printExports(w, sorted) } // If there are resources in this module, register the module with the runtime. if len(mod.resources) != 0 { mod.genResourceModule(w) } return w.String() } // genResourceModule generates a ResourceModule definition and the code to register an instance thereof with the // Pulumi runtime. The generated ResourceModule supports the deserialization of resource references into fully- // hydrated Resource instances. If this is the root module, this function also generates a ResourcePackage // definition and its registration to support rehydrating providers. func (mod *modContext) genResourceModule(w io.Writer) { contract.Assert(len(mod.resources) != 0) // Check for provider-only modules. var provider *schema.Resource if providerOnly := len(mod.resources) == 1 && mod.resources[0].IsProvider; providerOnly { provider = mod.resources[0] } else { registrations, first := codegen.StringSet{}, true for _, r := range mod.resources { if r.IsProvider { contract.Assert(provider == nil) provider = r continue } registrations.Add(mod.pkg.TokenToRuntimeModule(r.Token)) if first { first = false fmt.Fprintf(w, "\n// Import resources to register:\n") } fileName := strings.TrimSuffix(mod.resourceFileName(r), ".ts") fmt.Fprintf(w, "import { %s } from \"./%s\";\n", resourceName(r), fileName) } fmt.Fprintf(w, "\nconst _module = {\n") fmt.Fprintf(w, " version: utilities.getVersion(),\n") fmt.Fprintf(w, " construct: (name: string, type: string, urn: string): pulumi.Resource => {\n") fmt.Fprintf(w, " switch (type) {\n") for _, r := range mod.resources { if r.IsProvider { continue } fmt.Fprintf(w, " case \"%v\":\n", r.Token) fmt.Fprintf(w, " return new %v(name, undefined, { urn })\n", resourceName(r)) } fmt.Fprintf(w, " default:\n") fmt.Fprintf(w, " throw new Error(`unknown resource type ${type}`);\n") fmt.Fprintf(w, " }\n") fmt.Fprintf(w, " },\n") fmt.Fprintf(w, "};\n") for _, name := range registrations.SortedValues() { fmt.Fprintf(w, "pulumi.runtime.registerResourceModule(\"%v\", \"%v\", _module)\n", mod.pkg.Name, name) } } if provider != nil { fmt.Fprintf(w, "\nimport { Provider } from \"./provider\";\n\n") fmt.Fprintf(w, "pulumi.runtime.registerResourcePackage(\"%v\", {\n", mod.pkg.Name) fmt.Fprintf(w, " version: utilities.getVersion(),\n") fmt.Fprintf(w, " constructProvider: (name: string, type: string, urn: string): pulumi.ProviderResource => {\n") fmt.Fprintf(w, " if (type !== \"%v\") {\n", provider.Token) fmt.Fprintf(w, " throw new Error(`unknown provider type ${type}`);\n") fmt.Fprintf(w, " }\n") fmt.Fprintf(w, " return new Provider(name, undefined, { urn });\n") fmt.Fprintf(w, " },\n") fmt.Fprintf(w, "});\n") } } func printExports(w io.Writer, exports []string) { fmt.Fprintf(w, "\n") fmt.Fprintf(w, "export {\n") for _, mod := range exports { fmt.Fprintf(w, " %s,\n", mod) } fmt.Fprintf(w, "};\n") } func (mod *modContext) hasEnums() bool { if mod.mod == "types" { return false } if len(mod.enums) > 0 { return true } if len(mod.children) > 0 { for _, mod := range mod.children { if mod.hasEnums() { return true } } } return false } func (mod *modContext) genEnums(buffer *bytes.Buffer, enums []*schema.EnumType) error { if len(mod.children) > 0 { children := codegen.NewStringSet() for _, mod := range mod.children { child := getChildMod(mod.mod) if mod.hasEnums() { children.Add(child) } } if len(children) > 0 { fmt.Fprintf(buffer, "// Export sub-modules:\n") sorted := children.SortedValues() for _, mod := range sorted { fmt.Fprintf(buffer, "import * as %[1]s from \"./%[1]s\";\n", mod) } printExports(buffer, sorted) } } if len(enums) > 0 { fmt.Fprintf(buffer, "\n") for i, enum := range enums { err := mod.genEnum(buffer, enum) if err != nil { return err } if i != len(enums)-1 { fmt.Fprintf(buffer, "\n") } } } return nil } // genPackageMetadata generates all the non-code metadata required by a Pulumi package. func genPackageMetadata(pkg *schema.Package, info NodePackageInfo, files fs) { // The generator already emitted Pulumi.yaml, so that leaves two more files to write out: // 1) package.json: minimal NPM package metadata // 2) tsconfig.json: instructions for TypeScript compilation files.add("package.json", []byte(genNPMPackageMetadata(pkg, info))) files.add("tsconfig.json", []byte(genTypeScriptProjectFile(info, files))) } type npmPackage struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description,omitempty"` Keywords []string `json:"keywords,omitempty"` Homepage string `json:"homepage,omitempty"` Repository string `json:"repository,omitempty"` License string `json:"license,omitempty"` Scripts map[string]string `json:"scripts,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` DevDependencies map[string]string `json:"devDependencies,omitempty"` PeerDependencies map[string]string `json:"peerDependencies,omitempty"` Resolutions map[string]string `json:"resolutions,omitempty"` Pulumi npmPulumiManifest `json:"pulumi,omitempty"` } type npmPulumiManifest struct { Resource bool `json:"resource,omitempty"` PluginDownloadURL string `json:"pluginDownloadURL,omitempty"` } func genNPMPackageMetadata(pkg *schema.Package, info NodePackageInfo) string { packageName := info.PackageName if packageName == "" { packageName = fmt.Sprintf("@pulumi/%s", pkg.Name) } devDependencies := map[string]string{} if info.TypeScriptVersion != "" { devDependencies["typescript"] = info.TypeScriptVersion } // Create info that will get serialized into an NPM package.json. npminfo := npmPackage{ Name: packageName, Version: "${VERSION}", Description: info.PackageDescription, Keywords: pkg.Keywords, Homepage: pkg.Homepage, Repository: pkg.Repository, License: pkg.License, // Ideally, this `scripts` section would include an install script that installs the provider, however, doing // so causes problems when we try to restore package dependencies, since we must do an install for that. So // we have another process that adds the install script when generating the package.json that we actually // publish. Scripts: map[string]string{ "build": "tsc", }, DevDependencies: devDependencies, Pulumi: npmPulumiManifest{ Resource: true, PluginDownloadURL: pkg.PluginDownloadURL, }, } // Copy the overlay dependencies, if any. for depk, depv := range info.Dependencies { if npminfo.Dependencies == nil { npminfo.Dependencies = make(map[string]string) } npminfo.Dependencies[depk] = depv } for depk, depv := range info.DevDependencies { if npminfo.DevDependencies == nil { npminfo.DevDependencies = make(map[string]string) } npminfo.DevDependencies[depk] = depv } for depk, depv := range info.PeerDependencies { if npminfo.PeerDependencies == nil { npminfo.PeerDependencies = make(map[string]string) } npminfo.PeerDependencies[depk] = depv } for resk, resv := range info.Resolutions { if npminfo.Resolutions == nil { npminfo.Resolutions = make(map[string]string) } npminfo.Resolutions[resk] = resv } // If there is no @pulumi/pulumi, add "latest" as a peer dependency (for npm linking style usage). sdkPack := "@pulumi/pulumi" if npminfo.Dependencies[sdkPack] == "" && npminfo.DevDependencies[sdkPack] == "" && npminfo.PeerDependencies[sdkPack] == "" { if npminfo.PeerDependencies == nil { npminfo.PeerDependencies = make(map[string]string) } npminfo.PeerDependencies["@pulumi/pulumi"] = "latest" } // Now write out the serialized form. npmjson, err := json.MarshalIndent(npminfo, "", " ") contract.Assert(err == nil) return string(npmjson) } func genTypeScriptProjectFile(info NodePackageInfo, files fs) string { w := &bytes.Buffer{} fmt.Fprintf(w, `{ "compilerOptions": { "outDir": "bin", "target": "es2016", "module": "commonjs", "moduleResolution": "node", "declaration": true, "sourceMap": true, "stripInternal": true, "experimentalDecorators": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, "strict": true }, "files": [ `) var tsFiles []string for f := range files { if path.Ext(f) == ".ts" { tsFiles = append(tsFiles, f) } } sort.Strings(tsFiles) for i, file := range tsFiles { var suffix string if i != len(tsFiles)-1 { suffix = "," } fmt.Fprintf(w, " \"%s\"%s\n", file, suffix) } fmt.Fprintf(w, ` ] } `) return w.String() } // generateModuleContextMap groups resources, types, and functions into NodeJS packages. func generateModuleContextMap(tool string, pkg *schema.Package, info NodePackageInfo, extraFiles map[string][]byte) (map[string]*modContext, NodePackageInfo, error) { // group resources, types, and functions into NodeJS packages modules := map[string]*modContext{} var getMod func(modName string) *modContext getMod = func(modName string) *modContext { if override, ok := info.ModuleToPackage[modName]; ok { modName = override } mod, ok := modules[modName] if !ok { mod = &modContext{ pkg: pkg, mod: modName, tool: tool, compatibility: info.Compatibility, modToPkg: info.ModuleToPackage, disableUnionOutputTypes: info.DisableUnionOutputTypes, } if modName != "" { parentName := path.Dir(modName) if parentName == "." { parentName = "" } parent := getMod(parentName) parent.children = append(parent.children, mod) } modules[modName] = mod } return mod } getModFromToken := func(token string) *modContext { return getMod(pkg.TokenToModule(token)) } // Create a temporary module for type information. types := &modContext{} // Create the config module if necessary. if len(pkg.Config) > 0 && info.Compatibility != kubernetes20 { // k8s SDK doesn't use config. _ = getMod("config") } visitObjectTypes(pkg.Config, func(t *schema.ObjectType, plain bool) { types.details(t).outputType = true }) scanResource := func(r *schema.Resource) { mod := getModFromToken(r.Token) mod.resources = append(mod.resources, r) visitObjectTypes(r.Properties, func(t *schema.ObjectType, _ bool) { types.details(t).outputType = true }) visitObjectTypes(r.InputProperties, func(t *schema.ObjectType, plain bool) { if r.IsProvider { types.details(t).outputType = true } types.details(t).inputType = true if plain { types.details(t).plainType = true } else { types.details(t).argsType = true } }) if r.StateInputs != nil { visitObjectTypes(r.StateInputs.Properties, func(t *schema.ObjectType, _ bool) { types.details(t).inputType = true types.details(t).argsType = true }) } } scanResource(pkg.Provider) for _, r := range pkg.Resources { scanResource(r) } // Clear the input and outputs sets: we want the visitors below to touch the transitive closure of types reachable // from function inputs and outputs, including types that have already been visited. for _, f := range pkg.Functions { mod := getModFromToken(f.Token) mod.functions = append(mod.functions, f) if f.Inputs != nil { visitObjectTypes(f.Inputs.Properties, func(t *schema.ObjectType, _ bool) { types.details(t).inputType = true types.details(t).plainType = true }) } if f.Outputs != nil { visitObjectTypes(f.Outputs.Properties, func(t *schema.ObjectType, _ bool) { types.details(t).outputType = true types.details(t).plainType = true }) } } if _, ok := modules["types"]; ok { return nil, info, errors.New("this provider has a `types` module which is reserved for input/output types") } // Create the types module. for _, t := range pkg.Types { switch typ := t.(type) { case *schema.ObjectType: types.types = append(types.types, typ) case *schema.EnumType: info.ContainsEnums = true mod := getModFromToken(typ.Token) mod.enums = append(mod.enums, typ) default: continue } } if len(types.types) > 0 { typeDetails, typeList := types.typeDetails, types.types types = getMod("types") types.typeDetails, types.types = typeDetails, typeList } // Add Typescript source files to the corresponding modules. Note that we only add the file names; the contents are // still laid out manually in GeneratePackage. for p := range extraFiles { if path.Ext(p) != ".ts" { continue } modName := path.Dir(p) if modName == "/" || modName == "." { modName = "" } mod := getMod(modName) mod.extraSourceFiles = append(mod.extraSourceFiles, p) } return modules, info, nil } // LanguageResource holds information about a resource to be used by downstream codegen. type LanguageResource struct { *schema.Resource Name string // The resource name (e.g., "FlowSchema") Package string // The name of the package containing the resource definition (e.g., "flowcontrol.v1alpha1") Properties []LanguageProperty // Properties of the resource } // LanguageProperty holds information about a resource property to be used by downstream codegen. type LanguageProperty struct { ConstValue string // If set, the constant value of the property (e.g., "flowcontrol.apiserver.k8s.io/v1alpha1") Name string // The name of the property (e.g., "FlowSchemaSpec") Package string // The package path containing the property definition (e.g., "outputs.flowcontrol.v1alpha1") } // LanguageResources returns a map of resources that can be used by downstream codegen. The map // key is the resource schema token. func LanguageResources(pkg *schema.Package) (map[string]LanguageResource, error) { resources := map[string]LanguageResource{} if err := pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil { return nil, err } info, _ := pkg.Language["nodejs"].(NodePackageInfo) modules, _, err := generateModuleContextMap("", pkg, info, nil) if err != nil { return nil, err } for modName, mod := range modules { for _, r := range mod.resources { packagePath := strings.Replace(modName, "/", ".", -1) lr := LanguageResource{ Resource: r, Name: resourceName(r), Package: packagePath, } for _, p := range r.Properties { lp := LanguageProperty{ Name: p.Name, } if p.ConstValue != nil { lp.ConstValue = mod.typeString(p.Type, false, false, false, false, p.ConstValue) } else { lp.Package = mod.typeString(p.Type, false, false, false, false, nil) } lr.Properties = append(lr.Properties, lp) } resources[r.Token] = lr } } return resources, nil } func GeneratePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) { // Decode node-specific info if err := pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil { return nil, err } info, _ := pkg.Language["nodejs"].(NodePackageInfo) modules, info, err := generateModuleContextMap(tool, pkg, info, extraFiles) if err != nil { return nil, err } pkg.Language["nodejs"] = info files := fs{} for p, f := range extraFiles { files.add(p, f) } for _, mod := range modules { if err := mod.gen(files); err != nil { return nil, err } } // Finally emit the package metadata (NPM, TypeScript, and so on). genPackageMetadata(pkg, info, files) return files, nil } const utilitiesFile = ` export function getEnv(...vars: string[]): string | undefined { for (const v of vars) { const value = process.env[v]; if (value) { return value; } } return undefined; } export function getEnvBoolean(...vars: string[]): boolean | undefined { const s = getEnv(...vars); if (s !== undefined) { // NOTE: these values are taken from https://golang.org/src/strconv/atob.go?s=351:391#L1, which is what // Terraform uses internally when parsing boolean values. if (["1", "t", "T", "true", "TRUE", "True"].find(v => v === s) !== undefined) { return true; } if (["0", "f", "F", "false", "FALSE", "False"].find(v => v === s) !== undefined) { return false; } } return undefined; } export function getEnvNumber(...vars: string[]): number | undefined { const s = getEnv(...vars); if (s !== undefined) { const f = parseFloat(s); if (!isNaN(f)) { return f; } } return undefined; } export function getVersion(): string { let version = require('./package.json').version; // Node allows for the version to be prefixed by a "v", while semver doesn't. // If there is a v, strip it off. if (version.indexOf('v') === 0) { version = version.slice(1); } return version; } `