Support TypeScript in a more first-class way

This change lets us set runtime specific options in Pulumi.yaml, which
will flow as arguments to the language hosts. We then teach the nodejs
host that when the `typescript` is set to `true` that it should load
ts-node before calling into user code. This allows using typescript
natively without an explicit compile step outside of Pulumi.

This works even when a tsconfig.json file is not present in the
application and should provide a nicer inner loop for folks writing
typescript (I'm pretty sure everyone has run into the "but I fixed
that bug!  Why isn't it getting picked up?  Oh, I forgot to run tsc"
problem.

Fixes #958
This commit is contained in:
Matt Ellis 2018-06-24 22:47:54 -07:00
parent 5a52c1c080
commit ce5eaa8343
22 changed files with 265 additions and 112 deletions

3
.gitignore vendored
View file

@ -9,6 +9,9 @@ coverage.cov
/.idea/
*.iml
# VSCode creates this binary when running tests in the debugger
**/debug.test
# Go tests run "in tree" and this folder will linger if they fail (the integration test framework cleans
# it up when they pass.)
**/command-output/

View file

@ -25,7 +25,10 @@ import (
)
func TestPrettyKeyForProject(t *testing.T) {
proj := &workspace.Project{Name: tokens.PackageName("test-package"), Runtime: "nodejs"}
proj := &workspace.Project{
Name: tokens.PackageName("test-package"),
RuntimeInfo: workspace.NewProjectRuntimeInfo("nodejs", nil),
}
assert.Equal(t, "foo", prettyKeyForProject(config.MustMakeKey("test-package", "foo"), proj))
assert.Equal(t, "other-package:bar", prettyKeyForProject(config.MustMakeKey("other-package", "bar"), proj))

View file

@ -353,10 +353,10 @@ func installDependencies() error {
// TODO[pulumi/pulumi#1307]: move to the language plugins so we don't have to hard code here.
var command string
var c *exec.Cmd
if strings.EqualFold(proj.Runtime, "nodejs") {
if strings.EqualFold(proj.RuntimeInfo.Name(), "nodejs") {
command = "npm install"
c = exec.Command("npm", "install") // nolint: gas, intentionally launching with partial path
} else if strings.EqualFold(proj.Runtime, "python") {
} else if strings.EqualFold(proj.RuntimeInfo.Name(), "python") {
command = "pip install -r requirements.txt"
c = exec.Command("pip", "install", "-r", "requirements.txt") // nolint: gas, intentionally launching with partial path
} else {

View file

@ -1,4 +1,7 @@
name: minimal
description: A minimal Pulumi program.
runtime: nodejs
runtime:
name: nodejs
options:
typescript: true

View file

@ -1,10 +1,5 @@
{
"name": "minimal",
"main": "bin/index.js",
"typings": "bin/index.d.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^3.0.0"
},

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}

View file

@ -351,7 +351,7 @@ func (pc *Client) CreateUpdate(
updateRequest := apitype.UpdateProgramRequest{
Name: string(pkg.Name),
Runtime: pkg.Runtime,
Runtime: pkg.RuntimeInfo.Name(),
Main: main,
Description: description,
Config: wireConfig,

View file

@ -130,7 +130,7 @@ func GetStackTags() (map[apitype.StackTagName]string, error) {
return nil, errors.Wrapf(err, "error loading project %q", projPath)
}
tags[apitype.ProjectNameTag] = proj.Name.String()
tags[apitype.ProjectRuntimeTag] = proj.Runtime
tags[apitype.ProjectRuntimeTag] = proj.RuntimeInfo.Name()
if proj.Description != nil {
tags[apitype.ProjectDescriptionTag] = *proj.Description
}

View file

@ -44,7 +44,7 @@ func ProjectInfoContext(projinfo *Projinfo, config plugin.ConfigSource, pluginEv
}
// Create a context for plugins.
ctx, err := plugin.NewContext(diag, nil, config, pluginEvents, pwd, tracingSpan)
ctx, err := plugin.NewContext(diag, nil, config, pluginEvents, pwd, projinfo.Proj.RuntimeInfo.Options(), tracingSpan)
if err != nil {
return "", "", nil, err
}

View file

@ -35,7 +35,7 @@ import (
func TestNullPlan(t *testing.T) {
t.Parallel()
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, nil, nil, "", nil)
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, nil, nil, "", nil, nil)
assert.Nil(t, err)
targ := &Target{Name: tokens.QName("null")}
prev := NewSnapshot(Manifest{}, nil)
@ -56,7 +56,7 @@ func TestErrorPlan(t *testing.T) {
// First trigger an error from Iterate:
{
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, nil, nil, "", nil)
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, nil, nil, "", nil, nil)
assert.Nil(t, err)
targ := &Target{Name: tokens.QName("errs")}
prev := NewSnapshot(Manifest{}, nil)
@ -71,7 +71,7 @@ func TestErrorPlan(t *testing.T) {
// Next trigger an error from Next:
{
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, nil, nil, "", nil)
ctx, err := plugin.NewContext(cmdutil.Diag(), nil, nil, nil, "", nil, nil)
assert.Nil(t, err)
targ := &Target{Name: tokens.QName("errs")}
prev := NewSnapshot(Manifest{}, nil)
@ -141,7 +141,7 @@ func TestBasicCRUDPlan(t *testing.T) {
// we don't actually execute the plan, so there's no need to implement the other functions.
}, nil
},
}, nil, nil, "", nil)
}, nil, nil, "", nil, nil)
assert.Nil(t, err)
// Setup a fake namespace/target combination.

View file

@ -162,7 +162,7 @@ func (iter *evalSourceIterator) forkRun(opts Options) {
go func() {
// Next, launch the language plugin.
run := func() error {
rt := iter.src.runinfo.Proj.Runtime
rt := iter.src.runinfo.Proj.RuntimeInfo.Name()
langhost, err := iter.src.plugctx.Host.LanguageRuntime(rt)
if err != nil {
return errors.Wrapf(err, "failed to launch language host %s", rt)

View file

@ -36,7 +36,7 @@ type Context struct {
// NewContext allocates a new context with a given sink and host. Note that the host is "owned" by this context from
// here forwards, such that when the context's resources are reclaimed, so too are the host's.
func NewContext(d diag.Sink, host Host, cfg ConfigSource, events Events,
pwd string, parentSpan opentracing.Span) (*Context, error) {
pwd string, runtimeOptions map[string]bool, parentSpan opentracing.Span) (*Context, error) {
ctx := &Context{
Diag: d,
Host: host,
@ -44,7 +44,7 @@ func NewContext(d diag.Sink, host Host, cfg ConfigSource, events Events,
tracingSpan: parentSpan,
}
if host == nil {
h, err := NewDefaultHost(ctx, cfg, events)
h, err := NewDefaultHost(ctx, cfg, events, runtimeOptions)
if err != nil {
return nil, err
}

View file

@ -78,11 +78,12 @@ type Events interface {
}
// NewDefaultHost implements the standard plugin logic, using the standard installation root to find them.
func NewDefaultHost(ctx *Context, config ConfigSource, events Events) (Host, error) {
func NewDefaultHost(ctx *Context, config ConfigSource, events Events, runtimeOptions map[string]bool) (Host, error) {
host := &defaultHost{
ctx: ctx,
config: config,
events: events,
runtimeOptions: runtimeOptions,
analyzerPlugins: make(map[tokens.QName]*analyzerPlugin),
languagePlugins: make(map[string]*languagePlugin),
resourcePlugins: make(map[tokens.Package]*resourcePlugin),
@ -116,6 +117,7 @@ type defaultHost struct {
ctx *Context // the shared context for this host.
config ConfigSource // the source for provider configuration parameters.
events Events // optional callbacks for plugin load events
runtimeOptions map[string]bool // options to pass to the language plugins.
analyzerPlugins map[tokens.QName]*analyzerPlugin // a cache of analyzer plugins and their processes.
languagePlugins map[string]*languagePlugin // a cache of language plugins and their processes.
resourcePlugins map[tokens.Package]*resourcePlugin // a cache of resource plugins and their processes.
@ -284,7 +286,7 @@ func (host *defaultHost) LanguageRuntime(runtime string) (LanguageRuntime, error
}
// If not, allocate a new one.
plug, err := NewLanguageRuntime(host, host.ctx, runtime)
plug, err := NewLanguageRuntime(host, host.ctx, runtime, host.runtimeOptions)
if err == nil && plug != nil {
info, infoerr := plug.GetPluginInfo()
if infoerr != nil {
@ -356,12 +358,12 @@ func (host *defaultHost) GetRequiredPlugins(info ProgInfo, kinds Flags) ([]works
if kinds&LanguagePlugins != 0 {
// First make sure the language plugin is present. We need this to load the required resource plugins.
// TODO: we need to think about how best to version this. For now, it always picks the latest.
lang, err := host.LanguageRuntime(info.Proj.Runtime)
lang, err := host.LanguageRuntime(info.Proj.RuntimeInfo.Name())
if err != nil {
return nil, errors.Wrapf(err, "failed to load language plugin %s", info.Proj.Runtime)
return nil, errors.Wrapf(err, "failed to load language plugin %s", info.Proj.RuntimeInfo.Name())
}
plugins = append(plugins, workspace.PluginInfo{
Name: info.Proj.Runtime,
Name: info.Proj.RuntimeInfo.Name(),
Kind: workspace.LanguagePlugin,
})

View file

@ -15,6 +15,7 @@
package plugin
import (
"fmt"
"strings"
"github.com/blang/semver"
@ -40,7 +41,7 @@ type langhost struct {
// NewLanguageRuntime binds to a language's runtime plugin and then creates a gRPC connection to it. If the
// plugin could not be found, or an error occurs while creating the child process, an error is returned.
func NewLanguageRuntime(host Host, ctx *Context, runtime string) (LanguageRuntime, error) {
func NewLanguageRuntime(host Host, ctx *Context, runtime string, options map[string]bool) (LanguageRuntime, error) {
// Load the plugin's path by using the standard workspace logic.
_, path, err := workspace.GetPluginPath(
workspace.LanguagePlugin, strings.Replace(runtime, tokens.QNameDelimiter, "_", -1), nil)
@ -53,7 +54,13 @@ func NewLanguageRuntime(host Host, ctx *Context, runtime string) (LanguageRuntim
})
}
plug, err := newPlugin(ctx, path, runtime, []string{host.ServerAddr()})
var args []string
for k, v := range options {
args = append(args, fmt.Sprintf("-%s=%t", k, v))
}
args = append(args, host.ServerAddr())
plug, err := newPlugin(ctx, path, runtime, args)
if err != nil {
return nil, err
}

View file

@ -139,8 +139,8 @@ type ProgramTestOptions struct {
Quick bool
// UpdateCommandlineFlags specifies flags to add to the `pulumi update` command line (e.g. "--color=raw")
UpdateCommandlineFlags []string
// SkipBuild indicates that the build step should be skipped (e.g. no `yarn build` for `nodejs` programs)
SkipBuild bool
// RunBuild indicates that the build step should be run (e.g. run `yarn build` for `nodejs` programs)
RunBuild bool
// CloudURL is an optional URL to override the default Pulumi Service API (https://api.pulumi-staging.io). The
// PULUMI_ACCESS_TOKEN environment variable must also be set to a valid access token for the target cloud.
@ -286,8 +286,8 @@ func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOpt
if overrides.ReportStats != nil {
opts.ReportStats = overrides.ReportStats
}
if overrides.SkipBuild {
opts.SkipBuild = overrides.SkipBuild
if overrides.RunBuild {
opts.RunBuild = overrides.RunBuild
}
return opts
}
@ -924,7 +924,7 @@ func (pt *programTester) copyTestToTemporaryDirectory() (string, string, error)
// For most projects, we will copy to a temporary directory. For Go projects, however, we must not perturb
// the source layout, due to GOPATH and vendoring. So, skip it for Go.
var tmpdir, projdir string
if projinfo.Proj.Runtime == "go" {
if projinfo.Proj.RuntimeInfo.Name() == "go" {
projdir = projinfo.Root
} else {
stackName := string(pt.opts.GetStackName())
@ -966,7 +966,7 @@ func (pt *programTester) getProjinfo(projectDir string) (*engine.Projinfo, error
// prepareProject runs setup necessary to get the project ready for `pulumi` commands.
func (pt *programTester) prepareProject(projinfo *engine.Projinfo) error {
// Based on the language, invoke the right routine to prepare the target directory.
switch rt := projinfo.Proj.Runtime; rt {
switch rt := projinfo.Proj.RuntimeInfo.Name(); rt {
case "nodejs":
return pt.prepareNodeJSProject(projinfo)
case "python":
@ -1016,7 +1016,7 @@ func (pt *programTester) prepareNodeJSProject(projinfo *engine.Projinfo) error {
}
}
if !pt.opts.SkipBuild {
if pt.opts.RunBuild {
// And finally compile it using whatever build steps are in the package.json file.
if err = pt.runYarnCommand("yarn-build", []string{"run", "build"}, cwd); err != nil {
return err

View file

@ -15,6 +15,7 @@
package workspace
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
@ -40,9 +41,9 @@ type Analyzers []tokens.QName
// TODO[pulumi/pulumi#423]: use DOM based marshalling so we can roundtrip the seralized structure perfectly.
// nolint: lll
type Project struct {
Name tokens.PackageName `json:"name" yaml:"name"` // a required fully qualified name.
Runtime string `json:"runtime" yaml:"runtime"` // a required runtime that executes code.
Main string `json:"main,omitempty" yaml:"main,omitempty"` // an optional override for the main program location.
Name tokens.PackageName `json:"name" yaml:"name"` // a required fully qualified name.
RuntimeInfo ProjectRuntimeInfo `json:"runtime" yaml:"runtime"` // a required runtime that executes code.
Main string `json:"main,omitempty" yaml:"main,omitempty"` // an optional override for the main program location.
Description *string `json:"description,omitempty" yaml:"description,omitempty"` // an optional informational description.
Author *string `json:"author,omitempty" yaml:"author,omitempty"` // an optional author.
@ -61,9 +62,10 @@ func (proj *Project) Validate() error {
if proj.Name == "" {
return errors.New("project is missing a 'name' attribute")
}
if proj.Runtime == "" {
if proj.RuntimeInfo.Name() == "" {
return errors.New("project is missing a 'runtime' attribute")
}
return nil
}
@ -124,6 +126,86 @@ func (ps *ProjectStack) Save(path string) error {
return ioutil.WriteFile(path, b, 0644)
}
type ProjectRuntimeInfo struct {
name string
options map[string]bool
}
func NewProjectRuntimeInfo(name string, options map[string]bool) ProjectRuntimeInfo {
return ProjectRuntimeInfo{
name: name,
options: options,
}
}
func (info *ProjectRuntimeInfo) Name() string {
return info.name
}
func (info *ProjectRuntimeInfo) Options() map[string]bool {
return info.options
}
func (info ProjectRuntimeInfo) MarshalYAML() (interface{}, error) {
if info.options == nil || len(info.options) == 0 {
return info.name, nil
}
return map[string]interface{}{
"name": info.name,
"options": info.options,
}, nil
}
func (info ProjectRuntimeInfo) MarshalJSON() ([]byte, error) {
if info.options == nil || len(info.options) == 0 {
return json.Marshal(info.name)
}
return json.Marshal(map[string]interface{}{
"name": info.name,
"options": info.options,
})
}
func (info *ProjectRuntimeInfo) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &info.name); err == nil {
return nil
}
var payload struct {
Name string `json:"name"`
Options map[string]bool `json:"options"`
}
if err := json.Unmarshal(data, &payload); err == nil {
info.name = payload.Name
info.options = payload.Options
return nil
}
return errors.New("runtime section must be a string or an object with name and options attributes")
}
func (info *ProjectRuntimeInfo) UnmarshalYAML(unmarshal func(interface{}) error) error {
if err := unmarshal(&info.name); err == nil {
return nil
}
var payload struct {
Name string `yaml:"name"`
Options map[string]bool `yaml:"options"`
}
if err := unmarshal(&payload); err == nil {
info.name = payload.Name
info.options = payload.Options
return nil
}
return errors.New("runtime section must be a string or an object with name and options attributes")
}
// LoadProject reads a project definition from a file.
func LoadProject(path string) (*Project, error) {
contract.Require(path != "", "path")

View file

@ -0,0 +1,36 @@
package workspace
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
func TestProjectRuntimeInfoRoundtripYAML(t *testing.T) {
doTest := func(marshal func(interface{}) ([]byte, error), unmarshal func([]byte, interface{}) error) {
ri := NewProjectRuntimeInfo("nodejs", nil)
byts, err := marshal(ri)
assert.NoError(t, err)
var riRountrip ProjectRuntimeInfo
err = unmarshal(byts, &riRountrip)
assert.NoError(t, err)
assert.Equal(t, "nodejs", riRountrip.Name())
assert.Nil(t, riRountrip.Options())
ri = NewProjectRuntimeInfo("nodejs", map[string]bool{
"typescript": true,
})
byts, err = marshal(ri)
assert.NoError(t, err)
err = unmarshal(byts, &riRountrip)
assert.NoError(t, err)
assert.Equal(t, "nodejs", riRountrip.Name())
assert.Equal(t, true, riRountrip.Options()["typescript"])
}
doTest(yaml.Marshal, yaml.Unmarshal)
doTest(json.Marshal, json.Unmarshal)
}

View file

@ -64,10 +64,13 @@ const (
// endpoint.
func main() {
var tracing string
var typescript bool
flag.StringVar(&tracing, "tracing", "",
"Emit tracing to a Zipkin-compatible tracing endpoint")
flag.BoolVar(&typescript, "typescript", true,
"Use ts-node at runtime to support typescript source natively")
flag.Parse()
args := flag.Args()
logging.InitLogging(false, 0, false)
cmdutil.InitTracing("pulumi-language-nodejs", "pulumi-langauge-nodejs", tracing)
@ -96,7 +99,7 @@ func main() {
// Fire up a gRPC server, letting the kernel choose a free port.
port, done, err := rpcutil.Serve(0, nil, []func(*grpc.Server) error{
func(srv *grpc.Server) error {
host := newLanguageHost(nodePath, runPath, engineAddress, tracing)
host := newLanguageHost(nodePath, runPath, engineAddress, tracing, typescript)
pulumirpc.RegisterLanguageRuntimeServer(srv, host)
return nil
},
@ -121,14 +124,16 @@ type nodeLanguageHost struct {
runPath string
engineAddress string
tracing string
typescript bool
}
func newLanguageHost(nodePath, runPath, engineAddress, tracing string) pulumirpc.LanguageRuntimeServer {
func newLanguageHost(nodePath, runPath, engineAddress, tracing string, typescript bool) pulumirpc.LanguageRuntimeServer {
return &nodeLanguageHost{
nodeBin: nodePath,
runPath: runPath,
engineAddress: engineAddress,
tracing: tracing,
typescript: typescript,
}
}
@ -264,29 +269,13 @@ func (host *nodeLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest
return nil, err
}
ourCmd, err := os.Executable()
if err != nil {
err = errors.Wrap(err, "failed to find our working directory")
return nil, err
}
// Older versions of the pulumi runtime used a custom node module (which only worked on node 6.10.X) to support
// closure serialization. While we no longer use this, we continue to ship this module with the language host in
// the SDK, so we can deploy programs using older versions of the Pulumi framework. So, for now, let's add this
// folder with our native modules to the NODE_PATH so Node can find it.
//
// TODO(ellismg)[pulumi/pulumi#1298]: Remove this block of code when we no longer need to support older
// @pulumi/pulumi versions.
env := os.Environ()
existingNodePath := os.Getenv("NODE_PATH")
if existingNodePath != "" {
env = append(env, fmt.Sprintf("NODE_PATH=%s/v6.10.2:%s", filepath.Dir(ourCmd), existingNodePath))
} else {
env = append(env, "NODE_PATH="+filepath.Dir(ourCmd)+"/v6.10.2")
}
env = append(env, pulumiConfigVar+"="+string(config))
if host.typescript {
env = append(env, "PULUMI_NODEJS_TYPESCRIPT=true")
}
if logging.V(5) {
commandStr := strings.Join(args, " ")
logging.V(5).Infoln("Language host launching process: ", host.nodeBin, commandStr)

View file

@ -16,9 +16,10 @@
import * as fs from "fs";
import * as minimist from "minimist";
import * as os from "os";
import * as path from "path";
import * as tsnode from "ts-node";
import * as util from "util";
import * as pulumi from "../../";
import { RunError } from "../../errors";
import * as log from "../../log";
import * as runtime from "../../runtime";
@ -180,9 +181,12 @@ export function main(args: string[]): void {
}
}
// If ther is a --dry-run directive, flip the switch. This controls whether we are planning vs. really doing it.
// If there is a --dry-run directive, flip the switch. This controls whether we are planning vs. really doing it.
const dryRun: boolean = !!(argv["dry-run"]);
// If this is a typescript project, we'll want to load node-ts
const typeScript: boolean = process.env["PULUMI_NODEJS_TYPESCRIPT"] === "true";
// If there is a monitor argument, connect to it.
const monitorAddr = argv["monitor"];
if (!monitorAddr) {
@ -202,6 +206,12 @@ export function main(args: string[]): void {
engineAddr: engineAddr,
});
if (typeScript) {
tsnode.register({
typeCheck: true,
});
}
// Pluck out the program and arguments.
if (argv._.length === 0) {
return printErrorUsageAndExit("error: Missing program to execute");

View file

@ -1,28 +1,29 @@
{
"name": "@pulumi/pulumi",
"version": "${VERSION}",
"description": "Pulumi's Node.js SDK",
"license": "Apache-2.0",
"repository": "https://github.com/pulumi/pulumi/sdk/nodejs",
"dependencies": {
"google-protobuf": "^3.5.0",
"grpc": "^1.12.2",
"minimist": "^1.2.0",
"protobufjs": "^6.8.6",
"require-from-string": "^2.0.1",
"source-map-support": "^0.4.16",
"typescript": "^3.0.0",
"read-package-tree": "^5.2.1"
},
"devDependencies": {
"@types/minimist": "^1.2.0",
"@types/mocha": "^2.2.42",
"@types/node": "^8.0.25",
"@types/read-package-tree": "^5.2.0",
"@types/semver": "^5.5.0",
"istanbul": "^0.4.5",
"mocha": "^3.5.0",
"node-gyp": "^3.6.2",
"tslint": "^5.7.0"
}
"name": "@pulumi/pulumi",
"version": "${VERSION}",
"description": "Pulumi's Node.js SDK",
"license": "Apache-2.0",
"repository": "https://github.com/pulumi/pulumi/sdk/nodejs",
"dependencies": {
"google-protobuf": "^3.5.0",
"grpc": "^1.12.2",
"minimist": "^1.2.0",
"protobufjs": "^6.8.6",
"require-from-string": "^2.0.1",
"source-map-support": "^0.4.16",
"ts-node": "^7.0.0",
"typescript": "^3.0.0",
"read-package-tree": "^5.2.1"
},
"devDependencies": {
"@types/minimist": "^1.2.0",
"@types/mocha": "^2.2.42",
"@types/node": "^8.0.25",
"@types/read-package-tree": "^5.2.0",
"@types/semver": "^5.5.0",
"istanbul": "^0.4.5",
"mocha": "^3.5.0",
"node-gyp": "^3.6.2",
"tslint": "^5.7.0"
}
}

View file

@ -133,6 +133,10 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
arrify@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
asap@^2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@ -215,6 +219,10 @@ browser-stdout@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f"
buffer-from@^1.0.0, buffer-from@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
builtin-modules@^1.0.0, builtin-modules@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@ -395,6 +403,10 @@ diff@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
diff@^3.1.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
diff@^3.2.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
@ -847,6 +859,10 @@ longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
make-error@^1.1.1:
version "1.3.4"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.4.tgz#19978ed575f9e9545d2ff8c13e33b5d18a67d535"
mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
@ -1274,6 +1290,13 @@ source-map-support@^0.4.16:
dependencies:
source-map "^0.5.6"
source-map-support@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13"
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
@ -1284,6 +1307,10 @@ source-map@^0.5.6, source-map@~0.5.1:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
source-map@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d"
@ -1406,6 +1433,19 @@ tough-cookie@~2.3.3:
dependencies:
punycode "^1.4.1"
ts-node@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.0.tgz#a94a13c75e5e1aa6b82814b84c68deb339ba7bff"
dependencies:
arrify "^1.0.0"
buffer-from "^1.1.0"
diff "^3.1.0"
make-error "^1.1.1"
minimist "^1.2.0"
mkdirp "^0.5.1"
source-map-support "^0.5.6"
yn "^2.0.0"
tslib@^1.8.0, tslib@^1.8.1:
version "1.9.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8"
@ -1560,3 +1600,7 @@ yargs@~3.10.0:
cliui "^2.1.0"
decamelize "^1.0.0"
window-size "0.1.0"
yn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"

View file

@ -294,8 +294,8 @@ func TestConfigSave(t *testing.T) {
// Initialize an empty stack.
path := filepath.Join(e.RootPath, "Pulumi.yaml")
err := (&workspace.Project{
Name: "testing-config",
Runtime: "nodejs",
Name: "testing-config",
RuntimeInfo: workspace.NewProjectRuntimeInfo("nodejs", nil),
}).Save(path)
assert.NoError(t, err)
e.RunCommand("pulumi", "login", "--cloud-url", e.LocalURL())