@sasjs/cli 6.1.0 → 6.2.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.
- package/build/commands/build/internal/mockSite/assets/persist.js +13 -6
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +636 -22
- package/build/commands/build/internal/mockSite/assets/sw.js +100 -11
- package/build/commands/build/internal/mockSite/mockSite.d.ts +11 -0
- package/build/commands/build/internal/mockSite/mockSite.js +87 -20
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +103 -11
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +61 -3
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +506 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +356 -1
- package/package.json +3 -3
|
@@ -40,6 +40,87 @@ const text = (body, status) =>
|
|
|
40
40
|
headers: { 'content-type': 'text/plain' }
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A byte body travels as base64. The runtime encodes it, and refuses a drive
|
|
45
|
+
* entry that will not decode before it hands the value over, so this is a plain
|
|
46
|
+
* decode - the corrupt-entry error lives in one place, runtime-core.js.
|
|
47
|
+
*/
|
|
48
|
+
const toBytes = (base64) => {
|
|
49
|
+
const binary = atob(String(base64 || '').replace(/\s+/g, ''))
|
|
50
|
+
const out = new Uint8Array(binary.length)
|
|
51
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ------------------------------------------------------------ webout headers
|
|
56
|
+
// A service declares the response it wants by writing `Name: value` lines to
|
|
57
|
+
// the file the preamble hands it as _SASJS_WEBOUT_HEADERS - the same channel
|
|
58
|
+
// %mp_streamfile uses on the SAS side, and the one the server reads. Only the
|
|
59
|
+
// two headers that change how a reply is delivered are applied: the content
|
|
60
|
+
// type, and the disposition that makes the browser save it under the name the
|
|
61
|
+
// service chose. Everything else (cache-control, cookies, ...) has no meaning
|
|
62
|
+
// on a static host.
|
|
63
|
+
const RESPONSE_HEADERS = ['content-type', 'content-disposition']
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parses what the service wrote. A header value is a byte string, so a value up
|
|
67
|
+
* to U+00FF is passed through as written - the same bytes node writes when the
|
|
68
|
+
* server sets the same header, which is what the mock has to match. HTAB is
|
|
69
|
+
* allowed inside a field value by HTTP and the server passes it through, so it
|
|
70
|
+
* is part of the set.
|
|
71
|
+
*
|
|
72
|
+
* 0x7F and 0x80-0x9F are outside it: the browser's Headers implementation strips
|
|
73
|
+
* those bytes from a value without complaining, which would save a download
|
|
74
|
+
* under a name with characters missing, so such a value is refused instead and
|
|
75
|
+
* the reply keeps its default delivery. CR, LF and NUL are refused for the same
|
|
76
|
+
* reason - they make the Response constructor throw.
|
|
77
|
+
*
|
|
78
|
+
* A filename outside ASCII belongs in RFC 6266 form
|
|
79
|
+
* (`filename*=UTF-8''r%C3%A9sum%C3%A9.csv`) - raw non-ASCII bytes in a header
|
|
80
|
+
* are not portable, on this host or on the server.
|
|
81
|
+
*/
|
|
82
|
+
const HEADER_VALUE = /^[\x09\x20-\x7E\xA0-\xFF]+$/
|
|
83
|
+
|
|
84
|
+
const parseHeaders = (content) => {
|
|
85
|
+
const headers = {}
|
|
86
|
+
String(content || '')
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.forEach((line) => {
|
|
91
|
+
const separator = line.indexOf(':')
|
|
92
|
+
if (separator < 1) return
|
|
93
|
+
const name = line.slice(0, separator).trim().toLowerCase()
|
|
94
|
+
const value = line.slice(separator + 1).trim()
|
|
95
|
+
if (!RESPONSE_HEADERS.includes(name)) return
|
|
96
|
+
// the last line for a name wins, and a value that cannot be sent removes
|
|
97
|
+
// the header - the rule the server applies in extractHeaders
|
|
98
|
+
if (HEADER_VALUE.test(value)) headers[name] = value
|
|
99
|
+
else delete headers[name]
|
|
100
|
+
})
|
|
101
|
+
return headers
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The reply a run produces: the webout, in the shape and under the name the
|
|
106
|
+
* service asked for. A debug reply is the webout framed with the log, so it
|
|
107
|
+
* stays text - honouring a download there would put the frame in the file.
|
|
108
|
+
*/
|
|
109
|
+
const weboutResponse = (result, debug) => {
|
|
110
|
+
if (debug)
|
|
111
|
+
return text(
|
|
112
|
+
[result.webout, LOGS_SEPARATOR, result.log, LOGS_SEPARATOR].join('\n')
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const headers = parseHeaders(result.headers)
|
|
116
|
+
if (result.binary) {
|
|
117
|
+
if (!headers['content-type'])
|
|
118
|
+
headers['content-type'] = 'application/octet-stream'
|
|
119
|
+
return new Response(toBytes(result.webout), { status: 200, headers })
|
|
120
|
+
}
|
|
121
|
+
return new Response(result.webout, { status: 200, headers })
|
|
122
|
+
}
|
|
123
|
+
|
|
43
124
|
const broadcast = async (message) => {
|
|
44
125
|
const clients = await self.clients.matchAll({ includeUncontrolled: true })
|
|
45
126
|
clients.forEach((client) => client.postMessage(message))
|
|
@@ -105,7 +186,12 @@ const handle = async (request, url) => {
|
|
|
105
186
|
}
|
|
106
187
|
|
|
107
188
|
if (path.endsWith('/SASjsApi/drive/file') && request.method === 'GET') {
|
|
108
|
-
|
|
189
|
+
const file = R.read(query._filePath || '')
|
|
190
|
+
return file.binary
|
|
191
|
+
? new Response(toBytes(file.contents), {
|
|
192
|
+
headers: { 'content-type': 'application/octet-stream' }
|
|
193
|
+
})
|
|
194
|
+
: text(file.contents)
|
|
109
195
|
}
|
|
110
196
|
|
|
111
197
|
if (
|
|
@@ -138,13 +224,7 @@ const handle = async (request, url) => {
|
|
|
138
224
|
)
|
|
139
225
|
}
|
|
140
226
|
const debug = String(merged._debug || '').length > 0
|
|
141
|
-
return
|
|
142
|
-
debug
|
|
143
|
-
? [result.webout, LOGS_SEPARATOR, result.log, LOGS_SEPARATOR].join(
|
|
144
|
-
'\n'
|
|
145
|
-
)
|
|
146
|
-
: result.webout
|
|
147
|
-
)
|
|
227
|
+
return weboutResponse(result, debug)
|
|
148
228
|
}
|
|
149
229
|
|
|
150
230
|
if (path.includes('/SASjsApi/session')) {
|
|
@@ -219,9 +299,18 @@ self.addEventListener('message', (event) => {
|
|
|
219
299
|
|
|
220
300
|
case 'sasjs-stats':
|
|
221
301
|
event.waitUntil(
|
|
222
|
-
R.ensureReady().then(() =>
|
|
223
|
-
|
|
224
|
-
|
|
302
|
+
R.ensureReady().then(() => {
|
|
303
|
+
try {
|
|
304
|
+
reply({ type: 'sasjs-stats', boot: R.bootInfo(), stats: R.stats() })
|
|
305
|
+
} catch (e) {
|
|
306
|
+
// a corrupt entry fails the whole total, so the page is answered
|
|
307
|
+
// with the reason instead of waiting out its own timeout
|
|
308
|
+
reply({
|
|
309
|
+
type: 'sasjs-stats',
|
|
310
|
+
error: (e && e.message) || String(e)
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
})
|
|
225
314
|
)
|
|
226
315
|
break
|
|
227
316
|
}
|
|
@@ -10,6 +10,17 @@ export interface MockSiteResult {
|
|
|
10
10
|
/** True when a captured drive snapshot was shipped with the site. */
|
|
11
11
|
snapshot: boolean;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The bounds on what the browser drive seeds. A byte entry is carried as base64
|
|
15
|
+
* in vfs-seed.json and held in the worker's memory, so a large artefact - an
|
|
16
|
+
* export, a media file, a vendored archive - makes the generated site
|
|
17
|
+
* unloadable: one file over the per-file bound, or enough files to pass the
|
|
18
|
+
* total, is left off the drive and named in a warning, as every non-text file
|
|
19
|
+
* was when the drive carried text only. Text has no bound: a program folder is
|
|
20
|
+
* text by nature and the drive has always carried it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_BINARY_SEED_BYTES: number;
|
|
23
|
+
export declare const MAX_SEED_BYTES: number;
|
|
13
24
|
/**
|
|
14
25
|
* Builds a static site that runs the project's JS services (its mocks) in the
|
|
15
26
|
* browser, so a frontend can be tested with no server and no SAS.
|
|
@@ -75,6 +75,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
75
75
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
76
|
};
|
|
77
77
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
78
|
+
exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = void 0;
|
|
78
79
|
exports.buildMockSite = buildMockSite;
|
|
79
80
|
var crypto_1 = require("crypto");
|
|
80
81
|
var fs_1 = __importDefault(require("fs"));
|
|
@@ -140,28 +141,58 @@ var copyFolder = function (from, to) {
|
|
|
140
141
|
*/
|
|
141
142
|
var PROGRAM_FOLDERS = ['services', 'jobs', 'tests'];
|
|
142
143
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
144
|
+
* The bounds on what the browser drive seeds. A byte entry is carried as base64
|
|
145
|
+
* in vfs-seed.json and held in the worker's memory, so a large artefact - an
|
|
146
|
+
* export, a media file, a vendored archive - makes the generated site
|
|
147
|
+
* unloadable: one file over the per-file bound, or enough files to pass the
|
|
148
|
+
* total, is left off the drive and named in a warning, as every non-text file
|
|
149
|
+
* was when the drive carried text only. Text has no bound: a program folder is
|
|
150
|
+
* text by nature and the drive has always carried it.
|
|
147
151
|
*/
|
|
148
|
-
|
|
152
|
+
exports.MAX_BINARY_SEED_BYTES = 10 * 1024 * 1024;
|
|
153
|
+
exports.MAX_SEED_BYTES = 25 * 1024 * 1024;
|
|
154
|
+
/**
|
|
155
|
+
* What the drive stores for a file: its text when the bytes survive a UTF-8
|
|
156
|
+
* round trip, and behind the byte marker (BINARY_PREFIX in runtime-core.js)
|
|
157
|
+
* otherwise. An archive, an image or an xlsx therefore lands on the drive as
|
|
158
|
+
* bytes, so a service that downloads a project-supplied file finds it.
|
|
159
|
+
*
|
|
160
|
+
* Text that begins with U+0000 is escaped, because the runtime reads a leading
|
|
161
|
+
* NUL as its own marker - the same rule the runtime applies to a write.
|
|
162
|
+
*/
|
|
163
|
+
var DRIVE_BINARY_PREFIX = '\u0000base64:';
|
|
164
|
+
var DRIVE_TEXT_PREFIX = '\u0000text:';
|
|
165
|
+
/** A byte count in MB, for the message that names a file too large to seed. */
|
|
166
|
+
var megabytes = function (bytes) {
|
|
167
|
+
return "".concat(Math.round((bytes / 1024 / 1024) * 10) / 10, " MB");
|
|
168
|
+
};
|
|
169
|
+
var readDriveFile = function (absolute) {
|
|
149
170
|
var buffer = fs_1.default.readFileSync(absolute);
|
|
150
171
|
var text = buffer.toString('utf8');
|
|
151
|
-
|
|
172
|
+
if (!Buffer.from(text, 'utf8').equals(buffer))
|
|
173
|
+
return {
|
|
174
|
+
contents: DRIVE_BINARY_PREFIX + buffer.toString('base64'),
|
|
175
|
+
// the raw size, which is what the seed bounds are measured in
|
|
176
|
+
bytes: buffer.length
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
contents: text.startsWith('\u0000') ? DRIVE_TEXT_PREFIX + text : text
|
|
180
|
+
};
|
|
152
181
|
};
|
|
153
182
|
var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
|
|
154
|
-
var e_3, _a, e_4, _b;
|
|
183
|
+
var e_3, _a, e_4, _b, e_5, _c;
|
|
155
184
|
if (exclude === void 0) { exclude = []; }
|
|
156
185
|
if (excludeFolders === void 0) { excludeFolders = []; }
|
|
157
186
|
var files = {};
|
|
158
187
|
var skipped = [];
|
|
159
188
|
var driveBase = "/sasjs/drive/files".concat(appLoc);
|
|
189
|
+
var seededBytes = 0;
|
|
160
190
|
// Every folder of the build goes on the drive, so a sync folder's content (a
|
|
161
191
|
// licence store, for instance) survives as well as the program folders.
|
|
162
192
|
var directories = fs_1.default
|
|
163
193
|
.readdirSync(buildFolder, { withFileTypes: true })
|
|
164
194
|
.filter(function (entry) { return entry.isDirectory(); });
|
|
195
|
+
var candidates = [];
|
|
165
196
|
try {
|
|
166
197
|
for (var directories_1 = __values(directories), directories_1_1 = directories_1.next(); !directories_1_1.done; directories_1_1 = directories_1.next()) {
|
|
167
198
|
var entry = directories_1_1.value;
|
|
@@ -172,22 +203,18 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
|
|
|
172
203
|
var key = "".concat(driveBase, "/").concat(entry.name, "/").concat(relative);
|
|
173
204
|
if (exclude.some(function (prefix) { return key.startsWith(prefix); }))
|
|
174
205
|
return "continue";
|
|
175
|
-
|
|
176
|
-
if (contents === null)
|
|
177
|
-
skipped.push(key);
|
|
178
|
-
else
|
|
179
|
-
files[key] = contents;
|
|
206
|
+
candidates.push({ key: key, absolute: path_1.default.join(folder, relative) });
|
|
180
207
|
};
|
|
181
208
|
try {
|
|
182
|
-
for (var
|
|
183
|
-
var relative =
|
|
209
|
+
for (var _d = (e_4 = void 0, __values(listFiles(folder))), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
210
|
+
var relative = _e.value;
|
|
184
211
|
_loop_1(relative);
|
|
185
212
|
}
|
|
186
213
|
}
|
|
187
214
|
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
188
215
|
finally {
|
|
189
216
|
try {
|
|
190
|
-
if (
|
|
217
|
+
if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
|
|
191
218
|
}
|
|
192
219
|
finally { if (e_4) throw e_4.error; }
|
|
193
220
|
}
|
|
@@ -200,6 +227,39 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
|
|
|
200
227
|
}
|
|
201
228
|
finally { if (e_3) throw e_3.error; }
|
|
202
229
|
}
|
|
230
|
+
// The budget is spent in path order, not in whatever order the folder listing
|
|
231
|
+
// returns, so the same project yields the same drive - and the same warning -
|
|
232
|
+
// on every machine and filesystem.
|
|
233
|
+
candidates.sort(function (first, second) {
|
|
234
|
+
return first.key < second.key ? -1 : first.key > second.key ? 1 : 0;
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
for (var candidates_1 = __values(candidates), candidates_1_1 = candidates_1.next(); !candidates_1_1.done; candidates_1_1 = candidates_1.next()) {
|
|
238
|
+
var candidate = candidates_1_1.value;
|
|
239
|
+
var file = readDriveFile(candidate.absolute);
|
|
240
|
+
// a byte entry counts against both bounds; a text entry counts against
|
|
241
|
+
// neither, because the drive has always carried the program folders
|
|
242
|
+
if (file.bytes !== undefined) {
|
|
243
|
+
if (file.bytes > exports.MAX_BINARY_SEED_BYTES) {
|
|
244
|
+
skipped.push({ key: candidate.key, bytes: file.bytes, limit: 'file' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (seededBytes + file.bytes > exports.MAX_SEED_BYTES) {
|
|
248
|
+
skipped.push({ key: candidate.key, bytes: file.bytes, limit: 'total' });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
seededBytes += file.bytes;
|
|
252
|
+
}
|
|
253
|
+
files[candidate.key] = file.contents;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
257
|
+
finally {
|
|
258
|
+
try {
|
|
259
|
+
if (candidates_1_1 && !candidates_1_1.done && (_c = candidates_1.return)) _c.call(candidates_1);
|
|
260
|
+
}
|
|
261
|
+
finally { if (e_5) throw e_5.error; }
|
|
262
|
+
}
|
|
203
263
|
return { files: files, skipped: skipped };
|
|
204
264
|
};
|
|
205
265
|
/**
|
|
@@ -282,7 +342,7 @@ var patchIndexHtml = function (html, version) {
|
|
|
282
342
|
function buildMockSite(target) {
|
|
283
343
|
return __awaiter(this, void 0, void 0, function () {
|
|
284
344
|
var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
285
|
-
var
|
|
345
|
+
var e_6, _e;
|
|
286
346
|
var _f, _g, _h;
|
|
287
347
|
return __generator(this, function (_j) {
|
|
288
348
|
switch (_j.label) {
|
|
@@ -332,8 +392,15 @@ function buildMockSite(target) {
|
|
|
332
392
|
'Build a target whose services are JavaScript (typically a mocks project).');
|
|
333
393
|
}
|
|
334
394
|
if (skipped.length) {
|
|
335
|
-
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("Left ".concat(skipped.length, "
|
|
336
|
-
"".concat(skipped
|
|
395
|
+
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("Left ".concat(skipped.length, " file(s) off the mock drive, over the ").concat(megabytes(exports.MAX_BINARY_SEED_BYTES), " per-file or ").concat(megabytes(exports.MAX_SEED_BYTES), " total the browser build seeds: ") +
|
|
396
|
+
"".concat(skipped
|
|
397
|
+
.slice(0, 3)
|
|
398
|
+
.map(function (file) {
|
|
399
|
+
return "".concat(file.key, " (").concat(megabytes(file.bytes), ", ").concat(file.limit === 'file'
|
|
400
|
+
? 'over the per-file limit'
|
|
401
|
+
: 'over the drive budget', ")");
|
|
402
|
+
})
|
|
403
|
+
.join(', ')).concat(skipped.length > 3 ? ', ...' : ''));
|
|
337
404
|
}
|
|
338
405
|
// Only now is the previous site expendable.
|
|
339
406
|
fs_1.default.rmSync(outputFolder, { recursive: true, force: true });
|
|
@@ -354,12 +421,12 @@ function buildMockSite(target) {
|
|
|
354
421
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
|
|
355
422
|
}
|
|
356
423
|
}
|
|
357
|
-
catch (
|
|
424
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
358
425
|
finally {
|
|
359
426
|
try {
|
|
360
427
|
if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
|
|
361
428
|
}
|
|
362
|
-
finally { if (
|
|
429
|
+
finally { if (e_6) throw e_6.error; }
|
|
363
430
|
}
|
|
364
431
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
365
432
|
// served from, and the app has to be controlled from /
|
|
@@ -228,23 +228,115 @@ describe('browser-hosted build', function () {
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
}); });
|
|
231
|
-
it('
|
|
231
|
+
it('carries a non-text file onto the drive as bytes', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
232
232
|
var png, result, seed;
|
|
233
|
+
return __generator(this, function (_a) {
|
|
234
|
+
switch (_a.label) {
|
|
235
|
+
case 0:
|
|
236
|
+
png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
237
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'fixtures.json'), '{}');
|
|
238
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'logo.png'), png);
|
|
239
|
+
// text that begins with the marker's own NUL is escaped, so it cannot be
|
|
240
|
+
// mistaken for a byte entry
|
|
241
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'odd.txt'), '\u0000base64:not really');
|
|
242
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
243
|
+
case 1:
|
|
244
|
+
result = _a.sent();
|
|
245
|
+
seed = readSeed();
|
|
246
|
+
// stored behind the marker the runtime decodes, and the text files beside it
|
|
247
|
+
// stay text
|
|
248
|
+
expect(seed.files["/sasjs/drive/files".concat(appLoc, "/mock-storage/logo.png")]).toBe('\u0000base64:' + png.toString('base64'));
|
|
249
|
+
expect(seed.files["/sasjs/drive/files".concat(appLoc, "/mock-storage/fixtures.json")]).toBe('{}');
|
|
250
|
+
expect(seed.files["/sasjs/drive/files".concat(appLoc, "/mock-storage/odd.txt")]).toBe('\u0000text:\u0000base64:not really');
|
|
251
|
+
expect(result.driveEntries).toBe(7); // 3 programs + 3 text files + the png
|
|
252
|
+
return [2 /*return*/];
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}); });
|
|
256
|
+
it('leaves a byte file over the seed bound off the drive, and names it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
257
|
+
var result, seed;
|
|
233
258
|
var _a;
|
|
234
259
|
return __generator(this, function (_b) {
|
|
235
260
|
switch (_b.label) {
|
|
236
261
|
case 0:
|
|
237
|
-
png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
238
262
|
write(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'fixtures.json'), '{}');
|
|
239
|
-
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', '
|
|
263
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'export.zip'), Buffer.alloc(mockSite_1.MAX_BINARY_SEED_BYTES + 1, 0x89));
|
|
240
264
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
241
265
|
case 1:
|
|
242
266
|
result = _b.sent();
|
|
243
267
|
seed = readSeed();
|
|
244
|
-
|
|
245
|
-
expect(seed.files["/sasjs/drive/files".concat(appLoc, "/mock-storage/
|
|
268
|
+
// one large artefact must not make the generated site unloadable
|
|
269
|
+
expect(seed.files["/sasjs/drive/files".concat(appLoc, "/mock-storage/export.zip")]).toBeUndefined();
|
|
246
270
|
expect(result.driveEntries).toBe(5); // 3 programs + 2 text files
|
|
247
|
-
expect((_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn).toHaveBeenCalledWith(expect.stringContaining('
|
|
271
|
+
expect((_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn).toHaveBeenCalledWith(expect.stringContaining('export.zip (10 MB'));
|
|
272
|
+
return [2 /*return*/];
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}); });
|
|
276
|
+
it('stops seeding bytes once the drive budget is spent, and names the file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
277
|
+
var chunk, _a, _b, name_1, seed, seeded;
|
|
278
|
+
var e_2, _c;
|
|
279
|
+
var _d;
|
|
280
|
+
return __generator(this, function (_e) {
|
|
281
|
+
switch (_e.label) {
|
|
282
|
+
case 0:
|
|
283
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'fixtures.json'), '{}');
|
|
284
|
+
chunk = Buffer.alloc(Math.floor(mockSite_1.MAX_BINARY_SEED_BYTES * 0.9), 0x89);
|
|
285
|
+
try {
|
|
286
|
+
for (_a = __values(['one.bin', 'two.bin', 'three.bin']), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
287
|
+
name_1 = _b.value;
|
|
288
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', name_1), chunk);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
292
|
+
finally {
|
|
293
|
+
try {
|
|
294
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
295
|
+
}
|
|
296
|
+
finally { if (e_2) throw e_2.error; }
|
|
297
|
+
}
|
|
298
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
299
|
+
// the budget is spent in path order, so the drive is the same on every host:
|
|
300
|
+
// one and three fit, two does not
|
|
301
|
+
];
|
|
302
|
+
case 1:
|
|
303
|
+
_e.sent();
|
|
304
|
+
seed = readSeed();
|
|
305
|
+
seeded = Object.keys(seed.files)
|
|
306
|
+
.filter(function (key) { return key.endsWith('.bin'); })
|
|
307
|
+
.map(function (key) { return key.slice(key.lastIndexOf('/') + 1); });
|
|
308
|
+
expect(seeded).toEqual(['one.bin', 'three.bin']);
|
|
309
|
+
expect((_d = process.logger) === null || _d === void 0 ? void 0 : _d.warn).toHaveBeenCalledWith(expect.stringContaining('two.bin (9 MB, over the drive budget)'));
|
|
310
|
+
return [2 /*return*/];
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}); });
|
|
314
|
+
it('seeds a project file the runtime reads back as its bytes', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
315
|
+
var png, runtime, served;
|
|
316
|
+
return __generator(this, function (_a) {
|
|
317
|
+
switch (_a.label) {
|
|
318
|
+
case 0:
|
|
319
|
+
png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
320
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'fixtures.json'), '{}');
|
|
321
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', 'logo.png'), png);
|
|
322
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
323
|
+
// the seed is only useful if the runtime decodes what the build wrote
|
|
324
|
+
];
|
|
325
|
+
case 1:
|
|
326
|
+
_a.sent();
|
|
327
|
+
global.self = {};
|
|
328
|
+
jest.resetModules();
|
|
329
|
+
runtime = require('../assets/runtime-core.js');
|
|
330
|
+
runtime.load(readSeed().files);
|
|
331
|
+
served = runtime.run({
|
|
332
|
+
code: [
|
|
333
|
+
"const file = '/sasjs/drive/files".concat(appLoc, "/mock-storage/logo.png'"),
|
|
334
|
+
"_webout = [fs.readFileSync(file).toString('hex'), fs.statSync(file).size].join('|')"
|
|
335
|
+
].join('\n')
|
|
336
|
+
});
|
|
337
|
+
delete global.self;
|
|
338
|
+
expect(served.error).toBeNull();
|
|
339
|
+
expect(served.webout).toBe("".concat(png.toString('hex'), "|").concat(png.length));
|
|
248
340
|
return [2 /*return*/];
|
|
249
341
|
}
|
|
250
342
|
});
|
|
@@ -355,8 +447,8 @@ describe('browser-hosted build', function () {
|
|
|
355
447
|
});
|
|
356
448
|
}); });
|
|
357
449
|
it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
358
|
-
var _a, _b, mockWebFolder,
|
|
359
|
-
var
|
|
450
|
+
var _a, _b, mockWebFolder, e_3_1;
|
|
451
|
+
var e_3, _c;
|
|
360
452
|
return __generator(this, function (_d) {
|
|
361
453
|
switch (_d.label) {
|
|
362
454
|
case 0:
|
|
@@ -387,14 +479,14 @@ describe('browser-hosted build', function () {
|
|
|
387
479
|
return [3 /*break*/, 1];
|
|
388
480
|
case 4: return [3 /*break*/, 7];
|
|
389
481
|
case 5:
|
|
390
|
-
|
|
391
|
-
|
|
482
|
+
e_3_1 = _d.sent();
|
|
483
|
+
e_3 = { error: e_3_1 };
|
|
392
484
|
return [3 /*break*/, 7];
|
|
393
485
|
case 6:
|
|
394
486
|
try {
|
|
395
487
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
396
488
|
}
|
|
397
|
-
finally { if (
|
|
489
|
+
finally { if (e_3) throw e_3.error; }
|
|
398
490
|
return [7 /*endfinally*/];
|
|
399
491
|
case 7:
|
|
400
492
|
expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
|
|
@@ -279,9 +279,8 @@ describe('page-side store', function () {
|
|
|
279
279
|
return [4 /*yield*/, window.SASjsMock.writeFiles({ '/sasjs/drive/b.txt': 'bb' })];
|
|
280
280
|
case 5:
|
|
281
281
|
_a.sent();
|
|
282
|
-
return [4 /*yield*/, expect(window.SASjsMock.
|
|
283
|
-
|
|
284
|
-
bytes: 2
|
|
282
|
+
return [4 /*yield*/, expect(window.SASjsMock.readAll()).resolves.toEqual({
|
|
283
|
+
'/sasjs/drive/b.txt': 'bb'
|
|
285
284
|
})];
|
|
286
285
|
case 6:
|
|
287
286
|
_a.sent();
|
|
@@ -295,6 +294,65 @@ describe('page-side store', function () {
|
|
|
295
294
|
}
|
|
296
295
|
});
|
|
297
296
|
}); });
|
|
297
|
+
it('takes the drive total from the worker, which owns the storage format', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
298
|
+
var _a, window, posted, reply, pending;
|
|
299
|
+
return __generator(this, function (_b) {
|
|
300
|
+
switch (_b.label) {
|
|
301
|
+
case 0:
|
|
302
|
+
_a = loadPersist(), window = _a.window, posted = _a.posted, reply = _a.reply;
|
|
303
|
+
pending = window.SASjsMock.stats();
|
|
304
|
+
return [4 /*yield*/, Promise.resolve()];
|
|
305
|
+
case 1:
|
|
306
|
+
_b.sent();
|
|
307
|
+
expect(posted).toContainEqual({ type: 'sasjs-stats' });
|
|
308
|
+
// the worker measures a byte entry as its bytes, not as base64
|
|
309
|
+
reply({ type: 'sasjs-stats', boot: {}, stats: { files: 2, bytes: 7 } });
|
|
310
|
+
return [4 /*yield*/, expect(pending).resolves.toEqual({ files: 2, bytes: 7 })];
|
|
311
|
+
case 2:
|
|
312
|
+
_b.sent();
|
|
313
|
+
return [2 /*return*/];
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}); });
|
|
317
|
+
it('fails the read when the worker cannot report, rather than returning NaN', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
318
|
+
var window, pending;
|
|
319
|
+
return __generator(this, function (_a) {
|
|
320
|
+
switch (_a.label) {
|
|
321
|
+
case 0:
|
|
322
|
+
jest.useFakeTimers();
|
|
323
|
+
window = loadPersist().window;
|
|
324
|
+
pending = expect(window.SASjsMock.stats()).rejects.toThrow('timed out waiting for sasjs-stats');
|
|
325
|
+
return [4 /*yield*/, jest.advanceTimersByTimeAsync(20000)];
|
|
326
|
+
case 1:
|
|
327
|
+
_a.sent();
|
|
328
|
+
return [4 /*yield*/, pending];
|
|
329
|
+
case 2:
|
|
330
|
+
_a.sent();
|
|
331
|
+
return [2 /*return*/];
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}); });
|
|
335
|
+
it('fails the read when the reply carries no stats', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
336
|
+
var _a, window, reply, pending;
|
|
337
|
+
return __generator(this, function (_b) {
|
|
338
|
+
switch (_b.label) {
|
|
339
|
+
case 0:
|
|
340
|
+
_a = loadPersist(), window = _a.window, reply = _a.reply;
|
|
341
|
+
pending = expect(window.SASjsMock.stats()).rejects.toThrow('carried no stats');
|
|
342
|
+
return [4 /*yield*/, Promise.resolve()
|
|
343
|
+
// a worker left over from an older build answers the message with no stats
|
|
344
|
+
];
|
|
345
|
+
case 1:
|
|
346
|
+
_b.sent();
|
|
347
|
+
// a worker left over from an older build answers the message with no stats
|
|
348
|
+
reply({ type: 'sasjs-stats' });
|
|
349
|
+
return [4 /*yield*/, pending];
|
|
350
|
+
case 2:
|
|
351
|
+
_b.sent();
|
|
352
|
+
return [2 /*return*/];
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}); });
|
|
298
356
|
it('writes nothing when a change set is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
299
357
|
var window;
|
|
300
358
|
return __generator(this, function (_a) {
|