@naanlang/naan 1.0.2 → 1.0.5

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 (56) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +10 -8
  3. package/bin/index.js +2 -2
  4. package/dist/env_web.js +172 -22
  5. package/dist/naan.min.js +6 -6
  6. package/frameworks/browser/https_request.nlg +120 -0
  7. package/frameworks/browser/sworker.js +299 -91
  8. package/frameworks/browser/terminals.nlg +28 -13
  9. package/frameworks/browser/workers.nlg +27 -12
  10. package/frameworks/client/apiclient.nlg +17 -4
  11. package/frameworks/client/psm_client.nlg +184 -81
  12. package/frameworks/common/common.nlg +105 -25
  13. package/frameworks/common/preferences.nlg +1 -0
  14. package/frameworks/common/watching.nlg +1 -0
  15. package/frameworks/node/apiserver.nlg +31 -15
  16. package/frameworks/node/filesystem.nlg +230 -46
  17. package/frameworks/node/gitter.nlg +27 -6
  18. package/frameworks/node/https_request.nlg +119 -0
  19. package/frameworks/node/node.nlg +1 -0
  20. package/frameworks/node/psm_server.nlg +113 -37
  21. package/frameworks/project/build.nlg +40 -23
  22. package/frameworks/project/proj_cliser.nlg +11 -3
  23. package/frameworks/project/proj_console.nlg +13 -4
  24. package/frameworks/project/proj_folder.nlg +10 -1
  25. package/frameworks/project/proj_lambda.nlg +13 -4
  26. package/frameworks/project/proj_static.nlg +11 -3
  27. package/frameworks/project/projects.nlg +128 -40
  28. package/frameworks/running/debugnub.nlg +2 -5
  29. package/frameworks/running/executors.nlg +1 -0
  30. package/frameworks/storage/csv.nlg +120 -0
  31. package/frameworks/storage/dbt_pouch.nlg +41 -25
  32. package/frameworks/storage/psm.nlg +108 -28
  33. package/frameworks/storage/psm_dbtables.nlg +7 -3
  34. package/frameworks/storage/resources.nlg +30 -2
  35. package/lib/browser/env_web.js +170 -20
  36. package/lib/browser/env_webworker.js +1 -1
  37. package/lib/browser/require.js +1 -1
  38. package/lib/core/naanlib.js +6 -6
  39. package/package.json +2 -1
  40. package/plugins/serviceAws/aws/aws-sdk-node.min.js +2 -0
  41. package/plugins/serviceAws/aws/aws-sdk.min.js +2 -88
  42. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +8 -7
  43. package/plugins/serviceAws/aws_dynamo.nlg +715 -148
  44. package/plugins/serviceAws/aws_dynextra.nlg +294 -0
  45. package/plugins/serviceAws/aws_lambda.nlg +11 -4
  46. package/plugins/serviceAws/aws_s3.nlg +44 -14
  47. package/plugins/serviceAws/dbt_aws.nlg +58 -45
  48. package/plugins/serviceAws/psm_aws.nlg +61 -40
  49. package/plugins/serviceAws/serviceAws.nlg +7 -4
  50. package/plugins/serviceGitHub/psm_github.nlg +41 -25
  51. package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
  52. package/test/harness.nlg +3 -1
  53. package/test/test_01_core.nlg +5 -5
  54. package/test/test_02_context.nlg +4 -4
  55. package/test/test_03_jsinterop.nlg +9 -3
  56. package/plugins/serviceAws/aws_cognito.nlg +0 -76
