mirror of https://github.com/pulumi/pulumi.git
1280 lines
38 KiB
Go
1280 lines
38 KiB
Go
// 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.
|
|
|
|
package nodejs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
const PulumiToken = "pulumi"
|
|
|
|
type generator struct {
|
|
// The formatter to use when generating code.
|
|
*format.Formatter
|
|
|
|
program *pcl.Program
|
|
diagnostics hcl.Diagnostics
|
|
|
|
asyncMain bool
|
|
configCreated bool
|
|
isComponent bool
|
|
}
|
|
|
|
func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) {
|
|
pcl.MapProvidersAsResources(program)
|
|
// Linearize the nodes into an order appropriate for procedural code generation.
|
|
nodes := pcl.Linearize(program)
|
|
|
|
g := &generator{
|
|
program: program,
|
|
}
|
|
g.Formatter = format.NewFormatter(g)
|
|
|
|
packages, err := program.PackageSnapshots()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for _, p := range packages {
|
|
if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
var index bytes.Buffer
|
|
err = g.genPreamble(&index, program)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// used to track declared variables in the main program
|
|
// since outputs have identifiers which can conflict with other program nodes' identifiers
|
|
// we switch the entry point to async which allows for declaring arbitrary output names
|
|
declaredNodeIdentifiers := map[string]bool{}
|
|
for _, n := range nodes {
|
|
if g.asyncMain {
|
|
break
|
|
}
|
|
switch x := n.(type) {
|
|
case *pcl.Resource:
|
|
if resourceRequiresAsyncMain(x) {
|
|
g.asyncMain = true
|
|
}
|
|
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
|
|
case *pcl.ConfigVariable:
|
|
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
|
|
case *pcl.LocalVariable:
|
|
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
|
|
case *pcl.Component:
|
|
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
|
|
case *pcl.OutputVariable:
|
|
if outputRequiresAsyncMain(x) {
|
|
g.asyncMain = true
|
|
}
|
|
|
|
outputIdentifier := makeValidIdentifier(x.Name())
|
|
if _, alreadyDeclared := declaredNodeIdentifiers[outputIdentifier]; alreadyDeclared {
|
|
g.asyncMain = true
|
|
}
|
|
}
|
|
}
|
|
|
|
indenter := func(f func()) { f() }
|
|
if g.asyncMain {
|
|
indenter = g.Indented
|
|
g.Fgenf(&index, "export = async () => {\n")
|
|
}
|
|
|
|
indenter(func() {
|
|
for _, n := range nodes {
|
|
g.genNode(&index, n)
|
|
}
|
|
|
|
if g.asyncMain {
|
|
var result *model.ObjectConsExpression
|
|
for _, n := range nodes {
|
|
if o, ok := n.(*pcl.OutputVariable); ok {
|
|
if result == nil {
|
|
result = &model.ObjectConsExpression{}
|
|
}
|
|
name := o.LogicalName()
|
|
result.Items = append(result.Items, model.ObjectConsItem{
|
|
Key: &model.LiteralValueExpression{Value: cty.StringVal(name)},
|
|
Value: g.lowerExpression(o.Value, o.Type()),
|
|
})
|
|
}
|
|
}
|
|
if result != nil {
|
|
g.Fgenf(&index, "%sreturn %v;\n", g.Indent, result)
|
|
}
|
|
}
|
|
})
|
|
|
|
if g.asyncMain {
|
|
g.Fgenf(&index, "}\n")
|
|
}
|
|
|
|
files := map[string][]byte{
|
|
"index.ts": index.Bytes(),
|
|
}
|
|
|
|
for componentDir, component := range program.CollectComponents() {
|
|
componentFilename := filepath.Base(componentDir)
|
|
componentName := component.DeclarationName()
|
|
componentGenerator := &generator{
|
|
program: component.Program,
|
|
isComponent: true,
|
|
}
|
|
|
|
componentGenerator.Formatter = format.NewFormatter(componentGenerator)
|
|
|
|
var componentBuffer bytes.Buffer
|
|
componentGenerator.genComponentResourceDefinition(&componentBuffer, componentName, component)
|
|
files[componentFilename+".ts"] = componentBuffer.Bytes()
|
|
}
|
|
|
|
return files, g.diagnostics, nil
|
|
}
|
|
|
|
func GenerateProject(
|
|
directory string, project workspace.Project,
|
|
program *pcl.Program, localDependencies map[string]string,
|
|
forceTsc bool,
|
|
) error {
|
|
files, diagnostics, err := GenerateProgram(program)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if diagnostics.HasErrors() {
|
|
return diagnostics
|
|
}
|
|
|
|
// Check the project for "main" as that changes where we write out files and some relative paths.
|
|
rootDirectory := directory
|
|
if project.Main != "" {
|
|
directory = filepath.Join(rootDirectory, project.Main)
|
|
// mkdir -p the subdirectory
|
|
err = os.MkdirAll(directory, 0o700)
|
|
if err != nil {
|
|
return fmt.Errorf("create main directory: %w", err)
|
|
}
|
|
}
|
|
|
|
// Set the runtime to "nodejs" then marshal to Pulumi.yaml
|
|
runtime := workspace.NewProjectRuntimeInfo("nodejs", nil)
|
|
if forceTsc {
|
|
runtime.SetOption("typescript", false)
|
|
}
|
|
project.Runtime = runtime
|
|
|
|
projectBytes, err := encoding.YAML.Marshal(project)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(path.Join(rootDirectory, "Pulumi.yaml"), projectBytes, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("write Pulumi.yaml: %w", err)
|
|
}
|
|
|
|
// Build the package.json
|
|
var packageJSON bytes.Buffer
|
|
fmt.Fprintf(&packageJSON, `{
|
|
"name": "%s",
|
|
"devDependencies": {
|
|
"@types/node": "^14"
|
|
},
|
|
"dependencies": {
|
|
"typescript": "^4.0.0",
|
|
`, project.Name.String())
|
|
|
|
// Check if pulumi is a local dependency, else add it as a normal range dependency
|
|
if pulumiArtifact, has := localDependencies[PulumiToken]; has {
|
|
fmt.Fprintf(&packageJSON, `"@pulumi/pulumi": "%s"`, pulumiArtifact)
|
|
} else {
|
|
fmt.Fprintf(&packageJSON, `"@pulumi/pulumi": "^3.0.0"`)
|
|
}
|
|
|
|
// For each package add a dependency line
|
|
packages, err := program.CollectNestedPackageSnapshots()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Sort the dependencies to ensure a deterministic package.json. Note that the typescript and
|
|
// @pulumi/pulumi dependencies are already added above and not sorted.
|
|
sortedPackageNames := make([]string, 0, len(packages))
|
|
for k := range packages {
|
|
sortedPackageNames = append(sortedPackageNames, k)
|
|
}
|
|
sort.Strings(sortedPackageNames)
|
|
for _, k := range sortedPackageNames {
|
|
p := packages[k]
|
|
if p.Name == PulumiToken {
|
|
continue
|
|
}
|
|
if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
|
|
return err
|
|
}
|
|
|
|
packageName := "@pulumi/" + p.Name
|
|
err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if langInfo, found := p.Language["nodejs"]; found {
|
|
nodeInfo, ok := langInfo.(NodePackageInfo)
|
|
if ok && nodeInfo.PackageName != "" {
|
|
packageName = nodeInfo.PackageName
|
|
}
|
|
}
|
|
|
|
dependencyTemplate := ",\n \"%s\": \"%s\""
|
|
if path, has := localDependencies[p.Name]; has {
|
|
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, path)
|
|
} else {
|
|
if p.Version != nil {
|
|
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, p.Version.String())
|
|
} else {
|
|
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, "*")
|
|
}
|
|
}
|
|
}
|
|
packageJSON.WriteString(`
|
|
}
|
|
}`)
|
|
|
|
files["package.json"] = packageJSON.Bytes()
|
|
|
|
// Add the language specific .gitignore
|
|
files[".gitignore"] = []byte(`/bin/
|
|
/node_modules/`)
|
|
|
|
// Add the basic tsconfig
|
|
var tsConfig bytes.Buffer
|
|
tsConfig.WriteString(`{
|
|
"compilerOptions": {
|
|
"strict": true,
|
|
"outDir": "bin",
|
|
"target": "es2016",
|
|
"module": "commonjs",
|
|
"moduleResolution": "node",
|
|
"sourceMap": true,
|
|
"experimentalDecorators": true,
|
|
"pretty": true,
|
|
"noFallthroughCasesInSwitch": true,
|
|
"noImplicitReturns": true,
|
|
"forceConsistentCasingInFileNames": true
|
|
},
|
|
"files": [
|
|
`)
|
|
|
|
fileNames := make([]string, 0, len(files))
|
|
for file := range files {
|
|
fileNames = append(fileNames, file)
|
|
}
|
|
sort.Strings(fileNames)
|
|
|
|
for i, file := range fileNames {
|
|
if strings.HasSuffix(file, ".ts") {
|
|
tsConfig.WriteString(" \"" + file + "\"")
|
|
lastFile := i == len(files)-1
|
|
if !lastFile {
|
|
tsConfig.WriteString(",\n")
|
|
} else {
|
|
tsConfig.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
tsConfig.WriteString(` ]
|
|
}`)
|
|
files["tsconfig.json"] = tsConfig.Bytes()
|
|
|
|
for filename, data := range files {
|
|
outPath := path.Join(directory, filename)
|
|
err := os.WriteFile(outPath, data, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("could not write output program: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// genLeadingTrivia generates the list of leading trivia assicated with a given token.
|
|
func (g *generator) genLeadingTrivia(w io.Writer, token syntax.Token) {
|
|
// TODO(pdg): whitespace?
|
|
for _, t := range token.LeadingTrivia {
|
|
if c, ok := t.(syntax.Comment); ok {
|
|
g.genComment(w, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// genTrailingTrivia generates the list of trailing trivia assicated with a given token.
|
|
func (g *generator) genTrailingTrivia(w io.Writer, token syntax.Token) {
|
|
// TODO(pdg): whitespace
|
|
for _, t := range token.TrailingTrivia {
|
|
if c, ok := t.(syntax.Comment); ok {
|
|
g.genComment(w, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// genTrivia generates the list of trivia assicated with a given token.
|
|
func (g *generator) genTrivia(w io.Writer, token syntax.Token) {
|
|
g.genLeadingTrivia(w, token)
|
|
g.genTrailingTrivia(w, token)
|
|
}
|
|
|
|
// genComment generates a comment into the output.
|
|
func (g *generator) genComment(w io.Writer, comment syntax.Comment) {
|
|
for _, l := range comment.Lines {
|
|
g.Fgenf(w, "%s//%s\n", g.Indent, l)
|
|
}
|
|
}
|
|
|
|
type programImports struct {
|
|
importStatements []string
|
|
preambleHelperMethods codegen.StringSet
|
|
}
|
|
|
|
func (g *generator) collectProgramImports(program *pcl.Program) programImports {
|
|
importSet := codegen.NewStringSet("@pulumi/pulumi")
|
|
preambleHelperMethods := codegen.NewStringSet()
|
|
var componentImports []string
|
|
|
|
npmToPuPkgName := make(map[string]string)
|
|
seenComponentImports := map[string]bool{}
|
|
for _, n := range program.Nodes {
|
|
switch n := n.(type) {
|
|
case *pcl.Resource:
|
|
pkg, _, _, _ := n.DecomposeToken()
|
|
if pkg == PulumiToken {
|
|
continue
|
|
}
|
|
pkgName := "@pulumi/" + pkg
|
|
if n.Schema != nil && n.Schema.PackageReference != nil {
|
|
def, err := n.Schema.PackageReference.Definition()
|
|
contract.AssertNoErrorf(err, "Should be able to retrieve definition for %s", n.Schema.Token)
|
|
if info, ok := def.Language["nodejs"].(NodePackageInfo); ok && info.PackageName != "" {
|
|
pkgName = info.PackageName
|
|
}
|
|
npmToPuPkgName[pkgName] = pkg
|
|
}
|
|
importSet.Add(pkgName)
|
|
case *pcl.Component:
|
|
componentDir := filepath.Base(n.DirPath())
|
|
componentName := n.DeclarationName()
|
|
dirAndName := componentDir + "-" + componentName
|
|
if _, ok := seenComponentImports[dirAndName]; !ok {
|
|
importStatement := fmt.Sprintf("import { %s } from \"./%s\";", componentName, componentDir)
|
|
componentImports = append(componentImports, importStatement)
|
|
seenComponentImports[dirAndName] = true
|
|
}
|
|
}
|
|
diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
|
|
if call, ok := n.(*model.FunctionCallExpression); ok {
|
|
if i := g.getFunctionImports(call); len(i) > 0 && i[0] != "" {
|
|
for _, importPackage := range i {
|
|
importSet.Add(importPackage)
|
|
}
|
|
}
|
|
if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok {
|
|
preambleHelperMethods.Add(helperMethodBody)
|
|
}
|
|
}
|
|
return n, nil
|
|
})
|
|
contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags)
|
|
}
|
|
|
|
sortedValues := importSet.SortedValues()
|
|
imports := slice.Prealloc[string](len(sortedValues))
|
|
for _, pkg := range sortedValues {
|
|
if pkg == "@pulumi/pulumi" {
|
|
continue
|
|
}
|
|
var as string
|
|
if puPkg, ok := npmToPuPkgName[pkg]; ok {
|
|
as = makeValidIdentifier(puPkg)
|
|
} else {
|
|
as = makeValidIdentifier(path.Base(pkg))
|
|
}
|
|
imports = append(imports, fmt.Sprintf("import * as %v from \"%v\";", as, pkg))
|
|
}
|
|
|
|
imports = append(imports, componentImports...)
|
|
sort.Strings(imports)
|
|
|
|
return programImports{
|
|
importStatements: imports,
|
|
preambleHelperMethods: preambleHelperMethods,
|
|
}
|
|
}
|
|
|
|
func (g *generator) genPreamble(w io.Writer, program *pcl.Program) error {
|
|
// Print the @pulumi/pulumi import at the top.
|
|
g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`)
|
|
|
|
programImports := g.collectProgramImports(program)
|
|
|
|
// Now sort the imports and emit them.
|
|
for _, i := range programImports.importStatements {
|
|
g.Fprintln(w, i)
|
|
}
|
|
g.Fprint(w, "\n")
|
|
|
|
// If we collected any helper methods that should be added, write them just before the main func
|
|
for _, preambleHelperMethodBody := range programImports.preambleHelperMethods.SortedValues() {
|
|
g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func componentElementType(pclType model.Type) string {
|
|
switch pclType {
|
|
case model.BoolType:
|
|
return "boolean"
|
|
case model.IntType, model.NumberType:
|
|
return "number"
|
|
case model.StringType:
|
|
return "string"
|
|
default:
|
|
switch pclType := pclType.(type) {
|
|
case *model.ListType:
|
|
elementType := componentElementType(pclType.ElementType)
|
|
return elementType + "[]"
|
|
case *model.MapType:
|
|
elementType := componentElementType(pclType.ElementType)
|
|
return fmt.Sprintf("Record<string, pulumi.Input<%s>>", elementType)
|
|
case *model.OutputType:
|
|
// something is already an output
|
|
// get only the element type because we are wrapping these in Output<T> anyway
|
|
return componentElementType(pclType.ElementType)
|
|
case *model.UnionType:
|
|
if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[0] == model.NoneType {
|
|
return componentElementType(pclType.ElementTypes[1])
|
|
} else if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[1] == model.NoneType {
|
|
return componentElementType(pclType.ElementTypes[0])
|
|
} else {
|
|
return "any"
|
|
}
|
|
default:
|
|
return "any"
|
|
}
|
|
}
|
|
}
|
|
|
|
func componentInputType(pclType model.Type) string {
|
|
elementType := componentElementType(pclType)
|
|
return fmt.Sprintf("pulumi.Input<%s>", elementType)
|
|
}
|
|
|
|
func componentOutputType(pclType model.Type) string {
|
|
elementType := componentElementType(pclType)
|
|
return fmt.Sprintf("pulumi.Output<%s>", elementType)
|
|
}
|
|
|
|
func (g *generator) genObjectTypedConfig(w io.Writer, objectType *model.ObjectType) {
|
|
attributeKeys := []string{}
|
|
for attributeKey := range objectType.Properties {
|
|
attributeKeys = append(attributeKeys, attributeKey)
|
|
}
|
|
|
|
// get deterministically sorted keys
|
|
sort.Strings(attributeKeys)
|
|
|
|
g.Fgenf(w, "{\n")
|
|
g.Indented(func() {
|
|
for _, attributeKey := range attributeKeys {
|
|
attributeType := objectType.Properties[attributeKey]
|
|
optional := "?"
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
typeName := componentInputType(attributeType)
|
|
g.Fgenf(w, "%s%s: %s,\n", attributeKey, optional, typeName)
|
|
}
|
|
})
|
|
g.Fgenf(w, "%s}", g.Indent)
|
|
}
|
|
|
|
func (g *generator) genComponentResourceDefinition(w io.Writer, componentName string, component *pcl.Component) {
|
|
// Print the @pulumi/pulumi import at the top.
|
|
g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`)
|
|
|
|
programImports := g.collectProgramImports(component.Program)
|
|
|
|
// Now sort the imports and emit them.
|
|
for _, i := range programImports.importStatements {
|
|
g.Fprintln(w, i)
|
|
}
|
|
g.Fprint(w, "\n")
|
|
|
|
// If we collected any helper methods that should be added, write them just before the main func
|
|
for _, preambleHelperMethodBody := range programImports.preambleHelperMethods.SortedValues() {
|
|
g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
|
|
}
|
|
|
|
configVars := component.Program.ConfigVariables()
|
|
|
|
if len(configVars) > 0 {
|
|
g.Fgenf(w, "interface %sArgs {\n", componentName)
|
|
g.Indented(func() {
|
|
for _, configVar := range configVars {
|
|
optional := "?"
|
|
if configVar.DefaultValue == nil {
|
|
optional = ""
|
|
}
|
|
if configVar.Description != "" {
|
|
g.Fgenf(w, "%s/**\n", g.Indent)
|
|
for _, line := range strings.Split(configVar.Description, "\n") {
|
|
g.Fgenf(w, "%s * %s\n", g.Indent, line)
|
|
}
|
|
g.Fgenf(w, "%s */\n", g.Indent)
|
|
}
|
|
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
switch configVarType := configVar.Type().(type) {
|
|
case *model.ObjectType:
|
|
// generate {...}
|
|
g.Fgenf(w, "%s%s: ", configVar.Name(), optional)
|
|
g.genObjectTypedConfig(w, configVarType)
|
|
g.Fgen(w, ",\n")
|
|
case *model.ListType:
|
|
switch elementType := configVarType.ElementType.(type) {
|
|
case *model.ObjectType:
|
|
// generate {...}[]
|
|
g.Fgenf(w, "%s%s: ", configVar.Name(), optional)
|
|
g.genObjectTypedConfig(w, elementType)
|
|
g.Fgen(w, "[],\n")
|
|
default:
|
|
typeName := componentInputType(configVar.Type())
|
|
g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
|
|
}
|
|
case *model.MapType:
|
|
switch elementType := configVarType.ElementType.(type) {
|
|
case *model.ObjectType:
|
|
// generate Record<string, {...}>
|
|
g.Fgenf(w, "%s%s: Record<string, ", configVar.Name(), optional)
|
|
g.genObjectTypedConfig(w, elementType)
|
|
g.Fgen(w, ">,\n")
|
|
default:
|
|
typeName := componentInputType(configVar.Type())
|
|
g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
|
|
}
|
|
default:
|
|
typeName := componentInputType(configVar.Type())
|
|
g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
|
|
}
|
|
}
|
|
})
|
|
g.Fgenf(w, "}\n\n")
|
|
}
|
|
|
|
outputs := component.Program.OutputVariables()
|
|
|
|
g.Fgenf(w, "export class %s extends pulumi.ComponentResource {\n", componentName)
|
|
g.Indented(func() {
|
|
for _, output := range outputs {
|
|
var outputType string
|
|
switch expr := output.Value.(type) {
|
|
case *model.ScopeTraversalExpression:
|
|
resource, ok := expr.Parts[0].(*pcl.Resource)
|
|
if ok && len(expr.Parts) == 1 {
|
|
pkg, module, memberName, diagnostics := resourceTypeName(resource)
|
|
g.diagnostics = append(g.diagnostics, diagnostics...)
|
|
|
|
if module != "" {
|
|
module = "." + module
|
|
}
|
|
|
|
qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)
|
|
// special case: the output is a Resource type
|
|
outputType = fmt.Sprintf("pulumi.Output<%s>", qualifiedMemberName)
|
|
} else {
|
|
outputType = componentOutputType(expr.Type())
|
|
}
|
|
default:
|
|
outputType = componentOutputType(expr.Type())
|
|
}
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
g.Fgenf(w, "public %s: %s;\n", output.Name(), outputType)
|
|
}
|
|
|
|
token := "components:index:" + componentName
|
|
|
|
if len(configVars) == 0 {
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
g.Fgen(w, "constructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n")
|
|
g.Indented(func() {
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
g.Fgenf(w, "super(\"%s\", name, {}, opts);\n", token)
|
|
})
|
|
} else {
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
argsTypeName := componentName + "Args"
|
|
g.Fgenf(w, "constructor(name: string, args: %s, opts?: pulumi.ComponentResourceOptions) {\n",
|
|
argsTypeName)
|
|
g.Indented(func() {
|
|
g.Fgenf(w, "%s", g.Indent)
|
|
g.Fgenf(w, "super(\"%s\", name, args, opts);\n", token)
|
|
})
|
|
}
|
|
|
|
// generate component resources and local variables
|
|
g.Indented(func() {
|
|
// assign default values to config inputs
|
|
for _, configVar := range configVars {
|
|
if configVar.DefaultValue != nil {
|
|
g.Fgenf(w, "%sargs.%s = args.%s || %v;\n",
|
|
g.Indent,
|
|
configVar.Name(),
|
|
configVar.Name(),
|
|
configVar.DefaultValue)
|
|
}
|
|
}
|
|
|
|
for _, node := range pcl.Linearize(component.Program) {
|
|
switch node := node.(type) {
|
|
case *pcl.LocalVariable:
|
|
g.genLocalVariable(w, node)
|
|
g.Fgen(w, "\n")
|
|
case *pcl.Component:
|
|
if node.Options == nil {
|
|
node.Options = &pcl.ResourceOptions{}
|
|
}
|
|
|
|
if node.Options.Parent == nil {
|
|
node.Options.Parent = model.ConstantReference(&model.Constant{
|
|
Name: "this",
|
|
})
|
|
}
|
|
g.genComponent(w, node)
|
|
g.Fgen(w, "\n")
|
|
case *pcl.Resource:
|
|
if node.Options == nil {
|
|
node.Options = &pcl.ResourceOptions{}
|
|
}
|
|
|
|
if node.Options.Parent == nil {
|
|
node.Options.Parent = model.ConstantReference(&model.Constant{
|
|
Name: "this",
|
|
})
|
|
}
|
|
g.genResource(w, node)
|
|
g.Fgen(w, "\n")
|
|
}
|
|
}
|
|
|
|
registeredOutputs := &model.ObjectConsExpression{}
|
|
for _, output := range outputs {
|
|
// assign the output fields
|
|
outputProperty := output.Name()
|
|
switch expr := output.Value.(type) {
|
|
case *model.ScopeTraversalExpression:
|
|
_, ok := expr.Parts[0].(*pcl.Resource)
|
|
if ok && len(expr.Parts) == 1 {
|
|
// special case: the output is a Resource type
|
|
g.Fgenf(w, "%sthis.%s = pulumi.output(%v);\n",
|
|
g.Indent, outputProperty,
|
|
g.lowerExpression(output.Value, output.Type()))
|
|
} else {
|
|
g.Fgenf(w, "%sthis.%s = %v;\n",
|
|
g.Indent, outputProperty,
|
|
g.lowerExpression(output.Value, output.Type()))
|
|
}
|
|
default:
|
|
g.Fgenf(w, "%sthis.%s = %v;\n",
|
|
g.Indent, outputProperty,
|
|
g.lowerExpression(output.Value, output.Type()))
|
|
}
|
|
// add the outputs to abject for registration
|
|
registeredOutputs.Items = append(registeredOutputs.Items, model.ObjectConsItem{
|
|
Key: &model.LiteralValueExpression{
|
|
Tokens: syntax.NewLiteralValueTokens(cty.StringVal(output.Name())),
|
|
Value: cty.StringVal(output.Name()),
|
|
},
|
|
Value: output.Value,
|
|
})
|
|
}
|
|
|
|
if len(outputs) == 0 {
|
|
g.Fgenf(w, "%sthis.registerOutputs();\n", g.Indent)
|
|
} else {
|
|
g.Fgenf(w, "%sthis.registerOutputs(%v);\n", g.Indent, registeredOutputs)
|
|
}
|
|
})
|
|
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
})
|
|
g.Fgen(w, "}\n")
|
|
}
|
|
|
|
func (g *generator) genNode(w io.Writer, n pcl.Node) {
|
|
switch n := n.(type) {
|
|
case *pcl.Resource:
|
|
g.genResource(w, n)
|
|
case *pcl.ConfigVariable:
|
|
g.genConfigVariable(w, n)
|
|
case *pcl.LocalVariable:
|
|
g.genLocalVariable(w, n)
|
|
case *pcl.OutputVariable:
|
|
g.genOutputVariable(w, n)
|
|
case *pcl.Component:
|
|
g.genComponent(w, n)
|
|
}
|
|
}
|
|
|
|
func resourceRequiresAsyncMain(r *pcl.Resource) bool {
|
|
if r.Options == nil || r.Options.Range == nil {
|
|
return false
|
|
}
|
|
|
|
return model.ContainsPromises(r.Options.Range.Type())
|
|
}
|
|
|
|
func outputRequiresAsyncMain(ov *pcl.OutputVariable) bool {
|
|
outputName := ov.LogicalName()
|
|
return makeValidIdentifier(outputName) != outputName
|
|
}
|
|
|
|
// resourceTypeName computes the NodeJS package, module, and type name for the given resource.
|
|
func resourceTypeName(r *pcl.Resource) (string, string, string, hcl.Diagnostics) {
|
|
// Compute the resource type from the Pulumi type token.
|
|
pcl.FixupPulumiPackageTokens(r)
|
|
pkg, module, member, diagnostics := r.DecomposeToken()
|
|
|
|
if r.Schema != nil {
|
|
module = moduleName(module, r.Schema.PackageReference)
|
|
}
|
|
|
|
return makeValidIdentifier(pkg), module, title(member), diagnostics
|
|
}
|
|
|
|
func moduleName(module string, pkg schema.PackageReference) string {
|
|
// Normalize module.
|
|
if pkg != nil {
|
|
def, err := pkg.Definition()
|
|
contract.AssertNoErrorf(err, "error loading package definition for %q", pkg.Name())
|
|
err = def.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
|
|
contract.AssertNoErrorf(err, "error importing nodejs language for %q", pkg.Name())
|
|
if lang, ok := def.Language["nodejs"]; ok {
|
|
pkgInfo := lang.(NodePackageInfo)
|
|
if m, ok := pkgInfo.ModuleToPackage[module]; ok {
|
|
module = m
|
|
}
|
|
}
|
|
}
|
|
return strings.ToLower(strings.ReplaceAll(module, "/", "."))
|
|
}
|
|
|
|
// makeResourceName returns the expression that should be emitted for a resource's "name" parameter given its base name
|
|
// and the count variable name, if any.
|
|
func (g *generator) makeResourceName(baseName, count string) string {
|
|
if count == "" {
|
|
if g.isComponent {
|
|
return fmt.Sprintf("`${name}-%s`", baseName)
|
|
}
|
|
return fmt.Sprintf(`"%s"`, baseName)
|
|
}
|
|
|
|
if g.isComponent {
|
|
return fmt.Sprintf("`${name}-%s-${%s}`", baseName, count)
|
|
}
|
|
return fmt.Sprintf("`%s-${%s}`", baseName, count)
|
|
}
|
|
|
|
func (g *generator) genResourceOptions(opts *pcl.ResourceOptions) string {
|
|
if opts == nil {
|
|
return ""
|
|
}
|
|
|
|
// Turn the resource options into an ObjectConsExpression and generate it.
|
|
var object *model.ObjectConsExpression
|
|
appendOption := func(name string, value model.Expression) {
|
|
if object == nil {
|
|
object = &model.ObjectConsExpression{}
|
|
}
|
|
object.Items = append(object.Items, model.ObjectConsItem{
|
|
Key: &model.LiteralValueExpression{
|
|
Tokens: syntax.NewLiteralValueTokens(cty.StringVal(name)),
|
|
Value: cty.StringVal(name),
|
|
},
|
|
Value: value,
|
|
})
|
|
}
|
|
|
|
if opts.Parent != nil {
|
|
appendOption("parent", opts.Parent)
|
|
}
|
|
if opts.Provider != nil {
|
|
appendOption("provider", opts.Provider)
|
|
}
|
|
if opts.DependsOn != nil {
|
|
appendOption("dependsOn", opts.DependsOn)
|
|
}
|
|
if opts.Protect != nil {
|
|
appendOption("protect", opts.Protect)
|
|
}
|
|
if opts.RetainOnDelete != nil {
|
|
appendOption("retainOnDelete", opts.RetainOnDelete)
|
|
}
|
|
if opts.IgnoreChanges != nil {
|
|
appendOption("ignoreChanges", opts.IgnoreChanges)
|
|
}
|
|
if opts.DeletedWith != nil {
|
|
appendOption("deletedWith", opts.DeletedWith)
|
|
}
|
|
|
|
if object == nil {
|
|
return ""
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
g.Fgenf(&buffer, ", %v", g.lowerExpression(object, nil))
|
|
return buffer.String()
|
|
}
|
|
|
|
// genResourceDeclaration handles the generation of instantiations of resources.
|
|
func (g *generator) genResourceDeclaration(w io.Writer, r *pcl.Resource, needsDefinition bool) {
|
|
pkg, module, memberName, diagnostics := resourceTypeName(r)
|
|
g.diagnostics = append(g.diagnostics, diagnostics...)
|
|
|
|
if module != "" {
|
|
module = "." + module
|
|
}
|
|
|
|
qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)
|
|
|
|
optionsBag := g.genResourceOptions(r.Options)
|
|
|
|
name := r.LogicalName()
|
|
variableName := makeValidIdentifier(r.Name())
|
|
|
|
if needsDefinition {
|
|
g.genTrivia(w, r.Definition.Tokens.GetType(""))
|
|
for _, l := range r.Definition.Tokens.GetLabels(nil) {
|
|
g.genTrivia(w, l)
|
|
}
|
|
g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())
|
|
}
|
|
|
|
instantiate := func(resName string) {
|
|
g.Fgenf(w, "new %s(%s, {", qualifiedMemberName, resName)
|
|
indenter := func(f func()) { f() }
|
|
if len(r.Inputs) > 1 {
|
|
indenter = g.Indented
|
|
}
|
|
indenter(func() {
|
|
fmtString := "%s: %.v"
|
|
if len(r.Inputs) > 1 {
|
|
fmtString = "\n" + g.Indent + "%s: %.v,"
|
|
}
|
|
|
|
for _, attr := range r.Inputs {
|
|
propertyName := attr.Name
|
|
if !isLegalIdentifier(propertyName) {
|
|
propertyName = fmt.Sprintf("%q", propertyName)
|
|
}
|
|
|
|
if r.Schema != nil {
|
|
destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: attr.Name})
|
|
g.diagnostics = append(g.diagnostics, diagnostics...)
|
|
g.Fgenf(w, fmtString, propertyName,
|
|
g.lowerExpression(attr.Value, destType.(model.Type)))
|
|
} else {
|
|
g.Fgenf(w, fmtString, propertyName, attr.Value)
|
|
}
|
|
}
|
|
})
|
|
if len(r.Inputs) > 1 {
|
|
g.Fgenf(w, "\n%s", g.Indent)
|
|
}
|
|
g.Fgenf(w, "}%s)", optionsBag)
|
|
}
|
|
|
|
if r.Options != nil && r.Options.Range != nil {
|
|
rangeType := r.Options.Range.Type()
|
|
rangeExpr := r.Options.Range
|
|
if model.ContainsOutputs(r.Options.Range.Type()) {
|
|
rangeExpr = g.lowerExpression(rangeExpr, rangeType)
|
|
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
|
|
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
|
|
} else {
|
|
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
|
|
}
|
|
|
|
switch expr := rangeExpr.(type) {
|
|
case *model.FunctionCallExpression:
|
|
if expr.Name == pcl.IntrinsicApply {
|
|
applyArgs, applyLambda := pcl.ParseApplyCall(expr)
|
|
// Step 1: generate the apply function call:
|
|
if len(applyArgs) == 1 {
|
|
// If we only have a single output, just generate a normal `.apply`
|
|
g.Fgenf(w, "%.20v.apply(", applyArgs[0])
|
|
} else {
|
|
// Otherwise, generate a call to `pulumi.all([]).apply()`.
|
|
g.Fgen(w, "pulumi.all([")
|
|
for i, o := range applyArgs {
|
|
if i > 0 {
|
|
g.Fgen(w, ", ")
|
|
}
|
|
g.Fgenf(w, "%v", o)
|
|
}
|
|
g.Fgen(w, "]).apply(")
|
|
}
|
|
|
|
// Step 2: apply lambda function arguments
|
|
switch len(applyLambda.Signature.Parameters) {
|
|
case 0:
|
|
g.Fgen(w, "()")
|
|
case 1:
|
|
g.Fgenf(w, "%s", applyLambda.Signature.Parameters[0].Name)
|
|
default:
|
|
g.Fgen(w, "([")
|
|
for i, p := range applyLambda.Signature.Parameters {
|
|
if i > 0 {
|
|
g.Fgen(w, ", ")
|
|
}
|
|
g.Fgenf(w, "%s", p.Name)
|
|
}
|
|
g.Fgen(w, "])")
|
|
}
|
|
|
|
// Step 3: The function body is where the resources are generated:
|
|
// The function body is also a non-output value so we rewrite the range of
|
|
// the resource declaration to this non-output value
|
|
g.Fgen(w, " => {\n")
|
|
g.Indented(func() {
|
|
r.Options.Range = applyLambda.Body
|
|
g.genResourceDeclaration(w, r, false)
|
|
})
|
|
g.Fgenf(w, "%s});\n", g.Indent)
|
|
return
|
|
}
|
|
|
|
// If we have anything else that returns output, just generate a normal `.apply`
|
|
g.Fgenf(w, "%.20v.apply(rangeBody => {\n", rangeExpr)
|
|
g.Indented(func() {
|
|
r.Options.Range = model.VariableReference(&model.Variable{
|
|
Name: "rangeBody",
|
|
VariableType: model.ResolveOutputs(rangeExpr.Type()),
|
|
})
|
|
g.genResourceDeclaration(w, r, false)
|
|
})
|
|
g.Fgenf(w, "%s});\n", g.Indent)
|
|
return
|
|
case *model.TupleConsExpression, *model.ForExpression:
|
|
// A list or list generator that contains outputs looks like list(output(T))
|
|
// ideally we want this to be output(list(T)) and then call apply:
|
|
// so we call pulumi.all to lift the elements of the list, then call apply
|
|
g.Fgenf(w, "pulumi.all(%.20v).apply(rangeBody => {\n", rangeExpr)
|
|
g.Indented(func() {
|
|
r.Options.Range = model.VariableReference(&model.Variable{
|
|
Name: "rangeBody",
|
|
VariableType: model.ResolveOutputs(rangeExpr.Type()),
|
|
})
|
|
g.genResourceDeclaration(w, r, false)
|
|
})
|
|
g.Fgenf(w, "%s});\n", g.Indent)
|
|
return
|
|
|
|
default:
|
|
// If we have anything else that returns output, just generate a normal `.apply`
|
|
g.Fgenf(w, "%.20v.apply(rangeBody => {\n", rangeExpr)
|
|
g.Indented(func() {
|
|
r.Options.Range = model.VariableReference(&model.Variable{
|
|
Name: "rangeBody",
|
|
VariableType: model.ResolveOutputs(rangeExpr.Type()),
|
|
})
|
|
g.genResourceDeclaration(w, r, false)
|
|
})
|
|
g.Fgenf(w, "%s});\n", g.Indent)
|
|
return
|
|
}
|
|
}
|
|
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
|
|
if needsDefinition {
|
|
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
|
|
}
|
|
|
|
g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
|
|
g.Indented(func() {
|
|
g.Fgenf(w, "%s%s = ", g.Indent, variableName)
|
|
instantiate(g.makeResourceName(name, ""))
|
|
g.Fgenf(w, ";\n")
|
|
})
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
} else {
|
|
if needsDefinition {
|
|
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
|
|
}
|
|
resKey := "key"
|
|
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
|
|
g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
|
|
resKey = "value"
|
|
} else {
|
|
rangeExpr := &model.FunctionCallExpression{
|
|
Name: "entries",
|
|
Args: []model.Expression{rangeExpr},
|
|
}
|
|
g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
|
|
}
|
|
|
|
resName := g.makeResourceName(name, "range."+resKey)
|
|
g.Indented(func() {
|
|
g.Fgenf(w, "%s%s.push(", g.Indent, variableName)
|
|
instantiate(resName)
|
|
g.Fgenf(w, ");\n")
|
|
})
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
}
|
|
} else {
|
|
g.Fgenf(w, "%sconst %s = ", g.Indent, variableName)
|
|
instantiate(g.makeResourceName(name, ""))
|
|
g.Fgenf(w, ";\n")
|
|
}
|
|
|
|
g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
|
|
}
|
|
|
|
func (g *generator) genResource(w io.Writer, r *pcl.Resource) {
|
|
g.genResourceDeclaration(w, r, true)
|
|
}
|
|
|
|
// genResource handles the generation of instantiations of non-builtin resources.
|
|
func (g *generator) genComponent(w io.Writer, component *pcl.Component) {
|
|
componentName := component.DeclarationName()
|
|
|
|
optionsBag := g.genResourceOptions(component.Options)
|
|
|
|
name := component.LogicalName()
|
|
variableName := makeValidIdentifier(component.Name())
|
|
|
|
g.genTrivia(w, component.Definition.Tokens.GetType(""))
|
|
for _, l := range component.Definition.Tokens.GetLabels(nil) {
|
|
g.genTrivia(w, l)
|
|
}
|
|
g.genTrivia(w, component.Definition.Tokens.GetOpenBrace())
|
|
configVars := component.Program.ConfigVariables()
|
|
instantiate := func(resName string) {
|
|
if len(configVars) == 0 {
|
|
g.Fgenf(w, "new %s(%s%s)", componentName, resName, optionsBag)
|
|
return
|
|
}
|
|
g.Fgenf(w, "new %s(%s, {", componentName, resName)
|
|
indenter := func(f func()) { f() }
|
|
if len(component.Inputs) > 1 {
|
|
indenter = g.Indented
|
|
}
|
|
indenter(func() {
|
|
fmtString := "%s: %.v"
|
|
if len(component.Inputs) > 1 {
|
|
fmtString = "\n" + g.Indent + "%s: %.v,"
|
|
}
|
|
|
|
for _, attr := range component.Inputs {
|
|
propertyName := attr.Name
|
|
if !isLegalIdentifier(propertyName) {
|
|
propertyName = fmt.Sprintf("%q", propertyName)
|
|
}
|
|
|
|
g.Fgenf(w, fmtString, propertyName,
|
|
g.lowerExpression(attr.Value, attr.Value.Type()))
|
|
}
|
|
})
|
|
if len(component.Inputs) > 1 {
|
|
g.Fgenf(w, "\n%s", g.Indent)
|
|
}
|
|
g.Fgenf(w, "}%s)", optionsBag)
|
|
}
|
|
|
|
if component.Options != nil && component.Options.Range != nil {
|
|
rangeType := model.ResolveOutputs(component.Options.Range.Type())
|
|
rangeExpr := g.lowerExpression(component.Options.Range, rangeType)
|
|
|
|
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
|
|
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, componentName)
|
|
g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
|
|
g.Indented(func() {
|
|
g.Fgenf(w, "%s%s = ", g.Indent, variableName)
|
|
instantiate(g.makeResourceName(name, ""))
|
|
g.Fgenf(w, ";\n")
|
|
})
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
} else {
|
|
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, componentName)
|
|
|
|
resKey := "key"
|
|
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
|
|
g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
|
|
resKey = "value"
|
|
} else {
|
|
rangeExpr := &model.FunctionCallExpression{
|
|
Name: "entries",
|
|
Args: []model.Expression{rangeExpr},
|
|
}
|
|
g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
|
|
}
|
|
|
|
resName := g.makeResourceName(name, "range."+resKey)
|
|
g.Indented(func() {
|
|
g.Fgenf(w, "%s%s.push(", g.Indent, variableName)
|
|
instantiate(resName)
|
|
g.Fgenf(w, ");\n")
|
|
})
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
}
|
|
} else {
|
|
g.Fgenf(w, "%sconst %s = ", g.Indent, variableName)
|
|
instantiate(g.makeResourceName(name, ""))
|
|
g.Fgenf(w, ";\n")
|
|
}
|
|
|
|
g.genTrivia(w, component.Definition.Tokens.GetCloseBrace())
|
|
}
|
|
|
|
func computeConfigTypeParam(configType model.Type) string {
|
|
switch pcl.UnwrapOption(configType) {
|
|
case model.StringType:
|
|
return "string"
|
|
case model.NumberType, model.IntType:
|
|
return "number"
|
|
case model.BoolType:
|
|
return "boolean"
|
|
case model.DynamicType:
|
|
return "any"
|
|
default:
|
|
switch complexType := pcl.UnwrapOption(configType).(type) {
|
|
case *model.ListType:
|
|
return fmt.Sprintf("Array<%s>", computeConfigTypeParam(complexType.ElementType))
|
|
case *model.MapType:
|
|
return fmt.Sprintf("Record<string, %s>", computeConfigTypeParam(complexType.ElementType))
|
|
case *model.ObjectType:
|
|
if len(complexType.Properties) == 0 {
|
|
return "any"
|
|
}
|
|
|
|
attributeKeys := []string{}
|
|
for attributeKey := range complexType.Properties {
|
|
attributeKeys = append(attributeKeys, attributeKey)
|
|
}
|
|
// get deterministically sorted attribute keys
|
|
sort.Strings(attributeKeys)
|
|
|
|
var elementTypes []string
|
|
for _, propertyName := range attributeKeys {
|
|
propertyType := complexType.Properties[propertyName]
|
|
elementType := fmt.Sprintf("%s?: %s", propertyName, computeConfigTypeParam(propertyType))
|
|
elementTypes = append(elementTypes, elementType)
|
|
}
|
|
|
|
return fmt.Sprintf("{%s}", strings.Join(elementTypes, ", "))
|
|
default:
|
|
return "any"
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) {
|
|
if !g.configCreated {
|
|
g.Fprintf(w, "%sconst config = new pulumi.Config();\n", g.Indent)
|
|
g.configCreated = true
|
|
}
|
|
|
|
getType := "Object"
|
|
switch pcl.UnwrapOption(v.Type()) {
|
|
case model.StringType:
|
|
getType = ""
|
|
case model.NumberType, model.IntType:
|
|
getType = "Number"
|
|
case model.BoolType:
|
|
getType = "Boolean"
|
|
}
|
|
|
|
typeParam := ""
|
|
if getType == "Object" {
|
|
// compute the type parameter T for the call to config.getObject<T>(...)
|
|
computedTypeParam := computeConfigTypeParam(v.Type())
|
|
if computedTypeParam != "any" {
|
|
// any is redundant
|
|
typeParam = fmt.Sprintf("<%s>", computedTypeParam)
|
|
}
|
|
}
|
|
|
|
getOrRequire := "get"
|
|
if v.DefaultValue == nil && !model.IsOptionalType(v.Type()) {
|
|
getOrRequire = "require"
|
|
}
|
|
|
|
if v.Description != "" {
|
|
for _, line := range strings.Split(v.Description, "\n") {
|
|
g.Fgenf(w, "%s// %s\n", g.Indent, line)
|
|
}
|
|
}
|
|
|
|
name := makeValidIdentifier(v.Name())
|
|
g.Fgenf(w, "%[1]sconst %[2]s = config.%[3]s%[4]s%[5]s(\"%[6]s\")",
|
|
g.Indent, name, getOrRequire, getType, typeParam, v.LogicalName())
|
|
if v.DefaultValue != nil && !model.IsOptionalType(v.Type()) {
|
|
g.Fgenf(w, " || %.v", g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()))
|
|
}
|
|
g.Fgenf(w, ";\n")
|
|
}
|
|
|
|
func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) {
|
|
g.genTrivia(w, v.Definition.Tokens.Name)
|
|
g.Fgenf(w, "%sconst %s = %.3v;\n", g.Indent, v.Name(), g.lowerExpression(v.Definition.Value, v.Type()))
|
|
}
|
|
|
|
func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) {
|
|
if g.asyncMain {
|
|
// skip generating the output variables as export constants
|
|
// when we are inside an async main program because we export them as a single object
|
|
return
|
|
}
|
|
|
|
// TODO(pdg): trivia
|
|
g.Fgenf(w, "%sexport const %s = %.3v;\n", g.Indent,
|
|
makeValidIdentifier(v.Name()), g.lowerExpression(v.Value, v.Type()))
|
|
}
|
|
|
|
func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) {
|
|
message := "not yet implemented: " + fmt.Sprintf(reason, vs...)
|
|
g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: message,
|
|
Detail: message,
|
|
})
|
|
g.Fgenf(w, "(() => throw new Error(%q))()", fmt.Sprintf(reason, vs...))
|
|
}
|