@sasjs/cli 6.27.2 → 6.28.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/intercept.js +40 -4
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +32 -1
- package/build/commands/build/internal/mockSite/assets/sw.js +191 -77
- package/build/commands/build/internal/mockSite/mockSite.js +81 -4
- package/build/commands/build/internal/mockSite/spec/intercept.spec.js +60 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +226 -8
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +6 -1
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +321 -41
- package/package.json +2 -2
|
@@ -57,6 +57,31 @@ var __values = (this && this.__values) || function(o) {
|
|
|
57
57
|
};
|
|
58
58
|
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
59
59
|
};
|
|
60
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
61
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
62
|
+
if (!m) return o;
|
|
63
|
+
var i = m.call(o), r, ar = [], e;
|
|
64
|
+
try {
|
|
65
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
66
|
+
}
|
|
67
|
+
catch (error) { e = { error: error }; }
|
|
68
|
+
finally {
|
|
69
|
+
try {
|
|
70
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
71
|
+
}
|
|
72
|
+
finally { if (e) throw e.error; }
|
|
73
|
+
}
|
|
74
|
+
return ar;
|
|
75
|
+
};
|
|
76
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
77
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
78
|
+
if (ar || !(i in from)) {
|
|
79
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
80
|
+
ar[i] = from[i];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
84
|
+
};
|
|
60
85
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
61
86
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
62
87
|
};
|
|
@@ -81,11 +106,22 @@ var createProject = function (root) {
|
|
|
81
106
|
write(path_1.default.join(build, 'server-ci.sas'), '%put build pack;\n');
|
|
82
107
|
write(path_1.default.join(build, 'server-ci.json'), '{"members":[]}\n');
|
|
83
108
|
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
84
|
-
'<!doctype html><html><head><title>App</title
|
|
109
|
+
'<!doctype html><html><head><title>App</title>',
|
|
110
|
+
'<link rel="stylesheet" href="theme-boot.css" />',
|
|
111
|
+
// a paint-critical script: it runs nothing that needs the worker, so the
|
|
112
|
+
// page marks it to stay in the parser rather than being deferred
|
|
113
|
+
'<script src="theme-boot.js" data-early></script>',
|
|
114
|
+
// the same marker on a module cannot be honoured - a module is deferred by
|
|
115
|
+
// specification - so the build must replay it and say why
|
|
116
|
+
'<script src="deferred.js" type="module" data-early></script>',
|
|
117
|
+
'</head><body>',
|
|
85
118
|
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\" servertype=\"SASJS\" loginmechanism=\"Redirected\"></sasjs>"),
|
|
86
119
|
'<script src="main.abc123.js" type="module"></script>',
|
|
87
120
|
'</body></html>'
|
|
88
121
|
].join('\n'));
|
|
122
|
+
write(path_1.default.join(root, 'client', 'dist', 'theme-boot.css'), 'html { background-color: #121212 }\n');
|
|
123
|
+
write(path_1.default.join(root, 'client', 'dist', 'deferred.js'), 'console.log("deferred")\n');
|
|
124
|
+
write(path_1.default.join(root, 'client', 'dist', 'theme-boot.js'), 'document.documentElement.style.backgroundColor = "#121212"\n');
|
|
89
125
|
write(path_1.default.join(root, 'client', 'dist', 'main.abc123.js'), 'console.log(1)\n');
|
|
90
126
|
};
|
|
91
127
|
describe('browser-hosted build', function () {
|
|
@@ -467,6 +503,182 @@ describe('browser-hosted build', function () {
|
|
|
467
503
|
}
|
|
468
504
|
});
|
|
469
505
|
}); });
|
|
506
|
+
/**
|
|
507
|
+
* A page's paint-critical script must run before the worker exists.
|
|
508
|
+
*
|
|
509
|
+
* The loader defers every script so the worker is ready before the app asks it
|
|
510
|
+
* for anything - right for the app, wrong for a script whose job is the canvas:
|
|
511
|
+
* the page stays unpainted (white) for the seconds the worker takes to claim
|
|
512
|
+
* it, which is the white flash a dark interface shows on every load. A tag the
|
|
513
|
+
* page marks `data-early` says it needs no worker, so the parser keeps it.
|
|
514
|
+
*/
|
|
515
|
+
it('leaves a data-early script to the parser, in the page', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
516
|
+
var html, specs;
|
|
517
|
+
return __generator(this, function (_a) {
|
|
518
|
+
switch (_a.label) {
|
|
519
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
520
|
+
case 1:
|
|
521
|
+
_a.sent();
|
|
522
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
523
|
+
specs = JSON.parse(html
|
|
524
|
+
.match(/data-scripts='([^']*)'/)[1]
|
|
525
|
+
.replace(/'/g, "'")
|
|
526
|
+
.replace(/&/g, '&'));
|
|
527
|
+
// still in the page, before the loader, and not replayed through it
|
|
528
|
+
expect(html).toMatch(/<script src="theme-boot\.js\?v=[0-9a-f]{10}" data-early><\/script>/);
|
|
529
|
+
expect(specs.map(function (spec) { return spec.src; })).not.toContain(expect.stringMatching(/theme-boot/));
|
|
530
|
+
expect(html.indexOf('theme-boot.js')).toBeLessThan(html.indexOf('host/boot.js'));
|
|
531
|
+
// the same marker on a module cannot be honoured: a module is deferred by
|
|
532
|
+
// specification, so it is replayed and the author is told
|
|
533
|
+
expect(html).not.toContain('<script src="deferred.js');
|
|
534
|
+
expect(specs.map(function (spec) { return spec.src; })).toContainEqual(expect.stringMatching(/^deferred\.js\?v=/));
|
|
535
|
+
expect(process.logger.warn).toHaveBeenCalledWith(expect.stringContaining('a data-early script is a module'));
|
|
536
|
+
return [2 /*return*/];
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}); });
|
|
540
|
+
it('gives a linked stylesheet a URL that follows its contents', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
541
|
+
var html;
|
|
542
|
+
return __generator(this, function (_a) {
|
|
543
|
+
switch (_a.label) {
|
|
544
|
+
case 0:
|
|
545
|
+
// the host caches an asset for two hours: a palette change behind an
|
|
546
|
+
// unchanged URL reaches a returning reader late, which for the canvas
|
|
547
|
+
// stylesheet is the white flash the fix exists to remove
|
|
548
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
549
|
+
case 1:
|
|
550
|
+
// the host caches an asset for two hours: a palette change behind an
|
|
551
|
+
// unchanged URL reaches a returning reader late, which for the canvas
|
|
552
|
+
// stylesheet is the white flash the fix exists to remove
|
|
553
|
+
_a.sent();
|
|
554
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
555
|
+
expect(html).toMatch(/<link rel="stylesheet" href="theme-boot\.css\?v=[0-9a-f]{10}" \/>/);
|
|
556
|
+
return [2 /*return*/];
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}); });
|
|
560
|
+
it("leaves a stylesheet alone when the file is not this build's", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
561
|
+
var html, specs;
|
|
562
|
+
return __generator(this, function (_a) {
|
|
563
|
+
switch (_a.label) {
|
|
564
|
+
case 0:
|
|
565
|
+
// a file this build does not own: guessing a version for it would break it.
|
|
566
|
+
// A URL that pins an `integrity` hash is versioned like any other - SRI
|
|
567
|
+
// digests the fetched bytes, so a `?v=` URL still satisfies the hash.
|
|
568
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
569
|
+
'<!doctype html><html><head><title>App</title>',
|
|
570
|
+
'<link rel="stylesheet" href="https://cdn.example.com/lib.css" />',
|
|
571
|
+
'<link rel="stylesheet" href="theme-boot.css" integrity="sha384-abc" crossorigin="anonymous" />',
|
|
572
|
+
'</head><body>',
|
|
573
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\"></sasjs>"),
|
|
574
|
+
'<script src="main.abc123.js" type="module" integrity="sha384-def"></script>',
|
|
575
|
+
'</body></html>'
|
|
576
|
+
].join('\n'));
|
|
577
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
578
|
+
case 1:
|
|
579
|
+
_a.sent();
|
|
580
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
581
|
+
specs = JSON.parse(html
|
|
582
|
+
.match(/data-scripts='([^']*)'/)[1]
|
|
583
|
+
.replace(/'/g, "'")
|
|
584
|
+
.replace(/&/g, '&'));
|
|
585
|
+
// not owned by this build: untouched
|
|
586
|
+
expect(html).toContain('href="https://cdn.example.com/lib.css"');
|
|
587
|
+
// owned, hashed: versioned, hash carried through
|
|
588
|
+
expect(html).toMatch(/href="theme-boot\.css\?v=[0-9a-f]{10}" integrity="sha384-abc" crossorigin="anonymous"/);
|
|
589
|
+
expect(specs[0].src).toMatch(/^main\.abc123\.js\?v=[0-9a-f]{10}$/);
|
|
590
|
+
expect(specs[0].integrity).toBe('sha384-def');
|
|
591
|
+
return [2 /*return*/];
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}); });
|
|
595
|
+
it('does not let a $ in a file name rewrite the page', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
596
|
+
var html;
|
|
597
|
+
return __generator(this, function (_a) {
|
|
598
|
+
switch (_a.label) {
|
|
599
|
+
case 0:
|
|
600
|
+
// `String.prototype.replace` interprets `$&`, `$1` and the rest in a
|
|
601
|
+
// replacement STRING, and these tags carry the page's own file names - so a
|
|
602
|
+
// `$&` in one would splice the matched markup into the src
|
|
603
|
+
write(path_1.default.join(root, 'client', 'dist', '$&x.js'), 'console.log("dollar")\n');
|
|
604
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
605
|
+
'<!doctype html><html><head><title>App</title>',
|
|
606
|
+
'<script src="$&x.js" data-early></script>',
|
|
607
|
+
'</head><body>',
|
|
608
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\"></sasjs>"),
|
|
609
|
+
'<script src="main.abc123.js"></script>',
|
|
610
|
+
'</body></html>'
|
|
611
|
+
].join('\n'));
|
|
612
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
613
|
+
case 1:
|
|
614
|
+
_a.sent();
|
|
615
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
616
|
+
expect(html).toContain("<script src=\"$&x.js?v=".concat(require('crypto')
|
|
617
|
+
.createHash('md5')
|
|
618
|
+
.update(fs_1.default.readFileSync(path_1.default.join(root, 'client', 'dist', '$&x.js')))
|
|
619
|
+
.digest('hex')
|
|
620
|
+
.slice(0, 10), "\" data-early></script>"));
|
|
621
|
+
// one script tag, not a nested one spliced in by the expansion
|
|
622
|
+
expect(html.split('<script').length - 1).toBe(2); // the early tag + the loader
|
|
623
|
+
return [2 /*return*/];
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}); });
|
|
627
|
+
it('versions a preload of the same stylesheet', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
628
|
+
var html, urls;
|
|
629
|
+
return __generator(this, function (_a) {
|
|
630
|
+
switch (_a.label) {
|
|
631
|
+
case 0:
|
|
632
|
+
// a preload left at the unversioned URL warms one cache entry while the
|
|
633
|
+
// stylesheet is fetched cold at another, which is the request it existed to save
|
|
634
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
635
|
+
'<!doctype html><html><head><title>App</title>',
|
|
636
|
+
'<link rel="preload" as="style" href="theme-boot.css" />',
|
|
637
|
+
'<link rel="stylesheet" href="theme-boot.css" />',
|
|
638
|
+
'</head><body>',
|
|
639
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\"></sasjs>"),
|
|
640
|
+
'<script src="main.abc123.js"></script>',
|
|
641
|
+
'</body></html>'
|
|
642
|
+
].join('\n'));
|
|
643
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
644
|
+
case 1:
|
|
645
|
+
_a.sent();
|
|
646
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
647
|
+
urls = __spreadArray([], __read(html.matchAll(/theme-boot\.css\?v=([0-9a-f]{10})/g)), false).map(function (match) { return match[1]; });
|
|
648
|
+
expect(urls).toHaveLength(2);
|
|
649
|
+
expect(urls[0]).toBe(urls[1]);
|
|
650
|
+
return [2 /*return*/];
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}); });
|
|
654
|
+
it('installs the loader for a page whose only script runs early', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
655
|
+
var html, specs;
|
|
656
|
+
return __generator(this, function (_a) {
|
|
657
|
+
switch (_a.label) {
|
|
658
|
+
case 0:
|
|
659
|
+
// the loader is what installs the worker, the bridge and the drive: a page
|
|
660
|
+
// whose one script is kept in the parser still needs it, and must not be told
|
|
661
|
+
// it has no scripts
|
|
662
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
663
|
+
'<!doctype html><html><head><title>App</title>',
|
|
664
|
+
'<script src="theme-boot.js" data-early></script>',
|
|
665
|
+
'</head><body>',
|
|
666
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\"></sasjs>"),
|
|
667
|
+
'</body></html>'
|
|
668
|
+
].join('\n'));
|
|
669
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
670
|
+
case 1:
|
|
671
|
+
_a.sent();
|
|
672
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
673
|
+
specs = html.match(/data-scripts='([^']*)'/)[1];
|
|
674
|
+
expect(html).toContain('<script src="host/boot.js?v=');
|
|
675
|
+
expect(html).toContain('data-early');
|
|
676
|
+
expect(specs).toBe('[]');
|
|
677
|
+
expect(process.logger.warn).not.toHaveBeenCalledWith(expect.stringContaining('no script tags found'));
|
|
678
|
+
return [2 /*return*/];
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}); });
|
|
470
682
|
it('points the app at itself and installs the boot loader', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
471
683
|
var html, specs;
|
|
472
684
|
return __generator(this, function (_a) {
|
|
@@ -490,9 +702,12 @@ describe('browser-hosted build', function () {
|
|
|
490
702
|
.match(/data-scripts='([^']*)'/)[1]
|
|
491
703
|
.replace(/'/g, "'")
|
|
492
704
|
.replace(/&/g, '&'));
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
expect(specs
|
|
705
|
+
// two: the module the page marked data-early (replayed, with a warning),
|
|
706
|
+
// then the bundle - both in the order the page had them
|
|
707
|
+
expect(specs).toHaveLength(2);
|
|
708
|
+
expect(specs[0].src).toMatch(/^deferred\.js\?v=[0-9a-f]{10}$/);
|
|
709
|
+
expect(specs[1].type).toBe('module');
|
|
710
|
+
expect(specs[1].src).toMatch(/^main\.abc123\.js\?v=[0-9a-f]{10}$/);
|
|
496
711
|
return [2 /*return*/];
|
|
497
712
|
}
|
|
498
713
|
});
|
|
@@ -503,7 +718,7 @@ describe('browser-hosted build', function () {
|
|
|
503
718
|
* bundle's name never changes, so the URL is what has to change.
|
|
504
719
|
*/
|
|
505
720
|
it('gives each app script a URL that follows its contents', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
506
|
-
var specs, first;
|
|
721
|
+
var specs, bundleSrc, first;
|
|
507
722
|
return __generator(this, function (_a) {
|
|
508
723
|
switch (_a.label) {
|
|
509
724
|
case 0:
|
|
@@ -512,23 +727,26 @@ describe('browser-hosted build', function () {
|
|
|
512
727
|
var tag = html.match(/data-scripts='([^']*)'/);
|
|
513
728
|
return JSON.parse(tag[1].replace(/'/g, "'").replace(/&/g, '&'));
|
|
514
729
|
};
|
|
730
|
+
bundleSrc = function () {
|
|
731
|
+
return specs().find(function (spec) { var _a; return (_a = spec.src) === null || _a === void 0 ? void 0 : _a.startsWith('main.'); });
|
|
732
|
+
};
|
|
515
733
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
516
734
|
case 1:
|
|
517
735
|
_a.sent();
|
|
518
|
-
first =
|
|
736
|
+
first = bundleSrc().src;
|
|
519
737
|
expect(first).toMatch(/^main\.abc123\.js\?v=[0-9a-f]{10}$/);
|
|
520
738
|
// the same file twice: nothing about the URL changes
|
|
521
739
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
522
740
|
case 2:
|
|
523
741
|
// the same file twice: nothing about the URL changes
|
|
524
742
|
_a.sent();
|
|
525
|
-
expect(
|
|
743
|
+
expect(bundleSrc().src).toBe(first);
|
|
526
744
|
// a different bundle: a different URL, which is the caching bust
|
|
527
745
|
write(path_1.default.join(root, 'client', 'dist', 'main.abc123.js'), 'console.log(2)\n');
|
|
528
746
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
529
747
|
case 3:
|
|
530
748
|
_a.sent();
|
|
531
|
-
expect(
|
|
749
|
+
expect(bundleSrc().src).not.toBe(first);
|
|
532
750
|
// and a script this build does not own is left exactly as the page asked
|
|
533
751
|
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
534
752
|
'<!doctype html><html><head><title>App</title></head><body>',
|
|
@@ -58,7 +58,12 @@ var loadRuntime = function () {
|
|
|
58
58
|
;
|
|
59
59
|
global.self = {};
|
|
60
60
|
jest.resetModules();
|
|
61
|
-
|
|
61
|
+
var runtime = require('../assets/runtime-core.js');
|
|
62
|
+
// the host declares its run times, and the site's worker does the same from its own
|
|
63
|
+
// `RUN_TIMES` (sw.js): an extension-less program is resolved in that order, so a runtime
|
|
64
|
+
// driven without a declaration resolves exact paths only
|
|
65
|
+
runtime.setRunTimes(['js', 'sas']);
|
|
66
|
+
return runtime;
|
|
62
67
|
};
|
|
63
68
|
var jsonResponse = function (body) { return ({
|
|
64
69
|
ok: true,
|