@sasjs/cli 6.30.1 → 6.31.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,303 @@
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/"}
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 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.
82
+ */
83
+ var assets = path_1.default.join(__dirname, '..', 'assets');
84
+ var page = function () { return fs_1.default.readFileSync(path_1.default.join(assets, 'api-index.html'), 'utf8'); };
85
+ var bodyOf = function (html) {
86
+ return html.slice(html.indexOf('<body>') + 6, html.lastIndexOf('</body>'));
87
+ };
88
+ var jsonResponse = function (body) { return ({
89
+ ok: true,
90
+ status: 200,
91
+ statusText: 'OK',
92
+ headers: { get: function () { return 'application/json'; } },
93
+ json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
94
+ return [2 /*return*/, body];
95
+ }); }); },
96
+ text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
97
+ return [2 /*return*/, JSON.stringify(body)];
98
+ }); }); }
99
+ }); };
100
+ var operations = [
101
+ {
102
+ id: 'GET /SASjsApi/user',
103
+ method: 'GET',
104
+ path: '/SASjsApi/user',
105
+ tag: 'User',
106
+ summary: 'List all users',
107
+ params: [],
108
+ requestBodies: [],
109
+ responses: [{ status: 200, description: 'Ok' }],
110
+ implemented: true
111
+ }
112
+ ];
113
+ var settle = function (check) { return __awaiter(void 0, void 0, void 0, function () {
114
+ var i;
115
+ return __generator(this, function (_a) {
116
+ switch (_a.label) {
117
+ case 0:
118
+ i = 0;
119
+ _a.label = 1;
120
+ case 1:
121
+ if (!(i < 400)) return [3 /*break*/, 4];
122
+ if (check())
123
+ return [2 /*return*/];
124
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
125
+ case 2:
126
+ _a.sent();
127
+ _a.label = 3;
128
+ case 3:
129
+ i++;
130
+ return [3 /*break*/, 1];
131
+ case 4: throw new Error('the page did not reach the state the case expected');
132
+ }
133
+ });
134
+ }); };
135
+ describe('the API console under a base path', function () {
136
+ afterEach(function () {
137
+ delete navigator.serviceWorker;
138
+ });
139
+ it('finds the folder from a URL without its trailing slash', function () { return __awaiter(void 0, void 0, void 0, function () {
140
+ var realCreate, sw, asked;
141
+ return __generator(this, function (_a) {
142
+ switch (_a.label) {
143
+ case 0:
144
+ // `/demo/api` is the shape a person types, and the page used to read that as
145
+ // 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');
148
+ document.body.innerHTML = bodyOf(page());
149
+ document.head.innerHTML =
150
+ '<meta name="sasjs-mock-version" content="2.0.0" />';
151
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
152
+ return __generator(this, function (_a) {
153
+ return [2 /*return*/, String(url).includes('operations.json')
154
+ ? jsonResponse(operations)
155
+ : jsonResponse({ format: 1, users: [] })];
156
+ });
157
+ }); });
158
+ realCreate = document.createElement.bind(document);
159
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
160
+ var node = realCreate(tag);
161
+ if (String(tag).toLowerCase() === 'script')
162
+ setTimeout(function () { return node.dispatchEvent(new Event('load')); }, 0);
163
+ return node;
164
+ });
165
+ sw = {
166
+ controller: {},
167
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
168
+ return [2 /*return*/, ({})];
169
+ }); }); }),
170
+ addEventListener: jest.fn()
171
+ };
172
+ Object.defineProperty(navigator, 'serviceWorker', {
173
+ value: sw,
174
+ configurable: true
175
+ });
176
+ jest.resetModules();
177
+ require('../assets/api-console.js');
178
+ return [4 /*yield*/, settle(function () { return sw.register.mock.calls.length === 1; })];
179
+ case 1:
180
+ _a.sent();
181
+ expect(sw.register.mock.calls[0][0]).toContain('/demo/sw.js?v=2.0.0');
182
+ expect(sw.register.mock.calls[0][1]).toEqual(expect.objectContaining({ scope: '/demo/' }));
183
+ return [4 /*yield*/, settle(function () { return document.querySelectorAll('#routes .route').length > 0; })];
184
+ case 2:
185
+ _a.sent();
186
+ asked = global.fetch.mock.calls.map(function (call) {
187
+ return String(call[0]);
188
+ });
189
+ expect(asked).toContain('/demo/host/identity.json');
190
+ history.replaceState({}, '', '/demo/api/');
191
+ return [2 /*return*/];
192
+ }
193
+ });
194
+ }); });
195
+ 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 () {
196
+ var calls, realCreate, sw, errors, errorSpy, warnSpy, asked;
197
+ return __generator(this, function (_a) {
198
+ switch (_a.label) {
199
+ case 0:
200
+ document.body.innerHTML = bodyOf(page());
201
+ document.head.innerHTML =
202
+ '<meta name="sasjs-mock-version" content="2.0.0" />';
203
+ calls = [];
204
+ global.fetch = jest.fn(function (url_1) {
205
+ var args_1 = [];
206
+ for (var _i = 1; _i < arguments.length; _i++) {
207
+ args_1[_i - 1] = arguments[_i];
208
+ }
209
+ return __awaiter(void 0, __spreadArray([url_1], __read(args_1), false), void 0, function (url, init) {
210
+ if (init === void 0) { init = {}; }
211
+ return __generator(this, function (_a) {
212
+ if (String(url).includes('operations.json'))
213
+ return [2 /*return*/, jsonResponse(operations)];
214
+ if (String(url).includes('identity.json'))
215
+ return [2 /*return*/, jsonResponse({ format: 1, users: [] })];
216
+ calls.push({ url: String(url), init: init });
217
+ return [2 /*return*/, jsonResponse([])];
218
+ });
219
+ });
220
+ });
221
+ realCreate = document.createElement.bind(document);
222
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
223
+ var node = realCreate(tag);
224
+ if (String(tag).toLowerCase() === 'script')
225
+ setTimeout(function () { return node.dispatchEvent(new Event('load')); }, 0);
226
+ return node;
227
+ });
228
+ sw = {
229
+ controller: {},
230
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
231
+ return [2 /*return*/, ({})];
232
+ }); }); }),
233
+ addEventListener: jest.fn()
234
+ };
235
+ Object.defineProperty(navigator, 'serviceWorker', {
236
+ value: sw,
237
+ configurable: true
238
+ });
239
+ errors = [];
240
+ errorSpy = jest
241
+ .spyOn(console, 'error')
242
+ .mockImplementation(function () {
243
+ var args = [];
244
+ for (var _i = 0; _i < arguments.length; _i++) {
245
+ args[_i] = arguments[_i];
246
+ }
247
+ return errors.push(args);
248
+ });
249
+ warnSpy = jest
250
+ .spyOn(console, 'warn')
251
+ .mockImplementation(function () {
252
+ var args = [];
253
+ for (var _i = 0; _i < arguments.length; _i++) {
254
+ args[_i] = arguments[_i];
255
+ }
256
+ return errors.push(args);
257
+ });
258
+ jest.resetModules();
259
+ require('../assets/api-console.js');
260
+ return [4 /*yield*/, settle(function () { return sw.register.mock.calls.length === 1; }).catch(function (e) {
261
+ throw new Error(e.message +
262
+ ' | errors: ' +
263
+ JSON.stringify(errors) +
264
+ ' | routes: ' +
265
+ (document.getElementById('routes') || {}).textContent);
266
+ })];
267
+ case 1:
268
+ _a.sent();
269
+ errorSpy.mockRestore();
270
+ warnSpy.mockRestore();
271
+ expect(sw.register.mock.calls[0][0]).toContain('/demo/sw.js?v=2.0.0');
272
+ expect(sw.register.mock.calls[0][1]).toEqual(expect.objectContaining({ scope: '/demo/' }));
273
+ // the page's data comes from the same folder, not from the origin root
274
+ return [4 /*yield*/, settle(function () { return document.querySelectorAll('#routes .route').length > 0; }).catch(function (e) {
275
+ throw new Error(e.message +
276
+ ' | routes: ' +
277
+ JSON.stringify((document.getElementById('routes') || {}).textContent) +
278
+ ' | fetched: ' +
279
+ JSON.stringify(global.fetch.mock.calls.map(function (c) { return String(c[0]); })) +
280
+ ' | errors: ' +
281
+ JSON.stringify(errors));
282
+ })];
283
+ case 2:
284
+ // the page's data comes from the same folder, not from the origin root
285
+ _a.sent();
286
+ asked = global.fetch.mock.calls.map(function (call) {
287
+ return String(call[0]);
288
+ });
289
+ // the page's own table is fetched relative to the page, so it follows the
290
+ // folder without any substitution; the host files are addressed absolutely
291
+ expect(asked).toContain('operations.json');
292
+ expect(asked).toContain('/demo/host/identity.json');
293
+ __spreadArray([], __read(document.querySelectorAll('#routes .route')), false)[0].click();
294
+ document.getElementById('send').click();
295
+ return [4 /*yield*/, settle(function () { return calls.length > 0; })];
296
+ case 3:
297
+ _a.sent();
298
+ expect(calls[0].url).toBe('/demo/SASjsApi/user');
299
+ return [2 /*return*/];
300
+ }
301
+ });
302
+ }); });
303
+ });
@@ -1042,6 +1042,92 @@ 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, listed, 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');
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
+ listed = /const API_CONSOLE_FILES = (\[[^\]]*\])/.exec(sw);
1073
+ expect(listed).not.toBeNull();
1074
+ expect(JSON.parse(listed[1]).sort()).toEqual(shipped);
1075
+ operations = JSON.parse(fs_1.default.readFileSync(path_1.default.join(folder, 'operations.json'), 'utf8'));
1076
+ expect(operations.length).toBeGreaterThan(20);
1077
+ createUser = operations.find(function (operation) { return operation.id === 'POST /SASjsApi/user'; });
1078
+ expect(createUser).toBeDefined();
1079
+ expect(createUser.params.map(function (parameter) { return parameter.name; })).toEqual(expect.arrayContaining(['username', 'password']));
1080
+ expect(createUser.method).toBe('POST');
1081
+ html = fs_1.default.readFileSync(path_1.default.join(folder, 'index.html'), 'utf8');
1082
+ expect(html).toContain('<script src="console.js"></script>');
1083
+ expect(html).not.toMatch(/<script(?![^>]*\ssrc=)[^>]*>[\s\S]*?<\/script>/);
1084
+ version = /<meta name="sasjs-mock-version" content="([^"]+)" \/>/.exec(html);
1085
+ expect(version).not.toBeNull();
1086
+ appHtml = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
1087
+ loader = /host\/boot\.js\?v=([^"']+)/.exec(appHtml);
1088
+ expect(loader).not.toBeNull();
1089
+ expect(version[1]).toBe(loader[1]);
1090
+ return [2 /*return*/];
1091
+ }
1092
+ });
1093
+ }); });
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 () {
1095
+ var own, result;
1096
+ return __generator(this, function (_a) {
1097
+ switch (_a.label) {
1098
+ case 0:
1099
+ own = path_1.default.join(root, 'client', 'dist', 'api');
1100
+ fs_1.default.mkdirSync(own, { recursive: true });
1101
+ fs_1.default.writeFileSync(path_1.default.join(own, 'index.html'), '<p>ours</p>');
1102
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
1103
+ // the project's file is not replaced, and the loss is reported rather than
1104
+ // silent: the build has no way to know which of the two the site wants
1105
+ ];
1106
+ case 1:
1107
+ result = _a.sent();
1108
+ // the project's file is not replaced, and the loss is reported rather than
1109
+ // silent: the build has no way to know which of the two the site wants
1110
+ 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'));
1114
+ return [2 /*return*/];
1115
+ }
1116
+ });
1117
+ }); });
1118
+ it('leaves the console out when the target asks for the app alone', function () { return __awaiter(void 0, void 0, void 0, function () {
1119
+ var result;
1120
+ return __generator(this, function (_a) {
1121
+ switch (_a.label) {
1122
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockApi: false }))];
1123
+ case 1:
1124
+ result = _a.sent();
1125
+ expect(result.apiConsole).toBe(false);
1126
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'api'))).toBe(false);
1127
+ return [2 /*return*/];
1128
+ }
1129
+ });
1130
+ }); });
1045
1131
  it('ships a recorded SAS session when the project has one', function () { return __awaiter(void 0, void 0, void 0, function () {
1046
1132
  var recording, result;
1047
1133
  var _a;