10
10
package main
11
11
12
12
import (
13
+ "errors"
13
14
"fmt"
14
15
"log"
15
16
"net/url"
@@ -48,6 +49,8 @@ func makeUICmd(d *dev) *cobra.Command {
48
49
49
50
// UIDirectories contains the absolute path to the root of each UI sub-project.
50
51
type UIDirectories struct {
52
+ workspace string
53
+ // workspace is the absolute path to ./ .
51
54
// root is the absolute path to ./pkg/ui.
52
55
root string
53
56
// clusterUI is the absolute path to ./pkg/ui/workspaces/cluster-ui.
@@ -58,6 +61,10 @@ type UIDirectories struct {
58
61
e2eTests string
59
62
// eslintPlugin is the absolute path to ./pkg/ui/workspaces/eslint-plugin-crdb.
60
63
eslintPlugin string
64
+ // protoOss is the absolute path to ./pkg/ui/workspaces/db-console/src/js/.
65
+ protoOss string
66
+ // protoCcl is the absolute path to ./pkg/ui/workspaces/db-console/ccl/src/js/.
67
+ protoCcl string
61
68
}
62
69
63
70
// getUIDirs computes the absolute path to the root of each UI sub-project.
@@ -68,14 +75,168 @@ func getUIDirs(d *dev) (*UIDirectories, error) {
68
75
}
69
76
70
77
return & UIDirectories {
78
+ workspace : workspace ,
71
79
root : filepath .Join (workspace , "./pkg/ui" ),
72
80
clusterUI : filepath .Join (workspace , "./pkg/ui/workspaces/cluster-ui" ),
73
81
dbConsole : filepath .Join (workspace , "./pkg/ui/workspaces/db-console" ),
74
82
e2eTests : filepath .Join (workspace , "./pkg/ui/workspaces/e2e-tests" ),
75
83
eslintPlugin : filepath .Join (workspace , "./pkg/ui/workspaces/eslint-plugin-crdb" ),
84
+ protoOss : filepath .Join (workspace , "./pkg/ui/workspaces/db-console/src/js" ),
85
+ protoCcl : filepath .Join (workspace , "./pkg/ui/workspaces/db-console/ccl/src/js" ),
76
86
}, nil
77
87
}
78
88
89
+ // assertNoLinkedNpmDeps looks for JS packages linked outside the Bazel
90
+ // workspace (typically via `pnpm link`). It returns an error if:
91
+ //
92
+ // 'targets' contains a Bazel target that requires the web UI
93
+ // AND
94
+ // a node_modules/ tree exists within pkg/ui (or its subtrees)
95
+ // AND
96
+ // a @cockroachlabs-scoped package is symlinked to an external directory
97
+ //
98
+ // (or if any error occurs while performing one of those checks).
99
+ func (d * dev ) assertNoLinkedNpmDeps (targets []buildTarget ) error {
100
+ uiWillBeBuilt := false
101
+ for _ , target := range targets {
102
+ // TODO: This could potentially be a bazel query, e.g.
103
+ // 'somepath(${target.fullName}, //pkg/ui/workspaces/db-console:*)' or
104
+ // similar, but with only two eligible targets it doesn't seem quite
105
+ // worth it.
106
+ if target .fullName == cockroachTarget || target .fullName == cockroachTargetOss {
107
+ uiWillBeBuilt = true
108
+ break
109
+ }
110
+ }
111
+ if ! uiWillBeBuilt {
112
+ // If no UI build is required, the presence of an externally-linked
113
+ // package doesn't matter.
114
+ return nil
115
+ }
116
+
117
+ // Find the current workspace and build some relevant absolute paths.
118
+ uiDirs , err := getUIDirs (d )
119
+ if err != nil {
120
+ return fmt .Errorf ("could not check for linked NPM dependencies: %w" , err )
121
+ }
122
+
123
+ jsPkgRoots := []string {
124
+ uiDirs .root ,
125
+ uiDirs .eslintPlugin ,
126
+ uiDirs .protoOss ,
127
+ uiDirs .protoCcl ,
128
+ uiDirs .clusterUI ,
129
+ uiDirs .dbConsole ,
130
+ uiDirs .e2eTests ,
131
+ }
132
+
133
+ type LinkedPackage struct {
134
+ name string
135
+ dir string
136
+ }
137
+
138
+ anyPackageEscapesWorkspace := false
139
+
140
+ // Check for symlinks in each package's node_modules/@cockroachlabs/ dir.
141
+ for _ , jsPkgRoot := range jsPkgRoots {
142
+ crlModulesPath := filepath .Join (jsPkgRoot , "node_modules/@cockroachlabs" )
143
+ crlDeps , err := d .os .ReadDir (crlModulesPath )
144
+
145
+ // If node_modules/@cockroachlabs doesn't exist, it's likely that JS
146
+ // dependencies haven't been installed outside the Bazel workspace.
147
+ // This is expected for non-UI devs, and is a safe state.
148
+ if errors .Is (err , os .ErrNotExist ) {
149
+ continue
150
+ }
151
+ if err != nil {
152
+ return fmt .Errorf ("could not @cockroachlabs/ packages: %w" , err )
153
+ }
154
+
155
+ linkedPackages := []LinkedPackage {}
156
+
157
+ // For each dependency in node_modules/@cockroachlabs/ ...
158
+ for _ , depName := range crlDeps {
159
+ // Ignore empty strings, which are produced by d.os.ReadDir in
160
+ // dry-run mode.
161
+ if depName == "" {
162
+ continue
163
+ }
164
+
165
+ // Resolve the possible symlink.
166
+ depPath := filepath .Join (crlModulesPath , depName )
167
+ resolved , err := d .os .Readlink (depPath )
168
+ if err != nil {
169
+ return fmt .Errorf ("could not evaluate symlink %s: %w" , depPath , err )
170
+ }
171
+
172
+ // Convert it to a path relative to the Bazel workspace root.
173
+ relativeToWorkspace , err := filepath .Rel (
174
+ uiDirs .workspace ,
175
+ filepath .Join (crlModulesPath , resolved ),
176
+ )
177
+ if err != nil {
178
+ return fmt .Errorf ("could not relitivize path %s: %w" , resolved , err )
179
+ }
180
+
181
+ // If it doesn't start with '..', it doesn't escape the Bazel
182
+ // workspace.
183
+ // TODO: Once Go 1.20 is supported here, switch to filepath.IsLocal.
184
+ if ! strings .HasPrefix (relativeToWorkspace , ".." ) {
185
+ continue
186
+ }
187
+
188
+ // This package escapes the Bazel workspace! Add it to the queue.
189
+ abs , err := filepath .Abs (relativeToWorkspace )
190
+ if err != nil {
191
+ return fmt .Errorf ("could not absolutize path %s: %w" , resolved , err )
192
+ }
193
+
194
+ linkedPackages = append (
195
+ linkedPackages ,
196
+ LinkedPackage {
197
+ name : "@cockroachlabs/" + depName ,
198
+ dir : abs ,
199
+ },
200
+ )
201
+ }
202
+
203
+ // If this internal package has no dependencies provided by pnpm link,
204
+ // move on without logging anything.
205
+ if len (linkedPackages ) == 0 {
206
+ continue
207
+ }
208
+
209
+ if ! anyPackageEscapesWorkspace {
210
+ anyPackageEscapesWorkspace = true
211
+ log .Println ("Externally-linked package(s) detected:" )
212
+ }
213
+
214
+ log .Printf ("pkg/ui/workspaces/%s:" , filepath .Base (jsPkgRoot ))
215
+ for _ , pkg := range linkedPackages {
216
+ log .Printf (" %s <- %s\n " , pkg .name , pkg .dir )
217
+ }
218
+ log .Println ()
219
+ }
220
+
221
+ if anyPackageEscapesWorkspace {
222
+ msg := strings .TrimSpace (`
223
+ At least one JS dependency is linked to another directory on your machine.
224
+ Bazel cannot see changes in these packages, which could lead to both
225
+ false-positive and false-negative behavior in the UI.
226
+ This build has been pre-emptively failed.
227
+
228
+ To build without the UI, run:
229
+ dev build short
230
+ To remove all linked dependencies, run:
231
+ dev ui clean --all
232
+ ` ) + "\n "
233
+
234
+ return fmt .Errorf ("%s" , msg )
235
+ }
236
+
237
+ return nil
238
+ }
239
+
79
240
// makeUIWatchCmd initializes the 'ui watch' subcommand, which sets up a
80
241
// live-reloading HTTP server for db-console and a file-watching rebuilder for
81
242
// cluster-ui.
0 commit comments