@sasjs/cli 6.15.0 → 6.16.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.
@@ -23,10 +23,19 @@
23
23
  * The drive is persisted by the page (host/persist.js) and the identity is not:
24
24
  * an account, group, permission rule or client created through these routes
25
25
  * lives in this worker's memory, so a worker the browser terminates starts again
26
- * from the fixture. Edits that must survive a restart belong on a server.
26
+ * from the fixture. Edits that must survive a restart belong on a server. The
27
+ * activity log the Admin routes report is in this worker's memory for the same
28
+ * reason.
29
+ *
30
+ * Every operation in @sasjs/api's route table is answered here: the drive, the
31
+ * sign-in flow, the records, the runs, the sessions, the diagnostics and logs,
32
+ * the directory import, the app-stream delete, the linter and the home page. The
33
+ * services are the package's; this file supplies the transport, the worker's own
34
+ * activity, and the browser's answers to questions a server asks its process.
27
35
  */
28
36
  importScripts('/host/runtime-core.js')
29
37
  importScripts('/host/api-browser.js')
38
+ importScripts('/host/lint-browser.js')
30
39
 
31
40
  const R = self.SASjsMockRuntime
32
41
 
@@ -36,6 +45,10 @@ const R = self.SASjsMockRuntime
36
45
  // and the few capabilities a static origin does not have, which the package
37
46
  // reports as a 501 naming the missing service.
38
47
  const API = self.SASjsApi
48
+ // The linter, bundled at build time from the installed @sasjs/api/code and
49
+ // @sasjs/lint (see mockSite.ts). It is a separate script because the linter is
50
+ // not part of the root package a service worker can import.
51
+ const CODE = self.SASjsCode
39
52
 
40
53
  const DRIVE_STORE = R.driveStore
41
54
  const IDENTITY_STORE = new API.MemoryIdentityStore(API.DEFAULT_IDENTITY)
