@sasjs/cli 6.30.1 → 6.32.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.
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __read = (this && this.__read) || function (o, n) {
39
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
40
+ if (!m) return o;
41
+ var i = m.call(o), r, ar = [], e;
42
+ try {
43
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
44
+ }
45
+ catch (error) { e = { error: error }; }
46
+ finally {
47
+ try {
48
+ if (r && !r.done && (m = i["return"])) m.call(i);
49
+ }
50
+ finally { if (e) throw e.error; }
51
+ }
52
+ return ar;
53
+ };
54
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
55
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
56
+ if (ar || !(i in from)) {
57
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
58
+ ar[i] = from[i];
59
+ }
60
+ }
61
+ return to.concat(ar || Array.prototype.slice.call(from));
62
+ };
63
+ var __importDefault = (this && this.__importDefault) || function (mod) {
64
+ return (mod && mod.__esModule) ? mod : { "default": mod };
65
+ };
66
+ Object.defineProperty(exports, "__esModule", { value: true });
67
+ /**
68
+ * @jest-environment jsdom
69
+ * @jest-environment-options {"url": "http://localhost/demo/api-console/"}
70
+ */
71
+ var fs_1 = __importDefault(require("fs"));
72
+ var path_1 = __importDefault(require("path"));
73
+ /**
74
+ * The console under a base path.
75
+ *
76
+ * `mockWebBasePath` is a supported option (a site published at `/demo`), and the
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.
81
+ */
82
+ var assets = path_1.default.join(__dirname, '..', 'assets');
83
+ var page = function () { return fs_1.default.readFileSync(path_1.default.join(assets, 'api-index.html'), 'utf8'); };
84
+ var bodyOf = function (html) {
85
+ return html.slice(html.indexOf('<body>') + 6, html.lastIndexOf('</body>'));
86
+ };
87
+ var jsonResponse = function (body) { return ({
88
+ ok: true,
89
+ status: 200,
90
+ statusText: 'OK',
91
+ headers: { get: function () { return 'application/json'; } },
92
+ json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
93
+ return [2 /*return*/, body];
94
+ }); }); },
95
+ text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
96
+ return [2 /*return*/, JSON.stringify(body)];
97
+ }); }); }
98
+ }); };
99
+ var operations = [
100
+ {
101
+ id: 'GET /SASjsApi/user',
102
+ method: 'GET',
103
+ path: '/SASjsApi/user',
104
+ tag: 'User',
105
+ summary: 'List all users',
106
+ params: [],
107
+ requestBodies: [],
108
+ responses: [{ status: 200, description: 'Ok' }],
109
+ implemented: true
110
+ }
111
+ ];
112
+ var settle = function (check) { return __awaiter(void 0, void 0, void 0, function () {
113
+ var i;
114
+ return __generator(this, function (_a) {
115
+ switch (_a.label) {
116
+ case 0:
117
+ i = 0;
118
+ _a.label = 1;
119
+ case 1:
120
+ if (!(i < 400)) return [3 /*break*/, 4];
121
+ if (check())
122
+ return [2 /*return*/];
123
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
124
+ case 2:
125
+ _a.sent();
126
+ _a.label = 3;
127
+ case 3:
128
+ i++;
129
+ return [3 /*break*/, 1];
130
+ case 4: throw new Error('the page did not reach the state the case expected');
131
+ }
132
+ });
133
+ }); };
134
+ describe('the API console under a base path', function () {
135
+ afterEach(function () {
136
+ delete navigator.serviceWorker;
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
+ }); });
251
+ it('finds the folder from a URL without its trailing slash', function () { return __awaiter(void 0, void 0, void 0, function () {
252
+ var realCreate, sw, asked;
253
+ return __generator(this, function (_a) {
254
+ switch (_a.label) {
255
+ case 0:
256
+ // `/demo/api` is the shape a person types, and the page used to read that as
257
+ // the origin root: every call went to /SASjsApi/... and the worker to /sw.js
258
+ history.replaceState({}, '', '/demo/api-console');
259
+ expect(location.pathname).toBe('/demo/api-console');
260
+ document.body.innerHTML = bodyOf(page());
261
+ document.head.innerHTML =
262
+ '<meta name="sasjs-mock-version" content="2.0.0" />';
263
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
264
+ return __generator(this, function (_a) {
265
+ return [2 /*return*/, String(url).includes('operations.json')
266
+ ? jsonResponse(operations)
267
+ : jsonResponse({ format: 1, users: [] })];
268
+ });
269
+ }); });
270
+ realCreate = document.createElement.bind(document);
271
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
272
+ var node = realCreate(tag);
273
+ if (String(tag).toLowerCase() === 'script')
274
+ setTimeout(function () { return node.dispatchEvent(new Event('load')); }, 0);
275
+ return node;
276
+ });
277
+ sw = {
278
+ controller: {},
279
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
280
+ return [2 /*return*/, ({})];
281
+ }); }); }),
282
+ addEventListener: jest.fn()
283
+ };
284
+ Object.defineProperty(navigator, 'serviceWorker', {
285
+ value: sw,
286
+ configurable: true
287
+ });
288
+ jest.resetModules();
289
+ require('../assets/api-console.js');
290
+ return [4 /*yield*/, settle(function () { return sw.register.mock.calls.length === 1; })];
291
+ case 1:
292
+ _a.sent();
293
+ expect(sw.register.mock.calls[0][0]).toContain('/demo/sw.js?v=2.0.0');
294
+ expect(sw.register.mock.calls[0][1]).toEqual(expect.objectContaining({ scope: '/demo/' }));
295
+ return [4 /*yield*/, settle(function () { return document.querySelectorAll('#routes .route').length > 0; })];
296
+ case 2:
297
+ _a.sent();
298
+ asked = global.fetch.mock.calls.map(function (call) {
299
+ return String(call[0]);
300
+ });
301
+ expect(asked).toContain('/demo/host/identity.json');
302
+ history.replaceState({}, '', '/demo/api-console/');
303
+ return [2 /*return*/];
304
+ }
305
+ });
306
+ }); });
307
+ it('registers the worker and calls the API under the folder the site lives in', function () { return __awaiter(void 0, void 0, void 0, function () {
308
+ var calls, realCreate, sw, errors, errorSpy, warnSpy, asked;
309
+ return __generator(this, function (_a) {
310
+ switch (_a.label) {
311
+ case 0:
312
+ document.body.innerHTML = bodyOf(page());
313
+ document.head.innerHTML =
314
+ '<meta name="sasjs-mock-version" content="2.0.0" />';
315
+ calls = [];
316
+ global.fetch = jest.fn(function (url_1) {
317
+ var args_1 = [];
318
+ for (var _i = 1; _i < arguments.length; _i++) {
319
+ args_1[_i - 1] = arguments[_i];
320
+ }
321
+ return __awaiter(void 0, __spreadArray([url_1], __read(args_1), false), void 0, function (url, init) {
322
+ if (init === void 0) { init = {}; }
323
+ return __generator(this, function (_a) {
324
+ if (String(url).includes('operations.json'))
325
+ return [2 /*return*/, jsonResponse(operations)];
326
+ if (String(url).includes('identity.json'))
327
+ return [2 /*return*/, jsonResponse({ format: 1, users: [] })];
328
+ calls.push({ url: String(url), init: init });
329
+ return [2 /*return*/, jsonResponse([])];
330
+ });
331
+ });
332
+ });
333
+ realCreate = document.createElement.bind(document);
334
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
335
+ var node = realCreate(tag);
336
+ if (String(tag).toLowerCase() === 'script')
337
+ setTimeout(function () { return node.dispatchEvent(new Event('load')); }, 0);
338
+ return node;
339
+ });
340
+ sw = {
341
+ controller: {},
342
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
343
+ return [2 /*return*/, ({})];
344
+ }); }); }),
345
+ addEventListener: jest.fn()
346
+ };
347
+ Object.defineProperty(navigator, 'serviceWorker', {
348
+ value: sw,
349
+ configurable: true
350
+ });
351
+ errors = [];
352
+ errorSpy = jest
353
+ .spyOn(console, 'error')
354
+ .mockImplementation(function () {
355
+ var args = [];
356
+ for (var _i = 0; _i < arguments.length; _i++) {
357
+ args[_i] = arguments[_i];
358
+ }
359
+ return errors.push(args);
360
+ });
361
+ warnSpy = jest
362
+ .spyOn(console, 'warn')
363
+ .mockImplementation(function () {
364
+ var args = [];
365
+ for (var _i = 0; _i < arguments.length; _i++) {
366
+ args[_i] = arguments[_i];
367
+ }
368
+ return errors.push(args);
369
+ });
370
+ jest.resetModules();
371
+ require('../assets/api-console.js');
372
+ return [4 /*yield*/, settle(function () { return sw.register.mock.calls.length === 1; }).catch(function (e) {
373
+ throw new Error(e.message +
374
+ ' | errors: ' +
375
+ JSON.stringify(errors) +
376
+ ' | routes: ' +
377
+ (document.getElementById('routes') || {}).textContent);
378
+ })];
379
+ case 1:
380
+ _a.sent();
381
+ errorSpy.mockRestore();
382
+ warnSpy.mockRestore();
383
+ expect(sw.register.mock.calls[0][0]).toContain('/demo/sw.js?v=2.0.0');
384
+ expect(sw.register.mock.calls[0][1]).toEqual(expect.objectContaining({ scope: '/demo/' }));
385
+ // the page's data comes from the same folder, not from the origin root
386
+ return [4 /*yield*/, settle(function () { return document.querySelectorAll('#routes .route').length > 0; }).catch(function (e) {
387
+ throw new Error(e.message +
388
+ ' | routes: ' +
389
+ JSON.stringify((document.getElementById('routes') || {}).textContent) +
390
+ ' | fetched: ' +
391
+ JSON.stringify(global.fetch.mock.calls.map(function (c) { return String(c[0]); })) +
392
+ ' | errors: ' +
393
+ JSON.stringify(errors));
394
+ })];
395
+ case 2:
396
+ // the page's data comes from the same folder, not from the origin root
397
+ _a.sent();
398
+ asked = global.fetch.mock.calls.map(function (call) {
399
+ return String(call[0]);
400
+ });
401
+ // the page's own table is fetched relative to the page, so it follows the
402
+ // folder without any substitution; the host files are addressed absolutely
403
+ expect(asked).toContain('operations.json');
404
+ expect(asked).toContain('/demo/host/identity.json');
405
+ __spreadArray([], __read(document.querySelectorAll('#routes .route')), false)[0].click();
406
+ document.getElementById('send').click();
407
+ return [4 /*yield*/, settle(function () { return calls.length > 0; })];
408
+ case 3:
409
+ _a.sent();
410
+ expect(calls[0].url).toBe('/demo/SASjsApi/user');
411
+ return [2 /*return*/];
412
+ }
413
+ });
414
+ }); });
415
+ });
@@ -1042,6 +1042,90 @@ describe('browser-hosted build', function () {
1042
1042
  }
1043
1043
  });
