github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/nodejs/gen_program.go (about) 1 // Copyright 2016-2020, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package nodejs 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "path" 23 "sort" 24 "strings" 25 26 "github.com/hashicorp/hcl/v2" 27 "github.com/pulumi/pulumi/pkg/v3/codegen" 28 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" 29 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format" 30 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" 31 "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" 32 "github.com/pulumi/pulumi/pkg/v3/codegen/schema" 33 "github.com/pulumi/pulumi/sdk/v3/go/common/encoding" 34 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 35 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 36 "github.com/zclconf/go-cty/cty" 37 ) 38 39 const PulumiToken = "pulumi" 40 41 type generator struct { 42 // The formatter to use when generating code. 43 *format.Formatter 44 45 program *pcl.Program 46 diagnostics hcl.Diagnostics 47 48 asyncMain bool 49 configCreated bool 50 } 51 52 func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) { 53 pcl.MapProvidersAsResources(program) 54 // Linearize the nodes into an order appropriate for procedural code generation. 55 nodes := pcl.Linearize(program) 56 57 g := &generator{ 58 program: program, 59 } 60 g.Formatter = format.NewFormatter(g) 61 62 // Creating a list to store and later print helper methods if they turn out to be needed 63 preambleHelperMethods := codegen.NewStringSet() 64 65 packages, err := program.PackageSnapshots() 66 if err != nil { 67 return nil, nil, err 68 } 69 for _, p := range packages { 70 if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil { 71 return nil, nil, err 72 } 73 } 74 75 var index bytes.Buffer 76 g.genPreamble(&index, program, preambleHelperMethods) 77 for _, n := range nodes { 78 if g.asyncMain { 79 break 80 } 81 switch x := n.(type) { 82 case *pcl.Resource: 83 if resourceRequiresAsyncMain(x) { 84 g.asyncMain = true 85 } 86 case *pcl.OutputVariable: 87 if outputRequiresAsyncMain(x) { 88 g.asyncMain = true 89 } 90 } 91 } 92 93 indenter := func(f func()) { f() } 94 if g.asyncMain { 95 indenter = g.Indented 96 g.Fgenf(&index, "export = async () => {\n") 97 } 98 99 indenter(func() { 100 for _, n := range nodes { 101 g.genNode(&index, n) 102 } 103 104 if g.asyncMain { 105 var result *model.ObjectConsExpression 106 for _, n := range nodes { 107 if o, ok := n.(*pcl.OutputVariable); ok { 108 if result == nil { 109 result = &model.ObjectConsExpression{} 110 } 111 name := o.LogicalName() 112 nameVar := makeValidIdentifier(o.Name()) 113 result.Items = append(result.Items, model.ObjectConsItem{ 114 Key: &model.LiteralValueExpression{Value: cty.StringVal(name)}, 115 Value: &model.ScopeTraversalExpression{ 116 RootName: nameVar, 117 Traversal: hcl.Traversal{hcl.TraverseRoot{Name: name}}, 118 Parts: []model.Traversable{&model.Variable{ 119 Name: nameVar, 120 VariableType: o.Type(), 121 }}, 122 }, 123 }) 124 } 125 } 126 if result != nil { 127 g.Fgenf(&index, "%sreturn %v;\n", g.Indent, result) 128 } 129 } 130 131 }) 132 133 if g.asyncMain { 134 g.Fgenf(&index, "}\n") 135 } 136 137 files := map[string][]byte{ 138 "index.ts": index.Bytes(), 139 } 140 return files, g.diagnostics, nil 141 } 142 143 func GenerateProject(directory string, project workspace.Project, program *pcl.Program) error { 144 files, diagnostics, err := GenerateProgram(program) 145 if err != nil { 146 return err 147 } 148 if diagnostics.HasErrors() { 149 return diagnostics 150 } 151 152 // Set the runtime to "nodejs" then marshal to Pulumi.yaml 153 project.Runtime = workspace.NewProjectRuntimeInfo("nodejs", nil) 154 projectBytes, err := encoding.YAML.Marshal(project) 155 if err != nil { 156 return err 157 } 158 files["Pulumi.yaml"] = projectBytes 159 160 // Build the pacakge.json 161 var packageJSON bytes.Buffer 162 packageJSON.WriteString(fmt.Sprintf(`{ 163 "name": "%s", 164 "devDependencies": { 165 "@types/node": "^14" 166 }, 167 "dependencies": { 168 "typescript": "^4.0.0", 169 "@pulumi/pulumi": "^3.0.0"`, project.Name.String())) 170 // For each package add a dependency line 171 packages, err := program.PackageSnapshots() 172 if err != nil { 173 return err 174 } 175 for _, p := range packages { 176 if p.Name == PulumiToken { 177 continue 178 } 179 if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil { 180 return err 181 } 182 183 packageName := "@pulumi/" + p.Name 184 err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}) 185 if err != nil { 186 return err 187 } 188 if langInfo, found := p.Language["nodejs"]; found { 189 nodeInfo, ok := langInfo.(NodePackageInfo) 190 if ok && nodeInfo.PackageName != "" { 191 packageName = nodeInfo.PackageName 192 } 193 } 194 dependencyTemplate := ",\n \"%s\": \"%s\"" 195 if p.Version != nil { 196 packageJSON.WriteString(fmt.Sprintf(dependencyTemplate, packageName, p.Version.String())) 197 } else { 198 packageJSON.WriteString(fmt.Sprintf(dependencyTemplate, packageName, "*")) 199 } 200 } 201 packageJSON.WriteString(` 202 } 203 }`) 204 205 files["package.json"] = packageJSON.Bytes() 206 207 // Add the language specific .gitignore 208 files[".gitignore"] = []byte(`/bin/ 209 /node_modules/`) 210 211 // Add the basic tsconfig 212 var tsConfig bytes.Buffer 213 tsConfig.WriteString(`{ 214 "compilerOptions": { 215 "strict": true, 216 "outDir": "bin", 217 "target": "es2016", 218 "module": "commonjs", 219 "moduleResolution": "node", 220 "sourceMap": true, 221 "experimentalDecorators": true, 222 "pretty": true, 223 "noFallthroughCasesInSwitch": true, 224 "noImplicitReturns": true, 225 "forceConsistentCasingInFileNames": true 226 }, 227 "files": [ 228 `) 229 230 for file := range files { 231 if strings.HasSuffix(file, ".ts") { 232 tsConfig.WriteString(" \"" + file + "\"\n") 233 } 234 } 235 236 tsConfig.WriteString(` ] 237 }`) 238 files["tsconfig.json"] = tsConfig.Bytes() 239 240 for filename, data := range files { 241 outPath := path.Join(directory, filename) 242 err := ioutil.WriteFile(outPath, data, 0600) 243 if err != nil { 244 return fmt.Errorf("could not write output program: %w", err) 245 } 246 } 247 248 return nil 249 } 250 251 // genLeadingTrivia generates the list of leading trivia assicated with a given token. 252 func (g *generator) genLeadingTrivia(w io.Writer, token syntax.Token) { 253 // TODO(pdg): whitespace? 254 for _, t := range token.LeadingTrivia { 255 if c, ok := t.(syntax.Comment); ok { 256 g.genComment(w, c) 257 } 258 } 259 } 260 261 // genTrailingTrivia generates the list of trailing trivia assicated with a given token. 262 func (g *generator) genTrailingTrivia(w io.Writer, token syntax.Token) { 263 // TODO(pdg): whitespace 264 for _, t := range token.TrailingTrivia { 265 if c, ok := t.(syntax.Comment); ok { 266 g.genComment(w, c) 267 } 268 } 269 } 270 271 // genTrivia generates the list of trivia assicated with a given token. 272 func (g *generator) genTrivia(w io.Writer, token syntax.Token) { 273 g.genLeadingTrivia(w, token) 274 g.genTrailingTrivia(w, token) 275 } 276 277 // genComment generates a comment into the output. 278 func (g *generator) genComment(w io.Writer, comment syntax.Comment) { 279 for _, l := range comment.Lines { 280 g.Fgenf(w, "%s//%s\n", g.Indent, l) 281 } 282 } 283 284 func (g *generator) genPreamble(w io.Writer, program *pcl.Program, preambleHelperMethods codegen.StringSet) { 285 // Print the @pulumi/pulumi import at the top. 286 g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`) 287 288 // Accumulate other imports for the various providers and packages. Don't emit them yet, as we need to sort them 289 // later on. 290 importSet := codegen.NewStringSet("@pulumi/pulumi") 291 npmToPuPkgName := make(map[string]string) 292 for _, n := range program.Nodes { 293 if r, isResource := n.(*pcl.Resource); isResource { 294 pkg, _, _, _ := r.DecomposeToken() 295 if pkg == PulumiToken { 296 continue 297 } 298 pkgName := "@pulumi/" + pkg 299 if r.Schema != nil && r.Schema.Package != nil { 300 if info, ok := r.Schema.Package.Language["nodejs"].(NodePackageInfo); ok && info.PackageName != "" { 301 pkgName = info.PackageName 302 } 303 npmToPuPkgName[pkgName] = pkg 304 } 305 importSet.Add(pkgName) 306 } 307 diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) { 308 if call, ok := n.(*model.FunctionCallExpression); ok { 309 if i := g.getFunctionImports(call); len(i) > 0 && i[0] != "" { 310 for _, importPackage := range i { 311 importSet.Add(importPackage) 312 } 313 } 314 if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name); ok { 315 preambleHelperMethods.Add(helperMethodBody) 316 } 317 } 318 return n, nil 319 }) 320 contract.Assert(len(diags) == 0) 321 } 322 323 var imports []string 324 for _, pkg := range importSet.SortedValues() { 325 if pkg == "@pulumi/pulumi" { 326 continue 327 } 328 var as string 329 if puPkg, ok := npmToPuPkgName[pkg]; ok { 330 as = makeValidIdentifier(puPkg) 331 } else { 332 as = makeValidIdentifier(path.Base(pkg)) 333 } 334 imports = append(imports, fmt.Sprintf("import * as %v from \"%v\";", as, pkg)) 335 } 336 sort.Strings(imports) 337 338 // Now sort the imports and emit them. 339 for _, i := range imports { 340 g.Fprintln(w, i) 341 } 342 g.Fprint(w, "\n") 343 344 // If we collected any helper methods that should be added, write them just before the main func 345 for _, preambleHelperMethodBody := range preambleHelperMethods.SortedValues() { 346 g.Fprintf(w, "%s\n\n", preambleHelperMethodBody) 347 } 348 } 349 350 func (g *generator) genNode(w io.Writer, n pcl.Node) { 351 switch n := n.(type) { 352 case *pcl.Resource: 353 g.genResource(w, n) 354 case *pcl.ConfigVariable: 355 g.genConfigVariable(w, n) 356 case *pcl.LocalVariable: 357 g.genLocalVariable(w, n) 358 case *pcl.OutputVariable: 359 g.genOutputVariable(w, n) 360 } 361 } 362 363 func resourceRequiresAsyncMain(r *pcl.Resource) bool { 364 if r.Options == nil || r.Options.Range == nil { 365 return false 366 } 367 368 return model.ContainsPromises(r.Options.Range.Type()) 369 } 370 371 func outputRequiresAsyncMain(ov *pcl.OutputVariable) bool { 372 outputName := ov.LogicalName() 373 if makeValidIdentifier(outputName) != outputName { 374 return true 375 } 376 377 return false 378 } 379 380 // resourceTypeName computes the NodeJS package, module, and type name for the given resource. 381 func resourceTypeName(r *pcl.Resource) (string, string, string, hcl.Diagnostics) { 382 // Compute the resource type from the Pulumi type token. 383 pcl.FixupPulumiPackageTokens(r) 384 pkg, module, member, diagnostics := r.DecomposeToken() 385 386 if r.Schema != nil { 387 module = moduleName(module, r.Schema.Package) 388 } 389 390 return makeValidIdentifier(pkg), module, title(member), diagnostics 391 } 392 393 func moduleName(module string, pkg *schema.Package) string { 394 // Normalize module. 395 if pkg != nil { 396 err := pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer}) 397 contract.AssertNoError(err) 398 if lang, ok := pkg.Language["nodejs"]; ok { 399 pkgInfo := lang.(NodePackageInfo) 400 if m, ok := pkgInfo.ModuleToPackage[module]; ok { 401 module = m 402 } 403 } 404 } 405 return strings.ToLower(strings.ReplaceAll(module, "/", ".")) 406 } 407 408 // makeResourceName returns the expression that should be emitted for a resource's "name" parameter given its base name 409 // and the count variable name, if any. 410 func (g *generator) makeResourceName(baseName, count string) string { 411 if count == "" { 412 return fmt.Sprintf(`"%s"`, baseName) 413 } 414 return fmt.Sprintf("`%s-${%s}`", baseName, count) 415 } 416 417 func (g *generator) genResourceOptions(opts *pcl.ResourceOptions) string { 418 if opts == nil { 419 return "" 420 } 421 422 // Turn the resource options into an ObjectConsExpression and generate it. 423 var object *model.ObjectConsExpression 424 appendOption := func(name string, value model.Expression) { 425 if object == nil { 426 object = &model.ObjectConsExpression{} 427 } 428 object.Items = append(object.Items, model.ObjectConsItem{ 429 Key: &model.LiteralValueExpression{ 430 Tokens: syntax.NewLiteralValueTokens(cty.StringVal(name)), 431 Value: cty.StringVal(name), 432 }, 433 Value: value, 434 }) 435 } 436 437 if opts.Parent != nil { 438 appendOption("parent", opts.Parent) 439 } 440 if opts.Provider != nil { 441 appendOption("provider", opts.Provider) 442 } 443 if opts.DependsOn != nil { 444 appendOption("dependsOn", opts.DependsOn) 445 } 446 if opts.Protect != nil { 447 appendOption("protect", opts.Protect) 448 } 449 if opts.IgnoreChanges != nil { 450 appendOption("ignoreChanges", opts.IgnoreChanges) 451 } 452 453 if object == nil { 454 return "" 455 } 456 457 var buffer bytes.Buffer 458 g.Fgenf(&buffer, ", %v", g.lowerExpression(object, nil)) 459 return buffer.String() 460 } 461 462 // genResource handles the generation of instantiations of non-builtin resources. 463 func (g *generator) genResource(w io.Writer, r *pcl.Resource) { 464 pkg, module, memberName, diagnostics := resourceTypeName(r) 465 g.diagnostics = append(g.diagnostics, diagnostics...) 466 467 if module != "" { 468 module = "." + module 469 } 470 471 qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName) 472 473 optionsBag := g.genResourceOptions(r.Options) 474 475 name := r.LogicalName() 476 variableName := makeValidIdentifier(r.Name()) 477 478 g.genTrivia(w, r.Definition.Tokens.GetType("")) 479 for _, l := range r.Definition.Tokens.GetLabels(nil) { 480 g.genTrivia(w, l) 481 } 482 g.genTrivia(w, r.Definition.Tokens.GetOpenBrace()) 483 484 instantiate := func(resName string) { 485 g.Fgenf(w, "new %s(%s, {", qualifiedMemberName, resName) 486 indenter := func(f func()) { f() } 487 if len(r.Inputs) > 1 { 488 indenter = g.Indented 489 } 490 indenter(func() { 491 fmtString := "%s: %.v" 492 if len(r.Inputs) > 1 { 493 fmtString = "\n" + g.Indent + "%s: %.v," 494 } 495 496 for _, attr := range r.Inputs { 497 propertyName := attr.Name 498 if !isLegalIdentifier(propertyName) { 499 propertyName = fmt.Sprintf("%q", propertyName) 500 } 501 502 destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: attr.Name}) 503 g.diagnostics = append(g.diagnostics, diagnostics...) 504 g.Fgenf(w, fmtString, propertyName, 505 g.lowerExpression(attr.Value, destType.(model.Type))) 506 } 507 }) 508 if len(r.Inputs) > 1 { 509 g.Fgenf(w, "\n%s", g.Indent) 510 } 511 g.Fgenf(w, "}%s)", optionsBag) 512 } 513 514 if r.Options != nil && r.Options.Range != nil { 515 rangeType := model.ResolveOutputs(r.Options.Range.Type()) 516 rangeExpr := g.lowerExpression(r.Options.Range, rangeType) 517 518 if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion { 519 g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName) 520 g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr) 521 g.Indented(func() { 522 g.Fgenf(w, "%s%s = ", g.Indent, variableName) 523 instantiate(g.makeResourceName(name, "")) 524 g.Fgenf(w, ";\n") 525 }) 526 g.Fgenf(w, "%s}\n", g.Indent) 527 } else { 528 g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName) 529 530 resKey := "key" 531 if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion { 532 g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr) 533 resKey = "value" 534 } else { 535 rangeExpr := &model.FunctionCallExpression{ 536 Name: "entries", 537 Args: []model.Expression{rangeExpr}, 538 } 539 g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr) 540 } 541 542 resName := g.makeResourceName(name, "range."+resKey) 543 g.Indented(func() { 544 g.Fgenf(w, "%s%s.push(", g.Indent, variableName) 545 instantiate(resName) 546 g.Fgenf(w, ");\n") 547 }) 548 g.Fgenf(w, "%s}\n", g.Indent) 549 } 550 } else { 551 g.Fgenf(w, "%sconst %s = ", g.Indent, variableName) 552 instantiate(g.makeResourceName(name, "")) 553 g.Fgenf(w, ";\n") 554 } 555 556 g.genTrivia(w, r.Definition.Tokens.GetCloseBrace()) 557 } 558 559 func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) { 560 // TODO(pdg): trivia 561 562 if !g.configCreated { 563 g.Fprintf(w, "%sconst config = new pulumi.Config();\n", g.Indent) 564 g.configCreated = true 565 } 566 567 getType := "Object" 568 switch v.Type() { 569 case model.StringType: 570 getType = "" 571 case model.NumberType, model.IntType: 572 getType = "Number" 573 case model.BoolType: 574 getType = "Boolean" 575 } 576 577 getOrRequire := "get" 578 if v.DefaultValue == nil { 579 getOrRequire = "require" 580 } 581 582 name := makeValidIdentifier(v.Name()) 583 g.Fgenf(w, "%[1]sconst %[2]s = config.%[3]s%[4]s(\"%[5]s\")", 584 g.Indent, name, getOrRequire, getType, v.LogicalName()) 585 if v.DefaultValue != nil { 586 g.Fgenf(w, " || %.v", g.lowerExpression(v.DefaultValue, v.DefaultValue.Type())) 587 } 588 g.Fgenf(w, ";\n") 589 } 590 591 func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) { 592 // TODO(pdg): trivia 593 g.Fgenf(w, "%sconst %s = %.3v;\n", g.Indent, v.Name(), g.lowerExpression(v.Definition.Value, v.Type())) 594 } 595 596 func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) { 597 // TODO(pdg): trivia 598 export := "export " 599 if g.asyncMain { 600 export = "" 601 } 602 g.Fgenf(w, "%s%sconst %s = %.3v;\n", g.Indent, export, 603 makeValidIdentifier(v.Name()), g.lowerExpression(v.Value, v.Type())) 604 } 605 606 func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) { 607 message := fmt.Sprintf("not yet implemented: %s", fmt.Sprintf(reason, vs...)) 608 g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{ 609 Severity: hcl.DiagError, 610 Summary: message, 611 Detail: message, 612 }) 613 g.Fgenf(w, "(() => throw new Error(%q))()", fmt.Sprintf(reason, vs...)) 614 }