@sasjs/cli 6.31.0 → 6.32.1

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,13 +23,34 @@
23
23
  'use strict'
24
24
 
25
25
  /**
26
- * The site's root, from the page's own URL: the console sits at `<base>/api/`,
27
- * and half a URL (no trailing slash) is the shape a person types.
26
+ * The site's root.
27
+ *
28
+ * Taken from the worker that controls this page, `<base>/sw.js`: that is the
29
+ * one statement of the base the page carries, and it holds wherever the console
30
+ * sits - a copy under `<base>/tools/api-console/` still calls the site's own
31
+ * API rather than a folder that happens to be two levels up. Only a worker
32
+ * whose root covers this page counts, since a page can be controlled by another
33
+ * site's worker, and one registered below the page names a folder inside it. A
34
+ * page with no such worker falls back to its own URL, one level below the root.
28
35
  */
29
36
  const base = (function () {
30
- const path = location.pathname.replace(/\/index\.html$/, '/')
31
- const cut = /\/api\/?$/.exec(path)
32
- return cut ? path.slice(0, cut.index + 1) : '/'
37
+ // this page's own folder, however it was typed: with the page, with the
38
+ // file, or without the trailing slash
39
+ let dir = location.pathname
40
+ if (/\/index\.html?$/.test(dir)) dir = dir.replace(/index\.html?$/, '')
41
+ if (!dir.endsWith('/')) dir += '/'
42
+ const parent = dir.replace(/[^/]+\/$/, '')
43
+
44
+ const script =
45
+ navigator.serviceWorker && navigator.serviceWorker.controller
46
+ ? navigator.serviceWorker.controller.scriptURL
47
+ : ''
48
+ if (script) {
49
+ const swPath = new URL(script).pathname
50
+ const root = /\/sw\.js$/.test(swPath) ? swPath.replace(/sw\.js$/, '') : ''
51
+ if (root && parent.startsWith(root)) return root
52
+ }
53
+ return parent || '/'
33
54
  })()
34
55
 
35
56
  const apiUrl = (path) => base + String(path).replace(/^\/+/, '')
@@ -76,7 +97,7 @@
76
97
  * Starts the demo backend for this page.
77
98
  *
78
99
  * The application's boot loader registers the worker; a visitor who lands on
79
- * `/api/` first has no loader, and every request would reach the static host
100
+ * `/api-console/` first has no loader, and every request would reach the static host
80
101
  * and 404. So: register it beside the site, wait for it to control this page,
81
102
  * then load `host/persist.js`, which is what hands the worker the drive this
82
103
  * browser already has and keeps whatever this page changes.
@@ -692,6 +713,14 @@
692
713
  )
693
714
  })
694
715
 
716
+ /** Where a reset returns to: this page, without the flag but with the route. */
717
+ const resetTarget = () => {
718
+ const query = new URLSearchParams(location.search)
719
+ query.delete('reset')
720
+ const rest = query.toString()
721
+ return location.pathname + (rest ? '?' + rest : '')
722
+ }
723
+
695
724
  /**
696
725
  * `?reset=1` starts the demo again from the state the build ships.
697
726
  *
@@ -719,7 +748,9 @@
719
748
  'The demo could not be reset: ' + ((e && e.message) || e)
720
749
  return true
721
750
  }
722
- location.replace(base + 'api/')
751
+ // back to this page, with the flag gone: the console's folder is the page's
752
+ // own, so a build that moves the folder cannot strand the redirect
753
+ location.replace(resetTarget())
723
754
  return true
724
755
  }
725
756
 
@@ -755,6 +786,10 @@
755
786
  if (chosen) select(chosen)
756
787
  }
757
788
 
789
+ // the redirect itself cannot be observed in a page that does not navigate; the
790
+ // choice of target can, and it is what a folder move would break
791
+ window.SASjsApiConsole = { resetTarget }
792
+
758
793
  start().catch((e) => {
759
794
  const list = el('routes')
760
795
  if (list) list.textContent = 'The console could not start: ' + e.message
@@ -9,7 +9,7 @@
9
9
  The API console for a browser-hosted build.
10
10
 
11
11
  Static, and deliberately so: no bundler, no framework, no network of its
12
- own beyond the API it is documenting. It is served from <base>/api/ and
12
+ own beyond the API it is documenting. It is served from <base>/api-console/ and
13
13
  every request it makes is the same request an application makes, so the
14
14
  service worker answers it and the answer is the mock's real answer.
15
15
 
@@ -37,6 +37,7 @@
37
37
  */
38
38
  importScripts('host/runtime-core.js')
39
39
  importScripts('host/api-browser.js')
40
+ importScripts('host/lint-browser.js')
40
41
 
41
42
  const R = self.SASjsMockRuntime
42
43
 
@@ -967,19 +968,18 @@ const LINT_CONFIG_FILE = '.sasjslint'
967
968
  * The linter, bundled at build time from the installed @sasjs/api/code and
968
969
  * @sasjs/lint (see mockSite.ts).
969
970
  *
970
- * Imported on the first code route rather than by the worker at boot: it is
971
- * around 40KB of the worker's payload for two routes (`code/lint` and
972
- * `code/format`), and the install has to finish before the page may make its
973
- * first request - so every visit pays it, including the ones that never open the
974
- * editor. The build stamps the version into the URL below, so the copy fetched
975
- * here is cache-busted with the rest of the host.
971
+ * Imported with the rest of the host, not on the first code route: a service
972
+ * worker may only `importScripts` while its own script is being evaluated, so a
973
+ * call made from a request handler fails with "The script at ... failed to load"
974
+ * - the linter would be missing exactly where it is needed. What stays lazy is the
975
+ * service built from it: `createCodeService` is cheap, and only a code route
976
+ * calls it.
976
977
  */
977
978
  let codeService
978
979
  let linter
979
980
 