1044
1044
  }); });
1045
+ /**
1046
+ * The API console is generated, not written.
1047
+ *
1048
+ * It is the page that lets a visitor try the API a mock site is a mock of, so
1049
+ * it has to list the operations the package actually answers - and be
1050
+ * generated from the same route table the worker dispatches, or the page and
1051
+ * the host drift.
1052
+ */
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, operations, createUser, html, version, appHtml, loader;
1055
+ return __generator(this, function (_a) {
1056
+ switch (_a.label) {
1057
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
1058
+ case 1:
1059
+ result = _a.sent();
1060
+ expect(result.apiConsole).toBe(true);
1061
+ folder = path_1.default.join(out(), 'api-console');
1062
+ shipped = fs_1.default.readdirSync(folder).sort();
1063
+ // every file the folder holds, and the worker's passthrough list is that same
1064
+ // list: a second copy in sw.js would let a new file be answered as an API path
1065
+ expect(shipped).toEqual([
1066
+ 'console.css',
1067
+ 'console.js',
1068
+ 'index.html',
1069
+ 'operations.json'
1070
+ ]);
1071
+ sw = fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8');
1072
+ expect(sw).not.toContain('API_CONSOLE_FILES');
1073
+ operations = JSON.parse(fs_1.default.readFileSync(path_1.default.join(folder, 'operations.json'), 'utf8'));
1074
+ expect(operations.length).toBeGreaterThan(20);
1075
+ createUser = operations.find(function (operation) { return operation.id === 'POST /SASjsApi/user'; });
1076
+ expect(createUser).toBeDefined();
1077
+ expect(createUser.params.map(function (parameter) { return parameter.name; })).toEqual(expect.arrayContaining(['username', 'password']));
1078
+ expect(createUser.method).toBe('POST');
1079
+ html = fs_1.default.readFileSync(path_1.default.join(folder, 'index.html'), 'utf8');
1080
+ expect(html).toContain('<script src="console.js"></script>');
1081
+ expect(html).not.toMatch(/<script(?![^>]*\ssrc=)[^>]*>[\s\S]*?<\/script>/);
1082
+ version = /<meta name="sasjs-mock-version" content="([^"]+)" \/>/.exec(html);
1083
+ expect(version).not.toBeNull();
1084
+ appHtml = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
1085
+ loader = /host\/boot\.js\?v=([^"']+)/.exec(appHtml);
1086
+ expect(loader).not.toBeNull();
1087
+ expect(version[1]).toBe(loader[1]);
1088
+ return [2 /*return*/];
1089
+ }
1090
+ });
1091
+ }); });
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 () {
1093
+ var own, result;
1094
+ return __generator(this, function (_a) {
1095
+ switch (_a.label) {
1096
+ case 0:
1097
+ own = path_1.default.join(root, 'client', 'dist', 'api-console');
1098
+ fs_1.default.mkdirSync(own, { recursive: true });
1099
+ fs_1.default.writeFileSync(path_1.default.join(own, 'index.html'), '<p>ours</p>');
1100
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
1101
+ // the project's file is not replaced, and the loss is reported rather than
1102
+ // silent: the build has no way to know which of the two the site wants
1103
+ ];
1104
+ case 1:
1105
+ result = _a.sent();
1106
+ // the project's file is not replaced, and the loss is reported rather than
1107
+ // silent: the build has no way to know which of the two the site wants
1108
+ expect(result.apiConsole).toBe(false);
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'));
1112
+ return [2 /*return*/];
1113
+ }
1114
+ });
1115
+ }); });
1116
+ it('leaves the console out when the target asks for the app alone', function () { return __awaiter(void 0, void 0, void 0, function () {
1117
+ var result;
1118
+ return __generator(this, function (_a) {
1119
+ switch (_a.label) {
1120
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockApi: false }))];
1121
+ case 1:
1122
+ result = _a.sent();
1123
+ expect(result.apiConsole).toBe(false);
1124
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'api-console'))).toBe(false);
1125
+ return [2 /*return*/];
1126
+ }
1127
+ });
1128
+ }); });
1045
1129
  it('ships a recorded SAS session when the project has one', function () { return __awaiter(void 0, void 0, void 0, function () {
1046
1130
  var recording, result;
1047
1131
  var _a;
@@ -2561,6 +2561,34 @@ describe('service worker host', function () {
2561
2561
  }
2562
2562
  });
2563
2563
  }); });
