@naanlang/naan 1.0.5 → 1.0.7

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.
@@ -11,6 +11,64 @@
11
11
  */
12
12
 
13
13
 
14
+ /*
15
+ * FileReader
16
+ *
17
+ * Read file(s) from the browser's host machine. This is passed a File object and returns a
18
+ * nearly-standard (error, blob, size) tuple. If progress is specified then this function is called
19
+ * with a percentage of completion from zero to 100 if available, or true otherwise. The progress
20
+ * callback is called with "false" when completed, with or without success
21
+ *
22
+ */
23
+
24
+ closure FileReader(file, progress, local pending, freader, result) {
25
+ pending = new(nonce)
26
+ freader = xnew(window.FileReader)
27
+ freader.addEventListener("load", function(event, local blob) {
28
+ blob = xnew(window.Blob, [freader.result], {
29
+ type: file.type
30
+ })
31
+ pending.signal(list(false, blob, event.total))
32
+ })
33
+ freader.addEventListener("error", function(error) {
34
+ pending.signal(list(error))
35
+ })
36
+ freader.AddEventListener("progress", function(event) {
37
+ if event.lengthComputable && event.total
38
+ progress(toint((event.loaded * 100) / event.total))
39
+ else
40
+ progress(true)
41
+ })
42
+ freader.readAsArrayBuffer(file)
43
+ result = pending.wait()
44
+ progress(false)
45
+ result
46
+ };
47
+
48
+
49
+ /*
50
+ * FileDownload
51
+ *
52
+ * Download a file from the browser, returning a standard error tuple.
53
+ *
54
+ */
55
+
56
+ closure FileDownload(file, progress, local error, blob, sizez, url, link) {
57
+ `(error, blob, size) = FileReader(file, progress)
58
+ if error
59
+ list(error)
60
+ else {
61
+ url = window.URL.createObjectURL(blob)
62
+ link = document.createElement("a")
63
+ link.href = url
64
+ link.download = file.name
65
+ link.click()
66
+ window.URL.revokeObjectURL(url)
67
+ list(false, file.name)
68
+ }
69
+ };
70
+
71
+
14
72
  /*
15
73
  * brorInit
16
74
  *
@@ -20,11 +78,18 @@
20
78
  */
21
79
 
22
80
  function brorInit(local manifest) {
23
- manifest = `(brorInit)
81
+ manifest = `(FileReader, FileDownload, brorInit)
24
82
 
25
83
  Naan.module.build(module.id, "browser", function(modobj, compobj) {
26
84
  require("../common").LiveImport()
27
85
  compobj.manifest = manifest
28
- runningExecutors = require("../running/executors.nlg")
86
+ modobj.exports.FileReader = FileReader
87
+ modobj.exports.FileDownload = FileDownload
88
+
89
+ function brorReload() {
90
+ window = js.w
91
+ document = window.document
92
+ }()
93
+ module.reload = brorReload
29
94
  })
30
95
  } ();
@@ -72,7 +72,7 @@ closure HttpsApiRequest(url, options,
72
72
  //
73
73
  // perform the fetch and return when complete
74
74
  //
75
- `(error, response) = await(js.w.fetch(urlq, fetchOptions))
75
+ `(error, response) = await(window.fetch(urlq, fetchOptions))
76
76
  if error
77
77
  return (list(Error("HttpsApiRequest fetch failed:", error, url)))
78
78
  contentType = response.headers.get("content-type")
@@ -113,7 +113,7 @@ function https_browserInit(local manifest) {
113
113
  manifest = `(HttpsApiRequest, https_browserInit)
114
114
 
115
115
  Naan.module.build(module.id, "https_request", function(modobj, compobj) {
116
- require("./browser.nlg")
116
+ require("browser.nlg")
117
117
  compobj.manifest = manifest
118
118
  modobj.exports.HttpsApiRequest = HttpsApiRequest
119
119
  })
@@ -36,7 +36,7 @@
36
36
  *
37
37
  */
38
38
 
39
- var CurrentCacheName = "Naanlang-1.0.5-2";
39
+ var CurrentCacheName = "Naanlang-1.0.7+2";
40
40
 
41
41
 
42
42
  //
@@ -89,11 +89,11 @@ var waitingForInit = []; // initialization functions, or
89
89
  var pubVersion = event.data.hereIsMyVersion;
90
90
  var sourceId = event.source.id; // client id of sender of message
91
91
  msgPorts[sourceId] = msgport;
92
- console.log("[1.0.5-2] received new msgport for", sourceId, pubID, "-", pubVersion);
93
- if (pubVersion != "1.0.5-2")
92
+ console.log("[1.0.7+2] received new msgport for", sourceId, pubID, "-", pubVersion);
93
+ if (pubVersion != "1.0.7+2")
94
94
  msgport.postMessage({ // notify new version available
95
95
  id: "upgrade",
96
- version: "1.0.5-2"
96
+ version: "1.0.7+2"
97
97
  });
98
98
  Reaper(); // clean up obsolete info
99
99
 
@@ -107,7 +107,7 @@ var waitingForInit = []; // initialization functions, or
107
107
  if (msg.id == "response")
108
108
  processResponse(msg);
109
109
  else if (msg.id == "text")
110
- console.log("[1.0.5-2] msg received:", msg.text);
110
+ console.log("[1.0.7+2] msg received:", msg.text);
111
111
  };
