@sasjs/cli 6.30.0 → 6.30.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.
@@ -147,7 +147,11 @@ var loadWorker = function (base, options) {
147
147
  }
148
148
  };
149
149
  global.self = self;
150
+ // what the worker pulled into itself, so a test can assert that a script it
151
+ // does not need at boot is not fetched at boot
152
+ var imported = [];
150
153
  global.importScripts = function (path) {
154
+ imported.push(path);
151
155
  if (path.includes('md5'))
152
156
  return;
153
157
  if (path.includes('api-browser'))
@@ -185,7 +189,7 @@ var loadWorker = function (base, options) {
185
189
  };
186
190
  require('../assets/sw.js');
187
191
  global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
188
- var target, fixture, fixture;
192
+ var target, fixture, fixture, shards, piece, id, requested, shards_1;
189
193
  var _a;
190
194
  return __generator(this, function (_b) {
191
195
  target = typeof input === 'string' ? input : (input && input.url) || String(input);
@@ -222,7 +226,29 @@ var loadWorker = function (base, options) {
222
226
  userId: mockSite_1.WEBOUT_SENTINELS.userId,
223
227
  apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
224
228
  })
225
- })];
229
+ })
230
+ // a sharded drive, when a test sets one up: the index answers the snapshot
231
+ // URL and each piece answers its own file, so the worker's fetch of a shard
232
+ // is the same request it makes against a static host
233
+ ];
234
+ shards = global.__sasjsShards;
235
+ if (shards && target.includes('vfs-snapshot')) {
236
+ piece = /vfs-snapshot-(\d+)\.json/.exec(target);
237
+ if (piece) {
238
+ id = Number(piece[1]);
239
+ requested = (global.__sasjsShardFetches =
240
+ global.__sasjsShardFetches || []);
241
+ requested.push(id);
242
+ return [2 /*return*/, jsonResponse({ format: mockSite_1.DRIVE_FORMAT, files: shards[id] || {} })];
243
+ }
244
+ return [2 /*return*/, jsonResponse(shards.index)];
245
+ }
246
+ if (target.includes('mock-config')) {
247
+ shards_1 = global.__sasjsShards;
248
+ return [2 /*return*/, jsonResponse(__assign({ appLoc: appLoc }, (shards_1
249
+ ? { snapshotShards: (shards_1.index.shards || []).length }
250
+ : {})))];
251
+ }
226
252
  return [2 /*return*/, target.includes('vfs-seed')
227
253
  ? jsonResponse({
228
254
  files: (_a = {},
@@ -266,6 +292,7 @@ var loadWorker = function (base, options) {
266
292
  dispatch: dispatch,
267
293
  message: message,
268
294
  lifecycle: lifecycle,
295
+ imported: imported,
269
296
  self: self,
270
297
  refuseWrites: function (value) {
271
298
  refusingWrites = value;
@@ -308,6 +335,8 @@ describe('service worker host', function () {
308
335
  delete global.importScripts;
309
336
  delete global.fetch;
310
337
  delete global.__sasjsIdentity;
338
+ delete global.__sasjsShards;
339
+ delete global.__sasjsShardFetches;
311
340
  delete global.__mockHostClients;
312
341
  });
313
342
  var seeded = function () {
@@ -1412,6 +1441,161 @@ describe('service worker host', function () {
1412
1441
  }
1413
1442
  });
1414
1443
  }); });
1444
+ /**
1445
+ * The linter is not part of the install.
1446
+ *
1447
+ * The page cannot make its first request until the worker has installed, so
1448
+ * anything the worker imports at boot is on the critical path of every
1449
+ * visit. The linter answers two routes; it is fetched when one of them is
1450
+ * called.
1451
+ */
1452
+ it('fetches the linter on the first code route, not at boot', function () { return __awaiter(void 0, void 0, void 0, function () {
1453
+ var worker, response;
1454
+ return __generator(this, function (_a) {
1455
+ switch (_a.label) {
1456
+ case 0:
1457
+ worker = seeded();
1458
+ expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(false);
1459
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({ code: 'data a;\nrun;\n' }))];
1460
+ case 1:
1461
+ response = _a.sent();
1462
+ expect(response.status).toBe(200);
1463
+ expect(worker.imported.some(function (path) { return path.includes('lint-browser'); })).toBe(true);
1464
+ return [2 /*return*/];
1465
+ }
1466
+ });
1467
+ }); });
1468
+ /**
1469
+ * A route that resolves a path from the drive is not left with a hole.
1470
+ *
1471
+ * The rules an edited file is linted against are found by walking up from
1472
+ * the file, so the request names neither path. The read that fails says
1473
+ * which piece it needs, and the request is answered again with it - the body
1474
+ * re-read from the copy taken before the first attempt.
1475
+ */
1476
+ it('fetches the piece a route resolves from the drive, not the one it names', function () { return __awaiter(void 0, void 0, void 0, function () {
1477
+ var worker, longLine, response, _a, _b, _c;
1478
+ return __generator(this, function (_d) {
1479
+ switch (_d.label) {
1480
+ case 0:
1481
+ ;
1482
+ global.__sasjsShards = {
1483
+ index: {
1484
+ format: mockSite_1.DRIVE_FORMAT,
1485
+ sharded: true,
1486
+ boot: 0,
1487
+ entries: {
1488
+ '/sasjs/drive/files/.sasjslint': { shard: 1, bytes: 20 },
1489
+ '/sasjs/drive/files/Public/app/test/prog.sas': {
1490
+ shard: 0,
1491
+ bytes: 12
1492
+ }
1493
+ },
1494
+ shards: [
1495
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
1496
+ { id: 1, file: 'host/vfs-snapshot-1.json' }
1497
+ ]
1498
+ },
1499
+ 0: { '/sasjs/drive/files/Public/app/test/prog.sas': 'data a; run;' },
1500
+ 1: {
1501
+ '/sasjs/drive/files/.sasjslint': JSON.stringify({
1502
+ maxLineLength: 200
1503
+ })
1504
+ }
1505
+ };
1506
+ global.__sasjsShardFetches = [];
1507
+ worker = loadWorker();
1508
+ return [4 /*yield*/, worker.runtime.load({})];
1509
+ case 1:
1510
+ _d.sent();
1511
+ longLine = 'x'.repeat(150);
1512
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({
1513
+ code: "data _null_;\n".concat(longLine, "\nrun;\n"),
1514
+ filePath: '/Public/app/test/prog.sas'
1515
+ }))];
1516
+ case 2:
1517
+ response = _d.sent();
1518
+ expect(response.status).toBe(200);
1519
+ // the line is over the linter's default 80 and under this drive's 200, so
1520
+ // the answer proves the rules file was read - from a piece the request
1521
+ // never named
1522
+ _a = expect;
1523
+ _c = (_b = JSON).stringify;
1524
+ return [4 /*yield*/, response.json()];
1525
+ case 3:
1526
+ // the line is over the linter's default 80 and under this drive's 200, so
1527
+ // the answer proves the rules file was read - from a piece the request
1528
+ // never named
1529
+ _a.apply(void 0, [_c.apply(_b, [_d.sent()])]).not.toContain('Line exceeds');
1530
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
1531
+ return [2 /*return*/];
1532
+ }
1533
+ });
1534
+ }); });
1535
+ /**
1536
+ * A request that needs a second piece is answered, body and all.
1537
+ *
1538
+ * The first retry fetches the piece the failed read named; a request that
1539
+ * reads two pieces one after another reaches the second retry, which fetches
1540
+ * the rest. Either way the body has to arrive again - a Request's body can
1541
+ * only be read once, so the first attempt keeps an unread copy and each
1542
+ * attempt after it is made from a copy of that.
1543
+ */
1544
+ it('replays a request that needs two pieces, with its body intact', function () { return __awaiter(void 0, void 0, void 0, function () {
1545
+ var near, far, worker, longLine, response, findings, _a, _b;
1546
+ var _c, _d, _e;
1547
+ return __generator(this, function (_f) {
1548
+ switch (_f.label) {
1549
+ case 0:
1550
+ near = '/sasjs/drive/files/Public/app/test/.sasjslint';
1551
+ far = '/sasjs/drive/files/.sasjslint';
1552
+ global.__sasjsShards = {
1553
+ index: {
1554
+ format: mockSite_1.DRIVE_FORMAT,
1555
+ sharded: true,
1556
+ boot: 0,
1557
+ entries: (_c = {},
1558
+ _c[near] = { shard: 1, bytes: 20 },
1559
+ _c[far] = { shard: 2, bytes: 20 },
1560
+ _c),
1561
+ shards: [
1562
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
1563
+ { id: 1, file: 'host/vfs-snapshot-1.json' },
1564
+ { id: 2, file: 'host/vfs-snapshot-2.json' }
1565
+ ]
1566
+ },
1567
+ 0: {},
1568
+ // the nearest rules file is unreadable as a config, so the walk goes on
1569
+ // to the drive root's - a second piece, in a second shard
1570
+ 1: (_d = {}, _d[near] = 'not json', _d),
1571
+ 2: (_e = {}, _e[far] = JSON.stringify({ maxLineLength: 200 }), _e)
1572
+ };
1573
+ global.__sasjsShardFetches = [];
1574
+ worker = loadWorker();
1575
+ return [4 /*yield*/, worker.runtime.load({})];
1576
+ case 1:
1577
+ _f.sent();
1578
+ longLine = 'x'.repeat(150);
1579
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest({
1580
+ code: "data _null_;\n".concat(longLine, "\nrun;\n"),
1581
+ filePath: '/Public/app/test/prog.sas'
1582
+ }))];
1583
+ case 2:
1584
+ response = _f.sent();
1585
+ expect(response.status).toBe(200);
1586
+ _b = (_a = JSON).stringify;
1587
+ return [4 /*yield*/, response.json()];
1588
+ case 3:
1589
+ findings = _b.apply(_a, [_f.sent()]);
1590
+ // the code arrived on the second attempt and the root rules applied: both
1591
+ // halves of the claim
1592
+ expect(findings).toContain('Doxygen');
1593
+ expect(findings).not.toContain('Line exceeds');
1594
+ expect(global.__sasjsShardFetches).toEqual([0, 1, 2]);
1595
+ return [2 /*return*/];
1596
+ }
1597
+ });
1598
+ }); });
1415
1599
  /**
1416
1600
  * The rules are the drive's, not the linter's defaults.
1417
1601
  *
@@ -2264,6 +2448,119 @@ describe('service worker host', function () {
2264
2448
  }
2265
2449
  });
2266
2450
  }); });
2451
+ /**
2452
+ * A sharded drive supplies the piece a request names, and only that one.
2453
+ *
2454
+ * The build splits a captured drive so the first page does not download every
2455
+ * app's bundle. That makes it the host's job to fetch the piece holding the
2456
+ * path a route reads - here a drive file, named in `_filePath` - and to leave
2457
+ * the pieces nobody asked for on the site.
2458
+ */
2459
+ it('fetches the piece a drive request names, and leaves the rest', function () { return __awaiter(void 0, void 0, void 0, function () {
2460
+ var small, large, worker, response, _a;
2461
+ var _b, _c, _d;
2462
+ return __generator(this, function (_e) {
2463
+ switch (_e.label) {
2464
+ case 0:
2465
+ small = '/sasjs/drive/files/config.json';
2466
+ large = '/sasjs/drive/files/data/large.csv';
2467
+ global.__sasjsShards = {
2468
+ index: {
2469
+ format: mockSite_1.DRIVE_FORMAT,
2470
+ sharded: true,
2471
+ boot: 0,
2472
+ entries: (_b = {},
2473
+ _b[small] = { shard: 0, bytes: 2 },
2474
+ _b[large] = { shard: 1, bytes: 64 },
2475
+ _b),
2476
+ shards: [
2477
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
2478
+ { id: 1, file: 'host/vfs-snapshot-1.json' }
2479
+ ]
2480
+ },
2481
+ 0: (_c = {}, _c[small] = '{}', _c),
2482
+ 1: (_d = {}, _d[large] = 'a,b\n1,2\n3,4\n', _d)
2483
+ };
2484
+ global.__sasjsShardFetches = [];
2485
+ worker = loadWorker();
2486
+ return [4 /*yield*/, worker.runtime.load({})
2487
+ // the boot shard arrived with the drive; the second piece did not
2488
+ ];
2489
+ case 1:
2490
+ _e.sent();
2491
+ // the boot shard arrived with the drive; the second piece did not
2492
+ expect(global.__sasjsShardFetches).toEqual([0]);
2493
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/large.csv'))];
2494
+ case 2:
2495
+ response = _e.sent();
2496
+ expect(response.status).toBe(200);
2497
+ _a = expect;
2498
+ return [4 /*yield*/, response.text()];
2499
+ case 3:
2500
+ _a.apply(void 0, [_e.sent()]).toContain('a,b');
2501
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
2502
+ // a second read of the same piece is answered from memory
2503
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/large.csv'))];
2504
+ case 4:
2505
+ // a second read of the same piece is answered from memory
2506
+ _e.sent();
2507
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
2508
+ return [2 /*return*/];
2509
+ }
2510
+ });
2511
+ }); });
2512
+ /**
2513
+ * An export is the whole drive, not the part in memory.
2514
+ *
2515
+ * `window.SASjsMock.download()` writes the file a project ships as
2516
+ * `mock-snapshot.json`, so a sharded drive must be fetched before it is
2517
+ * dumped - otherwise the next build ships a snapshot that has quietly lost
2518
+ * every app the visitor never opened.
2519
+ */
2520
+ it('exports every piece of a sharded drive, not only the loaded ones', function () { return __awaiter(void 0, void 0, void 0, function () {
2521
+ var small, large, worker, _a, replies, pending, exported;
2522
+ var _b, _c, _d;
2523
+ return __generator(this, function (_e) {
2524
+ switch (_e.label) {
2525
+ case 0:
2526
+ small = '/sasjs/drive/files/config.json';
2527
+ large = '/sasjs/drive/files/data/large.csv';
2528
+ global.__sasjsShards = {
2529
+ index: {
2530
+ format: mockSite_1.DRIVE_FORMAT,
2531
+ sharded: true,
2532
+ boot: 0,
2533
+ entries: (_b = {},
2534
+ _b[small] = { shard: 0, bytes: 2 },
2535
+ _b[large] = { shard: 1, bytes: 8 },
2536
+ _b),
2537
+ shards: [
2538
+ { id: 0, file: 'host/vfs-snapshot-0.json' },
2539
+ { id: 1, file: 'host/vfs-snapshot-1.json' }
2540
+ ]
2541
+ },
2542
+ 0: (_c = {}, _c[small] = '{}', _c),
2543
+ 1: (_d = {}, _d[large] = 'a,b\n1,2\n', _d)
2544
+ };
2545
+ global.__sasjsShardFetches = [];
2546
+ worker = loadWorker();
2547
+ return [4 /*yield*/, worker.runtime.load({})];
2548
+ case 1:
2549
+ _e.sent();
2550
+ _a = worker.message({ type: 'sasjs-snapshot' }), replies = _a.replies, pending = _a.pending;
2551
+ return [4 /*yield*/, pending
2552
+ // the piece nobody read was fetched for the export
2553
+ ];
2554
+ case 2:
2555
+ _e.sent();
2556
+ // the piece nobody read was fetched for the export
2557
+ expect(global.__sasjsShardFetches).toEqual([0, 1]);
2558
+ exported = replies.find(function (reply) { return reply.type === 'sasjs-snapshot'; });
2559
+ expect(Object.keys(exported.files).sort()).toEqual([large, small].sort());
2560
+ return [2 /*return*/];
2561
+ }
2562
+ });
2563
+ }); });
2267
2564
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
2268
2565
  var dispatch, response, _a;
