@sasjs/cli 6.30.0 → 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.
- package/build/commands/build/internal/mockSite/assets/api-console.css +365 -0
- package/build/commands/build/internal/mockSite/assets/api-console.js +762 -0
- package/build/commands/build/internal/mockSite/assets/api-index.html +83 -0
- package/build/commands/build/internal/mockSite/assets/boot.js +23 -1
- package/build/commands/build/internal/mockSite/assets/persist.js +76 -5
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +413 -50
- package/build/commands/build/internal/mockSite/assets/sw.js +156 -15
- package/build/commands/build/internal/mockSite/mockSite.d.ts +25 -0
- package/build/commands/build/internal/mockSite/mockSite.js +311 -24
- package/build/commands/build/internal/mockSite/spec/apiConsole.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/apiConsole.spec.js +1371 -0
- package/build/commands/build/internal/mockSite/spec/apiConsoleBasePath.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/apiConsoleBasePath.spec.js +303 -0
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +50 -3
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +217 -3
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +179 -1
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +546 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +407 -53
- package/build/commands/version/spec/version.spec.js +11 -1
- package/build/commands/version/version.d.ts +5 -3
- package/build/commands/version/version.js +17 -7
- package/build/types/mockWebBasePath.d.ts +9 -0
- package/package.json +13 -32
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -100,9 +100,11 @@ var loadBoot = function (specs, options) {
|
|
|
100
100
|
};
|
|
101
101
|
var listeners = {};
|
|
102
102
|
var controller = options.controller || 'present';
|
|
103
|
+
var timelineAtRegister = [];
|
|
103
104
|
var navigator = {
|
|
104
105
|
serviceWorker: {
|
|
105
106
|
register: function (url, init) {
|
|
107
|
+
timelineAtRegister = timeline.slice();
|
|
106
108
|
registered.push(url + ' ' + JSON.stringify(init));
|
|
107
109
|
return options.registerFails
|
|
108
110
|
? Promise.reject(new Error('nope'))
|
|
@@ -127,7 +129,11 @@ var loadBoot = function (specs, options) {
|
|
|
127
129
|
global.navigator = navigator;
|
|
128
130
|
global.window = options.bootError
|
|
129
131
|
? { __sasjsMockReady: Promise.resolve({ error: options.bootError }) }
|
|
130
|
-
:
|
|
132
|
+
: options.driveError
|
|
133
|
+
? {
|
|
134
|
+
__sasjsMockReady: Promise.resolve({ driveError: options.driveError })
|
|
135
|
+
}
|
|
136
|
+
: {};
|
|
131
137
|
global.sessionStorage = {
|
|
132
138
|
getItem: function (key) { return (key in stored ? stored[key] : null); },
|
|
133
139
|
setItem: function (key, value) {
|
|
@@ -146,7 +152,7 @@ var loadBoot = function (specs, options) {
|
|
|
146
152
|
late = true;
|
|
147
153
|
(listeners['controllerchange'] || []).forEach(function (fn) { return fn(); });
|
|
148
154
|
};
|
|
149
|
-
return { appended: appended, timeline: timeline, registered: registered, stored: stored, claim: claim };
|
|
155
|
+
return { appended: appended, timeline: timeline, registered: registered, stored: stored, claim: claim, timelineAtRegister: timelineAtRegister };
|
|
150
156
|
};
|
|
151
157
|
var settle = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
152
158
|
var i;
|
|
@@ -336,6 +342,29 @@ describe('the boot loader', function () {
|
|
|
336
342
|
}
|
|
337
343
|
});
|
|
338
344
|
}); });
|
|
345
|
+
it('starts the drive restore before the worker is registered', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
346
|
+
var timelineAtRegister;
|
|
347
|
+
return __generator(this, function (_a) {
|
|
348
|
+
switch (_a.label) {
|
|
349
|
+
case 0:
|
|
350
|
+
timelineAtRegister = loadBoot([{ src: 'sasjs.js' }]).timelineAtRegister;
|
|
351
|
+
return [4 /*yield*/, settle()
|
|
352
|
+
// persist.js reads the page's own IndexedDB and hands the drive to the
|
|
353
|
+
// worker as soon as there is one, so nothing about it needs the worker to be
|
|
354
|
+
// ready: sequenced after the registration it is another download plus a
|
|
355
|
+
// store read on top of the install, which is what made a first visit slow
|
|
356
|
+
];
|
|
357
|
+
case 1:
|
|
358
|
+
_a.sent();
|
|
359
|
+
// persist.js reads the page's own IndexedDB and hands the drive to the
|
|
360
|
+
// worker as soon as there is one, so nothing about it needs the worker to be
|
|
361
|
+
// ready: sequenced after the registration it is another download plus a
|
|
362
|
+
// store read on top of the install, which is what made a first visit slow
|
|
363
|
+
expect(timelineAtRegister).toEqual(['host/persist.js']);
|
|
364
|
+
return [2 /*return*/];
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}); });
|
|
339
368
|
it('keeps the attributes an app relies on', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
340
369
|
var appended, app;
|
|
341
370
|
return __generator(this, function (_a) {
|
|
@@ -430,6 +459,21 @@ describe('the boot loader', function () {
|
|
|
430
459
|
}
|
|
431
460
|
});
|
|
432
461
|
}); });
|
|
462
|
+
it('reports a drive that was restored from less than the whole capture', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
463
|
+
return __generator(this, function (_a) {
|
|
464
|
+
switch (_a.label) {
|
|
465
|
+
case 0:
|
|
466
|
+
// the page still runs - a missing piece only fails the file that needed it -
|
|
467
|
+
// so the error is the only thing that says the drive is incomplete
|
|
468
|
+
loadBoot([], { driveError: 'the drive index answered 500' });
|
|
469
|
+
return [4 /*yield*/, settle()];
|
|
470
|
+
case 1:
|
|
471
|
+
_a.sent();
|
|
472
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('the drive index answered 500'));
|
|
473
|
+
return [2 /*return*/];
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}); });
|
|
433
477
|
it('refuses a boot config it cannot read instead of guessing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
434
478
|
var timeline;
|
|
435
479
|
return __generator(this, function (_a) {
|
|
@@ -527,7 +571,10 @@ describe('the boot loader', function () {
|
|
|
527
571
|
// into the next test's globals
|
|
528
572
|
_b.sent();
|
|
529
573
|
expect(stored['mock-sw-reloaded']).toEqual('1');
|
|
530
|
-
|
|
574
|
+
// the drive restore is requested before the registration (it overlaps the
|
|
575
|
+
// install), so it is already in flight when the page decides to reload; the
|
|
576
|
+
// reload discards it, which costs nothing the cache does not already hold
|
|
577
|
+
expect(timeline).toEqual(['host/persist.js', 'reload']);
|
|
531
578
|
return [2 /*return*/];
|
|
532
579
|
}
|
|
533
580
|
});
|
|
@@ -140,6 +140,12 @@ describe('browser-hosted build', function () {
|
|
|
140
140
|
var readConfig = function () {
|
|
141
141
|
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'mock-config.json'), 'utf8'));
|
|
142
142
|
};
|
|
143
|
+
var readIndex = function () {
|
|
144
|
+
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'vfs-snapshot.json'), 'utf8'));
|
|
145
|
+
};
|
|
146
|
+
var readShard = function (id) {
|
|
147
|
+
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', "vfs-snapshot-".concat(id, ".json")), 'utf8')).files;
|
|
148
|
+
};
|
|
143
149
|
beforeEach(function () {
|
|
144
150
|
root = fs_1.default.mkdtempSync(path_1.default.join(require('os').tmpdir(), 'sasjs-mock-'));
|
|
145
151
|
createProject(root);
|
|
@@ -558,6 +564,66 @@ describe('browser-hosted build', function () {
|
|
|
558
564
|
}
|
|
559
565
|
});
|
|
560
566
|
}); });
|
|
567
|
+
/**
|
|
568
|
+
* The loader replays the app's tags only once the worker controls the page.
|
|
569
|
+
* Right for answerability, but it also puts the largest asset on the site
|
|
570
|
+
* behind the largest prerequisite - so a classic script is hinted in the head,
|
|
571
|
+
* where the parser reaches it before the loader runs.
|
|
572
|
+
*/
|
|
573
|
+
it('hints a classic app script so it downloads while the worker installs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
574
|
+
var html, hint;
|
|
575
|
+
return __generator(this, function (_a) {
|
|
576
|
+
switch (_a.label) {
|
|
577
|
+
case 0:
|
|
578
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
579
|
+
'<!doctype html><html><head><title>App</title></head><body>',
|
|
580
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\" servertype=\"SASJS\"></sasjs>"),
|
|
581
|
+
'<script src="main.abc123.js"></script>',
|
|
582
|
+
'<script src="deferred.js" type="module"></script>',
|
|
583
|
+
'</body></html>'
|
|
584
|
+
].join('\n'));
|
|
585
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
586
|
+
case 1:
|
|
587
|
+
_a.sent();
|
|
588
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
589
|
+
hint = html.match(/<link rel="preload" as="script" href="([^"]+)" \/>/);
|
|
590
|
+
expect(hint).not.toBeNull();
|
|
591
|
+
// the hint names the URL the replayed tag carries, version query included: a
|
|
592
|
+
// mismatch is a second download rather than a warm cache
|
|
593
|
+
expect(hint[1]).toMatch(/^main\.abc123\.js\?v=[0-9a-f]{10}$/);
|
|
594
|
+
expect(html).toContain("\"src\":\"".concat(hint[1], "\""));
|
|
595
|
+
// and it has to be in the head, ahead of the loader that replays the tag
|
|
596
|
+
expect(html.indexOf('rel="preload"')).toBeLessThan(html.indexOf('host/boot.js'));
|
|
597
|
+
// a module is fetched with different credentials semantics, so a script hint
|
|
598
|
+
// would not match its tag - it is replayed without one
|
|
599
|
+
expect(html).not.toMatch(/as="script" href="deferred/);
|
|
600
|
+
return [2 /*return*/];
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}); });
|
|
604
|
+
it('leaves a script this build does not own unhinted', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
605
|
+
var html;
|
|
606
|
+
return __generator(this, function (_a) {
|
|
607
|
+
switch (_a.label) {
|
|
608
|
+
case 0:
|
|
609
|
+
// a page may load a script from somewhere else; a version guess or a
|
|
610
|
+
// cross-origin hint only breaks it
|
|
611
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
612
|
+
'<!doctype html><html><head><title>App</title></head><body>',
|
|
613
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\" servertype=\"SASJS\"></sasjs>"),
|
|
614
|
+
'<script src="https://cdn.example.com/lib.js"></script>',
|
|
615
|
+
'<script src="not-in-this-build.js"></script>',
|
|
616
|
+
'</body></html>'
|
|
617
|
+
].join('\n'));
|
|
618
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
619
|
+
case 1:
|
|
620
|
+
_a.sent();
|
|
621
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
622
|
+
expect(html).not.toContain('rel="preload"');
|
|
623
|
+
return [2 /*return*/];
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}); });
|
|
561
627
|
it("leaves a stylesheet alone when the file is not this build's", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
562
628
|
var html, specs;
|
|
563
629
|
return __generator(this, function (_a) {
|
|
@@ -889,7 +955,7 @@ describe('browser-hosted build', function () {
|
|
|
889
955
|
});
|
|
890
956
|
}); });
|
|
891
957
|
it('ships a captured drive snapshot when the project has one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
892
|
-
var snapshot, result,
|
|
958
|
+
var snapshot, result, index;
|
|
893
959
|
var _a;
|
|
894
960
|
return __generator(this, function (_b) {
|
|
895
961
|
switch (_b.label) {
|
|
@@ -905,11 +971,159 @@ describe('browser-hosted build', function () {
|
|
|
905
971
|
case 1:
|
|
906
972
|
result = _b.sent();
|
|
907
973
|
expect(result.snapshot).toBe(true);
|
|
908
|
-
|
|
909
|
-
expect(
|
|
974
|
+
index = readIndex();
|
|
975
|
+
expect(index.sharded).toBe(true);
|
|
976
|
+
expect(Object.keys(index.entries).length).toBe(2);
|
|
977
|
+
// two small files under one folder: one boot shard, no request of their own
|
|
978
|
+
expect(result.snapshotShards).toBe(1);
|
|
979
|
+
expect(index.boot).toBe(0);
|
|
980
|
+
expect(readShard(0)).toEqual(snapshot.files);
|
|
910
981
|
// the runtime prefers a snapshot over the seed, so it must not be mistaken
|
|
911
982
|
// for one of the seeded folders
|
|
912
983
|
expect(readConfig().snapshot).toBe(true);
|
|
984
|
+
expect(readConfig().snapshotShards).toBe(1);
|
|
985
|
+
return [2 /*return*/];
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}); });
|
|
989
|
+
/**
|
|
990
|
+
* A captured drive is not one download.
|
|
991
|
+
*
|
|
992
|
+
* The first page of a demonstration site reads a listing and a handful of
|
|
993
|
+
* logos; the bundles behind the tiles are opened by whoever clicks them. One
|
|
994
|
+
* file for the lot makes every visitor pay for all of it before the page
|
|
995
|
+
* renders, so the build splits it: small files in the boot shard, an app's
|
|
996
|
+
* bundle in its own.
|
|
997
|
+
*/
|
|
998
|
+
it('ships a large capture as a boot shard plus a piece per bundle', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
999
|
+
var bundle, capture, result, index, bundlePath, bundleShard, bootBytes, total;
|
|
1000
|
+
var _a;
|
|
1001
|
+
return __generator(this, function (_b) {
|
|
1002
|
+
switch (_b.label) {
|
|
1003
|
+
case 0:
|
|
1004
|
+
bundle = 'x'.repeat(mockSite_1.SHARD_FILE_BYTES + 1024);
|
|
1005
|
+
capture = {
|
|
1006
|
+
files: (_a = {
|
|
1007
|
+
'/sasjs/drive/files/.sasjs/apps.json': '{"apps":[]}'
|
|
1008
|
+
},
|
|
1009
|
+
_a["/sasjs/drive/files/Public/app/demo/logo.png"] = 'not really a png',
|
|
1010
|
+
_a["/sasjs/drive/files/Public/app/demo/services/web/index.bundle.js"] = bundle,
|
|
1011
|
+
_a)
|
|
1012
|
+
};
|
|
1013
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-snapshot.json'), JSON.stringify(capture));
|
|
1014
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1015
|
+
case 1:
|
|
1016
|
+
result = _b.sent();
|
|
1017
|
+
index = readIndex();
|
|
1018
|
+
expect(result.snapshotShards).toBe(2);
|
|
1019
|
+
// what a first page reads is in the boot shard
|
|
1020
|
+
expect(index.entries['/sasjs/drive/files/.sasjs/apps.json'].shard).toBe(0);
|
|
1021
|
+
expect(index.entries["/sasjs/drive/files/Public/app/demo/logo.png"].shard).toBe(0);
|
|
1022
|
+
bundlePath = "/sasjs/drive/files/Public/app/demo/services/web/index.bundle.js";
|
|
1023
|
+
bundleShard = index.entries[bundlePath].shard;
|
|
1024
|
+
expect(bundleShard).not.toBe(0);
|
|
1025
|
+
expect(readShard(bundleShard)[bundlePath]).toBe(bundle);
|
|
1026
|
+
bootBytes = index.shards[0].bytes;
|
|
1027
|
+
total = Object.keys(index.entries).reduce(function (sum, key) { return sum + index.entries[key].bytes; }, 0);
|
|
1028
|
+
expect(bootBytes).toBeLessThan(total / 10);
|
|
1029
|
+
return [2 /*return*/];
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}); });
|
|
1033
|
+
it('refuses a capture written by a runtime of another format', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1034
|
+
return __generator(this, function (_a) {
|
|
1035
|
+
switch (_a.label) {
|
|
1036
|
+
case 0:
|
|
1037
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-snapshot.json'), JSON.stringify({ format: mockSite_1.DRIVE_FORMAT + 1, files: {} }));
|
|
1038
|
+
return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target())).rejects.toThrow(/format/)];
|
|
1039
|
+
case 1:
|
|
1040
|
+
_a.sent();
|
|
1041
|
+
return [2 /*return*/];
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
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);
|
|
913
1127
|
return [2 /*return*/];
|
|
914
1128
|
}
|
|
915
1129
|
});
|