980
981
  const code = () => {
981
982
  if (codeService) return codeService
982
- importScripts('host/lint-browser.js')
983
983
  linter = self.SASjsCode
984
984
  codeService = linter.createCodeService({ lintConfig })
985
985
  return codeService
@@ -1732,22 +1732,6 @@ const handle = async (request, url) => {
1732
1732
  return response
1733
1733
  }
1734
1734
 
1735
- // The console's own files under `/api/`, replaced at build time from the list the
1736
- // builder writes the folder from, so the two cannot drift apart.
1737
- const API_CONSOLE_FILES = [
1738
- 'index.html',
1739
- 'console.js',
1740
- 'console.css',
1741
- 'operations.json'
1742
- ]
1743
-
1744
- const CONSOLE_FILE = new RegExp(
1745
- '^/api/(' +
1746
- API_CONSOLE_FILES.map((name) => name.replace(/\./g, '\\.')).join('|') +
1747
- ')$',
1748
- 'i'
1749
- )
1750
-
1751
1735
  self.addEventListener('fetch', (event) => {
1752
1736
  const url = new URL(event.request.url)
1753
1737
  if (url.origin !== self.location.origin) return
@@ -1780,20 +1764,13 @@ self.addEventListener('fetch', (event) => {
1780
1764
  // listing is the server's web route, and an interface that lists apps asks for
1781
1765
  // it on every visit.
1782
1766
  //
1783
- // `/api` and `/api/` on their own are the console page - `api/index.html`,
1784
- // which this build writes - so the worker stays out of the way there and the
1785
- // static host answers it. Every other `/api/...` is an API call on the short
1786
- // spelling, which routeRequest canonicalises to `/SASjsApi`.
1767
+ // `/api` and `/api/` on their own are left to the static host: a site's own
1768
+ // application can answer that route (this framework's server interface does),
1769
+ // and the worker has no business putting an API answer in front of it. Every
1770
+ // other `/api/...` is an API call on the short spelling, which routeRequest
1771
+ // canonicalises to `/SASjsApi`.
1787
1772
  if (/^\/api\/?$/i.test(path)) return
1788
1773
 
1789
- // And the console's own files, which live under that prefix: a path naming one
1790
- // of them is the page's, not an API call, so the static host answers it. Named
1791
- // one by one on purpose - an extension test would also pass through
1792
- // `/api/admin/logs/foo.log`, which is a real call on the short spelling. The
1793
- // list is replaced at build time with the one the builder writes the folder
1794
- // from, so a file added there cannot arrive here as an API path.
1795
- if (CONSOLE_FILE.test(path)) return
1796
-
1797
1774
  if (!/^\/(SASjsApi|SASLogon|AppStream|api|app)(\/|$)/i.test(path)) return
1798
1775
  event.respondWith(handle(event.request, url))
1799
1776
  })
@@ -14,7 +14,7 @@ export interface MockSiteResult {
14
14
  * large folder or file. Zero when the site ships no snapshot.
15
15
  */
16
16
  snapshotShards: number;
17
- /** True when the site carries the API console at <base>/api/. */
17
+ /** True when the site carries the API console at <base>/api-console/. */
18
18
  apiConsole: boolean;
19
19
  /** True when a recorded SAS session was shipped, for .sas runs to answer. */
20
20
  sasTranscript: boolean;
@@ -1008,7 +1008,15 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
1008
1008
  }
1009
1009
  return { patched: patched, warnings: warnings };
1010
1010
  };
1011
- /** The files the console consists of, under `<output>/api`. */
1011
+ /**
1012
+ * The folder the console lives in, under the site root.
1013
+ *
1014
+ * Not `api`: an application can answer `/api` itself - this framework's own server
1015
+ * interface does - and a page there would collide with its route, so a site
1016
+ * refresh on that screen would land on the console instead of the application.
1017
+ */
1018
+ var API_CONSOLE_DIR = 'api-console';
1019
+ /** The files the console consists of, under `<output>/api-console`. */
1012
1020
  var API_CONSOLE_FILES = [
1013
1021
  'index.html',
1014
1022
  'console.js',
@@ -1016,7 +1024,7 @@ var API_CONSOLE_FILES = [
1016
1024
  'operations.json'
1017
1025
  ];
1018
1026
  /**
1019
- * The API console: a page at `<base>/api/` that lists every operation the API
1027
+ * The API console: a page at `<base>/api-console/` that lists every operation the API
1020
1028
  * document declares and sends them against this host.
1021
1029
  *
1022
1030
  * The route table is the same one the worker answers from (`ROUTES` from
@@ -1028,8 +1036,8 @@ var API_CONSOLE_FILES = [
1028
1036
  * a visit costs the demo nothing until someone types the URL.
1029
1037
  */
1030
1038
  var writeApiConsole = function (outputFolder, version) {
1031
- var folder = path_1.default.join(outputFolder, 'api');
1032
- // `<base>/api/` is this build's, not the project's: a site that ships its own
1039
+ var folder = path_1.default.join(outputFolder, API_CONSOLE_DIR);
1040
+ // `<base>/api-console/` is this build's, not the project's: a site that ships its own
1033
1041
  // files there would have them replaced without a word, so it keeps them and
1034
1042
  // the console is left out
1035
1043
  var taken = API_CONSOLE_FILES.filter(function (file) {
@@ -1040,7 +1048,7 @@ var writeApiConsole = function (outputFolder, version) {
1040
1048
  written: false,
1041
1049
  operations: 0,
1042
1050
  warning: "The site already carries ".concat(taken
1043
- .map(function (file) { return 'api/' + file; })
1051
+ .map(function (file) { return API_CONSOLE_DIR + '/' + file; })
1044
1052
  .join(', '), ", so the API console was not written. ") +
1045
1053
  'Move those files, or set streamConfig.mockApi to false to silence this.'
1046
1054
  };
@@ -1260,10 +1268,7 @@ function buildMockSite(target) {
1260
1268
  .readFileSync(swPath, 'utf8')
1261
1269
  .replace("importScripts('host/runtime-core.js')", "importScripts('host/runtime-core.js?v=".concat(hostVersion, "')"))
1262
1270
  .replace("importScripts('host/api-browser.js')", "importScripts('host/api-browser.js?v=".concat(hostVersion, "')"))
1263
- .replace("importScripts('host/lint-browser.js')", "importScripts('host/lint-browser.js?v=".concat(hostVersion, "')"))
1264
- // the console's file list, from the one place that writes the folder: a
1265
- // second copy here would let a new file be served as an API path
1266
- .replace(/const API_CONSOLE_FILES = \[[^\]]*\]/, "const API_CONSOLE_FILES = ".concat(JSON.stringify(API_CONSOLE_FILES))));
1271
+ .replace("importScripts('host/lint-browser.js')", "importScripts('host/lint-browser.js?v=".concat(hostVersion, "')")));
1267
1272
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
1268
1273
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
1269
1274
  snapshotShards = 0;
@@ -675,7 +675,7 @@ describe('the API console', function () {
675
675
  case 0:
676
676
  jest.resetModules();
677
677
  document.body.innerHTML = bodyOf(page());
678
- history.replaceState({}, '', '/api/?route=' + encodeURIComponent('GET /SASjsApi/user'));
678
+ history.replaceState({}, '', '/api-console/?route=' + encodeURIComponent('GET /SASjsApi/user'));
679
679
  global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
680
680
  return __generator(this, function (_a) {
681
681
  return [2 /*return*/, String(url).includes('operations.json')
@@ -699,7 +699,7 @@ describe('the API console', function () {
699
699
  return [3 /*break*/, 1];
700
700
  case 4:
701
701
  expect(document.getElementById('title').textContent).toBe('GET /SASjsApi/user');
702
- history.replaceState({}, '', '/api/');
702
+ history.replaceState({}, '', '/api-console/');
703
703
  return [2 /*return*/];
704
704
  }
705
705
  });
@@ -1093,7 +1093,7 @@ describe('the API console', function () {
1093
1093
  // jsdom does not navigate, and the redirect is the last thing the page does:
1094
1094
  // what matters here is that the drive was reset before it
1095
1095
  return [4 /*yield*/, loadConsole(function () {
1096
- history.replaceState({}, '', '/api/?reset=1');
1096
+ history.replaceState({}, '', '/api-console/?reset=1');
1097
1097
  window.SASjsMock = { reset: reset };
1098
1098
  })];
1099
1099
  case 1:
@@ -1105,7 +1105,7 @@ describe('the API console', function () {
1105
1105
  _a.sent();
1106
1106
  expect(reset).toHaveBeenCalled();
1107
1107
  expect(sessionStorage.length).toBe(0);
1108
- history.replaceState({}, '', '/api/');
1108
+ history.replaceState({}, '', '/api-console/');
1109
1109
  return [2 /*return*/];
1110
1110
  }
1111
1111
  });
@@ -1125,7 +1125,7 @@ describe('the API console', function () {
1125
1125
  return [4 /*yield*/, loadConsole(function () {
1126
1126
  // no host bridge on this page at all
1127
1127
  delete window.SASjsMock;
1128
- history.replaceState({}, '', '/api/?reset=1');
1128
+ history.replaceState({}, '', '/api-console/?reset=1');
1129
1129
  })];
1130
1130
  case 1:
1131
1131
  _a.sent();
@@ -1136,7 +1136,7 @@ describe('the API console', function () {
1136
1136
  _a.sent();
1137
1137
  expect(global.indexedDB.deleteDatabase).toHaveBeenCalledWith('sasjs-mock-host');
1138
1138
  expect(document.getElementById('routes').textContent).toContain('still holds the drive');
1139
- history.replaceState({}, '', '/api/');
1139
+ history.replaceState({}, '', '/api-console/');
1140
1140
  delete global.indexedDB;
1141
1141
  return [2 /*return*/];
1142
1142
  }
@@ -1152,7 +1152,7 @@ describe('the API console', function () {
1152
1152
  }); }); });
1153
1153
  sessionStorage.setItem('mock-console-token', 'kept');
1154
1154
  return [4 /*yield*/, loadConsole(function () {
1155
- history.replaceState({}, '', '/api/?reset=0');
1155
+ history.replaceState({}, '', '/api-console/?reset=0');
1156
1156
  window.SASjsMock = { reset: reset };
1157
1157
  })];
1158
1158
  case 1:
@@ -1162,7 +1162,7 @@ describe('the API console', function () {
1162
1162
  _a.sent();
1163
1163
  expect(reset).not.toHaveBeenCalled();
1164
1164
  expect(sessionStorage.getItem('mock-console-token')).toBe('kept');
1165
- history.replaceState({}, '', '/api/');
1165
+ history.replaceState({}, '', '/api-console/');
1166
1166
  return [2 /*return*/];
1167
1167
  }
1168
1168
  });
@@ -1331,6 +1331,72 @@ describe('the API console', function () {
1331
1331
  }
1332
1332
  });
1333
1333
  }); });
1334
+ it('says so, rather than reloading again, when the worker never claims the page', function () { return __awaiter(void 0, void 0, void 0, function () {
1335
+ var error, host, i;
1336
+ return __generator(this, function (_a) {
1337
+ switch (_a.label) {
1338
+ case 0:
1339
+ error = jest.spyOn(console, 'error').mockImplementation(function () { });
1340
+ host = withServiceWorker();
1341
+ sessionStorage.setItem('mock-console-reloaded', '1');
1342
+ return [4 /*yield*/, loadConsole()];
1343
+ case 1:
1344
+ _a.sent();
1345
+ i = 0;
1346
+ _a.label = 2;
1347
+ case 2:
1348
+ if (!(i < 200)) return [3 /*break*/, 5];
1349
+ if (error.mock.calls.some(function (call) { return /take control/.test(String(call[0])); }))
1350
+ return [3 /*break*/, 5];
1351
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 25); })];
1352
+ case 3:
1353
+ _a.sent();
1354
+ _a.label = 4;
1355
+ case 4:
1356
+ i++;
1357
+ return [3 /*break*/, 2];
1358
+ case 5:
1359
+ expect(error.mock.calls.some(function (call) { return /take control/.test(String(call[0])); })).toBe(true);
1360
+ expect(host.sw.register).toHaveBeenCalled();
1361
+ error.mockRestore();
1362
+ return [2 /*return*/];
1363
+ }
1364
+ });
1365
+ }); });
1366
+ it('returns to its own page after a reset, wherever that page is', function () { return __awaiter(void 0, void 0, void 0, function () {
1367
+ var deleted, page;
1368
+ return __generator(this, function (_a) {
1369
+ switch (_a.label) {
1370
+ case 0:
1371
+ deleted = [];
1372
+ global.indexedDB = {
1373
+ deleteDatabase: function (name) {
1374
+ deleted.push(name);
1375
+ var request = {};
1376
+ setTimeout(function () { return request.onsuccess && request.onsuccess(); }, 5);
1377
+ return request;
1378
+ }
1379
+ };
1380
+ return [4 /*yield*/, loadConsole(function () {
1381
+ delete window.SASjsMock;
1382
+ history.replaceState({}, '', '/demo/api-console/?reset=1&route=GET%20%2FSASjsApi%2Fuser');
1383
+ })];
1384
+ case 1:
1385
+ _a.sent();
1386
+ return [4 /*yield*/, settle(function () { return deleted.length > 0; })];
1387
+ case 2:
1388
+ _a.sent();
1389
+ page = window.SASjsApiConsole;
1390
+ expect(page).toBeDefined();
1391
+ // the folder is the page's, the flag goes, and the route the visitor was on
1392
+ // stays: a reset should not cost them the deep link
1393
+ expect(page.resetTarget()).toBe('/demo/api-console/?route=GET+%2FSASjsApi%2Fuser');
1394
+ delete global.indexedDB;
1395
+ history.replaceState({}, '', '/api-console/');
1396
+ return [2 /*return*/];
1397
+ }
1398
+ });
1399
+ }); });
1334
1400
  it('drops the token when the visitor goes back to the demo administrator', function () { return __awaiter(void 0, void 0, void 0, function () {
1335
1401
  var calls, back;
1336
1402
  return __generator(this, function (_a) {
@@ -66,7 +66,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
66
66
  Object.defineProperty(exports, "__esModule", { value: true });
67
67
  /**
68
68
  * @jest-environment jsdom
69
- * @jest-environment-options {"url": "http://localhost/demo/api/"}
69
+ * @jest-environment-options {"url": "http://localhost/demo/api-console/"}
70
70
  */
71
71
  var fs_1 = __importDefault(require("fs"));
72
72
  var path_1 = __importDefault(require("path"));
@@ -74,11 +74,10 @@ var path_1 = __importDefault(require("path"));
74
74
  * The console under a base path.
75
75
  *
76
76
  * `mockWebBasePath` is a supported option (a site published at `/demo`), and the
77
- * console derives everything from its own URL: the API calls come from
78
- * `location.pathname` with `/api/` cut off, and the worker is registered beside
79
- * the site. Both are right at the origin root by accident there - this file runs
80
- * the same page at `/demo/api/`, where the wrong derivation is silent: the page
81
- * still renders, and every call goes to a path the host does not answer.
77
+ * console works out its root from the worker it registered - `<base>/sw.js` - with
78
+ * its own URL as the fallback. This file runs the page at `/demo/api-console/`,
79
+ * where a wrong derivation is silent: the page still renders, and every call goes
80
+ * to a path the host does not answer.
82
81
  */
83
82
  var assets = path_1.default.join(__dirname, '..', 'assets');
84
83
  var page = function () { return fs_1.default.readFileSync(path_1.default.join(assets, 'api-index.html'), 'utf8'); };
@@ -136,6 +135,119 @@ describe('the API console under a base path', function () {
136
135
  afterEach(function () {
137
136
  delete navigator.serviceWorker;
138
137
  });
138
+ it('takes the root from the worker that covers it, not from its own folder', function () { return __awaiter(void 0, void 0, void 0, function () {
139
+ var realCreate, sw, asked;
140
+ return __generator(this, function (_a) {
141
+ switch (_a.label) {
142
+ case 0:
143
+ // the shapes where the two disagree are the only ones that prove the worker
144
+ // is read at all: the site's worker at `/demo/sw.js` controlling a console
145
+ // two folders down still names `/demo/`, which the folder alone cannot
146
+ document.body.innerHTML = bodyOf(page());
147
+ document.head.innerHTML =
148
+ '<meta name="sasjs-mock-version" content="3.0.0" />';
149
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
150
+ return __generator(this, function (_a) {
151
+ return [2 /*return*/, String(url).includes('operations.json')
152
+ ? jsonResponse(operations)
153
+ : jsonResponse({ format: 1, users: [] })];
154
+ });
155
+ }); });
156
+ realCreate = document.createElement.bind(document);
157
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
158
+ var node = realCreate(tag);
159
+ if (String(tag).toLowerCase() === 'script')
160
+ setTimeout(function () { return node.dispatchEvent(new Event('load')); }, 0);
161
+ return node;
162
+ });
163
+ sw = {
164
+ controller: { scriptURL: 'http://localhost/demo/sw.js' },
165
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
166
+ return [2 /*return*/, ({})];
167
+ }); }); }),
168
+ addEventListener: jest.fn()
169
+ };
170
+ Object.defineProperty(navigator, 'serviceWorker', {
171
+ value: sw,
172
+ configurable: true
173
+ });
174
+ history.replaceState({}, '', '/demo/tools/api-console/');
175
+ jest.resetModules();
176
+ require('../assets/api-console.js');
177
+ return [4 /*yield*/, settle(function () {
178
+ return global.fetch.mock.calls.some(function (call) {
179
+ return String(call[0]).includes('identity.json');
180
+ });
181
+ })];
182
+ case 1:
183
+ _a.sent();
184
+ asked = global.fetch.mock.calls.map(function (call) {
185
+ return String(call[0]);
186
+ });
187
+ // the site's root, not the folder two levels up from the page
188
+ expect(asked).toContain('/demo/host/identity.json');
189
+ expect(asked.some(function (url) { return url.startsWith('/demo/tools/'); })).toBe(false);
190
+ history.replaceState({}, '', '/demo/tools/api-console/');
191
+ return [2 /*return*/];
192
+ }
193
+ });
194
+ }); });
195
+ it('ignores a worker that belongs to another site', function () { return __awaiter(void 0, void 0, void 0, function () {
196
+ var realCreate, sw, asked;
197
+ return __generator(this, function (_a) {
198
+ switch (_a.label) {
199
+ case 0:
200
+ // a page can be controlled by some other site's worker; its `sw.js` names a
201
+ // root that has nothing to do with this console, so the page's own folder has
202
+ // to win
203
+ document.body.innerHTML = bodyOf(page());
204
+ document.head.innerHTML =
205
+ '<meta name="sasjs-mock-version" content="3.0.0" />';
206
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
207
+ return __generator(this, function (_a) {
208
+ return [2 /*return*/, String(url).includes('operations.json')
209
+ ? jsonResponse(operations)
210
+ : jsonResponse({ format: 1, users: [] })];
211
+ });
212
+ }); });
213
+ realCreate = document.createElement.bind(document);
214
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
215
+ var node = realCreate(tag);
216
+ if (String(tag).toLowerCase() === 'script')
217
+ setTimeout(function () { return node.dispatchEvent(new Event('load')); }, 0);
218
+ return node;
219
+ });
220
+ sw = {
221
+ controller: { scriptURL: 'http://localhost/other/sw.js' },
222
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
223
+ return [2 /*return*/, ({})];
224
+ }); }); }),
225
+ addEventListener: jest.fn()
226
+ };
227
+ Object.defineProperty(navigator, 'serviceWorker', {
228
+ value: sw,
229
+ configurable: true
230
+ });
231
+ history.replaceState({}, '', '/demo/api-console/');
232
+ jest.resetModules();
233
+ require('../assets/api-console.js');
234
+ return [4 /*yield*/, settle(function () {
235
+ return global.fetch.mock.calls.some(function (call) {
236
+ return String(call[0]).includes('identity.json');
237
+ });
238
+ })];
239
+ case 1:
240
+ _a.sent();
241
+ asked = global.fetch.mock.calls.map(function (call) {
242
+ return String(call[0]);
243
+ });
244
+ expect(asked).toContain('/demo/host/identity.json');
245
+ expect(asked.some(function (url) { return url.startsWith('/other/'); })).toBe(false);
246
+ history.replaceState({}, '', '/demo/api-console/');
247
+ return [2 /*return*/];
248
+ }
249
+ });
250
+ }); });
139
251
  it('finds the folder from a URL without its trailing slash', function () { return __awaiter(void 0, void 0, void 0, function () {
140
252
  var realCreate, sw, asked;
141
253
  return __generator(this, function (_a) {
@@ -143,8 +255,8 @@ describe('the API console under a base path', function () {
143
255
  case 0:
144
256
  // `/demo/api` is the shape a person types, and the page used to read that as
145
257
  // the origin root: every call went to /SASjsApi/... and the worker to /sw.js
146
- history.replaceState({}, '', '/demo/api');
147
- expect(location.pathname).toBe('/demo/api');
258
+ history.replaceState({}, '', '/demo/api-console');
259
+ expect(location.pathname).toBe('/demo/api-console');
148
260
  document.body.innerHTML = bodyOf(page());
149
261
  document.head.innerHTML =
150
262
  '<meta name="sasjs-mock-version" content="2.0.0" />';
@@ -187,7 +299,7 @@ describe('the API console under a base path', function () {
187
299
  return String(call[0]);
188
300
  });
189
301
  expect(asked).toContain('/demo/host/identity.json');
190
- history.replaceState({}, '', '/demo/api/');
302
+ history.replaceState({}, '', '/demo/api-console/');
191
303
  return [2 /*return*/];
192
304
  }
193
305
  });
@@ -1051,14 +1051,14 @@ describe('browser-hosted build', function () {
1051
1051
  * the host drift.
1052
1052
  */
1053
1053
  it('ships an API console built from the route table', function () { return __awaiter(void 0, void 0, void 0, function () {
1054
- var result, folder, shipped, sw, listed, operations, createUser, html, version, appHtml, loader;
1054
+ var result, folder, shipped, sw, operations, createUser, html, version, appHtml, loader;
1055
1055
  return __generator(this, function (_a) {
1056
1056
  switch (_a.label) {
1057
1057
  case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
1058
1058
  case 1:
1059
1059
  result = _a.sent();
1060
1060
  expect(result.apiConsole).toBe(true);
1061
- folder = path_1.default.join(out(), 'api');
1061
+ folder = path_1.default.join(out(), 'api-console');
1062
1062
  shipped = fs_1.default.readdirSync(folder).sort();
1063
1063
  // every file the folder holds, and the worker's passthrough list is that same
1064
1064
  // list: a second copy in sw.js would let a new file be answered as an API path
@@ -1069,9 +1069,7 @@ describe('browser-hosted build', function () {
1069
1069
  'operations.json'
1070
1070
  ]);
1071
1071
  sw = fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8');
1072
- listed = /const API_CONSOLE_FILES = (\[[^\]]*\])/.exec(sw);
1073
- expect(listed).not.toBeNull();
1074
- expect(JSON.parse(listed[1]).sort()).toEqual(shipped);
1072
+ expect(sw).not.toContain('API_CONSOLE_FILES');
1075
1073
  operations = JSON.parse(fs_1.default.readFileSync(path_1.default.join(folder, 'operations.json'), 'utf8'));
1076
1074
  expect(operations.length).toBeGreaterThan(20);
1077
1075
  createUser = operations.find(function (operation) { return operation.id === 'POST /SASjsApi/user'; });
@@ -1091,12 +1089,12 @@ describe('browser-hosted build', function () {
1091
1089
  }
1092
1090
  });
1093
1091
  }); });
1094
- it('leaves the console out when the site ships its own api folder, and says so', function () { return __awaiter(void 0, void 0, void 0, function () {
1092
+ it('leaves the console out when the site ships its own console folder, and says so', function () { return __awaiter(void 0, void 0, void 0, function () {
1095
1093
  var own, result;
1096
1094
  return __generator(this, function (_a) {
1097
1095
  switch (_a.label) {
1098
1096
  case 0:
1099
- own = path_1.default.join(root, 'client', 'dist', 'api');
1097
+ own = path_1.default.join(root, 'client', 'dist', 'api-console');
1100
1098
  fs_1.default.mkdirSync(own, { recursive: true });
1101
1099
  fs_1.default.writeFileSync(path_1.default.join(own, 'index.html'), '<p>ours</p>');
1102
1100
  return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
@@ -1108,9 +1106,9 @@ describe('browser-hosted build', function () {
1108
1106
  // the project's file is not replaced, and the loss is reported rather than
1109
1107
  // silent: the build has no way to know which of the two the site wants
1110
1108
  expect(result.apiConsole).toBe(false);
1111
- expect(fs_1.default.readFileSync(path_1.default.join(out(), 'api', 'index.html'), 'utf8')).toBe('<p>ours</p>');
1112
- expect(fs_1.default.existsSync(path_1.default.join(out(), 'api', 'console.js'))).toBe(false);
1113
- expect(process.logger.warn).toHaveBeenCalledWith(expect.stringContaining('api/index.html'));
1109
+ expect(fs_1.default.readFileSync(path_1.default.join(out(), 'api-console', 'index.html'), 'utf8')).toBe('<p>ours</p>');
1110
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'api-console', 'console.js'))).toBe(false);
1111
+ expect(process.logger.warn).toHaveBeenCalledWith(expect.stringContaining('api-console/index.html'));
1114
1112
  return [2 /*return*/];
1115
1113
  }
1116
1114
  });
@@ -1123,7 +1121,7 @@ describe('browser-hosted build', function () {
1123
1121
  case 1:
1124
1122
  result = _a.sent();
1125
1123
  expect(result.apiConsole).toBe(false);
1126
- expect(fs_1.default.existsSync(path_1.default.join(out(), 'api'))).toBe(false);
1124
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'api-console'))).toBe(false);
1127
1125
  return [2 /*return*/];
1128
1126
  }
1129
1127
  });
@@ -86,7 +86,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
86
86
  return (mod && mod.__esModule) ? mod : { "default": mod };
87
87
  };
88
88
  Object.defineProperty(exports, "__esModule", { value: true });
89
- var fs_1 = __importDefault(require("fs"));
90
89
  var path_1 = __importDefault(require("path"));
91
90
  var api_1 = require("@sasjs/api");
92
91
  var os_1 = __importDefault(require("os"));
@@ -1443,25 +1442,27 @@ describe('service worker host', function () {
1443
1442
  });
1444
1443
  }); });
1445
1444
  /**
1446
- * The linter is not part of the install.
1445
+ * The linter is imported with the host, because it has to be.
1447
1446
  *
1448
- * The page cannot make its first request until the worker has installed, so
1449
- * anything the worker imports at boot is on the critical path of every
1450
- * visit. The linter answers two routes; it is fetched when one of them is
1451
- * called.
1447
+ * A service worker may only `importScripts` while its own script is being
1448
+ * evaluated: a call made from a request handler fails with "The script at ...
1449
+ * failed to load", which is what a lazy import gave a real browser - the two
1450
+ * code routes answered 500. So the import is at boot, and the assertion is
1451
+ * that a code route answers *without* importing anything at request time.
1452
1452
  */
1453
- it('fetches the linter on the first code route, not at boot', function () { return __awaiter(void 0, void 0, void 0, function () {
1454
- var worker, response;
1453
+ it('imports the linter at boot and answers a code route without importing', function () { return __awaiter(void 0, void 0, void 0, function () {
1454
+ var worker, before, response;
1455
1455
  return __generator(this, function (_a) {
1456
1456
  switch (_a.label) {
1457
1457
  case 0:
1458
1458
  worker = seeded();
1459
- expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(false);
1459
+ expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(true);
1460
+ before = worker.imported.length;
1460
1461
  return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({ code: 'data a;\nrun;\n' }))];
1461
1462
  case 1:
1462
1463
  response = _a.sent();
1463
1464
  expect(response.status).toBe(200);
1464
- expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(true);
1465
+ expect(worker.imported.length).toBe(before);
1465
1466
  return [2 /*return*/];
1466
1467
  }
1467
1468
  });
@@ -2566,46 +2567,25 @@ describe('service worker host', function () {
2566
2567
  * The console's own files are the page, not an API call.
2567
2568
  *
2568
2569
  * `/api/...` is the API's short spelling, so the worker answers it - except for
2569
- * the four files the console is made of, which the static host serves. Getting
2570
- * that wrong answers the page's own script with an API 404, and the console
2571
- * renders nothing.
2570
+ * `/api` itself, which a site's own application may answer.
2572
2571
  */
2573
- it('leaves the API console to the static host, and still answers /api calls', function () { return __awaiter(void 0, void 0, void 0, function () {
2574
- var worker, source, listed, files, files_1, files_1_1, name_1, log, response;
2575
- var e_2, _a;
2576
- return __generator(this, function (_b) {
2577
- switch (_b.label) {
2572
+ it('leaves /api to the site, and answers the short spelling below it', function () { return __awaiter(void 0, void 0, void 0, function () {
2573
+ var worker, response;
2574
+ return __generator(this, function (_a) {
2575
+ switch (_a.label) {
2578
2576
  case 0:
2579
2577
  worker = seeded();
2580
- source = fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'assets', 'sw.js'), 'utf8');
2581
- listed = /const API_CONSOLE_FILES = \[([^\]]*)\]/.exec(source);
2582
- expect(listed).not.toBeNull();
2583
- files = listed[1]
2584
- .split(',')
2585
- .map(function (entry) { return entry.trim().replace(/^'|'$/g, ''); })
2586
- .filter(Boolean);
2587
- expect(files.length).toBeGreaterThan(3);
2578
+ // a site's own application can answer /api (the server interface does), so the
2579
+ // worker hands it on; `/api/<something>` is an API call on the short spelling
2580
+ expect(worker.dispatch('/api')).toBeUndefined();
2588
2581
  expect(worker.dispatch('/api/')).toBeUndefined();
2589
- try {
2590
- for (files_1 = __values(files), files_1_1 = files_1.next(); !files_1_1.done; files_1_1 = files_1.next()) {
2591
- name_1 = files_1_1.value;
2592
- expect(worker.dispatch("/api/".concat(name_1))).toBeUndefined();
2593
- }
2594
- }
2595
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
2596
- finally {
2597
- try {
2598
- if (files_1_1 && !files_1_1.done && (_a = files_1.return)) _a.call(files_1);
2599
- }
2600
- finally { if (e_2) throw e_2.error; }
2601
- }
2602
- return [4 /*yield*/, worker.dispatch('/api/admin/logs/missing.log')];
2603
- case 1:
2604
- log = _b.sent();
2605
- expect(log.status).not.toBeUndefined();
2582
+ // the console has a folder of its own, which is not an API prefix: the static
2583
+ // host serves it and the worker never sees it
2584
+ expect(worker.dispatch('/api-console/console.js')).toBeUndefined();
2585
+ expect(worker.dispatch('/api-console/operations.json')).toBeUndefined();
2606
2586
  return [4 /*yield*/, worker.dispatch('/api/user')];
2607
- case 2:
2608
- response = _b.sent();
2587
+ case 1:
2588
+ response = _a.sent();
2609
2589
  expect(response.status).toBe(200);
2610
2590
  return [2 /*return*/];
2611
2591
  }
@@ -2630,8 +2610,8 @@ describe('service worker host', function () {
2630
2610
  });
2631
2611
  }); });
2632
2612
  it('publishes, lists, serves and removes an app through the package surface', function () { return __awaiter(void 0, void 0, void 0, function () {
2633
- var dispatch, _a, _b, path_2, listing, _c, e_3_1, _d, _e, path_3, landing, e_4_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_5_1, _j, _k, path_5, page, _l, e_6_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
2634
- var e_3, _q, e_4, _r, e_5, _s, e_6, _t;
2613
+ var dispatch, _a, _b, path_2, listing, _c, e_2_1, _d, _e, path_3, landing, e_3_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_4_1, _j, _k, path_5, page, _l, e_5_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
2614
+ var e_2, _q, e_3, _r, e_4, _s, e_5, _t;
2635
2615
  return __generator(this, function (_u) {
2636
2616
  switch (_u.label) {
2637
2617
  case 0:
@@ -2658,14 +2638,14 @@ describe('service worker host', function () {
2658
2638
  return [3 /*break*/, 2];
2659
2639
  case 6: return [3 /*break*/, 9];
2660
2640
  case 7:
2661
- e_3_1 = _u.sent();
2662
- e_3 = { error: e_3_1 };
2641
+ e_2_1 = _u.sent();
2642
+ e_2 = { error: e_2_1 };
2663
2643
  return [3 /*break*/, 9];
2664
2644
  case 8:
2665
2645
  try {
2666
2646
  if (_b && !_b.done && (_q = _a.return)) _q.call(_a);
2667
2647
  }
2668
- finally { if (e_3) throw e_3.error; }
2648
+ finally { if (e_2) throw e_2.error; }
2669
2649
  return [7 /*endfinally*/];
2670
2650
  case 9:
2671
2651
  _u.trys.push([9, 14, 15, 16]);
@@ -2685,14 +2665,14 @@ describe('service worker host', function () {
2685
2665
  return [3 /*break*/, 10];
2686
2666
  case 13: return [3 /*break*/, 16];
2687
2667
  case 14:
2688
- e_4_1 = _u.sent();
2689
- e_4 = { error: e_4_1 };
2668
+ e_3_1 = _u.sent();
2669
+ e_3 = { error: e_3_1 };
2690
2670
  return [3 /*break*/, 16];
2691
2671
  case 15:
2692
2672
  try {
2693
2673
  if (_e && !_e.done && (_r = _d.return)) _r.call(_d);
2694
2674
  }
2695
- finally { if (e_4) throw e_4.error; }
2675
+ finally { if (e_3) throw e_3.error; }
2696
2676
  return [7 /*endfinally*/];
2697
2677
  case 16: return [4 /*yield*/, dispatch('/AppStream')];
2698
2678
  case 17:
@@ -2745,14 +2725,14 @@ describe('service worker host', function () {
2745
2725
  return [3 /*break*/, 22];
2746
2726
  case 26: return [3 /*break*/, 29];
2747
2727
  case 27:
2748
- e_5_1 = _u.sent();
2749
- e_5 = { error: e_5_1 };
2728
+ e_4_1 = _u.sent();
2729
+ e_4 = { error: e_4_1 };
2750
2730
  return [3 /*break*/, 29];
2751
2731
  case 28:
2752
2732
  try {
2753
2733
  if (_g && !_g.done && (_s = _f.return)) _s.call(_f);
2754
2734
  }
2755
- finally { if (e_5) throw e_5.error; }
2735
+ finally { if (e_4) throw e_4.error; }
2756
2736
  return [7 /*endfinally*/];
2757
2737
  case 29:
2758
2738
  _u.trys.push([29, 35, 36, 37]);
@@ -2779,14 +2759,14 @@ describe('service worker host', function () {
2779
2759
  return [3 /*break*/, 30];
2780
2760
  case 34: return [3 /*break*/, 37];
2781
2761
  case 35:
2782
- e_6_1 = _u.sent();
2783
- e_6 = { error: e_6_1 };
2762
+ e_5_1 = _u.sent();
2763
+ e_5 = { error: e_5_1 };
2784
2764
  return [3 /*break*/, 37];
2785
2765
  case 36:
2786
2766
  try {
2787
2767
  if (_k && !_k.done && (_t = _j.return)) _t.call(_j);
2788
2768
  }
2789
- finally { if (e_6) throw e_6.error; }
2769
+ finally { if (e_5) throw e_5.error; }
2790
2770
  return [7 /*endfinally*/];
2791
2771
  case 37: return [4 /*yield*/, dispatch("/app/".concat(app.id))];
2792
2772
  case 38:
@@ -3735,7 +3715,7 @@ describe('service worker host', function () {
3735
3715
  }); });
3736
3716
  });
3737
3717
  describe('conformance corpus', function () {
3738
- var e_7, _a, e_8, _b;
3718
+ var e_6, _a, e_7, _b;
3739
3719
  /**
3740
3720
  * The corpus is what "the same API" means: the cases @sasjs/api runs against
3741
3721
  * its own services, and against a live sasjs/server, run here against this
@@ -3744,7 +3724,7 @@ describe('service worker host', function () {
3744
3724
  * used to have - and it is a test rather than a review.
3745
3725
  */
3746
3726
  var httpFor = function (testCase) {
3747
- var e_9, _a;
3727
+ var e_8, _a;
3748
3728
  var _b;
3749
3729
  var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
3750
3730
  var query = new URLSearchParams();
@@ -3755,12 +3735,12 @@ describe('service worker host', function () {
3755
3735
  query.set(key, String(value));
3756
3736
  }
3757
3737
  }
3758
- catch (e_9_1) { e_9 = { error: e_9_1 }; }
3738
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
3759
3739
  finally {
3760
3740
  try {
3761
3741
  if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
3762
3742
  }
3763
- finally { if (e_9) throw e_9.error; }
3743
+ finally { if (e_8) throw e_8.error; }
3764
3744
  }
3765
3745
  var suffix = query.toString() ? "?".concat(query) : '';
3766
3746
  var init = { method: method };
@@ -3791,8 +3771,8 @@ describe('service worker host', function () {
3791
3771
  * does.
3792
3772
  */
3793
3773
  var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
3794
- var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_10_1;
3795
- var e_11, _g, e_10, _h;
3774
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_9_1;
3775
+ var e_10, _g, e_9, _h;
3796
3776
  return __generator(this, function (_j) {
3797
3777
  switch (_j.label) {
3798
3778
  case 0:
@@ -3804,12 +3784,12 @@ describe('service worker host', function () {
3804
3784
  text["/sasjs/drive/files".concat(file)] = contents;
3805
3785
  }
3806
3786
  }
3807
- catch (e_11_1) { e_11 = { error: e_11_1 }; }
3787
+ catch (e_10_1) { e_10 = { error: e_10_1 }; }
3808
3788
  finally {
3809
3789
  try {
3810
3790
  if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
3811
3791
  }
3812
- finally { if (e_11) throw e_11.error; }
3792
+ finally { if (e_10) throw e_10.error; }
3813
3793
  }
3814
3794
  return [4 /*yield*/, runtime.load(text)];
3815
3795
  case 1:
@@ -3832,14 +3812,14 @@ describe('service worker host', function () {
3832
3812
  return [3 /*break*/, 3];
3833
3813
  case 6: return [3 /*break*/, 9];
3834
3814
  case 7:
3835
- e_10_1 = _j.sent();
3836
- e_10 = { error: e_10_1 };
3815
+ e_9_1 = _j.sent();
3816
+ e_9 = { error: e_9_1 };
3837
3817
  return [3 /*break*/, 9];
3838
3818
  case 8:
3839
3819
  try {
3840
3820
  if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
3841
3821
  }
3842
- finally { if (e_10) throw e_10.error; }
3822
+ finally { if (e_9) throw e_9.error; }
3843
3823
  return [7 /*endfinally*/];
3844
3824
  case 9: return [2 /*return*/];
3845
3825
  }
@@ -3950,12 +3930,12 @@ describe('service worker host', function () {
3950
3930
  _loop_1(testCase);
3951
3931
  }
3952
3932
  }
3953
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
3933
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
3954
3934
  finally {
3955
3935
  try {
3956
3936
  if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
3957
3937
  }
3958
- finally { if (e_7) throw e_7.error; }
3938
+ finally { if (e_6) throw e_6.error; }
3959
3939
  }
3960
3940
  /** The app a removal case names, as the corpus states it. */
3961
3941
  var CORPUS_APP = {
@@ -4024,7 +4004,7 @@ describe('service worker host', function () {
4024
4004
  return __generator(this, function (_a) {
4025
4005
  files = {};
4026
4006
  walk = function (members, prefix) {
4027
- var e_12, _a;
4007
+ var e_11, _a;
4028
4008
  var _b;
4029
4009
  try {
4030
4010
  for (var _c = __values(members || []), _d = _c.next(); !_d.done; _d = _c.next()) {
@@ -4039,12 +4019,12 @@ describe('service worker host', function () {
4039
4019
  : String((_b = member.code) !== null && _b !== void 0 ? _b : '');
4040
4020
  }
4041
4021
  }
4042
- catch (e_12_1) { e_12 = { error: e_12_1 }; }
4022
+ catch (e_11_1) { e_11 = { error: e_11_1 }; }
4043
4023
  finally {
4044
4024
  try {
4045
4025
  if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
4046
4026
  }
4047
- finally { if (e_12) throw e_12.error; }
4027
+ finally { if (e_11) throw e_11.error; }
4048
4028
  }
4049
4029
  };
4050
4030
  walk(tree === null || tree === void 0 ? void 0 : tree.members, '');
@@ -4061,7 +4041,7 @@ describe('service worker host', function () {
4061
4041
  */
4062
4042
  var deployRequest = function (testCase) { return __awaiter(void 0, void 0, void 0, function () {
4063
4043
  var _a, requestPath, init, body, archive, form, _b, _c, _d, field, value;
4064
- var e_13, _e;
4044
+ var e_12, _e;
4065
4045
  return __generator(this, function (_f) {
4066
4046
  switch (_f.label) {
4067
4047
  case 0:
@@ -4080,12 +4060,12 @@ describe('service worker host', function () {
4080
4060
  form.set(field, String(value));
4081
4061
  }
4082
4062
  }
4083
- catch (e_13_1) { e_13 = { error: e_13_1 }; }
4063
+ catch (e_12_1) { e_12 = { error: e_12_1 }; }
4084
4064
  finally {
4085
4065
  try {
4086
4066
  if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
4087
4067
  }
4088
- finally { if (e_13) throw e_13.error; }
4068
+ finally { if (e_12) throw e_12.error; }
4089
4069
  }
4090
4070
  form.set('file', new Blob([new Uint8Array(archive)]), 'deploy.zip');
4091
4071
  return [2 /*return*/, { requestPath: requestPath, init: { method: 'POST', body: form } }];
@@ -4094,8 +4074,8 @@ describe('service worker host', function () {
4094
4074
  }); };
4095
4075
  var _loop_2 = function (testCase) {
4096
4076
  it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
4097
- var worker, _a, requestPath, init, response, listing, ids, _b, _c, path_6, file, e_14_1, _d, _e, path_7, file, e_15_1;
4098
- var e_14, _f, e_15, _g;
4077
+ var worker, _a, requestPath, init, response, listing, ids, _b, _c, path_6, file, e_13_1, _d, _e, path_7, file, e_14_1;
4078
+ var e_13, _f, e_14, _g;
4099
4079
  var _h, _j, _k;
4100
4080
  return __generator(this, function (_l) {
4101
4081
  switch (_l.label) {
@@ -4147,14 +4127,14 @@ describe('service worker host', function () {
4147
4127
  return [3 /*break*/, 9];
4148
4128
  case 12: return [3 /*break*/, 15];
4149
4129
  case 13:
4150
- e_14_1 = _l.sent();
4151
- e_14 = { error: e_14_1 };
4130
+ e_13_1 = _l.sent();
4131
+ e_13 = { error: e_13_1 };
4152
4132
  return [3 /*break*/, 15];
4153
4133
  case 14:
4154
4134
  try {
4155
4135
  if (_c && !_c.done && (_f = _b.return)) _f.call(_b);
4156
4136
  }
4157
- finally { if (e_14) throw e_14.error; }
4137
+ finally { if (e_13) throw e_13.error; }
4158
4138
  return [7 /*endfinally*/];
4159
4139
  case 15:
4160
4140
  _l.trys.push([15, 20, 21, 22]);
@@ -4173,14 +4153,14 @@ describe('service worker host', function () {
4173
4153
  return [3 /*break*/, 16];
4174
4154
  case 19: return [3 /*break*/, 22];
4175
4155
  case 20:
4176
- e_15_1 = _l.sent();
4177
- e_15 = { error: e_15_1 };
4156
+ e_14_1 = _l.sent();
4157
+ e_14 = { error: e_14_1 };
4178
4158
  return [3 /*break*/, 22];
4179
4159
  case 21:
4180
4160
  try {
4181
4161
  if (_e && !_e.done && (_g = _d.return)) _g.call(_d);
4182
4162
  }
4183
- finally { if (e_15) throw e_15.error; }
4163
+ finally { if (e_14) throw e_14.error; }
4184
4164
  return [7 /*endfinally*/];
4185
4165
  case 22: return [2 /*return*/];
4186
4166
  }
@@ -4213,12 +4193,12 @@ describe('service worker host', function () {
4213
4193
  _loop_2(testCase);
4214
4194
  }
4215
4195
  }
4216
- catch (e_8_1) { e_8 = { error: e_8_1 }; }
4196
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
4217
4197
  finally {
4218
4198
  try {
4219
4199
  if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
4220
4200
  }
4221
- finally { if (e_8) throw e_8.error; }
4201
+ finally { if (e_7) throw e_7.error; }
4222
4202
  }
4223
4203
  });
4224
4204
  /**
@@ -4230,7 +4210,7 @@ describe('service worker host', function () {
4230
4210
  * are the package's.
4231
4211
  */
4232
4212
  describe('identity and record conformance', function () {
4233
- var e_16, _a;
4213
+ var e_15, _a;
4234
4214
  /**
4235
4215
  * Boots the worker from a fixture, the way the page does: the case's own
4236
4216
  * identity, and `subject: null` for no implicit caller, so a case that
@@ -4263,7 +4243,7 @@ describe('service worker host', function () {
4263
4243
  };
4264
4244
  /** The request a case describes, as HTTP: path parameters filled, token sent. */
4265
4245
  var httpForCase = function (testCase) {
4266
- var e_17, _a;
4246
+ var e_16, _a;
4267
4247
  var _b = __read(testCase.route.split(' '), 2), method = _b[0], routePath = _b[1];
4268
4248
  var filled = routePath.replace(/\{(\w+)\}/g, function (_match, name) {
4269
4249
  return encodeURIComponent(String((testCase.request.params || {})[name]));
@@ -4276,12 +4256,12 @@ describe('service worker host', function () {
4276
4256
  query.set(key, String(value));
4277
4257
  }
4278
4258
  }
4279
- catch (e_17_1) { e_17 = { error: e_17_1 }; }
4259
+ catch (e_16_1) { e_16 = { error: e_16_1 }; }
4280
4260
  finally {
4281
4261
  try {
4282
4262
  if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
4283
4263
  }
4284
- finally { if (e_17) throw e_17.error; }
4264
+ finally { if (e_16) throw e_16.error; }
4285
4265
  }
4286
4266
  var suffix = query.toString() ? "?".concat(query) : '';
4287
4267
  var headers = {};
@@ -4403,12 +4383,12 @@ describe('service worker host', function () {
4403
4383
  _loop_3(testCase);
4404
4384
  }
4405
4385
  }
4406
- catch (e_16_1) { e_16 = { error: e_16_1 }; }
4386
+ catch (e_15_1) { e_15 = { error: e_15_1 }; }
4407
4387
  finally {
4408
4388
  try {
4409
4389
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
4410
4390
  }
4411
- finally { if (e_16) throw e_16.error; }
4391
+ finally { if (e_15) throw e_15.error; }
4412
4392
  }
4413
4393
  it('answers as the fixture administrator when no subject was chosen', function () { return __awaiter(void 0, void 0, void 0, function () {
4414
4394
  var worker, response, rules;
@@ -21,7 +21,7 @@ declare module '@sasjs/utils/types/config' {
21
21
  */
22
22
  mockWebBasePath?: string;
23
23
  /**
24
- * Whether the mock site carries the API console at `<base>/api/`.
24
+ * Whether the mock site carries the API console at `<base>/api-console/`.
25
25
  *
26
26
  * On unless it is set to false: the console is how a demonstration site lets
27
27
  * a visitor try the API it is a mock of, and a page nothing links to costs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/cli",
3
- "version": "6.31.0",
3
+ "version": "6.32.1",
4
4
  "description": "Command line interface for SASjs",
5
5
  "engines": {
6
6
  "node": "^22.14.0 || >=24.10.0"