2564
+ /**
2565
+ * The console's own files are the page, not an API call.
2566
+ *
2567
+ * `/api/...` is the API's short spelling, so the worker answers it - except for
2568
+ * `/api` itself, which a site's own application may answer.
2569
+ */
2570
+ it('leaves /api to the site, and answers the short spelling below it', function () { return __awaiter(void 0, void 0, void 0, function () {
2571
+ var worker, response;
2572
+ return __generator(this, function (_a) {
2573
+ switch (_a.label) {
2574
+ case 0:
2575
+ worker = seeded();
2576
+ // a site's own application can answer /api (the server interface does), so the
2577
+ // worker hands it on; `/api/<something>` is an API call on the short spelling
2578
+ expect(worker.dispatch('/api')).toBeUndefined();
2579
+ expect(worker.dispatch('/api/')).toBeUndefined();
2580
+ // the console has a folder of its own, which is not an API prefix: the static
2581
+ // host serves it and the worker never sees it
2582
+ expect(worker.dispatch('/api-console/console.js')).toBeUndefined();
2583
+ expect(worker.dispatch('/api-console/operations.json')).toBeUndefined();
2584
+ return [4 /*yield*/, worker.dispatch('/api/user')];
2585
+ case 1:
2586
+ response = _a.sent();
2587
+ expect(response.status).toBe(200);
2588
+ return [2 /*return*/];
2589
+ }
2590
+ });
2591
+ }); });
2564
2592
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
2565
2593
  var dispatch, response, _a;
