@sasjs/cli 6.16.0 → 6.17.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/sw.js +56 -3
- package/build/commands/build/internal/mockSite/mockSite.js +27 -13
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +94 -5
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +40 -0
- package/package.json +1 -1
|
@@ -859,6 +859,56 @@ const routeRequest = async (request, url) => {
|
|
|
859
859
|
try {
|
|
860
860
|
await R.ensureReady()
|
|
861
861
|
|
|
862
|
+
// The app stream is the server's own web surface rather than part of the
|
|
863
|
+
// API: the listing, the landing page and the redirect from one to the other
|
|
864
|
+
// are routes sasjs/server answers, and an interface asks for the listing on
|
|
865
|
+
// every visit. Unanswered, those requests reach the static host and the page
|
|
866
|
+
// parses this site's index.html as JSON.
|
|
867
|
+
//
|
|
868
|
+
// The answers mirror the server's, because a caller cannot tell the two
|
|
869
|
+
// hosts apart otherwise: `/AppStream` redirects to `/AppStream/`, the landing
|
|
870
|
+
// page is served, and only a name under it is missing. The listing is empty
|
|
871
|
+
// rather than read from `registry()`, which records this deployment's own
|
|
872
|
+
// folder so the delete route has something to remove: the stream really does
|
|
873
|
+
// serve nothing at `/AppStream/<name>/` here, and a listing that named it
|
|
874
|
+
// would offer a tile that 404s.
|
|
875
|
+
// `path` has its trailing slashes stripped, and these four answers differ by
|
|
876
|
+
// exactly that, so they are decided on the request's own pathname.
|
|
877
|
+
const stream = url.pathname
|
|
878
|
+
|
|
879
|
+
if (/^\/AppStream$/i.test(stream))
|
|
880
|
+
return new Response(null, {
|
|
881
|
+
status: 301,
|
|
882
|
+
headers: { location: '/AppStream/' }
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
if (
|
|
886
|
+
/^\/AppStream\/$/i.test(stream) ||
|
|
887
|
+
/^\/AppStream\/index\.html$/i.test(stream)
|
|
888
|
+
)
|
|
889
|
+
return new Response(
|
|
890
|
+
[
|
|
891
|
+
'<!doctype html><html><head><meta charset="utf-8">',
|
|
892
|
+
'<title>App Stream</title></head><body>',
|
|
893
|
+
'<h1>SASjs App Stream</h1>',
|
|
894
|
+
'<p>This browser host serves no apps: nothing is published on a static site.</p>',
|
|
895
|
+
'</body></html>'
|
|
896
|
+
].join(''),
|
|
897
|
+
{ status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } }
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
if (/^\/AppStream\/apps\.json$/i.test(stream)) return json({ apps: [] })
|
|
901
|
+
|
|
902
|
+
if (/^\/AppStream\//i.test(stream)) {
|
|
903
|
+
return json(
|
|
904
|
+
{
|
|
905
|
+
message:
|
|
906
|
+
'This browser host serves no apps: nothing is published on a static site.'
|
|
907
|
+
},
|
|
908
|
+
404
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
|
|
862
912
|
// The package's own matcher, so a route is recognised however its path is
|
|
863
913
|
// cased and its parameters are read the way the document states them.
|
|
864
914
|
const matched = API.findRoute(request.method, path)
|
|
@@ -975,10 +1025,13 @@ self.addEventListener('fetch', (event) => {
|
|
|
975
1025
|
return
|
|
976
1026
|
}
|
|
977
1027
|
|
|
978
|
-
// The
|
|
1028
|
+
// The host's own paths are answered here, including one the table does not
|
|
979
1029
|
// have - falling through to the static host would put the SPA's index.html
|
|
980
|
-
// behind a typo in a caller's URL
|
|
981
|
-
|
|
1030
|
+
// behind a typo in a caller's URL, or behind a listing an interface parses as
|
|
1031
|
+
// JSON. `/AppStream` is one of those: the package serves app deployments, but
|
|
1032
|
+
// the listing is the server's web route, and an interface that lists apps asks
|
|
1033
|
+
// for it on every visit.
|
|
1034
|
+
if (!/^\/(SASjsApi|SASLogon|AppStream)(\/|$)/i.test(url.pathname)) return
|
|
982
1035
|
event.respondWith(handle(event.request, url))
|
|
983
1036
|
})
|
|
984
1037
|
|
|
@@ -472,12 +472,12 @@ var assertSafeOutputFolder = function (outputFolder, projectDir, buildFolder, we
|
|
|
472
472
|
!isSameOrChild(buildFolder, outputFolder))
|
|
473
473
|
throw refusal('that folder is outside the project');
|
|
474
474
|
};
|
|
475
|
-
var
|
|
475
|
+
var countPrograms = function (files, extension) {
|
|
476
476
|
return Object.keys(files).filter(function (key) {
|
|
477
477
|
var inProgramFolder = PROGRAM_FOLDERS.some(function (folder) {
|
|
478
478
|
return key.includes("/".concat(folder, "/"));
|
|
479
479
|
});
|
|
480
|
-
return inProgramFolder && key.toLowerCase().endsWith(
|
|
480
|
+
return inProgramFolder && key.toLowerCase().endsWith(extension);
|
|
481
481
|
}).length;
|
|
482
482
|
};
|
|
483
483
|
/** The script-tag attributes that change how an app's script runs. */
|
|
@@ -646,11 +646,11 @@ var patchIndexHtml = function (html, version) {
|
|
|
646
646
|
*/
|
|
647
647
|
function buildMockSite(target) {
|
|
648
648
|
return __awaiter(this, void 0, void 0, function () {
|
|
649
|
-
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;
|
|
649
|
+
var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
650
650
|
var e_8, _e;
|
|
651
|
-
var _f, _g, _h;
|
|
652
|
-
return __generator(this, function (
|
|
653
|
-
switch (
|
|
651
|
+
var _f, _g, _h, _j;
|
|
652
|
+
return __generator(this, function (_k) {
|
|
653
|
+
switch (_k.label) {
|
|
654
654
|
case 0:
|
|
655
655
|
projectDir = process.projectDir || process.cwd();
|
|
656
656
|
appLocRaw = target.appLoc;
|
|
@@ -662,7 +662,7 @@ function buildMockSite(target) {
|
|
|
662
662
|
path_1.default.join(projectDir, 'sasjsbuild');
|
|
663
663
|
return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
|
|
664
664
|
case 1:
|
|
665
|
-
if (!(
|
|
665
|
+
if (!(_k.sent())) {
|
|
666
666
|
throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
|
|
667
667
|
}
|
|
668
668
|
webSourceRaw = (_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.webSourcePath;
|
|
@@ -672,7 +672,7 @@ function buildMockSite(target) {
|
|
|
672
672
|
webFolder = path_1.default.resolve(projectDir, webSourceRaw);
|
|
673
673
|
return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
|
|
674
674
|
case 2:
|
|
675
|
-
if (!(
|
|
675
|
+
if (!(_k.sent())) {
|
|
676
676
|
throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
|
|
677
677
|
}
|
|
678
678
|
if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
|
|
@@ -690,12 +690,26 @@ function buildMockSite(target) {
|
|
|
690
690
|
excluded.push("/sasjs/drive/files".concat(appLoc, "/services/").concat(streamConfig.streamWebFolder, "/"));
|
|
691
691
|
}
|
|
692
692
|
_a = seedFromBuild(buildFolder, appLoc, excluded, [outputFolder]), seed = _a.files, skipped = _a.skipped;
|
|
693
|
-
jsPrograms =
|
|
693
|
+
jsPrograms = countPrograms(seed, '.js');
|
|
694
|
+
sasPrograms = countPrograms(seed, '.sas');
|
|
694
695
|
driveEntries = Object.keys(seed).length;
|
|
695
|
-
|
|
696
|
-
|
|
696
|
+
// A build holding SAS and no JavaScript is a SAS target: its services cannot
|
|
697
|
+
// run here, and seeding them would produce a site whose buttons answer 400.
|
|
698
|
+
// A build holding NEITHER is a frontend host - the app is the whole thing and
|
|
699
|
+
// the worker answers its API - which is what an interface with no services
|
|
700
|
+
// compiles to, and the case this site is built for.
|
|
701
|
+
if (jsPrograms === 0 && sasPrograms > 0) {
|
|
702
|
+
throw new Error("No .js programs in ".concat(buildFolder, "/services, /jobs or /tests, but ").concat(sasPrograms, " SAS program(s). This target builds SAS code, which cannot run in a browser. ") +
|
|
697
703
|
'Build a target whose services are JavaScript (typically a mocks project).');
|
|
698
704
|
}
|
|
705
|
+
// No PROGRAMS is a different thing again: a compile that produced nothing, or
|
|
706
|
+
// a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
|
|
707
|
+
// behind that is not a program folder but does reach the drive, so the count
|
|
708
|
+
// that decides this is of programs, not of files.
|
|
709
|
+
if (jsPrograms + sasPrograms === 0) {
|
|
710
|
+
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
|
|
711
|
+
'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
|
|
712
|
+
}
|
|
699
713
|
// The package supplies the drive rules the worker answers with, so an install
|
|
700
714
|
// without its browser build is a build that cannot run. Checked here, with the
|
|
701
715
|
// other refusals, so a missing dependency leaves the previous site in place -
|
|
@@ -706,7 +720,7 @@ function buildMockSite(target) {
|
|
|
706
720
|
'or check that @sasjs/api still publishes build/browser.js.');
|
|
707
721
|
}
|
|
708
722
|
if (skipped.length) {
|
|
709
|
-
(
|
|
723
|
+
(_j = process.logger) === null || _j === void 0 ? void 0 : _j.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: ") +
|
|
710
724
|
"".concat(skipped
|
|
711
725
|
.slice(0, 3)
|
|
712
726
|
.map(function (file) {
|
|
@@ -753,7 +767,7 @@ function buildMockSite(target) {
|
|
|
753
767
|
case 3:
|
|
754
768
|
// 2. the linter, bundled from the installed packages so the code routes answer
|
|
755
769
|
// with the rules this CLI resolved rather than a second implementation
|
|
756
|
-
|
|
770
|
+
_k.sent();
|
|
757
771
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
758
772
|
// served from, and the app has to be controlled from /
|
|
759
773
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
|
|
@@ -690,6 +690,95 @@ describe('browser-hosted build', function () {
|
|
|
690
690
|
}
|
|
691
691
|
});
|
|
692
692
|
}); });
|
|
693
|
+
/**
|
|
694
|
+
* The case this build exists for: an interface with no services at all.
|
|
695
|
+
* sasjs/server's own frontend compiles to nothing, and the worker answers its
|
|
696
|
+
* API - so the target must build, ship the web app, and say out loud that the
|
|
697
|
+
* drive it ships is empty.
|
|
698
|
+
*/
|
|
699
|
+
it('builds an interface that has no services, and says the drive is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
700
|
+
var _a, _b, folder, warn, result;
|
|
701
|
+
var e_4, _c;
|
|
702
|
+
var _d;
|
|
703
|
+
return __generator(this, function (_e) {
|
|
704
|
+
switch (_e.label) {
|
|
705
|
+
case 0:
|
|
706
|
+
try {
|
|
707
|
+
// nothing a drive could be seeded from: no services, no sync folder
|
|
708
|
+
for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
709
|
+
folder = _b.value;
|
|
710
|
+
fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
|
|
711
|
+
recursive: true,
|
|
712
|
+
force: true
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
717
|
+
finally {
|
|
718
|
+
try {
|
|
719
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
720
|
+
}
|
|
721
|
+
finally { if (e_4) throw e_4.error; }
|
|
722
|
+
}
|
|
723
|
+
warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
|
|
724
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
725
|
+
case 1:
|
|
726
|
+
result = _e.sent();
|
|
727
|
+
expect(result.jsPrograms).toBe(0);
|
|
728
|
+
expect(result.driveEntries).toBe(0);
|
|
729
|
+
expect(fs_1.default.existsSync(path_1.default.join(out(), 'index.html'))).toBe(true);
|
|
730
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('ships with an empty drive'));
|
|
731
|
+
warn.mockRestore();
|
|
732
|
+
return [2 /*return*/];
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
}); });
|
|
736
|
+
/**
|
|
737
|
+
* A stale build folder is the case the warning exists for: `sasjs doc` and
|
|
738
|
+
* other commands leave folders behind that are not program folders, so a count
|
|
739
|
+
* of files on the drive says nothing about whether anything was compiled.
|
|
740
|
+
*/
|
|
741
|
+
it('warns when the build folder holds only folders that are not programs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
742
|
+
var _a, _b, folder, warn, result;
|
|
743
|
+
var e_5, _c;
|
|
744
|
+
var _d;
|
|
745
|
+
return __generator(this, function (_e) {
|
|
746
|
+
switch (_e.label) {
|
|
747
|
+
case 0:
|
|
748
|
+
try {
|
|
749
|
+
for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
750
|
+
folder = _b.value;
|
|
751
|
+
fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
|
|
752
|
+
recursive: true,
|
|
753
|
+
force: true
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
758
|
+
finally {
|
|
759
|
+
try {
|
|
760
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
761
|
+
}
|
|
762
|
+
finally { if (e_5) throw e_5.error; }
|
|
763
|
+
}
|
|
764
|
+
write(path_1.default.join(root, 'sasjsbuild', 'docs', 'index.html'), '<html></html>');
|
|
765
|
+
warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
|
|
766
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
767
|
+
// the drive is NOT empty - the docs folder reaches it - so the file count
|
|
768
|
+
// alone would have kept quiet
|
|
769
|
+
];
|
|
770
|
+
case 1:
|
|
771
|
+
result = _e.sent();
|
|
772
|
+
// the drive is NOT empty - the docs folder reaches it - so the file count
|
|
773
|
+
// alone would have kept quiet
|
|
774
|
+
expect(result.driveEntries).toBeGreaterThan(0);
|
|
775
|
+
expect(result.jsPrograms).toBe(0);
|
|
776
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('No compiled programs'));
|
|
777
|
+
warn.mockRestore();
|
|
778
|
+
return [2 /*return*/];
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}); });
|
|
693
782
|
it('refuses to build without a web app', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
694
783
|
return __generator(this, function (_a) {
|
|
695
784
|
switch (_a.label) {
|
|
@@ -773,8 +862,8 @@ describe('browser-hosted build', function () {
|
|
|
773
862
|
});
|
|
774
863
|
}); });
|
|
775
864
|
it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
776
|
-
var _a, _b, mockWebFolder,
|
|
777
|
-
var
|
|
865
|
+
var _a, _b, mockWebFolder, e_6_1;
|
|
866
|
+
var e_6, _c;
|
|
778
867
|
return __generator(this, function (_d) {
|
|
779
868
|
switch (_d.label) {
|
|
780
869
|
case 0:
|
|
@@ -805,14 +894,14 @@ describe('browser-hosted build', function () {
|
|
|
805
894
|
return [3 /*break*/, 1];
|
|
806
895
|
case 4: return [3 /*break*/, 7];
|
|
807
896
|
case 5:
|
|
808
|
-
|
|
809
|
-
|
|
897
|
+
e_6_1 = _d.sent();
|
|
898
|
+
e_6 = { error: e_6_1 };
|
|
810
899
|
return [3 /*break*/, 7];
|
|
811
900
|
case 6:
|
|
812
901
|
try {
|
|
813
902
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
814
903
|
}
|
|
815
|
-
finally { if (
|
|
904
|
+
finally { if (e_6) throw e_6.error; }
|
|
816
905
|
return [7 /*endfinally*/];
|
|
817
906
|
case 7:
|
|
818
907
|
expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
|
|
@@ -1157,6 +1157,46 @@ describe('service worker host', function () {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
});
|
|
1159
1159
|
}); });
|
|
1160
|
+
it('answers the app stream as the server does, with nothing in it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1161
|
+
var dispatch, listing, _a, root, landing, _b, app, _c;
|
|
1162
|
+
return __generator(this, function (_d) {
|
|
1163
|
+
switch (_d.label) {
|
|
1164
|
+
case 0:
|
|
1165
|
+
dispatch = seeded().dispatch;
|
|
1166
|
+
return [4 /*yield*/, dispatch('/AppStream/apps.json')];
|
|
1167
|
+
case 1:
|
|
1168
|
+
listing = _d.sent();
|
|
1169
|
+
expect(listing.status).toBe(200);
|
|
1170
|
+
_a = expect;
|
|
1171
|
+
return [4 /*yield*/, listing.json()];
|
|
1172
|
+
case 2:
|
|
1173
|
+
_a.apply(void 0, [_d.sent()]).toEqual({ apps: [] });
|
|
1174
|
+
return [4 /*yield*/, dispatch('/AppStream')];
|
|
1175
|
+
case 3:
|
|
1176
|
+
root = _d.sent();
|
|
1177
|
+
expect(root.status).toBe(301);
|
|
1178
|
+
expect(root.headers.get('location')).toBe('/AppStream/');
|
|
1179
|
+
return [4 /*yield*/, dispatch('/AppStream/')];
|
|
1180
|
+
case 4:
|
|
1181
|
+
landing = _d.sent();
|
|
1182
|
+
expect(landing.status).toBe(200);
|
|
1183
|
+
expect(landing.headers.get('content-type')).toContain('text/html');
|
|
1184
|
+
_b = expect;
|
|
1185
|
+
return [4 /*yield*/, landing.text()];
|
|
1186
|
+
case 5:
|
|
1187
|
+
_b.apply(void 0, [_d.sent()]).toContain('serves no apps');
|
|
1188
|
+
return [4 /*yield*/, dispatch('/AppStream/Demo/')];
|
|
1189
|
+
case 6:
|
|
1190
|
+
app = _d.sent();
|
|
1191
|
+
expect(app.status).toBe(404);
|
|
1192
|
+
_c = expect;
|
|
1193
|
+
return [4 /*yield*/, app.json()];
|
|
1194
|
+
case 7:
|
|
1195
|
+
_c.apply(void 0, [(_d.sent()).message]).toContain('serves no apps');
|
|
1196
|
+
return [2 /*return*/];
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
}); });
|
|
1160
1200
|
it('leaves a request to another origin alone', function () {
|
|
1161
1201
|
var dispatch = seeded().dispatch;
|
|
1162
1202
|
expect(dispatch('https://other.example.com/SASjsApi/info')).toBeUndefined();
|
package/package.json
CHANGED