@naanlang/naan 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/index.js +5 -2
  4. package/dist/env_web.js +11 -1
  5. package/dist/naan.min.js +5 -5
  6. package/frameworks/browser/https_request.nlg +2 -0
  7. package/frameworks/browser/sworker.js +23 -23
  8. package/frameworks/browser/terminals.nlg +132 -20
  9. package/frameworks/browser/workers.nlg +4 -1
  10. package/frameworks/browser/ws_client.nlg +9 -3
  11. package/frameworks/client/apiclient.nlg +8 -6
  12. package/frameworks/client/psm_client.nlg +4 -4
  13. package/frameworks/client/relaycon.nlg +5 -1
  14. package/frameworks/common/common.nlg +10 -4
  15. package/frameworks/node/apiserver.nlg +190 -4
  16. package/frameworks/node/worker.nlg +112 -10
  17. package/frameworks/node/ws_client.nlg +10 -3
  18. package/frameworks/project/build.nlg +13 -11
  19. package/frameworks/project/projects.nlg +7 -5
  20. package/frameworks/running/executors.nlg +124 -34
  21. package/frameworks/running/taskexec.nlg +5 -1
  22. package/lib/browser/env_web.js +17 -7
  23. package/lib/browser/env_webworker.js +22 -3
  24. package/lib/core/naanlib.js +5 -5
  25. package/lib/env_node.js +15 -4
  26. package/lib/env_nodeworker.js +21 -1
  27. package/package.json +8 -5
  28. package/plugins/serviceAws/aws/aws-sdk-node.min.js +2 -2
  29. package/plugins/serviceAws/aws/aws-sdk.min.js +3 -2
  30. package/plugins/serviceAws/aws_dynamo.nlg +19 -19
  31. package/plugins/serviceAws/dbt_aws.nlg +1 -1
  32. package/plugins/serviceYC/generic_api.yaml +36 -0
  33. package/plugins/serviceYC/naanide-relay-index.js +148 -0
  34. package/plugins/serviceYC/serviceYC.nlg +60 -0
  35. package/plugins/serviceYC/yc_apigateway.nlg +205 -0
  36. package/plugins/serviceYC/yc_function.nlg +163 -0
  37. package/plugins/serviceYC/yc_vm.nlg +207 -0
  38. package/test/test_02_context.nlg +1 -1
  39. package/test/test_03_jsinterop.nlg +2 -2
  40. package/test/test_05_strings.nlg +7 -4
  41. package/test/test_07_lingo.nlg +1 -1