@@ -304,6 +317,175 @@ const tablesOf = (files) => {
304
317
 
305
318
  const RUNTIME = new BrowserRuntime()
306
319
 
320
+ // ------------------------------------------------------------- host reports
321
+ // The Admin routes ask a host what it is doing. A browser has no process, but it
322
+ // does have a page, a heap and this worker's own activity, so the reports are
323
+ // built from those rather than refused: the request log below is the log file,
324
+ // and the memory reading is `performance.memory` where the browser exposes it.
325
+ const STARTED = Date.now()
326
+ const HOST_LOG_LIMIT = 2000
327
+ const HOST_LOG = []
328
+ const LOG_FILE = 'app.log'
329
+ // the moment the last line was written, which is the file's mtime - not the
330
+ // moment a report read it
331
+ let LOG_UPDATED = null
332
+
333
+ const logEntry = (line) => {
334
+ LOG_UPDATED = new Date().toISOString()
335
+ HOST_LOG.push(`${LOG_UPDATED} ${line}`)
336
+ if (HOST_LOG.length > HOST_LOG_LIMIT)
337
+ HOST_LOG.splice(0, HOST_LOG.length - HOST_LOG_LIMIT)
338
+ }
339
+
340
+ const logText = () => HOST_LOG.join('\n')
341
+ const bytesOf = (text) => new TextEncoder().encode(String(text)).length
342
+
343
+ const adminReports = {
344
+ diagnostics: async () => {
345
+ const perf = self.performance || {}
346
+ const heap = perf.memory || {}
347
+ const deviceBytes = Number(navigator.deviceMemory || 0) * 1024 ** 3
348
+ const total = Number(heap.jsHeapSizeLimit || deviceBytes || 0)
349
+ const used = Number(heap.usedJSHeapSize || 0)
350
+ return {
351
+ build: { version: 'dev', gitHash: 'unknown', buildDate: null },
352
+ server: {
353
+ // a page is not a process: the fields exist so a diagnostics screen
354
+ // renders, and their values describe this worker rather than inventing
355
+ // one behind it
356
+ mode: 'browser',
357
+ uptimeSeconds: (Date.now() - STARTED) / 1000,
358
+ startedAt: new Date(STARTED).toISOString(),
359
+ pid: 0,
360
+ nodeVersion: 'n/a',
361
+ platform: navigator.platform || 'browser',
362
+ arch: 'browser',
363
+ cpuCount: navigator.hardwareConcurrency || 1,
364
+ cpuModel: 'unknown',
365
+ loadAverage: [],
366
+ runTimes: ['js'],
367
+ sasjsRoot: '/'
368
+ },
369
+ memory: {
370
+ rssBytes: used,
371
+ heapTotalBytes: total,
372
+ heapUsedBytes: used,
373
+ externalBytes: 0,
374
+ systemTotalBytes: deviceBytes || total,
375
+ systemFreeBytes: Math.max(0, (deviceBytes || total) - used)
376
+ },
377
+ // a static origin cannot read the device's filesystem, which the document
378
+ // allows the host to state
379
+ filesystem: null,
380
+ folders: [],
381
+ logs: {
382
+ folder: 'worker memory',
383
+ // no file until a request has written one, and the stamp is the last
384
+ // write rather than the moment this report was assembled
385
+ fileCount: HOST_LOG.length ? 1 : 0,
386
+ totalBytes: bytesOf(logText()),
387
+ newestFile: HOST_LOG.length ? LOG_FILE : null,
388
+ newestModified: LOG_UPDATED
389
+ }
390
+ }
391
+ },
392
+ logs: async () =>
393
+ HOST_LOG.length
394
+ ? [
395
+ {
396
+ name: LOG_FILE,
397
+ sizeBytes: bytesOf(logText()),
398
+ modified: LOG_UPDATED
399
+ }
400
+ ]
401
+ : [],
402
+ logTail: async (fileName, lines) => {
403
+ if (fileName !== LOG_FILE || !HOST_LOG.length) return undefined
404
+ const tail = HOST_LOG.slice(-lines)
405
+ const content = tail.join('\n')
406
+ return {
407
+ fileName,
408
+ lines: tail.length,
409
+ bytesRead: bytesOf(content),
410
+ truncated: tail.length < HOST_LOG.length,
411
+ content
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * The directory the LDAP import reads.
418
+ *
419
+ * A browser has no bind to make, so the source is the fixture this host already
420
+ * signs in with: every account and group it names is present, so the import
421
+ * answers the route (the package owns the idempotency) and creates nothing. A
422
+ * host with a real directory supplies its own source.
423
+ */
424
+ const directorySource = {
425
+ users: async () =>
426
+ API.DEFAULT_IDENTITY.users.map((user) => ({
427
+ username: user.username,
428
+ displayName: user.displayName
429
+ })),
430
+ groups: async () =>
431
+ API.DEFAULT_IDENTITY.groups.map((group) => ({
432
+ name: group.name,
433
+ members: group.users || []
434
+ }))
435
+ }
436
+
437
+ /**
438
+ * The app streams this host published: the site itself.
439
+ *
440
+ * The folder is the identity, so unpublishing the app the site was built for
441
+ * answers `success`; any other folder is the documented 404. The static files
442
+ * stay where they are - a worker cannot delete the site it is served from - so
443
+ * this is a record of the publication, not a filesystem operation.
444
+ */
445
+ let registryPromise
446
+ const registry = () => {
447
+ if (!registryPromise)
448
+ registryPromise = (async () => {
449
+ let appLoc
450
+ try {
451
+ const config = await (await fetch('/host/mock-config.json')).json()
452
+ appLoc = config && config.appLoc
453
+ } catch (e) {
454
+ appLoc = undefined
455
+ }
456
+ return new API.MemoryAppStreamRegistry(appLoc ? [appLoc] : [])
457
+ })()
458
+ return registryPromise
459
+ }
460
+
461
+ /**
462
+ * The page at `/`.
463
+ *
464
+ * The site's own index.html is already in the build, so the home route reads it
465
+ * from the static host and hands the package the page and a token of its
466
+ * contents. The package decides what to do with the token; these builds do not
467
+ * reference `index.bundle.js`, so in practice it is returned unchanged.
468
+ */
469
+ const contentVersion = (text) => {
470
+ let hash = 0x811c9dc5
471
+ for (let i = 0; i < text.length; i++) {
472
+ hash ^= text.charCodeAt(i)
473
+ hash = Math.imul(hash, 0x01000193)
474
+ }
475
+ return (hash >>> 0).toString(16)
476
+ }
477
+
478
+ const homeService = API.createHomeService(async () => {
479
+ try {
480
+ const response = await fetch('/index.html', { cache: 'no-store' })
481
+ if (!response.ok) return undefined
482
+ const html = await response.text()
483
+ return { html, version: contentVersion(html) }
484
+ } catch (e) {
485
+ return undefined
486
+ }
487
+ })
488
+
307
489
  const SERVICES = {
308
490
  drive: API.createDriveService(DRIVE_STORE),
309
491
  info: API.createInfoService(API.BROWSER_INFO),
@@ -315,11 +497,17 @@ const SERVICES = {
315
497
  runTimes: ['js']
316
498
  }),
317
499
  session: API.createSessionService(IDENTITY_STORE, RUNTIME),
318
- authConfig: API.createAuthConfigService({})
319
- // `admin`, `directory`, `code` and `home` are deliberately absent: this host
320
- // has no process to report on, no LDAP connection to import from, and the
321
- // linter is a node-only dependency. The package answers a 501 naming the
322
- // capability rather than a 404 that reads like a typo in the caller.
500
+ authConfig: API.createAuthConfigService({}),
501
+ admin: API.createAdminService(adminReports),
502
+ directory: API.createDirectoryService(IDENTITY_STORE, directorySource),
503
+ registry: {
504
+ unpublish: async (appLoc) => (await registry()).unpublish(appLoc)
505
+ },
506
+ home: homeService,
507
+ // the package's own code service over the bundled linter; the host passes no
508
+ // lint config, so a buffer is linted against the linter's defaults - a mock
509
+ // has no `.sasjslint` tree to resolve one from
510
+ code: CODE.createCodeService({})
323
511
  }
324
512
 
325
513
  /** The routes this host performs itself, because the answer is not JSON. */
@@ -663,7 +851,7 @@ const contentsResponse = (answer, path) => {
663
851
  })
664
852
  }
665
853
 
666
- const handle = async (request, url) => {
854
+ const routeRequest = async (request, url) => {
667
855
  const path = url.pathname.replace(/\/+$/, '') || '/'
668
856
  const query = {}
669
857
  for (const [k, v] of url.searchParams.entries()) query[k] = v
@@ -710,6 +898,14 @@ const handle = async (request, url) => {
710
898
  const answer = await API.dispatch(SERVICES, routeId, apiRequest)
711
899
  if (request.method !== 'GET') await persistChanges()
712
900
 
901
+ // the shell is a page, not a file the caller named: the document states
902
+ // text/html for it, and the extension of `/` says nothing
903
+ if (routeId === 'GET /')
904
+ return new Response(answer.contents, {
905
+ status: answer.status,
906
+ headers: { 'content-type': 'text/html; charset=utf-8' }
907
+ })
908
+
713
909
  if (answer.contents !== undefined)
714
910
  return contentsResponse(
715
911
  answer,
@@ -718,6 +914,7 @@ const handle = async (request, url) => {
718
914
  return json(answer.body, answer.status)
719
915
  } catch (e) {
720
916
  const message = (e && e.stack) || String(e)
917
+ logEntry(`${request.method} ${path} ERROR ${(e && e.message) || message}`)
721
918
  broadcast({ type: 'sasjs-host-error', path, message })
722
919
  // A refusal the API decided on carries its status and, where the route
723
920
  // answers an envelope, the body to send. The drive's routes send the message
@@ -736,14 +933,51 @@ const handle = async (request, url) => {
736
933
  }
737
934
  }
738
935
 
936
+ /**
937
+ * One request, answered and written to the host's activity log.
938
+ *
939
+ * The log is what the Admin routes report: a browser has no server log file, so
940
+ * the worker keeps its own - method, path, status and how long it took - and the
941
+ * three admin routes read it back.
942
+ */
943
+ const handle = async (request, url) => {
944
+ const started = Date.now()
945
+ const response = await routeRequest(request, url)
946
+ logEntry(
947
+ `${request.method} ${url.pathname} ${response.status} ${
948
+ Date.now() - started
949
+ }ms`
950
+ )
951
+ return response
952
+ }
953
+
739
954
  self.addEventListener('fetch', (event) => {
740
955
  const url = new URL(event.request.url)
741
956
  if (url.origin !== self.location.origin) return
742
- // The site's own page is the home route, and the static host serves it: a
743
- // worker that answered `/` with the page it copied would be serving the app
744
- // from two places. The API's own paths are answered here, including one the
745
- // table does not have - falling through to the static host would put the SPA's
746
- // index.html behind a typo in a caller's URL.
957
+
958
+ // The site's own page is answered by the home route, but a navigation must
959
+ // never be at the API's mercy: if that route throws or answers an error, the
960
+ // request falls back to the static host, which holds the same index.html. A
961
+ // broken API route can cost a client its data; it must not cost the app its
962
+ // page.
963
+ if (url.pathname === '/') {
964
+ event.respondWith(
965
+ (async () => {
966
+ try {
967
+ const response = await handle(event.request, url)
968
+ if (response && response.status < 400) return response
969
+ } catch (e) {
970
+ /* fall through to the static host */
971
+ }
972
+ return fetch(event.request)
973
+ })()
974
+ )
975
+ return
976
+ }
977
+
978
+ // The API's own paths are answered here, including one the table does not
979
+ // have - falling through to the static host would put the SPA's index.html
980
+ // behind a typo in a caller's URL.
747
981
  if (!/^\/(SASjsApi|SASLogon)(\/|$)/i.test(url.pathname)) return
748
982
  event.respondWith(handle(event.request, url))
749
983
  })
@@ -28,6 +28,16 @@ export declare const WEBOUT_SENTINELS: {
28
28
  userId: string;
29
29
  apiServerUrl: string;
30
30
  };
31
+ /**
32
+ * Builds `lint-browser.js` from the installed packages.
33
+ *
34
+ * The shim sources are written to the system temp folder, never beside this
35
+ * module: an installed package can be read-only (a global install, a pnpm store
36
+ * symlink, a container that builds as another user), and a build that writes
37
+ * inside it fails with EACCES for exactly the users least able to diagnose it.
38
+ * Resolution is kept by naming the CLI's `node_modules` through `nodePaths`.
39
+ */
40
+ export declare const buildLintBundle: (destination: string) => void;
31
41
  /**
32
42
  * The bounds on what the browser drive seeds. A byte entry is carried as base64
33
43
  * in vfs-seed.json and held in the worker's memory, so a large artefact - an
@@ -1,4 +1,8 @@
1
1
  "use strict";
2
+ var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
3
+ if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
4
+ return cooked;
5
+ };
2
6
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
7
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
8
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -75,11 +79,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
75
79
  return (mod && mod.__esModule) ? mod : { "default": mod };
76
80
  };
77
81
  Object.defineProperty(exports, "__esModule", { value: true });
78
- exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.WEBOUT_SENTINELS = void 0;
82
+ exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.buildLintBundle = exports.WEBOUT_SENTINELS = void 0;
79
83
  exports.buildMockSite = buildMockSite;
80
84
  var crypto_1 = require("crypto");
81
85
  var fs_1 = __importDefault(require("fs"));
86
+ var os_1 = __importDefault(require("os"));
82
87
  var path_1 = __importDefault(require("path"));
88
+ var esbuild_1 = require("esbuild");
83
89
  var utils_1 = require("@sasjs/utils");
84
90
  /**
85
91
  * The webout() block is the one @sasjs/utils generates for the server, emitted
@@ -116,8 +122,141 @@ var ASSETS_FOLDER = path_1.default.join(__dirname, 'assets');
116
122
  */
117
123
  var API_BROWSER_BUILD = path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
118
124
  var toPosix = function (value) { return value.split(path_1.default.sep).join('/'); };
119
- var listFiles = function (folder, base) {
125
+ /**
126
+ * The linter, as a service worker can run it.
127
+ *
128
+ * `@sasjs/api/code` is a node-only entry because `@sasjs/lint` resolves its
129
+ * configuration by walking a filesystem - but the linter's core (`lintText`,
130
+ * `formatText`) reads only the text it is given and the configuration it is
131
+ * passed, so the two can run anywhere the module graph can be built. This
132
+ * bundle is that graph: the package's own code service, the linter it wraps, and
133
+ * shims for the four node modules reached at import time (`path` and `os` for
134
+ * resolution, `fs` and `@sasjs/utils/file` for the config lookup that a mock
135
+ * never makes, because it hands the linter the default configuration).
136
+ *
137
+ * It is built from the installed @sasjs/api and @sasjs/lint on every mock
138
+ * build, so the rules a client sees are the version the CLI resolved - a
139
+ * committed bundle would be a second copy to drift.
140
+ */
141
+ var LINT_BUNDLE_FILES = {
142
+ 'entry.js': [
143
+ "import './process.js'",
144
+ "import { createCodeService } from '@sasjs/api/code'",
145
+ 'globalThis.SASjsCode = { createCodeService }'
146
+ ].join('\n'),
147
+ 'process.js': [
148
+ '// the linter reads process.env for a default and process.cwd() for a root;',
149
+ '// neither is a real lookup in a worker, and both are answered in one place',
150
+ 'if (typeof globalThis.process === "undefined")',
151
+ ' globalThis.process = {',
152
+ ' env: {},',
153
+ ' platform: "browser",',
154
+ ' cwd: () => "/",',
155
+ ' versions: {},',
156
+ ' argv: [],',
157
+ ' version: "v0"',
158
+ ' }'
159
+ ].join('\n'),
160
+ 'fs.js': [
161
+ 'const unavailable = (name) => () => {',
162
+ ' throw new Error(name + " is not available in the browser host")',
163
+ '}',
164
+ 'module.exports = {',
165
+ ' readFileSync: unavailable("readFileSync"),',
166
+ ' writeFileSync: unavailable("writeFileSync"),',
167
+ ' existsSync: () => false,',
168
+ ' promises: {}',
169
+ '}'
170
+ ].join('\n'),
171
+ 'os.js': [
172
+ 'module.exports = {',
173
+ ' homedir: () => "/",',
174
+ ' platform: () => "browser",',
175
+ ' tmpdir: () => "/tmp",',
176
+ ' EOL: "\\n"',
177
+ '}'
178
+ ].join('\n'),
179
+ 'utils-file.js': [
180
+ 'const unavailable = (name) => async () => {',
181
+ ' throw new Error(name + " is not available in the browser host")',
182
+ '}',
183
+ 'module.exports = {',
184
+ ' readFile: unavailable("readFile"),',
185
+ ' writeFile: unavailable("writeFile"),',
186
+ ' fileExists: async () => false,',
187
+ ' listFilesInFolder: async () => []',
188
+ '}'
189
+ ].join('\n'),
190
+ 'path.js': String.raw(templateObject_1 || (templateObject_1 = __makeTemplateObject(["// the posix half of path, which is all the linter uses: the rules name files,\n// and a mock hands the linter no folder to walk\nconst SEP = String.fromCharCode(92)\nconst clean = (value) =>\n String(value).split(SEP).join(\"/\").replace(//+/g, \"/\")\nconst strip = (value) => clean(value).replace(//+$/, \"\")\nconst basename = (value) => strip(value).split(\"/\").pop() || \"\"\nconst extname = (value) => {\n const base = basename(value)\n const dot = base.lastIndexOf(\".\")\n return dot > 0 ? base.slice(dot) : \"\"\n}\nconst dirname = (value) => {\n const parts = clean(value).split(\"/\")\n parts.pop()\n return parts.join(\"/\") || \".\"\n}\nconst join = (...parts) => clean(parts.filter(Boolean).join(\"/\"))\nmodule.exports = {\n sep: \"/\",\n delimiter: \":\",\n basename,\n extname,\n dirname,\n join,\n resolve: (...parts) => clean(parts.filter(Boolean).join(\"/\")),\n relative: (from, to) => clean(to).replace(clean(from) + \"/\", \"\"),\n isAbsolute: (value) => String(value).startsWith(\"/\"),\n normalize: clean,\n parse: (value) => ({\n root: \"\",\n dir: dirname(value),\n base: basename(value),\n ext: extname(value),\n name: basename(value).slice(0, basename(value).length - extname(value).length)\n })\n}"], ["// the posix half of path, which is all the linter uses: the rules name files,\n// and a mock hands the linter no folder to walk\nconst SEP = String.fromCharCode(92)\nconst clean = (value) =>\n String(value).split(SEP).join(\"/\").replace(/\\/+/g, \"/\")\nconst strip = (value) => clean(value).replace(/\\/+$/, \"\")\nconst basename = (value) => strip(value).split(\"/\").pop() || \"\"\nconst extname = (value) => {\n const base = basename(value)\n const dot = base.lastIndexOf(\".\")\n return dot > 0 ? base.slice(dot) : \"\"\n}\nconst dirname = (value) => {\n const parts = clean(value).split(\"/\")\n parts.pop()\n return parts.join(\"/\") || \".\"\n}\nconst join = (...parts) => clean(parts.filter(Boolean).join(\"/\"))\nmodule.exports = {\n sep: \"/\",\n delimiter: \":\",\n basename,\n extname,\n dirname,\n join,\n resolve: (...parts) => clean(parts.filter(Boolean).join(\"/\")),\n relative: (from, to) => clean(to).replace(clean(from) + \"/\", \"\"),\n isAbsolute: (value) => String(value).startsWith(\"/\"),\n normalize: clean,\n parse: (value) => ({\n root: \"\",\n dir: dirname(value),\n base: basename(value),\n ext: extname(value),\n name: basename(value).slice(0, basename(value).length - extname(value).length)\n })\n}"])))
191
+ };
192
+ /**
193
+ * The `node_modules` the CLI's own dependency lives in.
194
+ *
195
+ * esbuild resolves a bare import from the entry file upwards, which is wrong for
196
+ * a scratch entry outside the install: the walk would miss the installed
197
+ * packages entirely. `nodePaths` states where to look instead, and it is taken
198
+ * from `require.resolve` so it is the tree the CLI actually loaded from - a
199
+ * hoisted install, a nested one, and a pnpm store symlink all answer with their
200
+ * own location.
201
+ */
202
+ var NODE_MODULES = (function () {
203
+ var resolved = require.resolve('@sasjs/api');
204
+ var marker = "".concat(path_1.default.sep, "node_modules").concat(path_1.default.sep);
205
+ var index = resolved.lastIndexOf(marker);
206
+ return index < 0 ? undefined : resolved.slice(0, index + marker.length - 1);
207
+ })();
208
+ /**
209
+ * Builds `lint-browser.js` from the installed packages.
210
+ *
211
+ * The shim sources are written to the system temp folder, never beside this
212
+ * module: an installed package can be read-only (a global install, a pnpm store
213
+ * symlink, a container that builds as another user), and a build that writes
214
+ * inside it fails with EACCES for exactly the users least able to diagnose it.
215
+ * Resolution is kept by naming the CLI's `node_modules` through `nodePaths`.
216
+ */
217
+ var buildLintBundle = function (destination) {
120
218
  var e_1, _a;
219
+ var work = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'sasjs-lint-'));
220
+ try {
221
+ try {
222
+ for (var _b = __values(Object.entries(LINT_BUNDLE_FILES)), _c = _b.next(); !_c.done; _c = _b.next()) {
223
+ var _d = __read(_c.value, 2), name_1 = _d[0], contents = _d[1];
224
+ fs_1.default.writeFileSync(path_1.default.join(work, name_1), contents);
225
+ }
226
+ }
227
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
228
+ finally {
229
+ try {
230
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
231
+ }
232
+ finally { if (e_1) throw e_1.error; }
233
+ }
234
+ (0, esbuild_1.buildSync)({
235
+ entryPoints: [path_1.default.join(work, 'entry.js')],
236
+ bundle: true,
237
+ platform: 'browser',
238
+ format: 'iife',
239
+ outfile: destination,
240
+ nodePaths: NODE_MODULES ? [NODE_MODULES] : [],
241
+ alias: {
242
+ fs: path_1.default.join(work, 'fs.js'),
243
+ path: path_1.default.join(work, 'path.js'),
244
+ os: path_1.default.join(work, 'os.js'),
245
+ '@sasjs/utils/file': path_1.default.join(work, 'utils-file.js')
246
+ },
247
+ logLevel: 'silent'
248
+ });
249
+ }
250
+ catch (error) {
251
+ throw new Error("The browser lint bundle could not be built from @sasjs/api/code: ".concat((error && error.message) || error));
252
+ }
253
+ finally {
254
+ fs_1.default.rmSync(work, { recursive: true, force: true });
255
+ }
256
+ };
257
+ exports.buildLintBundle = buildLintBundle;
258
+ var listFiles = function (folder, base) {
259
+ var e_2, _a;
121
260
  if (base === void 0) { base = folder; }
122
261
  var entries = fs_1.default.readdirSync(folder, { withFileTypes: true });
123
262
  var files = [];
@@ -131,17 +270,17 @@ var listFiles = function (folder, base) {
131
270
  files.push(toPosix(path_1.default.relative(base, absolute)));
132
271
  }
133
272
  }
134
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
273
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
135
274
  finally {
136
275
  try {
137
276
  if (entries_1_1 && !entries_1_1.done && (_a = entries_1.return)) _a.call(entries_1);
138
277
  }
139
- finally { if (e_1) throw e_1.error; }
278
+ finally { if (e_2) throw e_2.error; }
140
279
  }
141
280
  return files;
142
281
  };
143
282
  var copyFolder = function (from, to) {
144
- var e_2, _a;
283
+ var e_3, _a;
145
284
  fs_1.default.mkdirSync(to, { recursive: true });
146
285
  try {
147
286
  for (var _b = __values(fs_1.default.readdirSync(from, { withFileTypes: true })), _c = _b.next(); !_c.done; _c = _b.next()) {
@@ -154,12 +293,12 @@ var copyFolder = function (from, to) {
154
293
  fs_1.default.copyFileSync(source, target);
155
294
  }
156
295
  }
157
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
296
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
158
297
  finally {
159
298
  try {
160
299
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
161
300
  }
162
- finally { if (e_2) throw e_2.error; }
301
+ finally { if (e_3) throw e_3.error; }
163
302
  }
164
303
  };
165
304
  /**
@@ -221,7 +360,7 @@ var readDriveFile = function (absolute) {
221
360
  };
222
361
  };
223
362
  var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
224
- var e_3, _a, e_4, _b, e_5, _c;
363
+ var e_4, _a, e_5, _b, e_6, _c;
225
364
  if (exclude === void 0) { exclude = []; }
226
365
  if (excludeFolders === void 0) { excludeFolders = []; }
227
366
  var files = {};
@@ -247,26 +386,26 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
247
386
  candidates.push({ key: key, absolute: path_1.default.join(folder, relative) });
248
387
  };
249
388
  try {
250
- for (var _d = (e_4 = void 0, __values(listFiles(folder))), _e = _d.next(); !_e.done; _e = _d.next()) {
389
+ for (var _d = (e_5 = void 0, __values(listFiles(folder))), _e = _d.next(); !_e.done; _e = _d.next()) {
251
390
  var relative = _e.value;
252
391
  _loop_1(relative);
253
392
  }
254
393
  }
255
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
394
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
256
395
  finally {
257
396
  try {
258
397
  if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
259
398
  }
260
- finally { if (e_4) throw e_4.error; }
399
+ finally { if (e_5) throw e_5.error; }
261
400
  }
262
401
  }
263
402
  }
264
- catch (e_3_1) { e_3 = { error: e_3_1 }; }
403
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
265
404
  finally {
266
405
  try {
267
406
  if (directories_1_1 && !directories_1_1.done && (_a = directories_1.return)) _a.call(directories_1);
268
407
  }
269
- finally { if (e_3) throw e_3.error; }
408
+ finally { if (e_4) throw e_4.error; }
270
409
  }
271
410
  // The budget is spent in path order, not in whatever order the folder listing
272
411
  // returns, so the same project yields the same drive - and the same warning -
@@ -294,12 +433,12 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
294
433
  files[candidate.key] = file.contents;
295
434
  }
296
435
  }
297
- catch (e_5_1) { e_5 = { error: e_5_1 }; }
436
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
298
437
  finally {
299
438
  try {
300
439
  if (candidates_1_1 && !candidates_1_1.done && (_c = candidates_1.return)) _c.call(candidates_1);
301
440
  }
302
- finally { if (e_5) throw e_5.error; }
441
+ finally { if (e_6) throw e_6.error; }
303
442
  }
304
443
  return { files: files, skipped: skipped };
305
444
  };
@@ -441,7 +580,7 @@ var patchIndexHtml = function (html, version) {
441
580
  var scripts = [];
442
581
  var tagPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>[ \t]*\r?\n?/gi;
443
582
  patched = patched.replace(tagPattern, function (tag, source, body) {
444
- var e_6, _a;
583
+ var e_7, _a;
445
584
  var attributes = parseAttributes(source);
446
585
  var type = (attributes.type || '').trim().toLowerCase();
447
586
  var inlined = !attributes.src;
@@ -466,16 +605,16 @@ var patchIndexHtml = function (html, version) {
466
605
  continue;
467
606
  if (attribute === 'type' && !attributes.type)
468
607
  continue;
469
- var name_1 = REPLAYED_ATTRIBUTES[attribute];
470
- spec[name_1] = attribute === 'nomodule' ? 'true' : attributes[attribute];
608
+ var name_2 = REPLAYED_ATTRIBUTES[attribute];
609
+ spec[name_2] = attribute === 'nomodule' ? 'true' : attributes[attribute];
471
610
  }
472
611
  }
473
- catch (e_6_1) { e_6 = { error: e_6_1 }; }
612
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
474
613
  finally {
475
614
  try {
476
615
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
477
616
  }
478
- finally { if (e_6) throw e_6.error; }
617
+ finally { if (e_7) throw e_7.error; }
479
618
  }
480
619
  scripts.push(spec);
481
620
  return '';
@@ -508,7 +647,7 @@ var patchIndexHtml = function (html, version) {
508
647
  function buildMockSite(target) {
509
648
  return __awaiter(this, void 0, void 0, function () {
510
649
  var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
511
- var e_7, _e;
650
+ var e_8, _e;
512
651
  var _f, _g, _h;
513
652
  return __generator(this, function (_j) {
514
653
  switch (_j.label) {
@@ -597,14 +736,24 @@ function buildMockSite(target) {
597
736
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
598
737
  }
599
738
  }
600
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
739
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
601
740
  finally {
602
741
  try {
603
742
  if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
604
743
  }
605
- finally { if (e_7) throw e_7.error; }
744
+ finally { if (e_8) throw e_8.error; }
606
745
  }
607
746
  fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
747
+ // 2. the linter, bundled from the installed packages so the code routes answer
748
+ // with the rules this CLI resolved rather than a second implementation
749
+ return [4 /*yield*/, (0, exports.buildLintBundle)(path_1.default.join(hostFolder, 'lint-browser.js'))
750
+ // sw.js sits at the site root: a worker can only control the folder it is
751
+ // served from, and the app has to be controlled from /
752
+ ];
753
+ case 3:
754
+ // 2. the linter, bundled from the installed packages so the code routes answer
755
+ // with the rules this CLI resolved rather than a second implementation
756
+ _j.sent();
608
757
  // sw.js sits at the site root: a worker can only control the folder it is
609
758
  // served from, and the app has to be controlled from /
610
759
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -630,6 +779,7 @@ function buildMockSite(target) {
630
779
  'intercept.js',
631
780
  'boot.js',
632
781
  'api-browser.js',
782
+ 'lint-browser.js',
633
783
  'webout-prelude.json'
634
784
  ]
635
785
  .map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
@@ -640,7 +790,8 @@ function buildMockSite(target) {
640
790
  fs_1.default.writeFileSync(swPath, fs_1.default
641
791
  .readFileSync(swPath, 'utf8')
642
792
  .replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
643
- .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')")));
793
+ .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')"))
794
+ .replace("importScripts('/host/lint-browser.js')", "importScripts('/host/lint-browser.js?v=".concat(hostVersion, "')")));
644
795
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
645
796
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
646
797
  if (hasSnapshot) {
@@ -673,3 +824,4 @@ function buildMockSite(target) {
673
824
  });
674
825
  });
675
826
  }
827
+ var templateObject_1;
@@ -169,7 +169,8 @@ describe('browser-hosted build', function () {
169
169
  'persist.js',
170
170
  'intercept.js',
171
171
  'boot.js',
172
- 'api-browser.js'
172
+ 'api-browser.js',
173
+ 'lint-browser.js'
173
174
  ]), _b = _a.next(); !_b.done; _b = _a.next()) {
174
175
  asset = _b.value;
175
176
  expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', asset))).toBe(true);
@@ -209,11 +210,33 @@ describe('browser-hosted build', function () {
209
210
  // a rebuild that changes either file has to change the URL
210
211
  expect(sw).toMatch(/importScripts\('\/host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
211
212
  expect(sw).toMatch(/importScripts\('\/host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
213
+ expect(sw).toMatch(/importScripts\('\/host\/lint-browser\.js\?v=[0-9a-f]{10}'\)/);
212
214
  expect(sw).not.toContain("importScripts('/host/runtime-core.js')");
213
215
  return [2 /*return*/];
214
216
  }
215
217
  });
216
218
  }); });
