-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
npm.go
164 lines (142 loc) 路 5.25 KB
/
npm.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 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 npm
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
uuid "github.com/gofrs/uuid"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
// Pack runs `npm pack` in the given directory, packaging the Node.js app located there into a
// tarball and returning it as `[]byte`. `stdout` is ignored for the command, as it does not
// generate useful data. If the `PULUMI_PREFER_YARN` environment variable is set, `yarn pack` is run
// instead of `npm pack`.
func Pack(ctx context.Context, dir string, stderr io.Writer) ([]byte, error) {
c, npm, bin, err := getCmd(ctx, "pack", false /*production*/)
if err != nil {
return nil, err
}
c.Dir = dir
// Note that `npm pack` doesn't have the ability to specify the resulting filename, since
// it's meant to be uploaded directly to npm, which means we have to get that information
// by parsing the output of the command. However, if we're using `yarn`, we can specify a
// filename.
var packfile string
if !npm {
// generate a new uuid
uuid, err := uuid.NewV4()
if err != nil {
return nil, err
}
packfile = fmt.Sprintf("%s.tgz", uuid.String())
c.Args = append(c.Args, "--filename", packfile)
}
// Run the command.
// `stdout` is ignored for the command, as it does not generate useful data.
var stdout bytes.Buffer
if err = runCmd(c, npm, &stdout, stderr); err != nil {
return nil, err
}
// If `npm` was used, parse the filename from the output.
if npm {
packfile = strings.TrimSpace(stdout.String())
}
defer os.Remove(packfile)
packTarball, err := ioutil.ReadFile(packfile)
if err != nil {
return nil, fmt.Errorf("%s pack completed successfully but the packed .tgz file was not generated", bin)
}
return packTarball, nil
}
// Install runs `npm install` in the given directory, installing the dependencies for the Node.js
// app located there. If the `PULUMI_PREFER_YARN` environment variable is set, `yarn install` is used
// instead of `npm install`.
func Install(ctx context.Context, dir string, production bool, stdout, stderr io.Writer) (string, error) {
c, npm, bin, err := getCmd(ctx, "install", production)
if err != nil {
return bin, err
}
c.Dir = dir
// Run the command.
if err = runCmd(c, npm, stdout, stderr); err != nil {
return bin, err
}
// Ensure the "node_modules" directory exists.
nodeModulesPath := filepath.Join(dir, "node_modules")
if _, err := os.Stat(nodeModulesPath); os.IsNotExist(err) {
return bin, fmt.Errorf("%s install reported success, but node_modules directory is missing", bin)
}
return bin, nil
}
// getCmd returns the exec.Cmd used to install NPM dependencies. It will either use `npm` or `yarn` depending
// on what is available on the current path, and if `PULUMI_PREFER_YARN` is truthy.
// The boolean return parameter indicates if `npm` is chosen or not (instead of `yarn`).
func getCmd(ctx context.Context, command string, production bool) (*exec.Cmd, bool, string, error) {
args := []string{command}
if production {
args = append(args, "--production")
}
if preferYarn() {
const file = "yarn"
yarnPath, err := exec.LookPath(file)
if err == nil {
return exec.CommandContext(ctx, yarnPath, args...), false, file, nil
}
logging.Warningf("could not find yarn on the $PATH, trying npm instead: %v", err)
}
const file = "npm"
npmPath, err := exec.LookPath(file)
if err != nil {
return nil, false, file, fmt.Errorf("could not find npm on the $PATH; npm is installed with Node.js "+
"available at https://nodejs.org/: %w", err)
}
// We pass `--loglevel=error` to prevent `npm` from printing warnings about missing
// `description`, `repository`, and `license` fields in the package.json file.
args = append(args, "--loglevel=error")
return exec.CommandContext(ctx, npmPath, args...), true, file, nil
}
// runCmd handles hooking up `stdout` and `stderr` and then runs the command.
func runCmd(c *exec.Cmd, npm bool, stdout, stderr io.Writer) error {
// Setup `stdout` and `stderr`.
// `stderr` is ignored when `yarn` is used because it outputs warnings like "package.json: No license field"
// to `stderr` that we don't need to show.
c.Stdout = stdout
var stderrBuffer bytes.Buffer
if npm {
c.Stderr = stderr
} else {
c.Stderr = &stderrBuffer
}
// Run the command.
if err := c.Run(); err != nil {
// If we failed, and we're using `yarn`, write out any bytes that were written to `stderr`.
if !npm {
stderr.Write(stderrBuffer.Bytes())
}
return err
}
return nil
}
// preferYarn returns true if the `PULUMI_PREFER_YARN` environment variable is set.
func preferYarn() bool {
return cmdutil.IsTruthy(os.Getenv("PULUMI_PREFER_YARN"))
}