@sasjs/cli 6.16.0 → 6.18.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 +31 -2
- package/build/commands/build/internal/mockSite/assets/sw.js +65 -4
- package/build/commands/build/internal/mockSite/mockSite.js +27 -14
- package/build/commands/build/internal/mockSite/spec/intercept.spec.js +26 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +114 -18
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +80 -0
- package/package.json +1 -1
|
@@ -31,6 +31,31 @@
|
|
|
31
31
|
if (DEBUG) console.log('[sasjs-mock]', ...args)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A file's contents, as text or as base64 with a flag.
|
|
36
|
+
*
|
|
37
|
+
* An upload is a file, not a string: a PNG, a workbook or an .sas7bdat read
|
|
38
|
+
* with `text()` arrives as replacement characters, and the drive would store
|
|
39
|
+
* the damage under a success status. The bytes are decoded as UTF-8 strictly -
|
|
40
|
+
* if that fails, they are not text by any reading a browser would accept - and
|
|
41
|
+
* carried as base64, which the worker turns back into the bytes the drive
|
|
42
|
+
* keeps.
|
|
43
|
+
*/
|
|
44
|
+
const contentsOf = async (value) => {
|
|
45
|
+
const bytes = new Uint8Array(await value.arrayBuffer())
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return { csv: new TextDecoder('utf-8', { fatal: true }).decode(bytes) }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
let binary = ''
|
|
51
|
+
const chunk = 0x8000
|
|
52
|
+
for (let i = 0; i < bytes.length; i += chunk)
|
|
53
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk))
|
|
54
|
+
|
|
55
|
+
return { csv: btoa(binary), binary: true }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
const readBody = async (body) => {
|
|
35
60
|
const params = {}
|
|
36
61
|
const tables = []
|
|
@@ -39,12 +64,16 @@
|
|
|
39
64
|
if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
40
65
|
for (const [key, value] of body.entries()) {
|
|
41
66
|
if (typeof value === 'string') params[key] = value
|
|
42
|
-
else
|
|
67
|
+
else {
|
|
68
|
+
const contents = await contentsOf(value)
|
|
69
|
+
|
|
43
70
|
tables.push({
|
|
44
71
|
name: key,
|
|
45
72
|
filename: value.name || key + '.csv',
|
|
46
|
-
csv:
|
|
73
|
+
csv: contents.csv,
|
|
74
|
+
...(contents.binary ? { binary: true } : {})
|
|
47
75
|
})
|
|
76
|
+
}
|
|
48
77
|
}
|
|
49
78
|
return { params, tables }
|
|
50
79
|
}
|
|
@@ -574,7 +574,15 @@ const readJsonBody = async (request) => {
|
|
|
574
574
|
name: table.name || `table${index + 1}`,
|
|
575
575
|
filename:
|
|
576
576
|
table.filename || `${table.name || 'table' + (index + 1)}.csv`,
|
|
577
|
-
|
|
577
|
+
// A file the bridge could not read as UTF-8 travels as base64 with a
|
|
578
|
+
// flag: writing its text would replace the bytes and store the damage
|
|
579
|
+
// under a success status, which is what a binary upload used to do.
|
|
580
|
+
contents:
|
|
581
|
+
table.csv === undefined
|
|
582
|
+
? ''
|
|
583
|
+
: table.binary
|
|
584
|
+
? toBytes(table.csv)
|
|
585
|
+
: table.csv
|
|
578
586
|
}))
|
|
579
587
|
}
|
|
580
588
|
}
|
|
@@ -859,6 +867,56 @@ const routeRequest = async (request, url) => {
|
|
|
859
867
|
try {
|
|
860
868
|
await R.ensureReady()
|
|
861
869
|
|
|
870
|
+
// The app stream is the server's own web surface rather than part of the
|
|
871
|
+
// API: the listing, the landing page and the redirect from one to the other
|
|
872
|
+
// are routes sasjs/server answers, and an interface asks for the listing on
|
|
873
|
+
// every visit. Unanswered, those requests reach the static host and the page
|
|
874
|
+
// parses this site's index.html as JSON.
|
|
875
|
+
//
|
|
876
|
+
// The answers mirror the server's, because a caller cannot tell the two
|
|
877
|
+
// hosts apart otherwise: `/AppStream` redirects to `/AppStream/`, the landing
|
|
878
|
+
// page is served, and only a name under it is missing. The listing is empty
|
|
879
|
+
// rather than read from `registry()`, which records this deployment's own
|
|
880
|
+
// folder so the delete route has something to remove: the stream really does
|
|
881
|
+
// serve nothing at `/AppStream/<name>/` here, and a listing that named it
|
|
882
|
+
// would offer a tile that 404s.
|
|
883
|
+
// `path` has its trailing slashes stripped, and these four answers differ by
|
|
884
|
+
// exactly that, so they are decided on the request's own pathname.
|
|
885
|
+
const stream = url.pathname
|
|
886
|
+
|
|
887
|
+
if (/^\/AppStream$/i.test(stream))
|
|
888
|
+
return new Response(null, {
|
|
889
|
+
status: 301,
|
|
890
|
+
headers: { location: '/AppStream/' }
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
if (
|
|
894
|
+
/^\/AppStream\/$/i.test(stream) ||
|
|
895
|
+
/^\/AppStream\/index\.html$/i.test(stream)
|
|
896
|
+
)
|
|
897
|
+
return new Response(
|
|
898
|
+
[
|
|
899
|
+
'<!doctype html><html><head><meta charset="utf-8">',
|
|
900
|
+
'<title>App Stream</title></head><body>',
|
|
901
|
+
'<h1>SASjs App Stream</h1>',
|
|
902
|
+
'<p>This browser host serves no apps: nothing is published on a static site.</p>',
|
|
903
|
+
'</body></html>'
|
|
904
|
+
].join(''),
|
|
905
|
+
{ status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } }
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
if (/^\/AppStream\/apps\.json$/i.test(stream)) return json({ apps: [] })
|
|
909
|
+
|
|
910
|
+
if (/^\/AppStream\//i.test(stream)) {
|
|
911
|
+
return json(
|
|
912
|
+
{
|
|
913
|
+
message:
|
|
914
|
+
'This browser host serves no apps: nothing is published on a static site.'
|
|
915
|
+
},
|
|
916
|
+
404
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
|
|
862
920
|
// The package's own matcher, so a route is recognised however its path is
|
|
863
921
|
// cased and its parameters are read the way the document states them.
|
|
864
922
|
const matched = API.findRoute(request.method, path)
|
|
@@ -975,10 +1033,13 @@ self.addEventListener('fetch', (event) => {
|
|
|
975
1033
|
return
|
|
976
1034
|
}
|
|
977
1035
|
|
|
978
|
-
// The
|
|
1036
|
+
// The host's own paths are answered here, including one the table does not
|
|
979
1037
|
// have - falling through to the static host would put the SPA's index.html
|
|
980
|
-
// behind a typo in a caller's URL
|
|
981
|
-
|
|
1038
|
+
// behind a typo in a caller's URL, or behind a listing an interface parses as
|
|
1039
|
+
// JSON. `/AppStream` is one of those: the package serves app deployments, but
|
|
1040
|
+
// the listing is the server's web route, and an interface that lists apps asks
|
|
1041
|
+
// for it on every visit.
|
|
1042
|
+
if (!/^\/(SASjsApi|SASLogon|AppStream)(\/|$)/i.test(url.pathname)) return
|
|
982
1043
|
event.respondWith(handle(event.request, url))
|
|
983
1044
|
})
|
|
984
1045
|
|
|
@@ -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, _k;
|
|
652
|
+
return __generator(this, function (_l) {
|
|
653
|
+
switch (_l.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 (!(_l.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 (!(_l.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,11 +690,24 @@ 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
|
-
|
|
697
|
-
|
|
696
|
+
// A build whose services are SAS is still a site worth having, and it is built:
|
|
697
|
+
// the programs are seeded on the drive, where a visitor can open them in Studio
|
|
698
|
+
// and read them - which is how a deployment's own backend code is inspected
|
|
699
|
+
// without a SAS licence and without a login. What they cannot do is run here,
|
|
700
|
+
// so the build says so rather than letting a Run button be the surprise.
|
|
701
|
+
if (jsPrograms === 0 && sasPrograms > 0) {
|
|
702
|
+
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("".concat(sasPrograms, " SAS program(s) and no JavaScript in ").concat(buildFolder, ": they are placed on the drive to read, and cannot run in a browser."));
|
|
703
|
+
}
|
|
704
|
+
// No PROGRAMS is a different thing again: a compile that produced nothing, or
|
|
705
|
+
// a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
|
|
706
|
+
// behind that is not a program folder but does reach the drive, so the count
|
|
707
|
+
// that decides this is of programs, not of files.
|
|
708
|
+
if (jsPrograms + sasPrograms === 0) {
|
|
709
|
+
(_j = process.logger) === null || _j === void 0 ? void 0 : _j.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
|
|
710
|
+
'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
|
|
698
711
|
}
|
|
699
712
|
// The package supplies the drive rules the worker answers with, so an install
|
|
700
713
|
// without its browser build is a build that cannot run. Checked here, with the
|
|
@@ -706,7 +719,7 @@ function buildMockSite(target) {
|
|
|
706
719
|
'or check that @sasjs/api still publishes build/browser.js.');
|
|
707
720
|
}
|
|
708
721
|
if (skipped.length) {
|
|
709
|
-
(
|
|
722
|
+
(_k = process.logger) === null || _k === void 0 ? void 0 : _k.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
723
|
"".concat(skipped
|
|
711
724
|
.slice(0, 3)
|
|
712
725
|
.map(function (file) {
|
|
@@ -753,7 +766,7 @@ function buildMockSite(target) {
|
|
|
753
766
|
case 3:
|
|
754
767
|
// 2. the linter, bundled from the installed packages so the code routes answer
|
|
755
768
|
// with the rules this CLI resolved rather than a second implementation
|
|
756
|
-
|
|
769
|
+
_l.sent();
|
|
757
770
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
758
771
|
// served from, and the app has to be controlled from /
|
|
759
772
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
|
|
@@ -210,6 +210,32 @@ describe('page-side bridge', function () {
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
}); });
|
|
213
|
+
it('carries a binary file as base64 rather than as damaged text', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
214
|
+
var fetchImpl, XHR, bytes, form, xhr, _a, init, payload;
|
|
215
|
+
return __generator(this, function (_b) {
|
|
216
|
+
switch (_b.label) {
|
|
217
|
+
case 0:
|
|
218
|
+
fetchImpl = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
219
|
+
return [2 /*return*/, jsonResponse('{"status":"success"}')];
|
|
220
|
+
}); }); });
|
|
221
|
+
XHR = loadBridge(fetchImpl).XHR;
|
|
222
|
+
bytes = new Uint8Array([
|
|
223
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
|
|
224
|
+
]);
|
|
225
|
+
form = new FormData();
|
|
226
|
+
form.append('file', new Blob([bytes], { type: 'image/png' }), 'logo.png');
|
|
227
|
+
xhr = new XHR();
|
|
228
|
+
return [4 /*yield*/, run(xhr, 'POST', '/SASjsApi/drive/file', form)];
|
|
229
|
+
case 1:
|
|
230
|
+
_b.sent();
|
|
231
|
+
_a = __read(fetchImpl.mock.calls[0], 2), init = _a[1];
|
|
232
|
+
payload = JSON.parse(init.body);
|
|
233
|
+
expect(payload.tables[0].binary).toBe(true);
|
|
234
|
+
expect(payload.tables[0].csv).toBe(btoa('PNG\r\n\n'));
|
|
235
|
+
return [2 /*return*/];
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}); });
|
|
213
239
|
it('parses the response when the app asked for json', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
214
240
|
var XHR, xhr;
|
|
215
241
|
return __generator(this, function (_a) {
|
|
@@ -667,25 +667,121 @@ describe('browser-hosted build', function () {
|
|
|
667
667
|
}
|
|
668
668
|
});
|
|
669
669
|
}); });
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
670
|
+
/**
|
|
671
|
+
* A SAS backend does not run in a browser, but the programs are still worth
|
|
672
|
+
* shipping: the drive holds them, so a visitor can open them in Studio and
|
|
673
|
+
* read what a deployment's services do. The build warns and carries on.
|
|
674
|
+
*/
|
|
675
|
+
it('builds a target whose services are SAS, and warns they cannot run', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
676
|
+
var warn, result, seed;
|
|
677
|
+
var _a;
|
|
678
|
+
return __generator(this, function (_b) {
|
|
679
|
+
switch (_b.label) {
|
|
680
|
+
case 0:
|
|
677
681
|
fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', 'services'), {
|
|
678
682
|
recursive: true,
|
|
679
683
|
force: true
|
|
680
684
|
});
|
|
681
685
|
write(path_1.default.join(root, 'sasjsbuild', 'services', 'public', 'getdata.sas'), '%put sas;\n');
|
|
682
|
-
|
|
683
|
-
|
|
686
|
+
warn = jest.spyOn((_a = process.logger) !== null && _a !== void 0 ? _a : console, 'warn');
|
|
687
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
688
|
+
case 1:
|
|
689
|
+
result = _b.sent();
|
|
690
|
+
expect(result.jsPrograms).toBe(0);
|
|
691
|
+
expect(result.driveEntries).toBeGreaterThan(0);
|
|
692
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('cannot run in a browser'));
|
|
693
|
+
seed = readSeed();
|
|
694
|
+
expect(Object.keys(seed.files).some(function (key) { return key.endsWith('/getdata.sas'); })).toBe(true);
|
|
695
|
+
warn.mockRestore();
|
|
696
|
+
return [2 /*return*/];
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
}); });
|
|
700
|
+
/**
|
|
701
|
+
* The case this build exists for: an interface with no services at all.
|
|
702
|
+
* sasjs/server's own frontend compiles to nothing, and the worker answers its
|
|
703
|
+
* API - so the target must build, ship the web app, and say out loud that the
|
|
704
|
+
* drive it ships is empty.
|
|
705
|
+
*/
|
|
706
|
+
it('builds an interface that has no services, and says the drive is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
707
|
+
var _a, _b, folder, warn, result;
|
|
708
|
+
var e_4, _c;
|
|
709
|
+
var _d;
|
|
710
|
+
return __generator(this, function (_e) {
|
|
711
|
+
switch (_e.label) {
|
|
712
|
+
case 0:
|
|
713
|
+
try {
|
|
714
|
+
// nothing a drive could be seeded from: no services, no sync folder
|
|
715
|
+
for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
716
|
+
folder = _b.value;
|
|
717
|
+
fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
|
|
718
|
+
recursive: true,
|
|
719
|
+
force: true
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
724
|
+
finally {
|
|
725
|
+
try {
|
|
726
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
727
|
+
}
|
|
728
|
+
finally { if (e_4) throw e_4.error; }
|
|
729
|
+
}
|
|
730
|
+
warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
|
|
731
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
732
|
+
case 1:
|
|
733
|
+
result = _e.sent();
|
|
734
|
+
expect(result.jsPrograms).toBe(0);
|
|
735
|
+
expect(result.driveEntries).toBe(0);
|
|
736
|
+
expect(fs_1.default.existsSync(path_1.default.join(out(), 'index.html'))).toBe(true);
|
|
737
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('ships with an empty drive'));
|
|
738
|
+
warn.mockRestore();
|
|
739
|
+
return [2 /*return*/];
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
}); });
|
|
743
|
+
/**
|
|
744
|
+
* A stale build folder is the case the warning exists for: `sasjs doc` and
|
|
745
|
+
* other commands leave folders behind that are not program folders, so a count
|
|
746
|
+
* of files on the drive says nothing about whether anything was compiled.
|
|
747
|
+
*/
|
|
748
|
+
it('warns when the build folder holds only folders that are not programs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
749
|
+
var _a, _b, folder, warn, result;
|
|
750
|
+
var e_5, _c;
|
|
751
|
+
var _d;
|
|
752
|
+
return __generator(this, function (_e) {
|
|
753
|
+
switch (_e.label) {
|
|
754
|
+
case 0:
|
|
755
|
+
try {
|
|
756
|
+
for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
757
|
+
folder = _b.value;
|
|
758
|
+
fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
|
|
759
|
+
recursive: true,
|
|
760
|
+
force: true
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
765
|
+
finally {
|
|
766
|
+
try {
|
|
767
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
768
|
+
}
|
|
769
|
+
finally { if (e_5) throw e_5.error; }
|
|
770
|
+
}
|
|
771
|
+
write(path_1.default.join(root, 'sasjsbuild', 'docs', 'index.html'), '<html></html>');
|
|
772
|
+
warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
|
|
773
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
774
|
+
// the drive is NOT empty - the docs folder reaches it - so the file count
|
|
775
|
+
// alone would have kept quiet
|
|
684
776
|
];
|
|
685
|
-
case
|
|
686
|
-
|
|
687
|
-
// the
|
|
688
|
-
|
|
777
|
+
case 1:
|
|
778
|
+
result = _e.sent();
|
|
779
|
+
// the drive is NOT empty - the docs folder reaches it - so the file count
|
|
780
|
+
// alone would have kept quiet
|
|
781
|
+
expect(result.driveEntries).toBeGreaterThan(0);
|
|
782
|
+
expect(result.jsPrograms).toBe(0);
|
|
783
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('No compiled programs'));
|
|
784
|
+
warn.mockRestore();
|
|
689
785
|
return [2 /*return*/];
|
|
690
786
|
}
|
|
691
787
|
});
|
|
@@ -773,8 +869,8 @@ describe('browser-hosted build', function () {
|
|
|
773
869
|
});
|
|
774
870
|
}); });
|
|
775
871
|
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
|
|
872
|
+
var _a, _b, mockWebFolder, e_6_1;
|
|
873
|
+
var e_6, _c;
|
|
778
874
|
return __generator(this, function (_d) {
|
|
779
875
|
switch (_d.label) {
|
|
780
876
|
case 0:
|
|
@@ -805,14 +901,14 @@ describe('browser-hosted build', function () {
|
|
|
805
901
|
return [3 /*break*/, 1];
|
|
806
902
|
case 4: return [3 /*break*/, 7];
|
|
807
903
|
case 5:
|
|
808
|
-
|
|
809
|
-
|
|
904
|
+
e_6_1 = _d.sent();
|
|
905
|
+
e_6 = { error: e_6_1 };
|
|
810
906
|
return [3 /*break*/, 7];
|
|
811
907
|
case 6:
|
|
812
908
|
try {
|
|
813
909
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
814
910
|
}
|
|
815
|
-
finally { if (
|
|
911
|
+
finally { if (e_6) throw e_6.error; }
|
|
816
912
|
return [7 /*endfinally*/];
|
|
817
913
|
case 7:
|
|
818
914
|
expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
|
|
@@ -1139,6 +1139,46 @@ describe('service worker host', function () {
|
|
|
1139
1139
|
});
|
|
1140
1140
|
}); });
|
|
1141
1141
|
});
|
|
1142
|
+
it('writes a binary upload as its bytes, not as the text it decodes to', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1143
|
+
var dispatch, image, created, read, _a, _b;
|
|
1144
|
+
return __generator(this, function (_c) {
|
|
1145
|
+
switch (_c.label) {
|
|
1146
|
+
case 0:
|
|
1147
|
+
dispatch = seeded().dispatch;
|
|
1148
|
+
image = new Uint8Array([
|
|
1149
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
|
|
1150
|
+
]);
|
|
1151
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file', {
|
|
1152
|
+
method: 'POST',
|
|
1153
|
+
headers: { 'content-type': 'application/json' },
|
|
1154
|
+
body: JSON.stringify({
|
|
1155
|
+
params: { filePath: '/Public/logo.png' },
|
|
1156
|
+
tables: [
|
|
1157
|
+
{
|
|
1158
|
+
name: 'file',
|
|
1159
|
+
filename: 'logo.png',
|
|
1160
|
+
csv: btoa(String.fromCharCode.apply(null, Array.from(image))),
|
|
1161
|
+
binary: true
|
|
1162
|
+
}
|
|
1163
|
+
]
|
|
1164
|
+
})
|
|
1165
|
+
})];
|
|
1166
|
+
case 1:
|
|
1167
|
+
created = _c.sent();
|
|
1168
|
+
expect(created.status).toBe(200);
|
|
1169
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/Public/logo.png'))];
|
|
1170
|
+
case 2:
|
|
1171
|
+
read = _c.sent();
|
|
1172
|
+
expect(read.status).toBe(200);
|
|
1173
|
+
_a = expect;
|
|
1174
|
+
_b = Uint8Array.bind;
|
|
1175
|
+
return [4 /*yield*/, read.arrayBuffer()];
|
|
1176
|
+
case 3:
|
|
1177
|
+
_a.apply(void 0, [new (_b.apply(Uint8Array, [void 0, _c.sent()]))()]).toEqual(image);
|
|
1178
|
+
return [2 /*return*/];
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
}); });
|
|
1142
1182
|
it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1143
1183
|
var dispatch, response, _a;
|
|
1144
1184
|
return __generator(this, function (_b) {
|
|
@@ -1157,6 +1197,46 @@ describe('service worker host', function () {
|
|
|
1157
1197
|
}
|
|
1158
1198
|
});
|
|
1159
1199
|
}); });
|
|
1200
|
+
it('answers the app stream as the server does, with nothing in it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1201
|
+
var dispatch, listing, _a, root, landing, _b, app, _c;
|
|
1202
|
+
return __generator(this, function (_d) {
|
|
1203
|
+
switch (_d.label) {
|
|
1204
|
+
case 0:
|
|
1205
|
+
dispatch = seeded().dispatch;
|
|
1206
|
+
return [4 /*yield*/, dispatch('/AppStream/apps.json')];
|
|
1207
|
+
case 1:
|
|
1208
|
+
listing = _d.sent();
|
|
1209
|
+
expect(listing.status).toBe(200);
|
|
1210
|
+
_a = expect;
|
|
1211
|
+
return [4 /*yield*/, listing.json()];
|
|
1212
|
+
case 2:
|
|
1213
|
+
_a.apply(void 0, [_d.sent()]).toEqual({ apps: [] });
|
|
1214
|
+
return [4 /*yield*/, dispatch('/AppStream')];
|
|
1215
|
+
case 3:
|
|
1216
|
+
root = _d.sent();
|
|
1217
|
+
expect(root.status).toBe(301);
|
|
1218
|
+
expect(root.headers.get('location')).toBe('/AppStream/');
|
|
1219
|
+
return [4 /*yield*/, dispatch('/AppStream/')];
|
|
1220
|
+
case 4:
|
|
1221
|
+
landing = _d.sent();
|
|
1222
|
+
expect(landing.status).toBe(200);
|
|
1223
|
+
expect(landing.headers.get('content-type')).toContain('text/html');
|
|
1224
|
+
_b = expect;
|
|
1225
|
+
return [4 /*yield*/, landing.text()];
|
|
1226
|
+
case 5:
|
|
1227
|
+
_b.apply(void 0, [_d.sent()]).toContain('serves no apps');
|
|
1228
|
+
return [4 /*yield*/, dispatch('/AppStream/Demo/')];
|
|
1229
|
+
case 6:
|
|
1230
|
+
app = _d.sent();
|
|
1231
|
+
expect(app.status).toBe(404);
|
|
1232
|
+
_c = expect;
|
|
1233
|
+
return [4 /*yield*/, app.json()];
|
|
1234
|
+
case 7:
|
|
1235
|
+
_c.apply(void 0, [(_d.sent()).message]).toContain('serves no apps');
|
|
1236
|
+
return [2 /*return*/];
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
}); });
|
|
1160
1240
|
it('leaves a request to another origin alone', function () {
|
|
1161
1241
|
var dispatch = seeded().dispatch;
|
|
1162
1242
|
expect(dispatch('https://other.example.com/SASjsApi/info')).toBeUndefined();
|
package/package.json
CHANGED