@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.
@@ -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
- // the one run time a browser holds; a stored program asks for another and
520
- // is refused with the package's own message
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 tag = bootTag(scripts, version, basePath);
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
- expect(html).toContain('"src":"main.abc123.js","type":"module"');
489
+ specs = JSON.parse(html
490
+ .match(/data-scripts='([^']*)'/)[1]
491
+ .replace(/&#39;/g, "'")
492
+ .replace(/&amp;/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(/&#39;/g, "'").replace(/&amp;/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
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.22.0"
128
+ "version": "6.23.1"
129
129
  }