2566
2594
  return __generator(this, function (_b) {
@@ -75,6 +75,11 @@ describe('versionMessage', function () {
75
75
  writePackage(root, '6.4.7-alpha.1');
76
76
  expect((0, version_1.versionMessage)(root)).toEqual("You are using a linked version of SASjs CLI (v6.4.7-alpha.1) running from sources at ".concat(root));
77
77
  });
78
+ it('leaves the placeholder version out of a source checkout', function () {
79
+ var root = path_1.default.join(tempRoot(), 'cli');
80
+ writePackage(root, '0.0.0');
81
+ expect((0, version_1.versionMessage)(root)).toEqual("You are using a linked version of SASjs CLI running from sources at ".concat(root));
82
+ });
78
83
  it('leaves the version out of a source checkout that has none', function () {
79
84
  var root = path_1.default.join(tempRoot(), 'cli');
80
85
  writePackage(root, undefined);
@@ -92,6 +97,11 @@ describe('versionMessage', function () {
92
97
  it('throws when an installed package carries no version', function () {
93
98
  var root = packageRoot();
94
99
  writePackage(root, undefined);
95
- expect(function () { return (0, version_1.versionMessage)(root); }).toThrow(/has no version in its package.json/);
100
+ expect(function () { return (0, version_1.versionMessage)(root); }).toThrow(/has no version in its package.json\./);
101
+ });
102
+ it('names the placeholder when an installed package was never stamped', function () {
103
+ var root = packageRoot();
104
+ writePackage(root, '0.0.0');
105
+ expect(function () { return (0, version_1.versionMessage)(root); }).toThrow(/carries the placeholder version 0\.0\.0, so it was never stamped from a tag/);
96
106
  });
97
107
  });
@@ -4,9 +4,11 @@
4
4
  * Reads the version from the package.json of the package that is running, so
5
5
  * it works for every install shape - global, project-local, `npx`, a temp
6
6
  * prefix, or embedded as a dependency. A package root outside a
7
- * `node_modules` folder is a source checkout (an `npm link`, or a local
8
- * build), and is reported as such; a source checkout carries no version until
9
- * semantic-release writes one, so the version is left out when it is absent.
7
+ * `node_modules` folder is a source checkout (an `npm link`, or a local build),
8
+ * and is reported as such. Its package.json carries the placeholder version
9
+ * (`0.0.0`): the version is the git tag, stamped in when the package is
10
+ * published, so from sources there is nothing to report and the version is left
11
+ * out.
10
12
  *
11
13
  * @param packageRoot - the folder holding the package.json of the running CLI.
12
14
  */
@@ -49,9 +49,11 @@ var path_1 = __importDefault(require("path"));
49
49
  * Reads the version from the package.json of the package that is running, so
50
50
  * it works for every install shape - global, project-local, `npx`, a temp
51
51
  * prefix, or embedded as a dependency. A package root outside a
52
- * `node_modules` folder is a source checkout (an `npm link`, or a local
53
- * build), and is reported as such; a source checkout carries no version until
54
- * semantic-release writes one, so the version is left out when it is absent.
52
+ * `node_modules` folder is a source checkout (an `npm link`, or a local build),
53
+ * and is reported as such. Its package.json carries the placeholder version
54
+ * (`0.0.0`): the version is the git tag, stamped in when the package is
55
+ * published, so from sources there is nothing to report and the version is left
56
+ * out.
55
57
  *
56
58
  * @param packageRoot - the folder holding the package.json of the running CLI.
57
59
  */
@@ -65,13 +67,21 @@ var versionMessage = function (packageRoot) {
65
67
  "Reinstall the CLI - 'npm i -g @sasjs/cli@latest'.");
66
68
  }
67
69
  var fromSources = !packageRoot.split(path_1.default.sep).includes('node_modules');
70
+ // the placeholder means "not stamped from a tag" - the same thing as absent
71
+ var stated = version && !/^0\.0\.0(-|$)/.test(version) ? version : undefined;
68
72
  if (fromSources) {
69
- return version
70
- ? "You are using a linked version of SASjs CLI (v".concat(version, ") running from sources at ").concat(packageRoot)
73
+ return stated
74
+ ? "You are using a linked version of SASjs CLI (v".concat(stated, ") running from sources at ").concat(packageRoot)
71
75
  : "You are using a linked version of SASjs CLI running from sources at ".concat(packageRoot);
72
76
  }
73
- if (!version) {
74
- throw new Error("The installed CLI at ".concat(packageRoot, " has no version in its package.json. ") +
77
+ if (!stated) {
78
+ // two ways to get here, and they mean different things to whoever is reading
79
+ // it: no version field at all, or the placeholder that was never stamped from
80
+ // a tag
81
+ var reason = version
82
+ ? "carries the placeholder version ".concat(version, ", so it was never stamped from a tag")
83
+ : 'has no version in its package.json';
84
+ throw new Error("The installed CLI at ".concat(packageRoot, " ").concat(reason, ". ") +
75
85
  "Reinstall it - 'npm i -g @sasjs/cli@latest'.");
76
86
  }
77
87
  return "You are using SASjs CLI v".concat(version);
@@ -20,5 +20,14 @@ declare module '@sasjs/utils/types/config' {
20
20
  * slash (`/demo`). Unset means the origin root.
21
21
  */
22
22
  mockWebBasePath?: string;
23
+ /**
24
+ * Whether the mock site carries the API console at `<base>/api-console/`.
25
+ *
26
+ * On unless it is set to false: the console is how a demonstration site lets
27
+ * a visitor try the API it is a mock of, and a page nothing links to costs
28
+ * the app nothing. A site whose output must be the app and nothing else
29
+ * turns it off.
30
+ */
31
+ mockApi?: boolean;
23
32
  }
24
33
  }