@sasjs/cli 6.29.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.
- 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 +182 -12
- package/build/commands/build/internal/mockSite/mockSite.d.ts +23 -0
- package/build/commands/build/internal/mockSite/mockSite.js +245 -29
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +50 -3
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +132 -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 +587 -4
- package/build/commands/create/spec/create.spec.js +22 -18
- package/build/commands/deploy/spec/cbd.spec.js +149 -75
- package/build/utils/test.d.ts +12 -0
- package/build/utils/test.js +66 -1
- package/build/utils/utils.d.ts +8 -0
- package/build/utils/utils.js +16 -4
- package/package.json +2 -2
- package/build/commands/create/spec/angularAppFiles.d.ts +0 -2
- package/build/commands/create/spec/angularAppFiles.js +0 -117
- package/build/commands/create/spec/minimalAppFiles.d.ts +0 -2
- package/build/commands/create/spec/minimalAppFiles.js +0 -44
- package/build/commands/create/spec/reactAppFiles.d.ts +0 -2
- package/build/commands/create/spec/reactAppFiles.js +0 -93
|
@@ -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,440 @@ 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
|
+
}); });
|
|
1599
|
+
/**
|
|
1600
|
+
* The rules are the drive's, not the linter's defaults.
|
|
1601
|
+
*
|
|
1602
|
+
* The Studio shows a visitor the drive's `.sasjslint` and invites them to
|
|
1603
|
+
* edit it, so a rules file nothing read would be a displayed lie. Each case
|
|
1604
|
+
* below changes a rule and reads the answer back through the route the
|
|
1605
|
+
* editor calls, because "the resolver was passed" is not the claim - "the
|
|
1606
|
+
* squiggle went away" is.
|
|
1607
|
+
*/
|
|
1608
|
+
describe('the drive’s .sasjslint', function () {
|
|
1609
|
+
var rootConfig = '/sasjs/drive/files/.sasjslint';
|
|
1610
|
+
/** 81 characters: over the default 80, under the 100 these cases set. */
|
|
1611
|
+
var longLine = 'x'.repeat(81);
|
|
1612
|
+
var code = "data _null_;\n".concat(longLine, "\nrun;\n");
|
|
1613
|
+
var tooLong = function (diagnostics) {
|
|
1614
|
+
return diagnostics.filter(function (diagnostic) {
|
|
1615
|
+
return String(diagnostic.message).startsWith('Line exceeds maximum length');
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
var lint = function (worker, body) { return __awaiter(void 0, void 0, void 0, function () {
|
|
1619
|
+
var response;
|
|
1620
|
+
return __generator(this, function (_a) {
|
|
1621
|
+
switch (_a.label) {
|
|
1622
|
+
case 0: return [4 /*yield*/, worker.dispatch('/SASjsApi/code/lint', runRequest(body))];
|
|
1623
|
+
case 1:
|
|
1624
|
+
response = _a.sent();
|
|
1625
|
+
expect(response.status).toBe(200);
|
|
1626
|
+
return [2 /*return*/, response.json()];
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}); };
|
|
1630
|
+
it('lints against the rules file at the drive root', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1631
|
+
var worker, _a, _b, _c, _d;
|
|
1632
|
+
var _e, _f;
|
|
1633
|
+
return __generator(this, function (_g) {
|
|
1634
|
+
switch (_g.label) {
|
|
1635
|
+
case 0:
|
|
1636
|
+
worker = seeded();
|
|
1637
|
+
return [4 /*yield*/, worker.runtime.load((_e = {},
|
|
1638
|
+
_e[rootConfig] = JSON.stringify({ maxLineLength: 100 }),
|
|
1639
|
+
_e[drivePath('program.sas')] = code,
|
|
1640
|
+
_e))];
|
|
1641
|
+
case 1:
|
|
1642
|
+
_g.sent();
|
|
1643
|
+
_a = expect;
|
|
1644
|
+
_b = tooLong;
|
|
1645
|
+
return [4 /*yield*/, lint(worker, { code: code, filePath: appLoc + '/program.sas' })];
|
|
1646
|
+
case 2:
|
|
1647
|
+
_a.apply(void 0, [_b.apply(void 0, [_g.sent()])]).toHaveLength(0);
|
|
1648
|
+
// the same buffer with the default 80 in place: the finding the rule
|
|
1649
|
+
// exists to report
|
|
1650
|
+
return [4 /*yield*/, worker.runtime.load((_f = {},
|
|
1651
|
+
_f[rootConfig] = JSON.stringify({ maxLineLength: 80 }),
|
|
1652
|
+
_f[drivePath('program.sas')] = code,
|
|
1653
|
+
_f))];
|
|
1654
|
+
case 3:
|
|
1655
|
+
// the same buffer with the default 80 in place: the finding the rule
|
|
1656
|
+
// exists to report
|
|
1657
|
+
_g.sent();
|
|
1658
|
+
_c = expect;
|
|
1659
|
+
_d = tooLong;
|
|
1660
|
+
return [4 /*yield*/, lint(worker, { code: code, filePath: appLoc + '/program.sas' })];
|
|
1661
|
+
case 4:
|
|
1662
|
+
_c.apply(void 0, [_d.apply(void 0, [_g.sent()])]).toHaveLength(1);
|
|
1663
|
+
return [2 /*return*/];
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
}); });
|
|
1667
|
+
it('re-reads the rules per request, so an edit takes effect', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1668
|
+
var worker, path, _a, _b, form, patched, _c, _d;
|
|
1669
|
+
var _e;
|
|
1670
|
+
return __generator(this, function (_f) {
|
|
1671
|
+
switch (_f.label) {
|
|
1672
|
+
case 0:
|
|
1673
|
+
worker = seeded();
|
|
1674
|
+
path = appLoc + '/program.sas';
|
|
1675
|
+
return [4 /*yield*/, worker.runtime.load((_e = {},
|
|
1676
|
+
_e[rootConfig] = JSON.stringify({ maxLineLength: 80 }),
|
|
1677
|
+
_e[drivePath('program.sas')] = code,
|
|
1678
|
+
_e))];
|
|
1679
|
+
case 1:
|
|
1680
|
+
_f.sent();
|
|
1681
|
+
_a = expect;
|
|
1682
|
+
_b = tooLong;
|
|
1683
|
+
return [4 /*yield*/, lint(worker, { code: code, filePath: path })];
|
|
1684
|
+
case 2:
|
|
1685
|
+
_a.apply(void 0, [_b.apply(void 0, [_f.sent()])]).toHaveLength(1);
|
|
1686
|
+
form = new FormData();
|
|
1687
|
+
form.set('file', new Blob([JSON.stringify({ maxLineLength: 100 })], {
|
|
1688
|
+
type: 'application/json'
|
|
1689
|
+
}), '.sasjslint');
|
|
1690
|
+
return [4 /*yield*/, worker.dispatch("/SASjsApi/drive/file?_filePath=".concat(encodeURIComponent('/.sasjslint')), { method: 'PATCH', body: form })];
|
|
1691
|
+
case 3:
|
|
1692
|
+
patched = _f.sent();
|
|
1693
|
+
expect(patched.status).toBe(200);
|
|
1694
|
+
_c = expect;
|
|
1695
|
+
_d = tooLong;
|
|
1696
|
+
return [4 /*yield*/, lint(worker, { code: code, filePath: path })];
|
|
1697
|
+
case 4:
|
|
1698
|
+
_c.apply(void 0, [_d.apply(void 0, [_f.sent()])]).toHaveLength(0);
|
|
1699
|
+
return [2 /*return*/];
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
}); });
|
|
1703
|
+
it('gives a folder its own rules for everything beneath it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1704
|
+
var worker, _a, _b, _c, _d;
|
|
1705
|
+
var _e;
|
|
1706
|
+
return __generator(this, function (_f) {
|
|
1707
|
+
switch (_f.label) {
|
|
1708
|
+
case 0:
|
|
1709
|
+
worker = seeded();
|
|
1710
|
+
return [4 /*yield*/, worker.runtime.load((_e = {},
|
|
1711
|
+
_e[rootConfig] = JSON.stringify({ maxLineLength: 80 }),
|
|
1712
|
+
// the nearer file relaxes the rule for this folder alone
|
|
1713
|
+
_e["/sasjs/drive/files".concat(appLoc, "/.sasjslint")] = JSON.stringify({
|
|
1714
|
+
maxLineLength: 200
|
|
1715
|
+
}),
|
|
1716
|
+
_e))];
|
|
1717
|
+
case 1:
|
|
1718
|
+
_f.sent();
|
|
1719
|
+
_a = expect;
|
|
1720
|
+
_b = tooLong;
|
|
1721
|
+
return [4 /*yield*/, lint(worker, {
|
|
1722
|
+
code: code,
|
|
1723
|
+
filePath: "".concat(appLoc, "/services/public/program.sas")
|
|
1724
|
+
})];
|
|
1725
|
+
case 2:
|
|
1726
|
+
_a.apply(void 0, [_b.apply(void 0, [_f.sent()])]).toHaveLength(0);
|
|
1727
|
+
// outside that folder the root file still applies
|
|
1728
|
+
_c = expect;
|
|
1729
|
+
_d = tooLong;
|
|
1730
|
+
return [4 /*yield*/, lint(worker, {
|
|
1731
|
+
code: code,
|
|
1732
|
+
filePath: '/home/sasjsadmin/program.sas'
|
|
1733
|
+
})];
|
|
1734
|
+
case 3:
|
|
1735
|
+
// outside that folder the root file still applies
|
|
1736
|
+
_c.apply(void 0, [_d.apply(void 0, [_f.sent()])]).toHaveLength(1);
|
|
1737
|
+
return [2 /*return*/];
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
}); });
|
|
1741
|
+
it('finds the rules for a path spelled the way a Windows client sends it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1742
|
+
var worker, _a, _b;
|
|
1743
|
+
var _c;
|
|
1744
|
+
return __generator(this, function (_d) {
|
|
1745
|
+
switch (_d.label) {
|
|
1746
|
+
case 0:
|
|
1747
|
+
worker = seeded();
|
|
1748
|
+
return [4 /*yield*/, worker.runtime.load((_c = {},
|
|
1749
|
+
_c["/sasjs/drive/files".concat(appLoc, "/.sasjslint")] = JSON.stringify({
|
|
1750
|
+
maxLineLength: 100
|
|
1751
|
+
}),
|
|
1752
|
+
_c))];
|
|
1753
|
+
case 1:
|
|
1754
|
+
_d.sent();
|
|
1755
|
+
_a = expect;
|
|
1756
|
+
_b = tooLong;
|
|
1757
|
+
return [4 /*yield*/, lint(worker, {
|
|
1758
|
+
code: code,
|
|
1759
|
+
filePath: "".concat(appLoc, "\\services\\public\\program.sas")
|
|
1760
|
+
})];
|
|
1761
|
+
case 2:
|
|
1762
|
+
_a.apply(void 0, [_b.apply(void 0, [_d.sent()])]).toHaveLength(0);
|
|
1763
|
+
return [2 /*return*/];
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
}); });
|
|
1767
|
+
it('skips a rules file that does not parse and uses the one above', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1768
|
+
var worker, _a, _b;
|
|
1769
|
+
var _c;
|
|
1770
|
+
return __generator(this, function (_d) {
|
|
1771
|
+
switch (_d.label) {
|
|
1772
|
+
case 0:
|
|
1773
|
+
worker = seeded();
|
|
1774
|
+
return [4 /*yield*/, worker.runtime.load((_c = {},
|
|
1775
|
+
_c[rootConfig] = JSON.stringify({ maxLineLength: 100 }),
|
|
1776
|
+
_c["/sasjs/drive/files".concat(appLoc, "/.sasjslint")] = '{ this is not json',
|
|
1777
|
+
_c))
|
|
1778
|
+
// a broken near file must not break linting, and must not silently
|
|
1779
|
+
// replace the rules that were working
|
|
1780
|
+
];
|
|
1781
|
+
case 1:
|
|
1782
|
+
_d.sent();
|
|
1783
|
+
// a broken near file must not break linting, and must not silently
|
|
1784
|
+
// replace the rules that were working
|
|
1785
|
+
_a = expect;
|
|
1786
|
+
_b = tooLong;
|
|
1787
|
+
return [4 /*yield*/, lint(worker, {
|
|
1788
|
+
code: code,
|
|
1789
|
+
filePath: "".concat(appLoc, "/services/public/program.sas")
|
|
1790
|
+
})];
|
|
1791
|
+
case 2:
|
|
1792
|
+
// a broken near file must not break linting, and must not silently
|
|
1793
|
+
// replace the rules that were working
|
|
1794
|
+
_a.apply(void 0, [_b.apply(void 0, [_d.sent()])]).toHaveLength(0);
|
|
1795
|
+
return [2 /*return*/];
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
}); });
|
|
1799
|
+
it('takes the drive root rules for a buffer with no file, and for a path that escapes the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1800
|
+
var worker, _a, _b, _c, _d;
|
|
1801
|
+
var _e;
|
|
1802
|
+
return __generator(this, function (_f) {
|
|
1803
|
+
switch (_f.label) {
|
|
1804
|
+
case 0:
|
|
1805
|
+
worker = seeded();
|
|
1806
|
+
return [4 /*yield*/, worker.runtime.load((_e = {},
|
|
1807
|
+
_e[rootConfig] = JSON.stringify({ maxLineLength: 100 }),
|
|
1808
|
+
_e))];
|
|
1809
|
+
case 1:
|
|
1810
|
+
_f.sent();
|
|
1811
|
+
_a = expect;
|
|
1812
|
+
_b = tooLong;
|
|
1813
|
+
return [4 /*yield*/, lint(worker, { code: code })];
|
|
1814
|
+
case 2:
|
|
1815
|
+
_a.apply(void 0, [_b.apply(void 0, [_f.sent()])]).toHaveLength(0);
|
|
1816
|
+
_c = expect;
|
|
1817
|
+
_d = tooLong;
|
|
1818
|
+
return [4 /*yield*/, lint(worker, {
|
|
1819
|
+
code: code,
|
|
1820
|
+
filePath: '../../outside/program.sas'
|
|
1821
|
+
})];
|
|
1822
|
+
case 3:
|
|
1823
|
+
_c.apply(void 0, [_d.apply(void 0, [_f.sent()])]).toHaveLength(0);
|
|
1824
|
+
return [2 /*return*/];
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
}); });
|
|
1828
|
+
it('falls back to the linter defaults when the drive has no rules file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1829
|
+
var worker, _a, _b;
|
|
1830
|
+
var _c;
|
|
1831
|
+
return __generator(this, function (_d) {
|
|
1832
|
+
switch (_d.label) {
|
|
1833
|
+
case 0:
|
|
1834
|
+
worker = seeded();
|
|
1835
|
+
return [4 /*yield*/, worker.runtime.load((_c = {}, _c[drivePath('program.sas')] = code, _c))];
|
|
1836
|
+
case 1:
|
|
1837
|
+
_d.sent();
|
|
1838
|
+
_a = expect;
|
|
1839
|
+
_b = tooLong;
|
|
1840
|
+
return [4 /*yield*/, lint(worker, {
|
|
1841
|
+
code: code,
|
|
1842
|
+
filePath: "".concat(appLoc, "/program.sas")
|
|
1843
|
+
})];
|
|
1844
|
+
case 2:
|
|
1845
|
+
_a.apply(void 0, [_b.apply(void 0, [_d.sent()])]).toHaveLength(1);
|
|
1846
|
+
return [2 /*return*/];
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
}); });
|
|
1850
|
+
it('formats with the rules file too', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1851
|
+
var worker, formatted, _a;
|
|
1852
|
+
var _b;
|
|
1853
|
+
return __generator(this, function (_c) {
|
|
1854
|
+
switch (_c.label) {
|
|
1855
|
+
case 0:
|
|
1856
|
+
worker = seeded();
|
|
1857
|
+
return [4 /*yield*/, worker.runtime.load((_b = {},
|
|
1858
|
+
_b[rootConfig] = JSON.stringify({
|
|
1859
|
+
hasDoxygenHeader: true,
|
|
1860
|
+
defaultHeader: '/**\n @brief the drive header\n**/'
|
|
1861
|
+
}),
|
|
1862
|
+
_b))];
|
|
1863
|
+
case 1:
|
|
1864
|
+
_c.sent();
|
|
1865
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/code/format', runRequest({ code: code, filePath: '/home/sasjsadmin/program.sas' }))];
|
|
1866
|
+
case 2:
|
|
1867
|
+
formatted = _c.sent();
|
|
1868
|
+
expect(formatted.status).toBe(200);
|
|
1869
|
+
_a = expect;
|
|
1870
|
+
return [4 /*yield*/, formatted.text()];
|
|
1871
|
+
case 3:
|
|
1872
|
+
_a.apply(void 0, [_c.sent()]).toContain('@brief the drive header');
|
|
1873
|
+
return [2 /*return*/];
|
|
1874
|
+
}
|
|
1875
|
+
});
|
|
1876
|
+
}); });
|
|
1877
|
+
});
|
|
1415
1878
|
it('keeps an app’s URL and tiles inside the folder the site is served from', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1416
1879
|
var worker, archive, form, published, app, listing, apps, landing, _a, page, _b;
|
|
1417
1880
|
return __generator(this, function (_c) {
|
|
@@ -1985,6 +2448,119 @@ describe('service worker host', function () {
|
|
|
1985
2448
|
}
|
|
1986
2449
|
});
|
|
1987
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
|
+
}); });
|
|
1988
2564
|
it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1989
2565
|
var dispatch, response, _a;
|
|
1990
2566
|
return __generator(this, function (_b) {
|
|
@@ -2556,16 +3132,23 @@ describe('service worker host', function () {
|
|
|
2556
3132
|
expect(seed.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-seed-ack' }));
|
|
2557
3133
|
runtime.load(files);
|
|
2558
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();
|
|
2559
3142
|
expect(snapshot.replies[0].type).toBe('sasjs-snapshot');
|
|
2560
3143
|
expect(Object.keys(snapshot.replies[0].files).length).toBe(1);
|
|
2561
3144
|
reset = message({ type: 'sasjs-reset' });
|
|
2562
3145
|
return [4 /*yield*/, reset.pending];
|
|
2563
|
-
case
|
|
3146
|
+
case 4:
|
|
2564
3147
|
_c.sent();
|
|
2565
3148
|
expect(reset.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-reset-ack' }));
|
|
2566
3149
|
stats = message({ type: 'sasjs-stats' });
|
|
2567
3150
|
return [4 /*yield*/, stats.pending];
|
|
2568
|
-
case
|
|
3151
|
+
case 5:
|
|
2569
3152
|
_c.sent();
|
|
2570
3153
|
expect(stats.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-stats' }));
|
|
2571
3154
|
return [2 /*return*/];
|