@sasjs/cli 6.17.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 +9 -1
- package/build/commands/build/internal/mockSite/mockSite.js +14 -15
- package/build/commands/build/internal/mockSite/spec/intercept.spec.js +26 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +21 -14
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +40 -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
|
}
|
|
@@ -648,9 +648,9 @@ function buildMockSite(target) {
|
|
|
648
648
|
return __awaiter(this, void 0, void 0, function () {
|
|
649
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, _j;
|
|
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'))) {
|
|
@@ -693,21 +693,20 @@ function buildMockSite(target) {
|
|
|
693
693
|
jsPrograms = countPrograms(seed, '.js');
|
|
694
694
|
sasPrograms = countPrograms(seed, '.sas');
|
|
695
695
|
driveEntries = Object.keys(seed).length;
|
|
696
|
-
// A build
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
//
|
|
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
701
|
if (jsPrograms === 0 && sasPrograms > 0) {
|
|
702
|
-
|
|
703
|
-
'Build a target whose services are JavaScript (typically a mocks project).');
|
|
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."));
|
|
704
703
|
}
|
|
705
704
|
// No PROGRAMS is a different thing again: a compile that produced nothing, or
|
|
706
705
|
// a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
|
|
707
706
|
// behind that is not a program folder but does reach the drive, so the count
|
|
708
707
|
// that decides this is of programs, not of files.
|
|
709
708
|
if (jsPrograms + sasPrograms === 0) {
|
|
710
|
-
(
|
|
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. ") +
|
|
711
710
|
'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
|
|
712
711
|
}
|
|
713
712
|
// The package supplies the drive rules the worker answers with, so an install
|
|
@@ -720,7 +719,7 @@ function buildMockSite(target) {
|
|
|
720
719
|
'or check that @sasjs/api still publishes build/browser.js.');
|
|
721
720
|
}
|
|
722
721
|
if (skipped.length) {
|
|
723
|
-
(
|
|
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: ") +
|
|
724
723
|
"".concat(skipped
|
|
725
724
|
.slice(0, 3)
|
|
726
725
|
.map(function (file) {
|
|
@@ -767,7 +766,7 @@ function buildMockSite(target) {
|
|
|
767
766
|
case 3:
|
|
768
767
|
// 2. the linter, bundled from the installed packages so the code routes answer
|
|
769
768
|
// with the rules this CLI resolved rather than a second implementation
|
|
770
|
-
|
|
769
|
+
_l.sent();
|
|
771
770
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
772
771
|
// served from, and the app has to be controlled from /
|
|
773
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,32 @@ 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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
expect(
|
|
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();
|
|
689
696
|
return [2 /*return*/];
|
|
690
697
|
}
|
|
691
698
|
});
|
|
@@ -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) {
|
package/package.json
CHANGED