@@ -0,0 +1,119 @@
1
+ /*
2
+ * https_node.nlg
3
+ *
4
+ * Https operations for NodeJS.
5
+ *
6
+ * column positioning: // // !
7
+ *
8
+ * Copyright (c) 2021-2022 by Richard C. Zulch
9
+ *
10
+ */
11
+
12
+
13
+ /*
14
+ * HttpsApiRequest
15
+ *
16
+ * Perform an API request from in NodeJS. This operates the same as HttpsApiRequest for browsers.
17
+ *
18
+ * Options:
19
+ * {
20
+ * method: <string> // HTTP method (defaults to GET/PUT)
21
+ * query: <dictionary> // query variables added to the URL
22
+ * putdata: <data> // data to put
23
+ * contentType: <string> // sets Content-Type header, otherwise auto-determined
24
+ * range: <string> // sets range header
25
+ * headers: [`(key, value)] // add header(s) to the request, overriding defaults
26
+ * }
27
+ *
28
+ */
29
+
30
+ closure HttpsApiRequest(url, options, local urlq, reqOptions, putdata, item, pending, chunks, request) {
31
+ if options.query
32
+ urlq = url.concat(EncodeQuery("?", options.query))
33
+ else
34
+ urlq = url
35
+ putdata = options.putdata
36
+ reqOptions = { }
37
+ if options.method
38
+ reqOptions.method = options.method
39
+ else if putdata
40
+ reqOptions.method = "POST"
41
+ else
42
+ reqOptions.method = "GET"
43
+ reqOptions.headers = { }
44
+ if putdata {
45
+ if options.contentType
46
+ reqOptions.headers['Content-Type'] = options.contentType
47
+ else if jsTypedArray(options.putdata)
48
+ reqOptions.headers['Content-Type'] = "application/octet-stream"
49
+ else if member(typeof(options.putdata), `(dictionary, array, xobject)) {
50
+ reqOptions.headers['Content-Type'] = "application/json"
51
+ `(error, putdata) = JsonStringify(options.putdata)
52
+ if error
53
+ return (list(Error("HttpsApiRequest encode:", url)))
54
+ }
55
+ fetchOptions.body = putdata
56
+ }
57
+ if array(options.headers)
58
+ for item in options.headers
59
+ reqOptions.headers[item.0] = item.1
60
+ pending = new(nonce)
61
+ chunks = []
62
+ request = nodeHttps.request(url, reqOptions, function (response) {
63
+ if response.statusCode < 200 || response.statusCode >= 300 {
64
+ pending.signal(list(Error("HttpsApiRequest statusCode:", url, response.statusCode, {
65
+ status: response.statusCode
66
+ })))
67
+ return
68
+ }
69
+ response.on("data", function (chunk) {
70
+ chunks.push(chunk)
71
+ })
72
+ response.on("end", function (local content, error) {
73
+ content = Buffer.concat(chunks)
74
+ if response.complete {
75
+ if response.headers["content-type"].startsWith("application/json") {
76
+ `(error, content) = JsonParse(content.toString())
77
+ if error
78
+ error = Error("HttpsApiRequest decode:", url, error)
79
+ else
80
+ content = new(content)
81
+ if array(content) { // deencapsulate to get API result
82
+ content = totuple(content)
83
+ error = content.0
84
+ content = content.1
85
+ }
86
+ }
87
+ }
88
+ else
89
+ error = Error("HttpsApiRequest terminated prematurely:", url)
90
+ pending.signal(list(error, content))
91
+ })
92
+ })
93
+ request.on("error", function(error) {
94
+ error = Error("HttpsApiRequest failed:", url, error)
95
+ pending.signal(list(error))
96
+ })
97
+ if data
98
+ request.write(data)
99
+ request.end()
100
+ pending.wait()
101
+ };
102
+
103
+
104
+ /*
105
+ * https_nodeInit
106
+ *
107
+ * Initialize HTTPS request operations for NodeJS.
108
+ *
109
+ */
110
+
111
+ function https_nodeInit(local manifest) {
112
+ manifest = `(HttpsGetJson, HttpsPutJson, https_nodeInit)
113
+
114
+ Naan.module.build(module.id, "https_request", function(modobj, compobj) {
115
+ require("./node.nlg")
116
+ compobj.manifest = manifest
117
+ modobj.exports.HttpsApiRequest = HttpsApiRequest
118
+ })
119
+ }();
@@ -75,6 +75,7 @@ function nodrInit(local manifest) {
75
75
  fs = js.r("fs")
76
76
  os = js.r("os")
77
77
  stream = js.r("stream")
78
+ nodeHttps = js.r("https")
78
79
  }()
79
80
  module.reload = nodrReload
80
81
  })
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2019-2021 by Richard C. Zulch
9
+ * Copyright (c) 2019-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -27,12 +27,18 @@ closure psmsFsView(rootpath, local view, gitter) {
27
27
  view = view.1
28
28
 
29
29
  // inherited
30
+ // -- view.folderPath(folderid, callback)
30
31
  // -- view.winDrives(callback)
31
32
  // -- view.testHidden(path, callback)
32
33
  // -- view.readLines(path, options, callback)
33
34
  // -- view.readFile(path, options, callback)
34
35
  // -- view.writeFile(path, data, options, callback)
35
- // -- view.info(path, callback)
36
+ // -- view.readlink(path, options, callback)
37
+ // -- view.symlink(target, path, options, callback)
38
+ // -- view.info(path, options, callback)
39
+ // -- view.setInfo(path, info, callback)
40
+ // -- view.setOwnerGroup(path, uid, gid, callback)
41
+ // -- view.setMode(path, mode, callback)
36
42
  // -- view.setTimes(path, atime, mtime, callback)
37
43
  // -- view.dirList(path, options, callback)
38
44
  // -- view.mkdir(path, callback)
@@ -81,16 +87,27 @@ closure psmsFsView(rootpath, local view, gitter) {
81
87
  }
82
88
  res.send(new(array, result))
83
89
  }
84
-
90
+
91
+ // op: folderPath
92
+ function folderPathApi() {
93
+ res.send(new(array, view.folderPath(req.query.folderid)))
94
+ }
95
+
85
96
  // op: readLines
86
97
  function readLinesApi() {
87
98
  res.send(new(array, view.readLines(path, req.query)))
88
99
  }
89
100
 
90
101
  // op: readFile
91
- function readFileApi(local options) {
102
+ function readFileApi(local error, localpath, options) {
103
+ `(error, localpath) = view.localPath(path)
104
+ if error {
105
+ res.send(new(array, list(error)))
106
+ return
107
+ }
92
108
  options = {
93
- dotfiles: "allow" }
109
+ dotfiles: "allow"
110
+ }
94
111
  if req.query.encoding == "binary"
95
112
  options.headers = {
96
113
  "Content-Type": "application/octet-stream"
@@ -99,7 +116,7 @@ closure psmsFsView(rootpath, local view, gitter) {
99
116
  options.headers = { // ### is this always the right default?
100
117
  "Content-Type": "text/html; charset=UTF-8"
101
118
  }
102
- res.sendFile(path, options, function (err, local status) {
119
+ res.sendFile(localpath, options, function (err, local status) {
103
120
  if err {
104
121
  if err.code == "ENOENT"
105
122
  status = 404
@@ -118,7 +135,18 @@ closure psmsFsView(rootpath, local view, gitter) {
118
135
  options.encoding = req.query.encoding
119
136
  res.send(new(array, view.writeFile(path, req.body, options)))
120
137
  }
121
-
138
+
139
+ // op: readlink
140
+ function readlinkApi() {
141
+ res.send(new(array, view.readlink(path)))
142
+ }
143
+
144
+ // op: symlink
145
+ function symlinkApi(local options) {
146
+ options = { }
147
+ res.send(new(array, view.symlink(req.query.target, path, options)))
148
+ }
149
+
122
150
  // op: delete
123
151
  function deleteApi(local options) {
124
152
  options = { }
@@ -130,8 +158,26 @@ closure psmsFsView(rootpath, local view, gitter) {
130
158
  }
131
159
 
132
160
  // op: info
133
- function infoApi() {
134
- res.send(new(array, view.info(path)))
161
+ function infoApi(local options) {
162
+ options - { }
163
+ if req.query.follow
164
+ options.follow = req.query.follow
165
+ res.send(new(array, view.info(path, options)))
166
+ }
167
+
168
+ // op: setInfo
169
+ function setInfoApi() {
170
+ res.send(new(array, view.setInfo(path, req.body)))
171
+ }
172
+
173
+ // op: setOwnerGroup
174
+ function setOwnerGroupApi() {
175
+ res.send(new(array, view.setOwnerGroup(path, req.query.uid, req.query.gid)))
176
+ }
177
+
178
+ // op: setMode
179
+ function setModeApi() {
180
+ res.send(new(array, view.setMode(path, req.query.mode)))
135
181
  }
136
182
 
137
183
  // op: setTimes
@@ -141,8 +187,8 @@ closure psmsFsView(rootpath, local view, gitter) {
141
187
 
142
188
  // op: dirList
143
189
  function dirListApi(local options) {
144
- if req.query.head
145
- options = { head: true }
190
+ if req.query.stat
191
+ options = { stat: true }
146
192
  res.send(new(array, view.dirList(path, options)))
147
193
  }
148
194
 
@@ -153,7 +199,7 @@ closure psmsFsView(rootpath, local view, gitter) {
153
199
 
154
200
  // op: tree
155
201
  function treeApi() {
156
- res.send(new(array, view.tree(path, req.query.depthlimit)))
202
+ res.send(new(array, view.tree(path, toint(req.query.depthlimit))))
157
203
  }
158
204
 
159
205
  // op: dirSearch
@@ -216,11 +262,17 @@ closure psmsFsView(rootpath, local view, gitter) {
216
262
 
217
263
  proc = {
218
264
  pathOp: pathOpApi
265
+ folderPath: folderPathApi
219
266
  readFile: readFileApi
220
267
  readLines: readLinesApi
221
268
  writeFile: writeFileApi
269
+ readlink: readlinkApi
270
+ symlink: symlinkApi
222
271
  delete: deleteApi
223
272
  info: infoApi
273
+ setInfo: setInfoApi
274
+ setOwnerGroup: setOwnerGroupApi
275
+ setMode: setModeApi
224
276
  setTimes: setTimesApi
225
277
  dirList: dirListApi
226
278
  mkdir: mkdirApi
@@ -263,38 +315,38 @@ closure psmsFsConnector(psm, api, local connClassID, connector, watch) {
263
315
  connector.vault = psm.vault(connClassID)
264
316
  connector.label = "Naan IDE Server"
265
317
  watch = psm.watchable(connector)
266
-
267
- // hashResourceID
268
- //
269
- // Make a stable hash based on the resource credentials that we can persist or share between
270
- // different domains accessing the same resource.
271
- connector.hashResourceID = function hashResourceID(resource, local ident) {
272
- ident = resource.urlName
273
- if resource.label
274
- ident = ident.concat(resource.label)
275
- HashSHA256(ident)
276
- }
277
-
318
+
278
319
  // findResource
279
320
  //
280
- // Find a resource with the specified contents, returning the resID or false. The loose criteria
281
- // is: if added, would the specified contents be redundant to an existing resource?
321
+ // Find a resource with the specified contents, returning the resID or false.
282
322
  connector.findResource = function findResource(resource, local resID, creds) {
283
323
  for resID in listResources().1 {
284
324
  creds = access(resID).1
285
- if resource.urlName == creds.urlName
286
- && (!resource.label || resource.label == creds.label)
325
+ if ((!resource.urlName || resource.urlName == creds.urlName)
326
+ && (!resource.label || resource.label == creds.label))
287
327
  return (resID)
288
328
  }
289
329
  false
290
330
  }
291
331
 
292
- // addResource
332
+ // writeResource
293
333
  //
294
- // Add credentials for a named resource. This is just a stub at the moment
295
-
296
- connector.addResource = function addResource(resource, local creds, errors, error, resID, data) {
334
+ // Add or update credentials for a named resource. If no resID is specified this adds a new
335
+ // resource; otherwise it updates an existing one. The resource is a dictionary comprising these
336
+ // keys:
337
+ // label - [optional] label we use for this resource
338
+ // urlName - URL (e.g. "http://host.com/node/fsapi")
339
+ // authSecret - access-control token for this URL
340
+ // hidden - [optional] don't enumerate to user
341
+ // locked - [optional] don't allow user delete
342
+ //
343
+ // The return value is an (error, resID) tuple. Error, if non-false, is a dictionary of field
344
+ // keys and error diagnostic strings.
345
+ connector.writeResource = function writeResource(resource, resID,
346
+ local creds, errors, error, data, change) {
347
+ // badField - test for a bad string
297
348
  function badField(str) { !string(str) || str.trim().length == 0 }
349
+
298
350
  creds = { urlName: resource.urlName }
299
351
  errors = { }
300
352
  if resource.label
@@ -303,6 +355,8 @@ closure psmsFsConnector(psm, api, local connClassID, connector, watch) {
303
355
  creds.label = resource.urlName
304
356
  if badField(creds.label)
305
357
  errors.label = "invalid label"
358
+ else if !resID && findResource({ label: creds.label })
359
+ errors.label = "duplicate name"
306
360
  if badField(resource.urlName)
307
361
  errors.urlName = "required field"
308
362
  if length(errors) > 0
@@ -312,14 +366,36 @@ closure psmsFsConnector(psm, api, local connClassID, connector, watch) {
312
366
  creds.hidden = resource.hidden
313
367
  if resource.locked
314
368
  creds.locked = resource.locked
315
- resID = hashResourceID(resource)
316
- `(error, data) = connector.vault.addResource(resID, creds)
369
+ if resID {
370
+ `(error, data) = connector.vault.updateResource(resID, creds)
371
+ change = { changed: [resID] }
372
+ } else {
373
+ resID = UUID()
374
+ `(error, data) = connector.vault.addResource(resID, creds)
375
+ change = { added: [resID] }
376
+ }
317
377
  if data {
318
- watch.notify(connClassID, { added: [resID] })
319
- data = resID } }
378
+ watch.notify(connClassID, change)
379
+ data = resID
380
+ }
381
+ }
320
382
  list(error, data)
321
383
  }
322
-
384
+
385
+ // addResource
386
+ //
387
+ // Add a new resource, returning a standard error tuple per writeResource.
388
+ connector.addResource = function addResource(resource) {
389
+ writeResource(resource)
390
+ }
391
+
392
+ // updateResource
393
+ //
394
+ // Update an existing resource, returning a standard error tuple per writeResource.
395
+ connector.updateResource = function updateResource(resID, resource) {
396
+ writeResource(resource, resID)
397
+ }
398
+
323
399
  // getResource
324
400
  //
325
401
  // Get credentials for a named resource, matching the dictionary semantics of addResource. The
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2019-2021 by Richard C. Zulch
9
+ * Copyright (c) 2019-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -267,15 +267,20 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
267
267
  //
268
268
  // applySubstitutions
269
269
  //
270
- // Apply substitutions from readSubstitutionFile to string, returning a new string.
270
+ // Apply substitutions from readSubstitutionFile to string, returning a tuple of the resulting
271
+ // text and the number of substitution patterns actually used from the supplied dictionary.
271
272
  //
272
- function applySubstitutions(text, subs) {
273
- text.replace(RegExp("[$][-A-Za-z0-9_./()\\\\]+[$]", "g"), function(match) {
274
- if subs[match.slice(1,-1)]
275
- { }
273
+ function applySubstitutions(text, subs, local subbed) {
274
+ subbed = { }
275
+ list(text.replace(RegExp("[$][-A-Za-z0-9_./()\\\\]+[$]", "g"), function(match, local subst) {
276
+ subst = subs[match.slice(1,-1)]
277
+ if subst {
278
+ subbed[match] = true
279
+ subst
280
+ }
276
281
  else
277
282
  match
278
- })
283
+ }), length(subbed))
279
284
  }
280
285
 
281
286
  //
@@ -573,7 +578,7 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
573
578
  //
574
579
  // Handling the catenates is a bit tricky. One set of buildrules can have multiple catentates,
575
580
  // distinguished by having a unique writepath derived from the destfile parameter. All groups
576
- // having the same writepath contribute to a single catenate, with the sources ordere first by
581
+ // having the same writepath contribute to a single catenate, with the sources ordered first by
577
582
  // group number (low to high) and then order in the source list, first to last. If any component
578
583
  // contributing to a catenate is out-of-date with respect to it then they are all built and
579
584
  // combined into the single output file. This computes a set of ordering integers for the parts
@@ -649,13 +654,13 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
649
654
  outfile = JSpath.join(outdirpath, group.destfile)
650
655
  else
651
656
  outfile = JSpath.join(outdirpath, source)
652
- fs.info(infile, function(error, instat, local listing, addgroup, file) {
657
+ fs.info(infile, false, function(error, instat, local listing, addgroup, file) {
653
658
  if error
654
659
  build.errorlist.push(error)
655
660
  else if instat.type != "directory"
656
661
  addEntry(infile, outfile, instat, sdex) // add execution entry if needed
657
662
  else { // add a new group for directory source
658
- `(error, listing) = fs.dirList(infile, { head: true })
663
+ `(error, listing) = fs.dirList(infile, { stat: true })
659
664
  if error
660
665
  build.errorlist.push(error)
661
666
  else {
@@ -754,19 +759,20 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
754
759
  entry.zip = true
755
760
  if !entry.writepath.toLowerCase().endsWith(".zip")
756
761
  entry.writepath = entry.writepath.concat(".zip") // always ends with .zip
762
+ outfile = entry.writepath
757
763
  }
758
764
  if entry.catenate || entry.zip {
759
765
  entry.catorder = group.groupno * sourcemax + sdex // order of groups then sources
760
766
  if build.testats[outfile] && instat.mtimeMs < build.testats[outfile].mtimeMs
761
767
  instat = build.testats[outfile] // make source at least as recent as template
762
- } else
768
+ } else if !entry.unzip
763
769
  entry.writepath = outfile
764
770
  ++pending.active
765
771
  if destcheck { // destcheck is special target for determining if task needed
766
772
  entry.destcheck = JSpath.join(outdirpath, destcheck.pop())
767
- fs.info(entry.destcheck, addIfOutOfDate)
773
+ fs.info(entry.destcheck, false, addIfOutOfDate)
768
774
  } else
769
- fs.info(outfile, addIfOutOfDate)
775
+ fs.info(outfile, false, addIfOutOfDate)
770
776
 
771
777
  function addIfOutOfDate(error, outstat, local entryop) {
772
778
  if error || toint(instat.mtimeMs) > toint(outstat.mtimeMs) {
@@ -861,9 +867,8 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
861
867
  if error
862
868
  build.errorlist.push(error)
863
869
  else if content.startsWith("$$") // prefix to cause version substitution
864
- build.templates[writepath] = applySubstitutions(content.slice(2), verFileDefs)
865
- else
866
- build.templates[writepath] = content
870
+ `(content) = applySubstitutions(content.slice(2), verFileDefs)
871
+ build.templates[writepath] = content
867
872
  }
868
873
  } ()
869
874
  }
@@ -890,13 +895,21 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
890
895
  `(error, result) = fs.unzip(entry.readpath, entry.writepath) // unzip source into destination
891
896
  if !error && entry.destcheck
892
897
  fs.touch(entry.destcheck) // show we completed
893
- } else if entry.copy
894
- `(error, result) = fs.copy(entry.readpath, entry.writepath, { overwrite: true }) // copy file from source into destination
898
+ } else if entry.copy {
899
+ `(error, result) = fs.util.deepcopy(entry.readpath, false, fs, dirpath, {
900
+ erase: true // not really needed
901
+ overwrite: true // overwrite files
902
+ force: true // ignore modify date
903
+ preserve: true // preserve attributes
904
+ })
905
+ if error // deepcopy returns a list of errors
906
+ error = error.0 // but we're only doing one item
907
+ }
895
908
  else if ((`(error, content) = fs.readFile(entry.readpath)) && !error) {
896
909
  if entry.license
897
- content = applySubstitutions(content, licFileDefs)
910
+ `(content) = applySubstitutions(content, licFileDefs)
898
911
  if entry.version
899
- content = applySubstitutions(content, verFileDefs)
912
+ `(content) = applySubstitutions(content, verFileDefs)
900
913
  if entry.naanpack
901
914
  `(error, content) = parseAndPack(content, entry.readpath, { naanpack: true })
902
915
  else if entry.jspack
@@ -962,11 +975,15 @@ closure Builder(rules, operation, fs, srcpath, destpath, local build, group, new
962
975
  docat = true
963
976
  }
964
977
  if docat { // catenate the files
965
- if template
966
- content = applySubstitutions(template, catsub)
978
+ if template {
979
+ `(content, result) = applySubstitutions(template, catsub)
980
+ if length(catsub) != result
981
+ build.errorlist.push(Error("Matched ", result, " of ", length(catsub),
982
+ "substitutions in template:", build.tepaths[writepath]))
983
+ }
967
984
  else
968
985
  content = catenates[writepath].map(function(entry){entry.content}).join("\n")
969
- if zip {
986
+ if dozip {
970
987
  zipmap = { }
971
988
  zipmap[JSpath.basename(writepath)] = content
972
989
  `(error, content) = zip(zipmap) // compress the catenated file
@@ -27,7 +27,7 @@ closure proj_cliserInstance(projman, where, projID, local project) {
27
27
  project.name = where.name
28
28
  project.where = where
29
29
  project.projID = projID
30
- project.config = projConfig(projman, where)
30
+ project.config = projConfig(projman, where, projID)
31
31
 
32
32
  // create
33
33
  //
@@ -63,8 +63,16 @@ closure proj_cliserInstance(projman, where, projID, local project) {
63
63
  //
64
64
  // Run the project by building and then launching.
65
65
  //
66
- project.run = closure run(buildonly) {
67
- project.config.run(buildonly)
66
+ project.run = closure run(stage, buildonly) {
67
+ project.config.run(stage, buildonly)
68
+ }
69
+
70
+ // publish
71
+ //
72
+ // Publish the project according to the stage.
73
+ //
74
+ project.publish = closure publish(stage) {
75
+ project.config.publish(stage)
68
76
  }
69
77
 
70
78
  // close()
@@ -27,7 +27,7 @@ closure proj_consoleInstance(projman, where, projID, local project) {
27
27
  project.name = where.name
28
28
  project.where = where
29
29
  project.projID = projID
30
- project.config = projConfig(projman, where)
30
+ project.config = projConfig(projman, where, projID)
31
31
  project.runPageDebug = true // switch to Debug page on run
32
32
 
33
33
  // create
@@ -60,9 +60,18 @@ closure proj_consoleInstance(projman, where, projID, local project) {
60
60
 
61
61
  // run
62
62
  //
63
- // Run the project by building and then launching.
64
- project.run = closure run(buildonly) {
65
- project.config.run(buildonly)
63
+ // Run the project by building and then launching.
64
+ //
65
+ project.run = closure run(stage, buildonly) {
66
+ project.config.run(stage, buildonly)
67
+ }
68
+
69
+ // publish
70
+ //
71
+ // Publish the project according to the stage.
72
+ //
73
+ project.publish = closure publish(stage) {
74
+ project.config.publish(stage)
66
75
  }
67
76
 
68
77
  // close
@@ -61,11 +61,20 @@ closure proj_folderInstance(projman, where, projID, local project) {
61
61
  // run
62
62
  //
63
63
  // Run the project by building and then launching.
64
- project.run = closure run(buildonly) {
64
+ project.run = closure run(stage, buildonly) {
65
65
  debuglog("folder run project stub")
66
66
  list(false, { ok: true })
67
67
  }
68
68
 
69
+ // publish
70
+ //
71
+ // Publish the project according to the stage.
72
+ //
73
+ project.publish = closure publish(stage) {
74
+ debuglog("folder publish project stub")
75
+ list(false, { ok: true })
76
+ }
77
+
69
78
  // close
70
79
  //
71
80
  // Close the project, releasing resources.
@@ -27,7 +27,7 @@ closure proj_lambdaInstance(projman, where, projID, local project) {
27
27
  project.name = where.name
28
28
  project.where = where
29
29
  project.projID = projID
30
- project.config = projConfig(projman, where)
30
+ project.config = projConfig(projman, where, projID)
31
31
  project.runPageDebug = true // switch to Debug page on run
32
32
 
33
33
  // create
@@ -60,9 +60,18 @@ closure proj_lambdaInstance(projman, where, projID, local project) {
60
60
 
61
61
  // run
62
62
  //
63
- // Run the project by building and then launching.
64
- project.run = closure run(buildonly) {
65
- project.config.run(buildonly)
63
+ // Run the project by building and then launching.
64
+ //
65
+ project.run = closure run(stage, buildonly) {
66
+ project.config.run(stage, buildonly)
67
+ }
68
+
69
+ // publish
70
+ //
71
+ // Publish the project according to the stage.
72
+ //
73
+ project.publish = closure publish(stage) {
74
+ project.config.publish(stage)
66
75
  }
67
76
 
68
77
  // close
@@ -26,7 +26,7 @@ closure proj_staticInstance(projman, where, projID, local project) {
26
26
  project.name = where.name
27
27
  project.where = where
28
28
  project.projID = projID
29
- project.config = projConfig(projman, where)
29
+ project.config = projConfig(projman, where, projID)
30
30
 
31
31
  // create
32
32
  //
@@ -62,8 +62,16 @@ closure proj_staticInstance(projman, where, projID, local project) {
62
62
  //
63
63
  // Run the project by building and then launching.
64
64
  //
65
- project.run = closure run(buildonly) {
66
- project.config.run(buildonly)
65
+ project.run = closure run(stage, buildonly) {
66
+ project.config.run(stage, buildonly)
67
+ }
68
+
69
+ // publish
70
+ //
71
+ // Publish the project according to the stage.
72
+ //
73
+ project.publish = closure publish(stage) {
74
+ project.config.publish(stage)
67
75
  }
68
76
 
69
77
  // close()