@@ -83,7 +83,16 @@ closure APIServer(options, local server, chroot) {
83
83
  if server.guid
84
84
  server.oururl = server.oururl.concat("?guid=", server.guid)
85
85
  App.api = server // used remotely by client/relaycon.nlg
86
- server.ws = WebSocketServer(server)
86
+ if options.gateway { // processes at /wsgw endpoint
87
+ server.ws = WebSocketGatewayServer()
88
+ `(error) = server.ws.open(server, options.gateway)
89
+ if error {
90
+ error = Error("APIServer: cannot establish websocket gateway", options.gateway, error)
91
+ ErrorDebuglog(error)
92
+ return (list(error))
93
+ }
94
+ } else // default is native websocket protocols
95
+ server.ws = WebSocketServer(server)
87
96
  WesRelay(server) // registers as subsystem
88
97
  server.psm = psmServerMod.MakeServerPSM(server, options.name)
89
98
  `(error, hostfs) = server.psm.conns.NodeFS.connect("Host", "NideFS", list(""))
@@ -267,7 +276,7 @@ closure APIServer(options, local server, chroot) {
267
276
 
268
277
  server.app.all("/psm", closure(req, res, local guid) {
269
278
  guid = req.get("x-naanlang-api-guid")
270
- if server.guid && guid != server.guid && req.method !== "OPTIONS"
279
+ if server.guid && guid != server.guid
271
280
  res.status(403).send("unauthorized")
272
281
  else if !server.hostfs
273
282
  res.status(503).send("host filesystem not available")
@@ -275,12 +284,187 @@ closure APIServer(options, local server, chroot) {
275
284
  server.hostfs.api(req, res)
276
285
  })
277
286
 
287
+ // "/wsgw"
288
+
289
+ server.app.all("/wsgw", closure(req, res) {
290
+ if !server.ws.requested
291
+ res.status(503).send("websocket gateway not available")
292
+ else
293
+ server.ws.requested(req, res)
294
+ res.set('Content-Type', 'application/json')
295
+ res.status(200).send("")
296
+ })
297
+
278
298
  // finis
279
299
 
280
300
  server
281
301
  };
282
302
 
283
303
 
304
+ /*
305
+ * WebSocketGatewayServer
306
+ *
307
+ * Process websocket operations forwarded by an API Gateway. This connects to the specified gateway
308
+ * component to process the details.
309
+ *
310
+ * Gateway handler methods:
311
+ * handler = Gateway(server) -- constructor, returns gateway object
312
+ * `(error) = open(wsgate) -- initialize for any API Gateway
313
+ * `(error, data) = requested(req, res) -- process incoming requests
314
+ * `(error) = send(connID, message) -- send a message
315
+ * close() -- close the gateway for all connections
316
+ *
317
+ */
318
+
319
+ closure WebSocketGatewayServer(local wsgate) {
320
+ wsgate = new(object, this)
321
+ wsgate.conns = {} // our connections indexed by connID
322
+ wsgate.subsystems = { } // registered subsystems
323
+
324
+ // open
325
+ //
326
+ // Open the server with the specified gateway.
327
+ //
328
+ wsgate.open = closure open(server, gatewayPath) {
329
+ try {
330
+ wsgate.handler = require(gatewayPath).Gateway(server)
331
+ result = wsgate.handler.open()
332
+ if result.0
333
+ return (list(result.0))
334
+ } catch {
335
+ return (list(exception))
336
+ }
337
+ wsgate.guid = server.guid
338
+ list(false, { ok: true })
339
+ }
340
+
341
+ // requested
342
+ //
343
+ // We have an incoming request translated from WS by the API Gateway. This finds out from our
344
+ // gateway handler the client this pertains to and connects/disconnects/messages as appropriate.
345
+ //
346
+ wsgate.requested = closure requested(req, res, local error, data, conn, message) {
347
+ // reqOpen
348
+ // Get our connection or make one.
349
+ function reqOpen(connID) {
350
+ conn = wsgate.conns[connID]
351
+ if !conn {
352
+ conn = WebSocketConn(wsgate, connID)
353
+ wsgate.conns[connID] = conn
354
+ for serverID in wsgate.subsystems
355
+ wsgate.subsystems[serverID].wsOpen(conn)
356
+ }
357
+ conn
358
+ }
359
+
360
+ `(error, data) = wsgate.handler.requested(req, res)
361
+ if data.op == "connect"
362
+ conn = reqOpen(data.connID)
363
+ else if data.op == "message"
364
+ reqOpen(data.connID).msgin(data.message)
365
+ else if data.op == "disconnect" {
366
+ conn = wsgate.conns[data.connID]
367
+ wsgate.conns[data.connID] = undefined
368
+ if conn {
369
+ conn.close(data.code, data.reason)
370
+ for serverID in wsgate.subsystems
371
+ wsgate.subsystems[serverID].wsClose(conn)
372
+ }
373
+ }
374
+ }
375
+
376
+ // close
377
+ //
378
+ // Respond to server closing.
379
+ //
380
+ wsgate.close = function close(conn, local serverID) {
381
+ wsgate.handler.close()
382
+ wsgate.handler = false
383
+ for serverID in wsgate.subsystems
384
+ wsgate.subsystems[serverID].wsClose()
385
+ }
386
+
387
+ // finis
388
+ wsgate
389
+ };
390
+
391
+
392
+ /*
393
+ * WebSocketConn
394
+ *
395
+ * WebSocketConn objects represent a connection with a particular client. These are created when
396
+ * a new client connects. The protocol is JSON with the top level being a dictionary having the
397
+ * following static keys specified by the client:
398
+ * serverID: defined by the server, this string defines where requests should be routed.
399
+ * clientID: defined by the client, this string defines where replies should be sent.
400
+ * These are class (code) designators. Further keys can be defined to identify a specific instance on
401
+ * the client or server.
402
+ *
403
+ */
404
+
405
+ closure WebSocketConn(wsgate, connID, local wsconn) {
406
+ global()
407
+ wsconn = new(object, this)
408
+ wsconn.info = {
409
+ destID: UUID()
410
+ }
411
+
412
+ // msgin
413
+ //
414
+ // This finds the appropriate target for an incoming message, which can be a relay message that
415
+ // we send on to the next node on the network.
416
+ //
417
+ wsconn.msgin = closure msgin(message, local error) {
418
+ if wsgate.guid && message.guid != wsgate.guid
419
+ error = Error("unauthorized")
420
+ else if message.init {
421
+ if message.init.destID {
422
+ wsconn.relay = true
423
+ wsgate.subsystems.Relay.wsChangeDestID(conn, wsconn.info.destID, message.init.destID)
424
+ }
425
+ wsconn.info = merge(wsconn.info, message.init)
426
+ }
427
+ else if message.respOp && wsgate.subsystems.Relay
428
+ wsgate.subsystems.Relay.wsReceive(message, wsconn) // with respOp, must be a relay
429
+ else if message.clientID && wsgate.subsystems[message.serverID]
430
+ wsgate.subsystems[message.serverID].wsReceive(message, wsconn)
431
+ else
432
+ error = Error("can't route message")
433
+ if error
434
+ ErrorDebuglog("WebSocketConn.msgin error:", error)
435
+ }
436
+
437
+ // on close
438
+ //
439
+ // Connection has closed.
440
+ //
441
+ wsconn.close = closure close(code, reason) {
442
+ if code != 1000 && code != 1001 && code != 1005 && code != 1006
443
+ debuglog("WebSocketConn: connection has closed:", code, reason, JSONstringify(wsconn.info))
444
+ wsconn.closed = {
445
+ code: code
446
+ reason: reason
447
+ }
448
+ }
449
+
450
+ // send
451
+ //
452
+ // Send a dictionary, which must have a clientID defined.
453
+ //
454
+ wsconn.send = function send(message) {
455
+ if !message.clientID || !message.serverID
456
+ throw("WebSocketConn.send: client and server IDs required")
457
+ if wsgate.closed
458
+ list(Error("cannot send - websocket closed"))
459
+ wsgate.handler.send(connID, JSONstringify(message))
460
+ list(false, { ok: true })
461
+ }
462
+
463
+ // finis
464
+ wsconn
465
+ };
466
+
467
+
284
468
  /*
285
469
  * WebSocketServer
286
470
  *
@@ -322,8 +506,9 @@ closure WebSocketServer(server, local websocks) {
322
506
  })
323
507
 
324
508
  // closeConn
509
+ //
325
510
  // respond to connection closing
326
-
511
+ //
327
512
  websocks.closeConn = function closeConn(conn, local serverID) {
328
513
  websocks.conns = websocks.conns.filter(function(item) {
329
514
  item !== conn
@@ -641,7 +826,8 @@ function apisLoadLibs() {
641
826
  };
642
827
 
643
828
  function ApisInit(local manifest) {
644
- manifest = `(APIServer, WebSocketServer, WesCon, WesRelay, apisLoadLibs, ApisInit)
829
+ manifest = `(APIServer, WebSocketGatewayServer, WebSocketConn, WebSocketServer, WesCon, WesRelay,
830
+ apisLoadLibs, ApisInit)
645
831
 
646
832
  Naan.module.build(module.id, "apiserver", function(modobj, compobj) {
647
833
  require("./node.nlg")
@@ -18,7 +18,7 @@
18
18
  *
19
19
  */
20
20
 
21
- closure workCurrent(workerID, startup, local current) {
21
+ closure workCurrent(workco, workerID, startup, local current) {
22
22
  global()
23
23
  current = new(object, this)
24
24
  current.workerID = workerID
@@ -31,9 +31,22 @@ closure workCurrent(workerID, startup, local current) {
31
31
  debugout: ONdebugOut.proc
32
32
  messageout: ONmessageOut.proc
33
33
  })
34
+ current.guid = 1
35
+ current.host = js.t // our magic Naan controller reference
34
36
 
35
- current.host = js.t // our magic Naan controller reference
36
-
37
+ // replyHook
38
+ //
39
+ // Intercept the ReplyMessage function to see if we should handle it.
40
+ //
41
+ function replyHook(data) {
42
+ if data._guid
43
+ workco.uworkers[data._guid].msgport.postMessage({
44
+ id: "targetreceive"
45
+ data: data
46
+ })
47
+ }
48
+ current.host.OnReplyHook(replyHook.proc)
49
+
37
50
  // ONtextOut
38
51
  // Process console text output by Naan.
39
52
 
@@ -79,6 +92,8 @@ closure workCurrent(workerID, startup, local current) {
79
92
  // Process message output.
80
93
 
81
94
  function ONmessageOut(data) {
95
+ if data._guid
96
+ return // already replied
82
97
  sendAll({
83
98
  id: "targetout",
84
99
  data: data
@@ -98,6 +113,7 @@ closure workCurrent(workerID, startup, local current) {
98
113
  // Notify all links that we have terminated.
99
114
 
100
115
  function exitNotify(exitCode, local link) {
116
+ workco.uworkers[current.guid] = undefined
101
117
  for link in current.links
102
118
  link.exitWorker(exitCode)
103
119
  }
@@ -157,7 +173,7 @@ closure workCurrent(workerID, startup, local current) {
157
173
  if !(linkref.link eq thislink)
158
174
  linkref.responder(msg)
159
175
  }
160
-
176
+
161
177
  //
162
178
  // vitalUpdate
163
179
  //
@@ -172,6 +188,81 @@ closure workCurrent(workerID, startup, local current) {
172
188
  };
173
189
 
174
190
 
191
+ /*
192
+ * workMainProxy
193
+ *
194
+ * Make a nodejs proxy representing the main thread. This runs inside the worker and allows it
195
+ * to do remote execution with Main.
196
+ *
197
+ */
198
+
199
+ closure workMainProxy(track, name, options, local target) {
200
+ global(js)
201
+ target = require("../running/executors.nlg").ExecutorBase(track, "mainProxy", name)
202
+ target.name = name
203
+
204
+ //
205
+ // target.PostMessage
206
+ //
207
+ // Post a message to the main thread.
208
+ //
209
+ target.PostMessage = function PostMessage(data) {
210
+ js.t.DispatchMessage(data)
211
+ list(false, { posted: true })
212
+ }
213
+
214
+ //
215
+ // onreceive
216
+ //
217
+ // Process a received message from the main thread, typically in response.
218
+ //
219
+ function onreceive(data) {
220
+ target.execReceived(data)
221
+ }
222
+
223
+ // destroy
224
+ //
225
+ // Destroy the execution instance.
226
+ //
227
+ target.destroy = function destroy() {
228
+ target.execClosed(Error("instance destroyed"))
229
+ track.delete(target)
230
+ }
231
+
232
+ js.t.OnReceive(onreceive.proc)
233
+ track.add(target)
234
+
235
+ // finis
236
+
237
+ list(false, target)
238
+ };
239
+
240
+
241
+ /*
242
+ * WorkerToMain
243
+ *
244
+ * Connect our worker to the main thread, returning a result tuple with an executor representing
245
+ * Main. Use this to create a context object for Main, allowing remote evaluation.
246
+ *
247
+ */
248
+
249
+ closure WorkerToMain(local nideRunning, track) {
250
+ // mainProxy -- link tracker to main thread proxy
251
+ function mainProxy(track, name, options) {
252
+ workMainProxy(track, name, options)
253
+ }
254
+
255
+ if !js.g {
256
+ error = Error("WorkerToMain only available in node workers")
257
+ return (list(error))
258
+ }
259
+ nideRunning = require("naanlib:frameworks/running/executors.nlg")
260
+ track = nideRunning.ExecutorTracker()
261
+ track.register(mainProxy, "proxy")
262
+ track.spawn("proxy") // returns result tuple
263
+ };
264
+
265
+
175
266
  /*
176
267
  * workThread
177
268
  *
@@ -179,12 +270,13 @@ closure workCurrent(workerID, startup, local current) {
179
270
  *
180
271
  */
181
272
 
182
- closure workThread(workerID, startup, local worker, options, subChannel, deathWatcher) {
273
+ closure workThread(workco, workerID, startup, local worker, options, subChannel, deathWatcher) {
183
274
  global(threads, JSpath)
184
275
  worker = new(object, this)
185
276
  worker.name = startup.name
186
277
  worker.workerID = workerID
187
278
  worker.links = []
279
+ worker.guid = UUID()
188
280
 
189
281
  // create our worker thread
190
282
 
@@ -207,6 +299,7 @@ closure workThread(workerID, startup, local worker, options, subChannel, deathWa
207
299
  // Notify all links that we have terminated.
208
300
 
209
301
  function exitNotify(exitCode, local link) {
302
+ workco.uworkers[worker.guid] = undefined
210
303
  for link in worker.links
211
304
  link.exitWorker(exitCode)
212
305
  worker.links = []
@@ -262,6 +355,12 @@ closure workThread(workerID, startup, local worker, options, subChannel, deathWa
262
355
  worker.links[0].responder({ // only send this to first link
263
356
  id: "started" // don't want multiple initializers
264
357
  }) }
358
+ else if msg.id == "targetsend" {
359
+ msg.data._guid = worker.guid
360
+ js.t.DispatchMessage({
361
+ id: "targetin"
362
+ data: msg.data
363
+ }) }
265
364
  else {
266
365
  if msg.id == "status"
267
366
  worker.lastStatus = milliseconds()
@@ -403,6 +502,7 @@ closure workController(api, local workco) {
403
502
  workco = new(object, this)
404
503
  workco.serverID = "Workers" // our serverID, used for communications
405
504
  workco.workers = { } // dictionary of workers by workerID
505
+ workco.uworkers = { } // dictionary of workers by GUID
406
506
  workco.links = new(weakmap)
407
507
  api.Register(workco.serverID, workco)
408
508
 
@@ -487,9 +587,10 @@ closure workController(api, local workco) {
487
587
  else if message.workerOp == "spawn" {
488
588
  if !worker { // create worker if needed
489
589
  if message.workerID == "NideServer"
490
- worker = workco.current = workCurrent(message.workerID, message.payload)
590
+ worker = workco.current = workCurrent(workco, message.workerID, message.payload)
491
591
  else
492
- worker = workThread(message.workerID, message.payload)
592
+ worker = workThread(workco, message.workerID, message.payload)
593
+ workco.uworkers[worker.guid] = worker
493
594
  workco.workers[message.workerID] = worker }
494
595
  if !link
495
596
  makeLink(message, conn) // make sure we have a link for responses
@@ -521,7 +622,7 @@ closure workController(api, local workco) {
521
622
  return } }
522
623
  error = Error("invalid parameters", message.workerOp, message.workerID)
523
624
  }
524
- call(makeSender(conn, message), "error", [error])
625
+ ErrorDebuglog("workController.wsReceive error:", error)
525
626
  }
526
627
 
527
628
  //
@@ -562,12 +663,13 @@ closure workController(api, local workco) {
562
663
  */
563
664
 
564
665
  function workInit(local manifest) {
565
-
566
- manifest = `(workCurrent, workThread, workLink, workController, workInit)
666
+ manifest = `(workCurrent, workMainProxy, WorkerToMain, workThread, workLink, workController,
667
+ workInit)
567
668
 
568
669
  Naan.module.build(module.id, "worker", function(modobj, compobj) {
569
670
  require("./node.nlg")
570
671
  compobj.manifest = manifest
571
672
  modobj.exports.WorkerController = workController
673
+ modobj.exports.WorkerToMain = WorkerToMain
572
674
  })
573
675
  }();
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * column positioning: // // !
7
7
  *
8
- * Copyright (c) 2023 by Richard C. Zulch
8
+ * Copyright (c) 2023-2024 by Richard C. Zulch
9
9
  *
10
10
  */
11
11
 
@@ -20,10 +20,13 @@
20
20
  *
21
21
  * Options:
22
22
  * {
23
+ * wss: <boolean> // true to use secure connection
23
24
  * onopen: <proc>(e) // connection has opened
24
25
  * onmessage: <proc>(e, message) // message received
25
26
  * onerror: <proc>(e) // error occurred
26
27
  * onclose: <proc>(e) // connection has closed
28
+ * query: <string> // query string starting with ? for insecure connections
29
+ * querys: <string> // query string starting with ? for secure connections
27
30
  * }
28
31
  *
29
32
  */
@@ -37,13 +40,17 @@ closure WebSocket(host, options, local wsock) {
37
40
  //
38
41
  // Connect with the host if not already connected, returning a result tuple.
39
42
  //
40
- wsock.connect = closure connect(local pending, error, ws) {
43
+ wsock.connect = closure connect(local error, ws, url, pending) {
41
44
  if wsock.ws
42
45
  return (list(false, { open: true }))
43
46
  `(error, ws) = await(js.i("ws"))
44
47
  if error
45
48
  return (list(error))
46
- wsock.ws = xnew(ws.default, "ws://${host}")
49
+ if options.wss
50
+ url = "wss://${host}${options.querys||''}"
51
+ else
52
+ url = "ws://${host}${options.query||''}"
53
+ wsock.ws = xnew(ws.default, url)
47
54
  pending = new(nonce)
48
55
 
49
56
  // onopen
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2019-2023 by Richard C. Zulch
9
+ * Copyright (c) 2019-2024 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -118,7 +118,7 @@
118
118
  * ]
119
119
  * }
120
120
  *
121
- * build = Builder(rulesDict, operation, fs, srcpath, destpath) // create a builder object
121
+ * build = Builder(rulesDict, operation, fs, srcpath, destpath, track) // create a builder object
122
122
  * build.increment() // increment the build counter
123
123
  * build.make() // run the build
124
124
  * build.clean() // clear all output folders
@@ -140,7 +140,7 @@
140
140
  *
141
141
  */
142
142
 
143
- closure Builder(rules, operation, fs, srcpath, destpath, local build, group, newgroup) {
143
+ closure Builder(rules, operation, fs, srcpath, destpath, track, local build, group, newgroup) {
144
144
  build = new(object, this)
145
145
  build.srcpath = srcpath
146
146
  build.destpath = destpath
@@ -393,7 +393,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
393
393
 
394
394
  closure zip(files, options, local result) {
395
395
  if !build.zipper
396
- build.zipper = ExecutorParallel({ // create worker for zipping
396
+ build.zipper = ExecutorParallel(track, { // create worker for zipping
397
397
  maxcount: 1 // stupid Zip lib can't do more than one
398
398
  basename: "zip"
399
399
  initeval: `(false, (zip1))
@@ -529,7 +529,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
529
529
 
530
530
  closure parseAndPack(text, fpath, options, local result) {
531
531
  if !build.parallel
532
- build.parallel = ExecutorParallel({ // create worker pool
532
+ build.parallel = ExecutorParallel(track, { // create worker pool
533
533
  basename: "build"
534
534
  initeval: `(false, (parseAndPack1))
535
535
  })
@@ -728,6 +728,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
728
728
  tasks: group.tasks
729
729
  readpath: infile,
730
730
  writepath: outfile
731
+ follow: group.follow || undefined
731
732
  }
732
733
  if group.input.startsWith("@")
733
734
  entry.insource = JSpath.join(group.input.substring(1), group.sources[sdex])
@@ -982,6 +983,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
982
983
  overwrite: true // overwrite files
983
984
  force: true // ignore modify date
984
985
  idmode: true // preserve ids and mode
986
+ follow: entry.follow // optional follow links
985
987
  })
986
988
  if error // deepcopy returns a list of errors
987
989
  error = error.0 // but we're only doing one item
@@ -1190,7 +1192,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
1190
1192
  //
1191
1193
  // Run the current build product by spawning a new worker.
1192
1194
  //
1193
- build.runSpawn = function runSpawn(track local error, maintext, executor, vpath, site) {
1195
+ build.runSpawn = function runSpawn(local error, maintext, executor, vpath, site) {
1194
1196
  `(error, maintext) = fs.readFile(JSpath.join(build.destpath, build.runway.main))
1195
1197
  if error
1196
1198
  error = Error("Builder: cannot read main file", error)
@@ -1228,7 +1230,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
1228
1230
  //
1229
1231
  // Run the current build product by opening a new window.
1230
1232
  //
1231
- build.runWindow = function runWindow(track
1233
+ build.runWindow = function runWindow(
1232
1234
  local title, main, url, features, vpath, error, window, site) {
1233
1235
  if !js.w
1234
1236
  return (list(Error('Run method "window" requires browser host')))
@@ -1260,13 +1262,13 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
1260
1262
  // Run the current build product, which would be after the make call. Returns a standard result
1261
1263
  // tuple.
1262
1264
  //
1263
- build.run = function run(track) {
1265
+ build.run = function run() {
1264
1266
  if build.runway.open
1265
1267
  fs.shellOpen(JSpath.join(build.destpath, build.runway.open), build.runway.args)
1266
1268
  else if build.runway.spawn
1267
- build.runSpawn(track)
1269
+ build.runSpawn()
1268
1270
  else if build.runway.window
1269
- build.runWindow(track)
1271
+ build.runWindow()
1270
1272
  else
1271
1273
  list(false, { ok: true })
1272
1274
  }
@@ -1276,7 +1278,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
1276
1278
  //
1277
1279
  // Refresh after updating the v-site, returning true iff an existing vsite was found.
1278
1280
  //
1279
- build.refresh = function refresh(track local url_origin, found) {
1281
+ build.refresh = function refresh(local url_origin, found) {
1280
1282
  if build.runway."v-site" {
1281
1283
  url_origin = js.w.location.origin.concat("/run", build.runway."v-site")
1282
1284
  for target in track.enumlist() {
@@ -296,7 +296,7 @@ closure projConnector(projman, projtype, local connector, watch) {
296
296
  *
297
297
  * Nide.proj/
298
298
  * nide_cliser.cfg - JSON definition of the project (see below)
299
- * NaanIDE_version.txt - [default] substitution file defining 1.2.0+1 etc.
299
+ * NaanIDE_version.txt - [default] substitution file defining 1.3.0+1 etc.
300
300
  * NaanIDE_version_builds.txt - [default] contains current build number as decimal string
301
301
  * build/ - [optional] default build output location
302
302
  * NaanIDE.lic - [optional] defines Zulch Laboratories, Inc. and other license info
@@ -614,7 +614,7 @@ closure projConfig(projman, where, projID, local procon, fs, configpath) {
614
614
  rules.typeID = procon.where_typeID
615
615
  if !rules.operation
616
616
  rules.operation = "debug"
617
- `(error, builder) = Builder(rules, rules.operation, fs, "", buildpath)
617
+ `(error, builder) = Builder(rules, rules.operation, fs, "", buildpath, projman.track)
618
618
  if error
619
619
  return (list(error))
620
620
  procon.builder = builder
@@ -628,10 +628,10 @@ closure projConfig(projman, where, projID, local procon, fs, configpath) {
628
628
  buildnum: buildnum
629
629
  })
630
630
  if !error {
631
- if builder.refresh(projman.track)
631
+ if builder.refresh()
632
632
  buildonly = !buildonly // reverse if refresh
633
633
  if !buildonly
634
- `(error) = builder.run(projman.track)
634
+ `(error) = builder.run()
635
635
  }
636
636
  builder.destroy()
637
637
  procon.builder = false
@@ -662,7 +662,9 @@ closure projConfig(projman, where, projID, local procon, fs, configpath) {
662
662
  publish = rules.publish
663
663
  where = procon.localDict.publish[stage].where
664
664
  if !rules || !where
665
- return (list(Error("no project publish location configured")))
665
+ return (list(Error("no project publish location set")))
666
+ if !publish.input || !publish.sources || !publish.output
667
+ return (list(Error("project configuration has no publish instructions")))
666
668
  if publish.cacheControl
667
669
  outOptions = {
668
670
  cacheControl: publish.cacheControl