219
+ it('bundles the linter from the installed packages, ready for a worker', function () { return __awaiter(void 0, void 0, void 0, function () {
220
+ var bundle, vm, realm;
221
+ return __generator(this, function (_a) {
222
+ switch (_a.label) {
223
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
224
+ case 1:
225
+ _a.sent();
226
+ bundle = fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'lint-browser.js'), 'utf8');
227
+ // the bundle carries the package's code service, not a second linter
228
+ expect(bundle).toContain('SASjsCode');
229
+ expect(bundle).toContain('createCodeService');
230
+ vm = require('vm');
231
+ realm = { globalThis: null, console: console };
232
+ realm.globalThis = realm;
233
+ vm.createContext(realm);
234
+ vm.runInContext(bundle, realm);
235
+ expect(typeof realm.SASjsCode.createCodeService).toBe('function');
236
+ return [2 /*return*/];
237
+ }
238
+ });
239
+ }); });
217
240
  it('names the package when its browser build is missing, and keeps the previous site', function () { return __awaiter(void 0, void 0, void 0, function () {
218
241
  var browserBuild, exists;
219
242
  return __generator(this, function (_a) {
@@ -35,17 +35,6 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
36
  }
37
37
  };
38
- var __values = (this && this.__values) || function(o) {
39
- var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
40
- if (m) return m.call(o);
41
- if (o && typeof o.length === "number") return {
42
- next: function () {
43
- if (o && i >= o.length) o = void 0;
44
- return { value: o && o[i++], done: !o };
45
- }
46
- };
47
- throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
48
- };
49
38
  var __read = (this && this.__read) || function (o, n) {
50
39
  var m = typeof Symbol === "function" && o[Symbol.iterator];
51
40
  if (!m) return o;
@@ -62,13 +51,26 @@ var __read = (this && this.__read) || function (o, n) {
62
51
  }
63
52
  return ar;
64
53
  };
54
+ var __values = (this && this.__values) || function(o) {
55
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
56
+ if (m) return m.call(o);
57
+ if (o && typeof o.length === "number") return {
58
+ next: function () {
59
+ if (o && i >= o.length) o = void 0;
60
+ return { value: o && o[i++], done: !o };
61
+ }
62
+ };
63
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
64
+ };
65
65
  var __importDefault = (this && this.__importDefault) || function (mod) {
66
66
  return (mod && mod.__esModule) ? mod : { "default": mod };
67
67
  };
68
68
  Object.defineProperty(exports, "__esModule", { value: true });
69
69
  var path_1 = __importDefault(require("path"));
70
70
  var api_1 = require("@sasjs/api");
71
+ var os_1 = __importDefault(require("os"));
71
72
  var yazl_1 = __importDefault(require("yazl"));
73
+ var mockSite_1 = require("../mockSite");
72
74
  /**
73
75
  * The other half of the parity claim: the service worker is what turns an
74
76
  * /SASjsApi/ request into a response, so its framing (webout body, the _debug
@@ -87,6 +89,19 @@ var drivePath = function (relative) {
87
89
  var apiBrowserBuild = function () {
88
90
  return path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
89
91
  };
92
+ /**
93
+ * The lint bundle the site ships, built once for the suite from the installed
94
+ * packages - the same call `sasjs build` makes, so the worker under test answers
95
+ * the code routes with the code the site carries.
96
+ */
97
+ var lintBundlePath;
98
+ var lintBundle = function () {
99
+ if (!lintBundlePath) {
100
+ lintBundlePath = path_1.default.join(os_1.default.tmpdir(), "sasjs-lint-".concat(process.pid, ".js"));
101
+ (0, mockSite_1.buildLintBundle)(lintBundlePath);
102
+ }
103
+ return lintBundlePath;
104
+ };
90
105
  var loadWorker = function () {
91
106
  jest.resetModules();
92
107
  var handlers = {};
@@ -108,6 +123,13 @@ var loadWorker = function () {
108
123
  return;
109
124
  if (path.includes('api-browser'))
110
125
  return require(apiBrowserBuild());
126
+ if (path.includes('lint-browser')) {
127
+ // the bundle assigns globalThis.SASjsCode, and this harness keeps the
128
+ // worker's `self` apart from the module global
129
+ require(lintBundle());
130
+ self.SASjsCode = global.SASjsCode;
131
+ return;
132
+ }
111
133
  require('../assets/runtime-core.js');
112
134
  // The corpus needs the same drive with its writes refused, without building
113
135
  // a second site. The flag is flipped after the case has seeded, because a
@@ -133,10 +155,20 @@ var loadWorker = function () {
133
155
  };
134
156
  };
135
157
  require('../assets/sw.js');
136
- global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
158
+ global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
159
+ var target;
137
160
  var _a;
138
161
  return __generator(this, function (_b) {
139
- return [2 /*return*/, String(url).includes('vfs-seed')
162
+ target = typeof input === 'string' ? input : (input && input.url) || String(input);
163
+ // a navigation back to the site root: the static host's copy of the page
164
+ if (new URL(target, origin).pathname === '/')
165
+ return [2 /*return*/, new Response('<html><body>static page</body></html>', {
166
+ status: 200,
167
+ headers: { 'content-type': 'text/html' }
168
+ })];
169
+ if (target.includes('index.html'))
170
+ return [2 /*return*/, new Response('<!doctype html><html><head><title>App</title></head><body>home</body></html>', { status: 200, headers: { 'content-type': 'text/html' } })];
171
+ return [2 /*return*/, target.includes('vfs-seed')
140
172
  ? jsonResponse({
141
173
  files: (_a = {},
142
174
  _a[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
@@ -699,85 +731,204 @@ describe('service worker host', function () {
699
731
  }
700
732
  });
701
733
  }); });
702
- it('answers the documented 501 for the capabilities a static origin lacks', function () { return __awaiter(void 0, void 0, void 0, function () {
703
- var dispatch, cases, cases_1, cases_1_1, _a, method, url, capability, response, _b, e_1_1, _c, _d, url, response, _e, e_2_1;
704
- var e_1, _f, e_2, _g;
705
- return __generator(this, function (_h) {
706
- switch (_h.label) {
734
+ it('answers the Admin reports from the worker activity log', function () { return __awaiter(void 0, void 0, void 0, function () {
735
+ var dispatch, empty, _a, afterFirst, _b, listed, lines, infoLines, diagnostics, report, logs, _c, tail, tailBody, wrongName, missing;
736
+ return __generator(this, function (_d) {
737
+ switch (_d.label) {
707
738
  case 0:
708
739
  dispatch = seeded().dispatch;
709
- cases = [
710
- ['GET', '/SASjsApi/admin/diagnostics', 'admin service'],
711
- ['GET', '/SASjsApi/admin/logs', 'admin service'],
712
- ['GET', '/SASjsApi/admin/logs/app.log', 'admin service'],
713
- [
714
- 'POST',
715
- '/SASjsApi/authConfig/synchroniseWithLDAP',
716
- 'directory service'
717
- ]
718
- ];
719
- _h.label = 1;
740
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/diagnostics')];
720
741
  case 1:
721
- _h.trys.push([1, 7, 8, 9]);
722
- cases_1 = __values(cases), cases_1_1 = cases_1.next();
723
- _h.label = 2;
742
+ empty = _d.sent();
743
+ _a = expect;
744
+ return [4 /*yield*/, empty.json()];
724
745
  case 2:
725
- if (!!cases_1_1.done) return [3 /*break*/, 6];
726
- _a = __read(cases_1_1.value, 3), method = _a[0], url = _a[1], capability = _a[2];
727
- return [4 /*yield*/, dispatch(url, { method: method })];
746
+ _a.apply(void 0, [(_d.sent()).logs]).toEqual({
747
+ folder: 'worker memory',
748
+ fileCount: 0,
749
+ totalBytes: 0,
750
+ newestFile: null,
751
+ newestModified: null
752
+ });
753
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
728
754
  case 3:
729
- response = _h.sent();
730
- expect(response.status).toBe(501);
755
+ afterFirst = _d.sent();
731
756
  _b = expect;
732
- return [4 /*yield*/, response.json()];
757
+ return [4 /*yield*/, afterFirst.json()];
733
758
  case 4:
734
- _b.apply(void 0, [(_h.sent()).message]).toContain(capability);
735
- _h.label = 5;
759
+ _b.apply(void 0, [(_d.sent()).files.length]).toBe(1);
760
+ // the file's mtime is the last write, not the moment a report was read:
761
+ // /info was the request immediately before this listing
762
+ return [4 /*yield*/, dispatch('/SASjsApi/info')];
736
763
  case 5:
737
- cases_1_1 = cases_1.next();
738
- return [3 /*break*/, 2];
739
- case 6: return [3 /*break*/, 9];
764
+ // the file's mtime is the last write, not the moment a report was read:
765
+ // /info was the request immediately before this listing
766
+ _d.sent();
767
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
768
+ case 6: return [4 /*yield*/, (_d.sent()).json()];
740
769
  case 7:
741
- e_1_1 = _h.sent();
742
- e_1 = { error: e_1_1 };
743
- return [3 /*break*/, 9];
744
- case 8:
745
- try {
746
- if (cases_1_1 && !cases_1_1.done && (_f = cases_1.return)) _f.call(cases_1);
747
- }
748
- finally { if (e_1) throw e_1.error; }
749
- return [7 /*endfinally*/];
770
+ listed = (_d.sent())
771
+ .files[0];
772
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/app.log?lines=20')];
773
+ case 8: return [4 /*yield*/, (_d.sent()).json()];
750
774
  case 9:
751
- _h.trys.push([9, 15, 16, 17]);
752
- _c = __values(['/SASjsApi/code/lint', '/SASjsApi/code/format']), _d = _c.next();
753
- _h.label = 10;
775
+ lines = (_d.sent()).content.split('\n');
776
+ infoLines = lines.filter(function (line) {
777
+ return line.includes('GET /SASjsApi/info 200');
778
+ });
779
+ expect(infoLines.length).toBe(1);
780
+ expect(infoLines[0].startsWith(listed.modified)).toBe(true);
781
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/diagnostics')];
754
782
  case 10:
755
- if (!!_d.done) return [3 /*break*/, 14];
756
- url = _d.value;
757
- return [4 /*yield*/, dispatch(url, runRequest({ code: 'data a;' }))];
783
+ diagnostics = _d.sent();
784
+ expect(diagnostics.status).toBe(200);
785
+ return [4 /*yield*/, diagnostics.json()];
758
786
  case 11:
759
- response = _h.sent();
760
- expect(response.status).toBe(501);
761
- _e = expect;
762
- return [4 /*yield*/, response.json()];
787
+ report = _d.sent();
788
+ expect(report.server.mode).toBe('browser');
789
+ expect(report.filesystem).toBeNull();
790
+ expect(report.logs.fileCount).toBe(1);
791
+ expect(typeof report.memory.heapTotalBytes).toBe('number');
792
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
763
793
  case 12:
764
- _e.apply(void 0, [(_h.sent()).message]).toContain('code service');
765
- _h.label = 13;
794
+ logs = _d.sent();
795
+ _c = expect;
796
+ return [4 /*yield*/, logs.json()];
766
797
  case 13:
767
- _d = _c.next();
768
- return [3 /*break*/, 10];
769
- case 14: return [3 /*break*/, 17];
798
+ _c.apply(void 0, [(_d.sent()).files.map(function (file) { return file.name; })]).toEqual([
799
+ 'app.log'
800
+ ]);
801
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/app.log?lines=5')];
802
+ case 14:
803
+ tail = _d.sent();
804
+ return [4 /*yield*/, tail.json()];
770
805
  case 15:
771
- e_2_1 = _h.sent();
772
- e_2 = { error: e_2_1 };
773
- return [3 /*break*/, 17];
806
+ tailBody = _d.sent();
807
+ expect(tail.status).toBe(200);
808
+ expect(tailBody.content).toContain('GET /SASjsApi/info 200');
809
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/secret.txt')];
774
810
  case 16:
775
- try {
776
- if (_d && !_d.done && (_g = _c.return)) _g.call(_c);
777
- }
778
- finally { if (e_2) throw e_2.error; }
811
+ wrongName = _d.sent();
812
+ expect(wrongName.status).toBe(400);
813
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/other.log')];
814
+ case 17:
815
+ missing = _d.sent();
816
+ expect(missing.status).toBe(404);
817
+ return [2 /*return*/];
818
+ }
819
+ });
820
+ }); });
821
+ it('imports the fixture directory, and unpublishes the site itself', function () { return __awaiter(void 0, void 0, void 0, function () {
822
+ var dispatch, synced, _a, unpublished, _b, other;
823
+ return __generator(this, function (_c) {
824
+ switch (_c.label) {
825
+ case 0:
826
+ dispatch = seeded().dispatch;
827
+ return [4 /*yield*/, dispatch('/SASjsApi/authConfig/synchroniseWithLDAP', { method: 'POST' })];
828
+ case 1:
829
+ synced = _c.sent();
830
+ expect(synced.status).toBe(200);
831
+ _a = expect;
832
+ return [4 /*yield*/, synced.json()];
833
+ case 2:
834
+ _a.apply(void 0, [_c.sent()]).toEqual({ userCount: 0, groupCount: 0 });
835
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy', {
836
+ method: 'DELETE',
837
+ headers: { 'content-type': 'application/json' },
838
+ body: JSON.stringify({
839
+ streamServiceName: 'Test',
840
+ appLoc: appLoc
841
+ })
842
+ })];
843
+ case 3:
844
+ unpublished = _c.sent();
845
+ expect(unpublished.status).toBe(200);
846
+ _b = expect;
847
+ return [4 /*yield*/, unpublished.json()];
848
+ case 4:
849
+ _b.apply(void 0, [_c.sent()]).toEqual({ status: 'success' });
850
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy', {
851
+ method: 'DELETE',
852
+ headers: { 'content-type': 'application/json' },
853
+ body: JSON.stringify({
854
+ streamServiceName: 'Other',
855
+ appLoc: '/Public/app/other'
856
+ })
857
+ })];
858
+ case 5:
859
+ other = _c.sent();
860
+ expect(other.status).toBe(404);
861
+ return [2 /*return*/];
862
+ }
863
+ });
864
+ }); });
865
+ it('lints and formats a buffer with the bundled linter', function () { return __awaiter(void 0, void 0, void 0, function () {
866
+ var dispatch, linted, diagnostics, formatted, _a;
867
+ return __generator(this, function (_b) {
868
+ switch (_b.label) {
869
+ case 0:
870
+ dispatch = seeded().dispatch;
871
+ return [4 /*yield*/, dispatch('/SASjsApi/code/lint', runRequest({ code: 'data a;\nrun;\n' }))];
872
+ case 1:
873
+ linted = _b.sent();
874
+ expect(linted.status).toBe(200);
875
+ return [4 /*yield*/, linted.json()];
876
+ case 2:
877
+ diagnostics = _b.sent();
878
+ expect(Array.isArray(diagnostics)).toBe(true);
879
+ expect(diagnostics.length).toBeGreaterThan(0);
880
+ expect(diagnostics[0].message).toContain('Doxygen');
881
+ return [4 /*yield*/, dispatch('/SASjsApi/code/format', runRequest({ code: 'data a;set b;run;' }))];
882
+ case 3:
883
+ formatted = _b.sent();
884
+ expect(formatted.status).toBe(200);
885
+ _a = expect;
886
+ return [4 /*yield*/, formatted.text()];
887
+ case 4:
888
+ _a.apply(void 0, [_b.sent()]).toContain('@brief');
889
+ return [2 /*return*/];
890
+ }
891
+ });
892
+ }); });
893
+ it('serves the build page at the home route, and falls back when it fails', function () { return __awaiter(void 0, void 0, void 0, function () {
894
+ var worker, response, _a, runtime, original, fallback, body;
895
+ return __generator(this, function (_b) {
896
+ switch (_b.label) {
897
+ case 0:
898
+ worker = seeded();
899
+ return [4 /*yield*/, worker.dispatch('/')];
900
+ case 1:
901
+ response = _b.sent();
902
+ expect(response.status).toBe(200);
903
+ expect(response.headers.get('content-type')).toContain('text/html');
904
+ _a = expect;
905
+ return [4 /*yield*/, response.text()];
906
+ case 2:
907
+ _a.apply(void 0, [_b.sent()]).toContain('<body>home</body>');
908
+ runtime = worker.self.SASjsMockRuntime;
909
+ original = runtime.ensureReady;
910
+ runtime.ensureReady = function () { return __awaiter(void 0, void 0, void 0, function () {
911
+ return __generator(this, function (_a) {
912
+ throw new Error('home route exploded');
913
+ });
914
+ }); };
915
+ _b.label = 3;
916
+ case 3:
917
+ _b.trys.push([3, , 6, 7]);
918
+ return [4 /*yield*/, worker.dispatch('/')];
919
+ case 4:
920
+ fallback = _b.sent();
921
+ expect(fallback.status).toBe(200);
922
+ return [4 /*yield*/, fallback.text()];
923
+ case 5:
924
+ body = _b.sent();
925
+ expect(body).toContain('static page');
926
+ expect(body).not.toContain('home route exploded');
927
+ return [3 /*break*/, 7];
928
+ case 6:
929
+ runtime.ensureReady = original;
779
930
  return [7 /*endfinally*/];
780
- case 17: return [2 /*return*/];
931
+ case 7: return [2 /*return*/];
781
932
  }
782
933
  });
783
934
  }); });
@@ -1856,7 +2007,7 @@ describe('service worker host', function () {
1856
2007
  }); });
1857
2008
  });
1858
2009
  describe('conformance corpus', function () {
1859
- var e_3, _a;
2010
+ var e_1, _a;
1860
2011
  /**
1861
2012
  * The corpus is what "the same API" means: the cases @sasjs/api runs against
1862
2013
  * its own services, and against a live sasjs/server, run here against this
@@ -1865,7 +2016,7 @@ describe('service worker host', function () {
1865
2016
  * used to have - and it is a test rather than a review.
1866
2017
  */
1867
2018
  var httpFor = function (testCase) {
1868
- var e_4, _a;
2019
+ var e_2, _a;
1869
2020
  var _b;
1870
2021
  var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
1871
2022
  var query = new URLSearchParams();
@@ -1876,12 +2027,12 @@ describe('service worker host', function () {
1876
2027
  query.set(key, String(value));
1877
2028
  }
1878
2029
  }
1879
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
2030
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
1880
2031
  finally {
1881
2032
  try {
1882
2033
  if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
1883
2034
  }
1884
- finally { if (e_4) throw e_4.error; }
2035
+ finally { if (e_2) throw e_2.error; }
1885
2036
  }
1886
2037
  var suffix = query.toString() ? "?".concat(query) : '';
1887
2038
  var init = { method: method };
@@ -1912,8 +2063,8 @@ describe('service worker host', function () {
1912
2063
  * does.
1913
2064
  */
1914
2065
  var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
1915
- var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_5_1;
1916
- var e_6, _g, e_5, _h;
2066
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_3_1;
2067
+ var e_4, _g, e_3, _h;
1917
2068
  return __generator(this, function (_j) {
1918
2069
  switch (_j.label) {
1919
2070
  case 0:
@@ -1925,12 +2076,12 @@ describe('service worker host', function () {
1925
2076
  text["/sasjs/drive/files".concat(file)] = contents;
1926
2077
  }
1927
2078
  }
1928
- catch (e_6_1) { e_6 = { error: e_6_1 }; }
2079
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
1929
2080
  finally {
1930
2081
  try {
1931
2082
  if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
1932
2083
  }
1933
- finally { if (e_6) throw e_6.error; }
2084
+ finally { if (e_4) throw e_4.error; }
1934
2085
  }
1935
2086
  return [4 /*yield*/, runtime.load(text)];
1936
2087
  case 1:
@@ -1953,14 +2104,14 @@ describe('service worker host', function () {
1953
2104
  return [3 /*break*/, 3];
1954
2105
  case 6: return [3 /*break*/, 9];
1955
2106
  case 7:
1956
- e_5_1 = _j.sent();
1957
- e_5 = { error: e_5_1 };
2107
+ e_3_1 = _j.sent();
2108
+ e_3 = { error: e_3_1 };
1958
2109
  return [3 /*break*/, 9];
1959
2110
  case 8:
1960
2111
  try {
1961
2112
  if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
1962
2113
  }
1963
- finally { if (e_5) throw e_5.error; }
2114
+ finally { if (e_3) throw e_3.error; }
1964
2115
  return [7 /*endfinally*/];
1965
2116
  case 9: return [2 /*return*/];
1966
2117
  }
@@ -2028,12 +2179,12 @@ describe('service worker host', function () {
2028
2179
  _loop_1(testCase);
2029
2180
  }
2030
2181
  }
2031
- catch (e_3_1) { e_3 = { error: e_3_1 }; }
2182
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
2032
2183
  finally {
2033
2184
  try {
2034
2185
  if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
2035
2186
  }
2036
- finally { if (e_3) throw e_3.error; }
2187
+ finally { if (e_1) throw e_1.error; }
2037
2188
  }
2038
2189
  });
2039
2190
  });
package/package.json CHANGED
@@ -78,6 +78,7 @@
78
78
  "axios": "1.20.0",
79
79
  "chalk": "4.1.2",
80
80
  "dotenv": "18.0.8",
81
+ "esbuild": "0.28.2",
81
82
  "js-base64": "3.9.4",
82
83
  "jsdom": "26.0.0",
83
84
  "lodash.uniqby": "4.7.0",
@@ -124,5 +125,5 @@
124
125
  "ws": "8.21.0",
125
126
  "nanoid": "3.3.18"
126
127
  },
127
- "version": "6.15.0"
128
+ "version": "6.16.0"
128
129
  }