112
112
 
113
113
  // send text to IDE log
@@ -118,7 +118,7 @@ var waitingForInit = []; // initialization functions, or
118
118
  // don't clutter the log
119
119
  msgport.postMessage({
120
120
  id: "text",
121
- text: "port received by 1.0.5-2",
121
+ text: "port received by 1.0.7+2",
122
122
  });
123
123
  */
124
124
  if (--pending === 0)
@@ -135,7 +135,7 @@ var waitingForInit = []; // initialization functions, or
135
135
  includeUncontrolled: true
136
136
  }).then(function(clientList) {
137
137
  clientList.every(function(client) {
138
- console.log("[1.0.5-2] requesting new msgport for", client.id);
138
+ console.log("[1.0.7+2] requesting new msgport for", client.id);
139
139
  ++pending;
140
140
  client.postMessage({ // tell client(s) we need this fetch source
141
141
  msg: "Naan_need_fetch_port",
@@ -181,12 +181,12 @@ function Reaper() {
181
181
  clients[clientList[clidex].id] = clientList[clidex];
182
182
  for (var sourceId in msgPorts)
183
183
  if (!clients[sourceId]) {
184
- console.log("[1.0.5-2] source gone:", sourceId);
184
+ console.log("[1.0.7+2] source gone:", sourceId);
185
185
  delete msgPorts[sourceId]; // no longer a source
186
186
  }
187
187
  for (var clientId in fetchPorts)
188
188
  if (!clients[clientId]) {
189
- console.log("[1.0.5-2] client gone:", clientId);
189
+ console.log("[1.0.7+2] client gone:", clientId);
190
190
  delete fetchPorts[clientId]; // no longer a client
191
191
  }
192
192
  for (var fqdex = 0; fqdex < fetchQueue.length; ++fqdex) {
@@ -220,13 +220,13 @@ function ClearCaches() {
220
220
  return (Promise.all(
221
221
  cacheNames.map(function(cacheName) {
222
222
  if (cacheName != CurrentCacheName) {
223
- console.log('[1.0.5-2] deleting old cache:', cacheName);
223
+ console.log('[1.0.7+2] deleting old cache:', cacheName);
224
224
  return (caches.delete(cacheName));
225
225
  }
226
226
  })
227
227
  ));
228
228
  }).then(function() { // claim all clients
229
- console.log('[1.0.5-2] claiming clients');
229
+ console.log('[1.0.7+2] claiming clients');
230
230
  return (self.clients.claim());
231
231
  });
232
232
  return (promise);
@@ -269,7 +269,7 @@ function GetClientResponse(event, urlpath) {
269
269
  msgport.postMessage({
270
270
  id: "fetch",
271
271
  seq: seqno,
272
- version: "1.0.5-2",
272
+ version: "1.0.7+2",
273
273
  request: {
274
274
  method: event.request.method,
275
275
  url: event.request.url
@@ -317,7 +317,7 @@ function GetClientResponse(event, urlpath) {
317
317
  */
318
318
 
319
319
  self.addEventListener('install', function(event) {
320
- console.log("[1.0.5-2] install");
320
+ console.log("[1.0.7+2] install");
321
321
  self.skipWaiting();
322
322
  });
323
323
 
@@ -339,7 +339,7 @@ self.addEventListener('fetch', function(event) {
339
339
  var promise;
340
340
  var url = new URL(event.request.url);
341
341
  var nocache = event.request.method != "GET"
342
- || url.search.length !== 0
342
+ || url.search.length !== 0 && url.searchParams.get("naanver") !== "cb-1.0.7+2"
343
343
  || event.request.headers.get('range');
344
344
  if (url.pathname.startsWith("/run/")) {
345
345
  nocache = true;
@@ -356,7 +356,7 @@ self.addEventListener('fetch', function(event) {
356
356
  }
357
357
  else
358
358
  promise = fetch(event.request).catch(function (e) {
359
- console.log("[1.0.5-2] fetch failed", e);
359
+ console.log("[1.0.7+2] fetch failed", e);
360
360
  return (new Response(undefined, {
361
361
  status: 404,
362
362
  statusText: "Fetch Failed"
@@ -385,14 +385,14 @@ self.addEventListener('fetch', function(event) {
385
385
  */
386
386
 
387
387
  self.addEventListener('activate', function(event) {
388
- console.log("[1.0.5-2] activate");
388
+ console.log("[1.0.7+2] activate");
389
389
  self.clients.matchAll({ // for debugging, list controlled clients
390
390
  includeUncontrolled: true
391
391
  }).then(function(clientList) {
392
392
  var urls = clientList.map(function(client) {
393
393
  return (client.url);
394
394
  });
395
- console.log('[1.0.5-2] matching clients:', urls.join(', '));
395
+ console.log('[1.0.7+2] matching clients:', urls.join(', '));
396
396
  });
397
397
  var promise = ClearCaches();
398
398
  if (event.waitUntil)
@@ -21,7 +21,7 @@
21
21
  closure TermHost(track, api, name, workerID, options,
22
22
  local target, nlregex, clientID, serverID)
23
23
  {
24
- target = runningExecutors.ExecutorBase(track, "Host", name)
24
+ target = require("../running/executors.nlg").ExecutorBase(track, "Host", name)
25
25
  clientID = "DebugWorkers"
26
26
  serverID = "Workers"
27
27
  nlregex = RegExp("\\n", "g")
@@ -35,7 +35,7 @@ closure TermHost(track, api, name, workerID, options,
35
35
  closure connect(local pending) {
36
36
  if target.ws
37
37
  return
38
- target.ws = xnew(js.w.WebSocket,"ws://".concat(js.w.location.host)) // ### use api parameter
38
+ target.ws = xnew(window.WebSocket,"ws://".concat(window.location.host)) // ### use api parameter
39
39
  pending = new(nonce)
40
40
 
41
41
  // onopen
@@ -52,7 +52,7 @@ closure TermHost(track, api, name, workerID, options,
52
52
 
53
53
  target.ws.onmessage = function onmessage(e, message, local msg) {
54
54
  try {
55
- message = new(js.w.JSON.parse(e.data))
55
+ message = new(window.JSON.parse(e.data))
56
56
  } catch {
57
57
  if true {
58
58
  debuglog("can't decode incoming ws message:", exception)
@@ -169,7 +169,7 @@ closure TermHost(track, api, name, workerID, options,
169
169
  // Send a message to the worker controller.
170
170
 
171
171
  target.sendControl = function sendControl(workerOp, payload) {
172
- target.ws.send(js.w.JSON.stringify({
172
+ target.ws.send(window.JSON.stringify({
173
173
  guid: api.guid
174
174
  clientID: clientID
175
175
  serverID: serverID
@@ -368,23 +368,23 @@ closure TermLocal(track, name, workerID, options,
368
368
  options = new(options)
369
369
  else
370
370
  options = { }
371
- target = runningExecutors.ExecutorBase(track, "Local", name)
371
+ target = require("../running/executors.nlg").ExecutorBase(track, "Local", name)
372
372
  nlregex = RegExp("\\n", "g")
373
373
  target.workerID = workerID
374
374
  target.msgQueue = []
375
375
  if workerID == "NaanIDE-Debug" {
376
- target.worker = js.w.open(js.w.location.href, "_blank")
376
+ target.worker = window.open(window.location.href, "_blank")
377
377
  target.worker.addEventListener("beforeunload", function(e) { // our target is closing
378
378
  destroy()
379
379
  })
380
- js.w.addEventListener("beforeunload", function(e) { // we are closing
380
+ window.addEventListener("beforeunload", function(e) { // we are closing
381
381
  // ### tell target we are closing (if needed)
382
382
  })
383
- listener = js.w
383
+ listener = window
384
384
  } else {
385
- if not js.w.Worker
385
+ if not window.Worker
386
386
  return (list(Error("Local environment does not support workers")))
387
- target.worker = xnew(js.w.Worker, "env_webworker.js")
387
+ target.worker = xnew(window.Worker, "env_webworker.js")
388
388
  listener = target.worker
389
389
  }
390
390
 
@@ -460,6 +460,8 @@ closure TermLocal(track, name, workerID, options,
460
460
  target.statusCB(target, msg.status)
461
461
  else if msg.id == "debugout"
462
462
  target.debugger.debugData(target, msg.data)
463
+ else if msg.id == "download"
464
+ FileDownload(xnew(window.File, msg.bits, msg.name, msg.options))
463
465
  else if msg.id == "loaded" {
464
466
  if !target.started
465
467
  target.worker.postMessage({
@@ -610,7 +612,7 @@ closure termIDE(track, name, workerID, naancont, title, local target, connected)
610
612
  track.update(target)
611
613
  return (target)
612
614
  }
613
- target = runningExecutors.ExecutorBase(track, "Local", name)
615
+ target = require("../running/executors.nlg").ExecutorBase(track, "Local", name)
614
616
  target.name = name
615
617
  target.title = title
616
618
  target.workerID = workerID
@@ -778,7 +780,7 @@ closure termInstallVirtualWatcher(track, naancont) {
778
780
  }
779
781
  }
780
782
 
781
- runningExecutors.TaskOnMessageHook(watchVsites)
783
+ require("../running/executors.nlg").TaskOnMessageHook(watchVsites)
782
784
  };
783
785
 
784
786
 
@@ -29,7 +29,7 @@ closure ServiceWorker(pubID, pubVersion, local serv, callback) {
29
29
  closure updateReg(local mchan) {
30
30
  if !serv.sw
31
31
  return
32
- mchan = xnew(js.w.MessageChannel)
32
+ mchan = xnew(window.MessageChannel)
33
33
  serv.msgport = mchan.port2
34
34
  serv.msgport.onmessage = workerMsg.proc
35
35
  serv.sw.postMessage({
@@ -112,13 +112,13 @@ closure ServiceWorker(pubID, pubVersion, local serv, callback) {
112
112
  serv.scope = scope
113
113
  if !scope
114
114
  return (makeResult(Error("service worker scope required")))
115
- if !js.w.navigator.serviceWorker
115
+ if !window.navigator.serviceWorker
116
116
  return (makeResult(Error("service workers not supported")))
117
- js.w.navigator.serviceWorker.addEventListener("message", function(event) {
117
+ window.navigator.serviceWorker.addEventListener("message", function(event) {
118
118
  if event.data.msg == "Naan_need_fetch_port"
119
119
  updateReg()
120
120
  })
121
- await(js.w.navigator.serviceWorker.register("sworker.js", { scope: scope }).then(function (reg) {
121
+ await(window.navigator.serviceWorker.register("sworker.js", { scope: scope }).then(function (reg) {
122
122
  return (reg.update())
123
123
  }).then(function (reg) {
124
124
  serv.reg = reg
@@ -211,7 +211,7 @@ closure ScopedServiceWorker(pubID, pubVersion, callback, local scoper, result) {
211
211
  }
212
212
 
213
213
  // read resource
214
- filepath = xnew(js.w.URL, data.request.url).pathname
214
+ filepath = xnew(window.URL, data.request.url).pathname
215
215
  filepath = DecodeURIComponent(filepath) // "/run/<scope>/path..."
216
216
  filepath = filepath.slice(4) // "/<scope>/path..."
217
217
  error = true // stays true iff not in scope
@@ -308,7 +308,7 @@ closure TrackedScope(track, scope, fs, rootpath, local trope, result) {
308
308
  trope = findTrackedScope(track, scope)
309
309
  if trope
310
310
  return (list(false, trope))
311
- trope = runningExecutors.ExecutorBase(track, "ServiceWorkers", scope)
311
+ trope = require("../running/executors.nlg").ExecutorBase(track, "ServiceWorkers", scope)
312
312
  serviceWorker.register(scope, closure(filepath, scope, init,
313
313
  local options, mimeType, error, data) {
314
314
  mimeType = {
@@ -37,7 +37,7 @@ closure MakeNideAPIclient(url, guid, local client) {
37
37
  request.addEventListener("error", function reqFailed(event, local errtext) {
38
38
  if request.status == 0
39
39
  errtext = "connectivity"
40
- ellse
40
+ else
41
41
  errtext = "request failed"
42
42
  error = Error(errtext, {
43
43
  status: request.status
@@ -136,10 +136,10 @@ closure psmcFsView(api, rootpath, local view, pathmod) {
136
136
  // readLines
137
137
  //
138
138
  // Read a lines of a file at the specified path with options:
139
- // matchRE: -- JavaScript regex
140
- // ignoreCase: -- ignore case during match
141
- // invert: -- return lines that DO NOT match the regex
142
- // returnText: -- return text of matched lines
139
+ // matchRE: -- JavaScript regex
140
+ // ignoreCase: -- ignore case during match
141
+ // invert: -- return lines that DO NOT match the regex
142
+ // returnText: -- return text of matched lines
143
143
  // When using a matchRE, the return value is an array of [ lineNumber, text ] pairs if
144
144
  // returnText is specified, or just an array of one-based line numbers otherwise.
145
145
  //
@@ -288,6 +288,10 @@ closure psmcFsView(api, rootpath, local view, pathmod) {
288
288
  // Search the specified file or recursive directory.
289
289
 
290
290
  view.dirSearch = function dirSearch(path, options, callback) {
291
+ if options.excludeDirs {
292
+ options = new(options)
293
+ options.excludeDirs = JSONstringify(options.excludeDirs)
294
+ }
291
295
  psmRemote(path, "dirSearch", options, false, callback)
292
296
  }
293
297
 
@@ -357,6 +361,10 @@ closure psmcFsView(api, rootpath, local view, pathmod) {
357
361
  // Grep the specified file or recursive directory.
358
362
 
359
363
  view.grepLines = function grepLines(path, options, callback) {
364
+ if options.excludeDirs {
365
+ options = new(options)
366
+ options.excludeDirs = JSONstringify(options.excludeDirs)
367
+ }
360
368
  psmRemote(path, "grepLines", options, false, callback)
361
369
  }
362
370
 
@@ -143,7 +143,7 @@ function JsonStringify(data) {
143
143
  jsScripts = { }; // script values become false on reload
144
144
 
145
145
  function JsLoadScript(libpath, globalID, local scriptTag, fpath) {
146
- if jsScripts[jsScripts]
146
+ if jsScripts[libpath]
147
147
  return // already loaded
148
148
  if libpath.startsWith("/")
149
149
  fpath = libpath
@@ -155,10 +155,10 @@ function JsLoadScript(libpath, globalID, local scriptTag, fpath) {
155
155
  } else if js.w { // load in browser context
156
156
  pending = new(nonce)
157
157
  scriptTag = js.w.document.createElement("script")
158
- scriptTag.src = js.w.location.origin.concat(fpath)
158
+ scriptTag.src = js.w.location.origin.concat(fpath, requireQuery())
159
159
  scriptTag.onload = function (event) {
160
160
  jsScripts[libpath] = js.w[globalID]
161
- pending.signal(JSZipLib)
161
+ pending.signal(js.w[globalID])
162
162
  true
163
163
  }
164
164
  scriptTag.onerror = function (event) {
@@ -303,7 +303,8 @@ JSONparse = false;
303
303
  Uint8ArrayFromString = false;
304
304
 
305
305
  function comrInit(local manifest) {
306
- manifest = `(EncodeQuery, ContentTypeFromFileExt, UUID, JsLoadScript, LoadComponentOnDemand, LiveExport, comrInit)
306
+ manifest = `(EncodeQuery, ContentTypeFromFileExt, UUID, JsonParse, JsonStringify, JsLoadScript,
307
+ LoadComponentOnDemand, LiveExport, comrInit)
307
308
 
308
309
  Naan.module.build(module.id, "common", closure(modobj, compobj) {
309
310
  compobj.manifest = manifest
@@ -373,10 +374,17 @@ function comrInit(local manifest) {
373
374
  result
374
375
  }
375
376
  //
376
- // DecodeBase64url - decode base64url to original string
377
+ // EncodeBase64url - decode base64url to original string
377
378
  // See https://en.wikipedia.org/wiki/Base64 for details on variants
378
379
  //
379
- DecodeBase64url = function decodeBase64url(base64url, local padding) {
380
+ EncodeBase64url = function encodeBase64url(data) {
381
+ data = EncodeBase64(data)
382
+ data.replace(RegExp("[+]", "g"), '-').replace(RegExp("/", "g"), '_')
383
+ }
384
+ //
385
+ // DecodeBase64url - decode base64url to original string
386
+ //
387
+ DecodeBase64url = function decodeBase64url(base64url) {
380
388
  base64url = base64url.replace(RegExp("-", "g"), '+').replace(RegExp("_", "g"), '/')
381
389
  DecodeBase64(base64url)
382
390
  }
@@ -411,7 +419,9 @@ function comrInit(local manifest) {
411
419
  }
412
420
  //
413
421
  // MD5 functions
414
- JsLoadScript("frameworks/browser/spark-md5/spark-md5.min.js", "SparkMD5")
422
+ if symbol(NideBuild) { // execute if we are not building
423
+ JsLoadScript("frameworks/browser/spark-md5/spark-md5.min.js", "SparkMD5")
424
+ }
415
425
  //
416
426
  // hasMD5 in hex
417
427
  HashMD5 = function browserMD5(data, raw) {
@@ -526,6 +536,7 @@ function comrInit(local manifest) {
526
536
  modobj.exports.JsonStringify = JsonStringify
527
537
  modobj.exports.JsonParse = JsonParse
528
538
  modobj.exports.Uint8ArrayFromString = Uint8ArrayFromString
539
+ modobj.exports.EncodeBase64url = EncodeBase64url
529
540
  modobj.exports.DecodeBase64url = DecodeBase64url
530
541
  modobj.exports.HashMD5 = HashMD5
531
542
  modobj.exports.HashMD5_base64 = HashMD5_base64
@@ -171,8 +171,8 @@ closure MakeAPI(port, options, callback, local chroot, server) {
171
171
  res.status(403).send("unauthorized")
172
172
  timeoutms = toint(req.query.timeout)
173
173
  after = toint(req.query.after)
174
- if not integer(timeoutms) or timeoutms > 10000
175
- res.status(400).send("timeout is integer milliseconds no greater than 10000")
174
+ if not integer(timeoutms) or timeoutms > 7200000
175
+ res.status(400).send("timeout is integer milliseconds no greater than 7200000")
176
176
  else if not integer(after)
177
177
  res.status(400).send("after must be an integer")
178
178
  else {
@@ -181,11 +181,11 @@ closure Filesystem(rootpath, local files, badCreatePathRegEx) {
181
181
  // readLines
182
182
  //
183
183
  // Read the contents of a file into an array of lines with the following options:
184
- // pattern: -- only return lines matching pattern
185
- // fixed -- pattern is a fixed string, not a regular expression
186
- // ignoreCase: -- ignore case during match
187
- // invert: -- return lines that DO NOT match the regex
188
- // returnText: -- return text of matched lines
184
+ // pattern: -- only return lines matching pattern
185
+ // fixed -- pattern is a fixed string, not a regular expression
186
+ // ignoreCase: -- ignore case during match
187
+ // invert: -- return lines that DO NOT match the regex
188
+ // returnText: -- return text of matched lines
189
189
  //
190
190
 
191
191
  files.readLines = closure readLines(path, options, callback,
@@ -748,21 +748,31 @@ closure Filesystem(rootpath, local files, badCreatePathRegEx) {
748
748
  // dirSearch
749
749
  //
750
750
  // Search the files in a directory for a pattern.
751
-
751
+ //
752
+ // Most options are handled by readLines, but the following is done here:
753
+ // excludeDirs: [dirnames] -- exclude the specified directory names
754
+
752
755
  files.dirSearch = closure dirSearch(path, options, callback,
753
- local error, localpaths, output) {
756
+ local error, localpaths, dexmatch, item, output) {
754
757
  if !callback
755
758
  return (syncAdapter(dirSearch, path, options))
756
759
  `(error, localpath) = localPath(path)
757
760
  if error
758
761
  return (asyncResult(callback, error))
762
+ if options.excludeDirs.0 { // match all provided paths
763
+ dexmatch = []
764
+ for item in options.excludeDirs // escape all special characters
765
+ dexmatch.push(item.replace(RegExp("[^A-Za-z0-9-_\n]","g"), "\\$&"))
766
+ dexmatch = RegExp(dexmatch.join("|"))
767
+ }
759
768
  output = []
760
769
  debuglog("dirSearch begins", localpath)
761
770
  asyncArray(localpaths, 10, closure(localpath, index, elements, local error, direnum, node, lines, line) {
762
771
  `(error, direnum) = dirList(localpath, { })
763
772
  if direnum.children
764
773
  for node in direnum.children
765
- localpaths.push(JSpath.join(localpath, node.name)) // more files or directories to examine
774
+ if !dexmatch || !node.name.match(dexmatch)
775
+ localpaths.push(JSpath.join(localpath, node.name)) // more files or directories to examine
766
776
  else {
767
777
  `(error, lines) = readLines(localpath, options)
768
778
  if error && localpaths.length == 1 {
@@ -986,22 +996,29 @@ closure Filesystem(rootpath, local files, badCreatePathRegEx) {
986
996
  //
987
997
  // Return an array of text matches in a file or recursive directory of files with the following
988
998
  // options:
989
- // pattern: -- pattern to find
990
- // ignoreCase: -- ignore case during match
991
- // invert: -- return lines that DO NOT match the regex
992
- // returnText: -- return text of matched lines
993
- // maxCount: -- integer maximum number of results (default 10,000)
999
+ // pattern: -- pattern to find
1000
+ // ignoreCase: -- ignore case during match
1001
+ // invert: -- return lines that DO NOT match the regex
1002
+ // returnText: -- return text of matched lines
1003
+ // excludeDirs: [dirnames] -- exclude the specified directory names
1004
+ // maxCount: -- integer maximum number of results (default 10,000)
994
1005
  // grep-specific options:
995
- // ignoreBinary: -- ignore binary files
1006
+ // ignoreBinary: -- ignore binary files
996
1007
  //
997
1008
  files.grepLines = closure grepLines(path, options, callback,
998
- local error, localpath, cmdargs, execOptions, grep, result, lineRE, maxcount) {
1009
+ local error, localpath, cmdargs, item, execOptions, grep, result, lineRE, maxcount) {
999
1010
  if !callback
1000
1011
  return (syncAdapter(grepLines, path, options))
1001
1012
  `(error, localpath) = localPath(path)
1002
1013
  if error
1003
1014
  return (asyncResult(callback, error))
1004
1015
  cmdargs = ["-rn"] // recursively search directories for files; report line numbers
1016
+ if options.excludeDirs.0 { // exclude a directory
1017
+ for item in options.excludeDirs {
1018
+ cmdargs.push("--exclude-dir")
1019
+ cmdargs.push(item)
1020
+ }
1021
+ }
1005
1022
  if options.ignoreCase
1006
1023
  cmdargs.push("-i")
1007
1024
  if options.invert
@@ -204,6 +204,7 @@ closure psmsFsView(rootpath, local view, gitter) {
204
204
 
205
205
  // op: dirSearch
206
206
  function dirSearchApi() {
207
+ req.query.excludeDirs = JSONparse(req.query.excludeDirs)
207
208
  res.send(new(array, view.dirSearch(path, req.query)))
208
209
  }
209
210
 
@@ -239,6 +240,7 @@ closure psmsFsView(rootpath, local view, gitter) {
239
240
 
240
241
  // op: grepLines
241
242
  function grepLinesApi() {
243
+ req.query.excludeDirs = JSONparse(req.query.excludeDirs)
242
244
  res.send(new(array, view.grepLines(path, req.query)))
243
245
  }
244
246
 
@@ -289,8 +291,15 @@ closure psmsFsView(rootpath, local view, gitter) {
289
291
  gitLog: gitLogApi
290
292
  gitShowFile: gitShowFileApi
291
293
  }[req.query.op]
292
- if proc
293
- proc()
294
+ if proc {
295
+ try {
296
+ proc()
297
+ } catch {
298
+ if true {
299
+ res.status(400).send("psm op failed: ".concat(req.query.op, ": ", exception))
300
+ }
301
+ }
302
+ }
294
303
  else
295
304
  res.status(400).send("invalid psm op: ".concat(req.query.op))
296
305
  }