2269
2566
  return __generator(this, function (_b) {
@@ -2835,16 +3132,23 @@ describe('service worker host', function () {
2835
3132
  expect(seed.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-seed-ack' }));
2836
3133
  runtime.load(files);
2837
3134
  snapshot = message({ type: 'sasjs-snapshot' });
3135
+ // the reply waits for the drive to be whole, so a sharded site's export is
3136
+ // not the part of it that happens to be in memory
3137
+ return [4 /*yield*/, snapshot.pending];
3138
+ case 3:
3139
+ // the reply waits for the drive to be whole, so a sharded site's export is
3140
+ // not the part of it that happens to be in memory
3141
+ _c.sent();
2838
3142
  expect(snapshot.replies[0].type).toBe('sasjs-snapshot');
2839
3143
  expect(Object.keys(snapshot.replies[0].files).length).toBe(1);
2840
3144
  reset = message({ type: 'sasjs-reset' });
2841
3145
  return [4 /*yield*/, reset.pending];
2842
- case 3:
3146
+ case 4:
2843
3147
  _c.sent();
2844
3148
  expect(reset.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-reset-ack' }));
2845
3149
  stats = message({ type: 'sasjs-stats' });
2846
3150
  return [4 /*yield*/, stats.pending];
2847
- case 4:
3151
+ case 5:
2848
3152
  _c.sent();
2849
3153
  expect(stats.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-stats' }));
2850
3154
  return [2 /*return*/];
package/package.json CHANGED
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.30.0"
128
+ "version": "6.30.1"
129
129
  }