@sasjs/cli 6.22.0 → 6.23.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/runtime-core.js +3 -0
- package/build/commands/build/internal/mockSite/assets/sw.js +113 -9
- package/build/commands/build/internal/mockSite/mockSite.d.ts +2 -0
- package/build/commands/build/internal/mockSite/mockSite.js +56 -5
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +92 -2
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +138 -1
- package/package.json +1 -1
|
@@ -1525,6 +1525,9 @@ const api = {
|
|
|
1525
1525
|
bytes: [...FILES.keys()].reduce((total, abs) => total + entrySizeAt(abs), 0)
|
|
1526
1526
|
}),
|
|
1527
1527
|
run: (payload) => runProgram(payload),
|
|
1528
|
+
// the file a program names, so the host can tell a .sas program from a .js one
|
|
1529
|
+
// and refuse the former instead of compiling SAS source as JavaScript
|
|
1530
|
+
resolveProgram: (program) => resolveProgram(program),
|
|
1528
1531
|
// the drive's bytes, for @sasjs/api's services: the rules that decide a
|
|
1529
1532
|
// status are the package's, so this host hands over storage and nothing else
|
|
1530
1533
|
driveStore,
|
|
@@ -181,19 +181,21 @@ const SEEDED_HEADER_LINE = String(R.HEADERS_SEED)
|
|
|
181
181
|
* it.
|
|
182
182
|
*/
|
|
183
183
|
const artifactOf = (result, debug) => {
|
|
184
|
-
if (debug)
|
|
184
|
+
if (debug) {
|
|
185
|
+
// webout SEP log SEP [listing] - the framing @sasjs/adapter and the Studio's
|
|
186
|
+
// parser both split on, so a recorded session reads the same as a real one.
|
|
187
|
+
const framed = [result.webout, LOGS_SEPARATOR, result.log, LOGS_SEPARATOR]
|
|
188
|
+
|
|
189
|
+
if (result.print) framed.push(result.print)
|
|
190
|
+
|
|
185
191
|
return {
|
|
186
|
-
contents:
|
|
187
|
-
result.webout,
|
|
188
|
-
LOGS_SEPARATOR,
|
|
189
|
-
result.log,
|
|
190
|
-
LOGS_SEPARATOR
|
|
191
|
-
].join('\n'),
|
|
192
|
+
contents: framed.join('\n'),
|
|
192
193
|
// the frame is text, and the service's own type and disposition are not
|
|
193
194
|
// applied to it - a download there would put the frame in the file
|
|
194
195
|
headers: { 'content-type': 'text/plain' },
|
|
195
196
|
log: result.log
|
|
196
197
|
}
|
|
198
|
+
}
|
|
197
199
|
|
|
198
200
|
const headers = parseHeaders(result.headers)
|
|
199
201
|
let contents = result.webout
|
|
@@ -231,6 +233,66 @@ const persistChanges = async () => {
|
|
|
231
233
|
await broadcast({ type: 'sasjs-persist', files })
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
// ----------------------------------------------------------- the SAS transcript
|
|
237
|
+
/**
|
|
238
|
+
* A recorded SAS session, for a project that ships one.
|
|
239
|
+
*
|
|
240
|
+
* A browser has no SAS binary. Rather than let a Run on a .sas program be a
|
|
241
|
+
* surprise, a project may record a session once and commit it as
|
|
242
|
+
* `sas-transcript.json`: the build copies it into the site and this answers a
|
|
243
|
+
* run of a program it names with that session's log, listing and webout. The
|
|
244
|
+
* recording is the demo's own data, so nothing here is invented for a project
|
|
245
|
+
* that does not ship one - those runs reach the runner and are refused exactly
|
|
246
|
+
* as before.
|
|
247
|
+
*
|
|
248
|
+
* Shape: `{ "<program path>": {"log": "...", "print": "...", "webout": "..."} }`,
|
|
249
|
+
* all three strings. The path is matched as written apart from the
|
|
250
|
+
* `/sasjs/drive/files` prefix and the `.sas` suffix, so a recording keeps the
|
|
251
|
+
* path a person sees; a bare file name is not enough, because that would answer
|
|
252
|
+
* every file of that name in the drive.
|
|
253
|
+
*/
|
|
254
|
+
const SAS_TRANSCRIPT = 'host/sas-transcript.json'
|
|
255
|
+
let TRANSCRIPT = null
|
|
256
|
+
|
|
257
|
+
const transcript = async () => {
|
|
258
|
+
if (TRANSCRIPT) return TRANSCRIPT
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(SAS_TRANSCRIPT, { cache: 'no-cache' })
|
|
261
|
+
TRANSCRIPT = response.ok ? await response.json() : {}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
TRANSCRIPT = {}
|
|
264
|
+
}
|
|
265
|
+
return TRANSCRIPT
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The drive path without its mount prefix or extension.
|
|
270
|
+
*
|
|
271
|
+
* A recording is written with the path a person sees (`/sasjs/drive/files/...`,
|
|
272
|
+
* ending in `.sas`); the route hands the request the path relative to the app,
|
|
273
|
+
* and Studio asks for it without the extension. Comparing the logical path makes
|
|
274
|
+
* those the same program without widening the match to the file name alone,
|
|
275
|
+
* which would answer every `report.sas` in the drive with a session recorded for
|
|
276
|
+
* some other file.
|
|
277
|
+
*/
|
|
278
|
+
const logicalPath = (value) =>
|
|
279
|
+
String(value || '')
|
|
280
|
+
.replace(/^\/sasjs\/drive\/files/i, '')
|
|
281
|
+
.replace(/\.sas$/i, '')
|
|
282
|
+
|
|
283
|
+
/** The recording naming this program, if the project shipped one. */
|
|
284
|
+
const transcriptFor = async (programPath) => {
|
|
285
|
+
const requested = logicalPath(programPath)
|
|
286
|
+
if (!requested) return undefined
|
|
287
|
+
|
|
288
|
+
const recorded = await transcript()
|
|
289
|
+
|
|
290
|
+
for (const key of Object.keys(recorded))
|
|
291
|
+
if (logicalPath(key) === requested) return recorded[key]
|
|
292
|
+
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
234
296
|
// ---------------------------------------------------------------- the runner
|
|
235
297
|
/**
|
|
236
298
|
* The work a run does, over the browser's drive.
|
|
@@ -250,6 +312,19 @@ class BrowserRuntime extends API.MemorySessionHost {
|
|
|
250
312
|
async run(request) {
|
|
251
313
|
const program = request.program || {}
|
|
252
314
|
const params = Object.assign({}, request.vars || {})
|
|
315
|
+
|
|
316
|
+
// A SAS program the recording does not name. This host has no SAS binary, so
|
|
317
|
+
// the answer is the refusal a client already understands - not a JavaScript
|
|
318
|
+
// SyntaxError quoting the SAS source, which is what handing it to the runner
|
|
319
|
+
// would produce. The file is resolved because the request may name the
|
|
320
|
+
// program without its extension.
|
|
321
|
+
if (program.code === undefined) {
|
|
322
|
+
const resolved = R.resolveProgram(program.path)
|
|
323
|
+
|
|
324
|
+
if (resolved && /\.sas$/i.test(resolved))
|
|
325
|
+
throw runFailure('"runTime" must be one of [js]')
|
|
326
|
+
}
|
|
327
|
+
|
|
253
328
|
const payload = { params, tables: tablesOf(request.files) }
|
|
254
329
|
if (program.code !== undefined) payload.code = program.code
|
|
255
330
|
else payload.program = program.path
|
|
@@ -516,8 +591,10 @@ const SERVICES = {
|
|
|
516
591
|
identity: IDENTITY,
|
|
517
592
|
records: RECORDS,
|
|
518
593
|
runtime: API.createRuntimeService(IDENTITY_STORE, RUNTIME, {
|
|
519
|
-
//
|
|
520
|
-
//
|
|
594
|
+
// The one run time this host executes. A recorded SAS session is answered
|
|
595
|
+
// before the runner (see SAS_TRANSCRIPT below), not by claiming a `sas` run
|
|
596
|
+
// time - a .sas program with no recording must still be refused, and a route
|
|
597
|
+
// that accepted `sas` would compile SAS source as JavaScript instead.
|
|
521
598
|
runTimes: ['js']
|
|
522
599
|
}),
|
|
523
600
|
session: API.createSessionService(IDENTITY_STORE, RUNTIME),
|
|
@@ -860,6 +937,33 @@ const runRoute = async (routeId, payload, subject) => {
|
|
|
860
937
|
runRequest.program = query._program || body._program
|
|
861
938
|
}
|
|
862
939
|
|
|
940
|
+
// A recorded SAS session is answered here, ahead of the run time check: a
|
|
941
|
+
// transcript is data, not a run time, and this host holds only `js`. Accepting
|
|
942
|
+
// `sas` instead would make the package compile SAS source as JavaScript and
|
|
943
|
+
// hand back a SyntaxError where every other .sas program gets a refusal.
|
|
944
|
+
// Neither a code run nor a trigger: a trigger has to answer with the session
|
|
945
|
+
// record the route builds, which a client parses as JSON, so a recording is not
|
|
946
|
+
// an answer there.
|
|
947
|
+
if (!code && !trigger) {
|
|
948
|
+
const recording = await transcriptFor(runRequest.program)
|
|
949
|
+
|
|
950
|
+
if (recording)
|
|
951
|
+
return artifactResponse(
|
|
952
|
+
artifactOf(
|
|
953
|
+
{
|
|
954
|
+
webout: recording.webout || '',
|
|
955
|
+
log: recording.log || '',
|
|
956
|
+
print: recording.print || '',
|
|
957
|
+
// the seed every session starts with, so a run without `_debug`
|
|
958
|
+
// answers text/html rather than leaving the type to the browser -
|
|
959
|
+
// the same rule runtime-core applies to a service that declares none
|
|
960
|
+
headers: SEEDED_HEADER_LINE
|
|
961
|
+
},
|
|
962
|
+
vars._debug !== undefined
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
}
|
|
966
|
+
|
|
863
967
|
if (trigger) {
|
|
864
968
|
runRequest.expiresAfterMins =
|
|
865
969
|
query.expiresAfterMins !== undefined
|
|
@@ -9,6 +9,8 @@ export interface MockSiteResult {
|
|
|
9
9
|
webFileCount: number;
|
|
10
10
|
/** True when a captured drive snapshot was shipped with the site. */
|
|
11
11
|
snapshot: boolean;
|
|
12
|
+
/** True when a recorded SAS session was shipped, for .sas runs to answer. */
|
|
13
|
+
sasTranscript: boolean;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* The webout() block is the one @sasjs/utils generates for the server, emitted
|
|
@@ -594,13 +594,55 @@ var markBrowserHost = function (html, warnings) {
|
|
|
594
594
|
return html;
|
|
595
595
|
return html.slice(0, start) + HOST_TAG + html.slice(start);
|
|
596
596
|
};
|
|
597
|
+
/**
|
|
598
|
+
* Each app script's URL carries a hash of the script's own contents.
|
|
599
|
+
*
|
|
600
|
+
* A static host caches an asset for as long as it says to, and GitHub Pages says
|
|
601
|
+
* two hours - while the app's bundle name never changes from one build to the
|
|
602
|
+
* next. So a publish that changes only the app reuses the previous URL, and a
|
|
603
|
+
* visitor inside that window runs the previous build against the new page: the
|
|
604
|
+
* screens the new build adds simply never appear, and the ones it removed keep
|
|
605
|
+
* working. Only the host's own files were versioned before (`?v=` on boot.js and
|
|
606
|
+
* the worker's imports), and those change with the CLI, not with the app.
|
|
607
|
+
*
|
|
608
|
+
* A src that names no file in the output is left alone: a page may load a
|
|
609
|
+
* script from somewhere this build does not own, and guessing a version for it
|
|
610
|
+
* would break it.
|
|
611
|
+
*
|
|
612
|
+
* The tags in the page are all this covers. A chunk the entry point loads later
|
|
613
|
+
* - `import('./chunk.js')`, a lazy route - keeps its own URL and is cached for
|
|
614
|
+
* the same window, because the request for it is made by the bundle's runtime
|
|
615
|
+
* and never passes through this file. Versioning those would mean rewriting the
|
|
616
|
+
* chunk map inside the bundle, which is the bundler's business, not the host's;
|
|
617
|
+
* a page whose chunks matter more than its entry should name them in the page,
|
|
618
|
+
* where this does reach them.
|
|
619
|
+
*/
|
|
620
|
+
var versionScripts = function (scripts, outputFolder) {
|
|
621
|
+
return scripts.map(function (script) {
|
|
622
|
+
var src = script.src;
|
|
623
|
+
if (!src)
|
|
624
|
+
return script;
|
|
625
|
+
var _a = __read(src.split('?'), 2), pathname = _a[0], query = _a[1];
|
|
626
|
+
var file = path_1.default.join(outputFolder, pathname.replace(/^(\.\/|\/)/, ''));
|
|
627
|
+
if (!fs_1.default.existsSync(file))
|
|
628
|
+
return script;
|
|
629
|
+
var hash = (0, crypto_1.createHash)('md5')
|
|
630
|
+
.update(fs_1.default.readFileSync(file))
|
|
631
|
+
.digest('hex')
|
|
632
|
+
.slice(0, 10);
|
|
633
|
+
return Object.assign({}, script, {
|
|
634
|
+
src: "".concat(pathname, "?v=").concat(hash).concat(query ? "&".concat(query) : '')
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
};
|
|
597
638
|
/**
|
|
598
639
|
* Points the app at its own origin and replaces its script tags with the boot
|
|
599
640
|
* loader. Both changes are reported when the expected markup is absent, rather
|
|
600
641
|
* than producing a site that silently talks to a real server.
|
|
601
642
|
*/
|
|
602
|
-
var patchIndexHtml = function (html, version, basePath) {
|
|
643
|
+
var patchIndexHtml = function (html, version, basePath, outputFolder) {
|
|
603
644
|
if (basePath === void 0) { basePath = ''; }
|
|
645
|
+
if (outputFolder === void 0) { outputFolder = ''; }
|
|
604
646
|
var warnings = [];
|
|
605
647
|
var patched = html;
|
|
606
648
|
patched = markBrowserHost(patched, warnings);
|
|
@@ -657,7 +699,10 @@ var patchIndexHtml = function (html, version, basePath) {
|
|
|
657
699
|
return '';
|
|
658
700
|
});
|
|
659
701
|
if (scripts.length) {
|
|
660
|
-
var
|
|
702
|
+
var versioned = outputFolder
|
|
703
|
+
? versionScripts(scripts, outputFolder)
|
|
704
|
+
: scripts;
|
|
705
|
+
var tag = bootTag(versioned, version, basePath);
|
|
661
706
|
patched = /<\/body>/i.test(patched)
|
|
662
707
|
? patched.replace(/<\/body>/i, "".concat(tag, "\n</body>"))
|
|
663
708
|
: patched + tag;
|
|
@@ -683,7 +728,7 @@ var patchIndexHtml = function (html, version, basePath) {
|
|
|
683
728
|
*/
|
|
684
729
|
function buildMockSite(target) {
|
|
685
730
|
return __awaiter(this, void 0, void 0, function () {
|
|
686
|
-
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
731
|
+
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, transcriptSource, hasTranscript, indexPath, _d, patched, warnings;
|
|
687
732
|
var e_8, _e;
|
|
688
733
|
var _f, _g, _h, _j, _k, _l;
|
|
689
734
|
return __generator(this, function (_m) {
|
|
@@ -854,6 +899,10 @@ function buildMockSite(target) {
|
|
|
854
899
|
if (hasSnapshot) {
|
|
855
900
|
fs_1.default.copyFileSync(snapshotSource, path_1.default.join(hostFolder, 'vfs-snapshot.json'));
|
|
856
901
|
}
|
|
902
|
+
transcriptSource = path_1.default.join(buildFolder, 'sas-transcript.json');
|
|
903
|
+
hasTranscript = fs_1.default.existsSync(transcriptSource);
|
|
904
|
+
if (hasTranscript)
|
|
905
|
+
fs_1.default.copyFileSync(transcriptSource, path_1.default.join(hostFolder, 'sas-transcript.json'));
|
|
857
906
|
// 3. the drive, laid out from the compile output
|
|
858
907
|
fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'vfs-seed.json'), JSON.stringify({ format: exports.DRIVE_FORMAT, files: seed }));
|
|
859
908
|
fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'mock-config.json'), JSON.stringify({
|
|
@@ -863,11 +912,12 @@ function buildMockSite(target) {
|
|
|
863
912
|
jsPrograms: jsPrograms,
|
|
864
913
|
programFolders: PROGRAM_FOLDERS,
|
|
865
914
|
snapshot: hasSnapshot,
|
|
915
|
+
sasTranscript: hasTranscript,
|
|
866
916
|
buildStamp: buildStamp,
|
|
867
917
|
builtAt: new Date().toISOString()
|
|
868
918
|
}, null, 2));
|
|
869
919
|
indexPath = path_1.default.join(outputFolder, 'index.html');
|
|
870
|
-
_d = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion, basePath), patched = _d.patched, warnings = _d.warnings;
|
|
920
|
+
_d = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion, basePath, outputFolder), patched = _d.patched, warnings = _d.warnings;
|
|
871
921
|
fs_1.default.writeFileSync(indexPath, patched);
|
|
872
922
|
warnings.forEach(function (warning) { var _a; return (_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn(warning); });
|
|
873
923
|
return [2 /*return*/, {
|
|
@@ -876,7 +926,8 @@ function buildMockSite(target) {
|
|
|
876
926
|
driveEntries: driveEntries,
|
|
877
927
|
jsPrograms: jsPrograms,
|
|
878
928
|
webFileCount: webFileCount,
|
|
879
|
-
snapshot: hasSnapshot
|
|
929
|
+
snapshot: hasSnapshot,
|
|
930
|
+
sasTranscript: hasTranscript
|
|
880
931
|
}];
|
|
881
932
|
}
|
|
882
933
|
});
|
|
@@ -468,7 +468,7 @@ describe('browser-hosted build', function () {
|
|
|
468
468
|
});
|
|
469
469
|
}); });
|
|
470
470
|
it('points the app at itself and installs the boot loader', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
471
|
-
var html;
|
|
471
|
+
var html, specs;
|
|
472
472
|
return __generator(this, function (_a) {
|
|
473
473
|
switch (_a.label) {
|
|
474
474
|
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
@@ -486,7 +486,60 @@ describe('browser-hosted build', function () {
|
|
|
486
486
|
// the app's own tags are replayed by the loader, in the order the page has
|
|
487
487
|
// them, and are gone from the page itself
|
|
488
488
|
expect(html).not.toContain('<script src="main.abc123.js"');
|
|
489
|
-
|
|
489
|
+
specs = JSON.parse(html
|
|
490
|
+
.match(/data-scripts='([^']*)'/)[1]
|
|
491
|
+
.replace(/'/g, "'")
|
|
492
|
+
.replace(/&/g, '&'));
|
|
493
|
+
expect(specs).toHaveLength(1);
|
|
494
|
+
expect(specs[0].type).toBe('module');
|
|
495
|
+
expect(specs[0].src).toMatch(/^main\.abc123\.js\?v=[0-9a-f]{10}$/);
|
|
496
|
+
return [2 /*return*/];
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}); });
|
|
500
|
+
/**
|
|
501
|
+
* A publish that changes only the app has to reach a reader whose browser
|
|
502
|
+
* still holds the previous copy. The host caches an asset for two hours and the
|
|
503
|
+
* bundle's name never changes, so the URL is what has to change.
|
|
504
|
+
*/
|
|
505
|
+
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;
|
|
507
|
+
return __generator(this, function (_a) {
|
|
508
|
+
switch (_a.label) {
|
|
509
|
+
case 0:
|
|
510
|
+
specs = function () {
|
|
511
|
+
var html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
512
|
+
var tag = html.match(/data-scripts='([^']*)'/);
|
|
513
|
+
return JSON.parse(tag[1].replace(/'/g, "'").replace(/&/g, '&'));
|
|
514
|
+
};
|
|
515
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
516
|
+
case 1:
|
|
517
|
+
_a.sent();
|
|
518
|
+
first = specs()[0].src;
|
|
519
|
+
expect(first).toMatch(/^main\.abc123\.js\?v=[0-9a-f]{10}$/);
|
|
520
|
+
// the same file twice: nothing about the URL changes
|
|
521
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
522
|
+
case 2:
|
|
523
|
+
// the same file twice: nothing about the URL changes
|
|
524
|
+
_a.sent();
|
|
525
|
+
expect(specs()[0].src).toBe(first);
|
|
526
|
+
// a different bundle: a different URL, which is the caching bust
|
|
527
|
+
write(path_1.default.join(root, 'client', 'dist', 'main.abc123.js'), 'console.log(2)\n');
|
|
528
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
529
|
+
case 3:
|
|
530
|
+
_a.sent();
|
|
531
|
+
expect(specs()[0].src).not.toBe(first);
|
|
532
|
+
// and a script this build does not own is left exactly as the page asked
|
|
533
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
534
|
+
'<!doctype html><html><head><title>App</title></head><body>',
|
|
535
|
+
"<sasjs serverurl=\"https://sas.example.com\" apploc=\"".concat(appLoc, "\"></sasjs>"),
|
|
536
|
+
'<script src="https://cdn.example.com/lib.js"></script>',
|
|
537
|
+
'</body></html>'
|
|
538
|
+
].join('\n'));
|
|
539
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
540
|
+
case 4:
|
|
541
|
+
_a.sent();
|
|
542
|
+
expect(specs()[0].src).toBe('https://cdn.example.com/lib.js');
|
|
490
543
|
return [2 /*return*/];
|
|
491
544
|
}
|
|
492
545
|
});
|
|
@@ -642,6 +695,43 @@ describe('browser-hosted build', function () {
|
|
|
642
695
|
}
|
|
643
696
|
});
|
|
644
697
|
}); });
|
|
698
|
+
it('ships a recorded SAS session when the project has one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
699
|
+
var recording, result;
|
|
700
|
+
var _a;
|
|
701
|
+
return __generator(this, function (_b) {
|
|
702
|
+
switch (_b.label) {
|
|
703
|
+
case 0:
|
|
704
|
+
recording = (_a = {},
|
|
705
|
+
_a["/sasjs/drive/files".concat(appLoc, "/services/public/report.sas")] = {
|
|
706
|
+
log: 'NOTE: Copyright (c) 2016 by SAS Institute Inc., Cary, NC, USA.',
|
|
707
|
+
print: 'The SAS System',
|
|
708
|
+
webout: '{"SYSSITE":"00000000"}'
|
|
709
|
+
},
|
|
710
|
+
_a);
|
|
711
|
+
write(path_1.default.join(root, 'sasjsbuild', 'sas-transcript.json'), JSON.stringify(recording));
|
|
712
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
713
|
+
case 1:
|
|
714
|
+
result = _b.sent();
|
|
715
|
+
expect(result.sasTranscript).toBe(true);
|
|
716
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'sas-transcript.json'), 'utf8'))).toEqual(recording);
|
|
717
|
+
expect(readConfig().sasTranscript).toBe(true);
|
|
718
|
+
return [2 /*return*/];
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}); });
|
|
722
|
+
it('ships no recording when the project has none', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
723
|
+
var result;
|
|
724
|
+
return __generator(this, function (_a) {
|
|
725
|
+
switch (_a.label) {
|
|
726
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
727
|
+
case 1:
|
|
728
|
+
result = _a.sent();
|
|
729
|
+
expect(result.sasTranscript).toBe(false);
|
|
730
|
+
expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', 'sas-transcript.json'))).toBe(false);
|
|
731
|
+
return [2 /*return*/];
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}); });
|
|
645
735
|
it('ships no snapshot when the project has none', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
646
736
|
var result;
|
|
647
737
|
return __generator(this, function (_a) {
|
|
@@ -163,7 +163,7 @@ var loadWorker = function (base) {
|
|
|
163
163
|
};
|
|
164
164
|
require('../assets/sw.js');
|
|
165
165
|
global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
|
|
166
|
-
var target;
|
|
166
|
+
var target, fixture;
|
|
167
167
|
var _a;
|
|
168
168
|
return __generator(this, function (_b) {
|
|
169
169
|
target = typeof input === 'string' ? input : (input && input.url) || String(input);
|
|
@@ -175,6 +175,10 @@ var loadWorker = function (base) {
|
|
|
175
175
|
})];
|
|
176
176
|
if (target.includes('index.html'))
|
|
177
177
|
return [2 /*return*/, new Response('<!doctype html><html><head><title>App</title></head><body>home</body></html>', { status: 200, headers: { 'content-type': 'text/html' } })];
|
|
178
|
+
if (target.includes('sas-transcript')) {
|
|
179
|
+
fixture = global.__sasTranscript;
|
|
180
|
+
return [2 /*return*/, fixture ? jsonResponse(fixture) : new Response('', { status: 404 })];
|
|
181
|
+
}
|
|
178
182
|
return [2 /*return*/, target.includes('vfs-seed')
|
|
179
183
|
? jsonResponse({
|
|
180
184
|
files: (_a = {},
|
|
@@ -286,6 +290,104 @@ describe('service worker host', function () {
|
|
|
286
290
|
}
|
|
287
291
|
});
|
|
288
292
|
}); });
|
|
293
|
+
it('answers a recorded SAS session with its log, listing and webout', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
294
|
+
var worker, response, body;
|
|
295
|
+
var _a, _b;
|
|
296
|
+
return __generator(this, function (_c) {
|
|
297
|
+
switch (_c.label) {
|
|
298
|
+
case 0:
|
|
299
|
+
// A browser has no SAS binary. A project may ship the transcript of one real
|
|
300
|
+
// session, and a program it names is answered with it - framed the way the
|
|
301
|
+
// adapter and the Studio split the three parts: webout SEP log SEP listing.
|
|
302
|
+
;
|
|
303
|
+
global.__sasTranscript = (_a = {},
|
|
304
|
+
_a["".concat(appLoc, "/services/public/report.sas")] = {
|
|
305
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
306
|
+
log: 'NOTE: Copyright (c) 2016 by SAS Institute Inc., Cary, NC, USA.',
|
|
307
|
+
print: 'The SAS System\n\n Obs customer\n 1 Northwind Ltd'
|
|
308
|
+
},
|
|
309
|
+
_a);
|
|
310
|
+
worker = loadWorker();
|
|
311
|
+
worker.runtime.load((_b = {},
|
|
312
|
+
_b[drivePath('services/public/report.sas')] = 'data work.class; run;',
|
|
313
|
+
_b));
|
|
314
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
315
|
+
encodeURIComponent("".concat(appLoc, "/services/public/report")) +
|
|
316
|
+
'&_debug=131')];
|
|
317
|
+
case 1:
|
|
318
|
+
response = _c.sent();
|
|
319
|
+
expect(response.status).toBe(200);
|
|
320
|
+
return [4 /*yield*/, response.text()];
|
|
321
|
+
case 2:
|
|
322
|
+
body = _c.sent();
|
|
323
|
+
expect(body).toContain('NOTE: Copyright');
|
|
324
|
+
expect(body).toContain('Northwind Ltd');
|
|
325
|
+
expect(body).toContain('"SYSSITE":"00000000"');
|
|
326
|
+
delete global.__sasTranscript;
|
|
327
|
+
return [2 /*return*/];
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}); });
|
|
331
|
+
it('gives a recorded run the seeded content type when not debugging', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
332
|
+
var worker, response, _a;
|
|
333
|
+
var _b, _c;
|
|
334
|
+
return __generator(this, function (_d) {
|
|
335
|
+
switch (_d.label) {
|
|
336
|
+
case 0:
|
|
337
|
+
// Without `_debug` the body is the webout alone, and it still has to carry
|
|
338
|
+
// the type a session is seeded with - an empty header set leaves the choice
|
|
339
|
+
// to the browser, which is the case the seed exists to prevent.
|
|
340
|
+
;
|
|
341
|
+
global.__sasTranscript = (_b = {},
|
|
342
|
+
_b["".concat(appLoc, "/services/public/report.sas")] = {
|
|
343
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
344
|
+
log: 'NOTE: recorded',
|
|
345
|
+
print: 'The SAS System'
|
|
346
|
+
},
|
|
347
|
+
_b);
|
|
348
|
+
worker = loadWorker();
|
|
349
|
+
worker.runtime.load((_c = {},
|
|
350
|
+
_c[drivePath('services/public/report.sas')] = 'data work.class; run;',
|
|
351
|
+
_c));
|
|
352
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
353
|
+
encodeURIComponent("".concat(appLoc, "/services/public/report")))];
|
|
354
|
+
case 1:
|
|
355
|
+
response = _d.sent();
|
|
356
|
+
expect(response.status).toBe(200);
|
|
357
|
+
expect(response.headers.get('content-type')).toContain('text/html');
|
|
358
|
+
_a = expect;
|
|
359
|
+
return [4 /*yield*/, response.text()];
|
|
360
|
+
case 2:
|
|
361
|
+
_a.apply(void 0, [_d.sent()]).toBe('{"SYSSITE":"00000000"}');
|
|
362
|
+
delete global.__sasTranscript;
|
|
363
|
+
return [2 /*return*/];
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}); });
|
|
367
|
+
it('refuses a .sas program no recording names, as it always did', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
368
|
+
var worker, response, body;
|
|
369
|
+
var _a;
|
|
370
|
+
return __generator(this, function (_b) {
|
|
371
|
+
switch (_b.label) {
|
|
372
|
+
case 0:
|
|
373
|
+
worker = loadWorker();
|
|
374
|
+
worker.runtime.load((_a = {},
|
|
375
|
+
_a[drivePath('services/public/other.sas')] = 'data work.class; run;',
|
|
376
|
+
_a));
|
|
377
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
378
|
+
encodeURIComponent("".concat(appLoc, "/services/public/other")))];
|
|
379
|
+
case 1:
|
|
380
|
+
response = _b.sent();
|
|
381
|
+
expect(response.status).toBe(400);
|
|
382
|
+
return [4 /*yield*/, response.text()];
|
|
383
|
+
case 2:
|
|
384
|
+
body = _b.sent();
|
|
385
|
+
expect(body).toContain('runTime');
|
|
386
|
+
expect(body).not.toContain('SyntaxError');
|
|
387
|
+
return [2 /*return*/];
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}); });
|
|
289
391
|
/**
|
|
290
392
|
* A worker whose drive holds one service that answers with a download, the
|
|
291
393
|
* way a streaming service does on the server: the body goes to the webout
|
|
@@ -1059,6 +1161,41 @@ describe('service worker host', function () {
|
|
|
1059
1161
|
}
|
|
1060
1162
|
});
|
|
1061
1163
|
}); });
|
|
1164
|
+
it('keeps a trigger a JSON answer, not the recorded transcript', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1165
|
+
var worker, response, body;
|
|
1166
|
+
var _a, _b;
|
|
1167
|
+
return __generator(this, function (_c) {
|
|
1168
|
+
switch (_c.label) {
|
|
1169
|
+
case 0:
|
|
1170
|
+
// A recording answers a run, not a trigger: the trigger route answers with
|
|
1171
|
+
// the session record a client parses as JSON, so returning the framed
|
|
1172
|
+
// webout there would break triggerProgram and the Studio.
|
|
1173
|
+
;
|
|
1174
|
+
global.__sasTranscript = (_a = {},
|
|
1175
|
+
_a["".concat(appLoc, "/services/public/report.sas")] = {
|
|
1176
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
1177
|
+
log: 'NOTE: recorded',
|
|
1178
|
+
print: 'The SAS System'
|
|
1179
|
+
},
|
|
1180
|
+
_a);
|
|
1181
|
+
worker = loadWorker();
|
|
1182
|
+
worker.runtime.load((_b = {},
|
|
1183
|
+
_b[drivePath('services/public/report.sas')] = 'data work.class; run;',
|
|
1184
|
+
_b));
|
|
1185
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/trigger?_program=' +
|
|
1186
|
+
encodeURIComponent("".concat(appLoc, "/services/public/report")), runRequest({ params: {} }))];
|
|
1187
|
+
case 1:
|
|
1188
|
+
response = _c.sent();
|
|
1189
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
1190
|
+
return [4 /*yield*/, response.json()];
|
|
1191
|
+
case 2:
|
|
1192
|
+
body = _c.sent();
|
|
1193
|
+
expect(body).not.toHaveProperty('webout');
|
|
1194
|
+
delete global.__sasTranscript;
|
|
1195
|
+
return [2 /*return*/];
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
}); });
|
|
1062
1199
|
it('refuses a deploy upload with no archive, and one that is not a zip', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1063
1200
|
var dispatch, empty, _a, form, notZip, _b;
|
|
1064
1201
|
return __generator(this, function (_c) {
|
package/package.json
CHANGED