@sasjs/cli 6.23.0 → 6.24.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.
@@ -462,7 +462,7 @@ const adminReports = {
462
462
  cpuCount: navigator.hardwareConcurrency || 1,
463
463
  cpuModel: 'unknown',
464
464
  loadAverage: [],
465
- runTimes: ['js'],
465
+ runTimes: ['js', 'sas'],
466
466
  sasjsRoot: '/'
467
467
  },
468
468
  memory: {
@@ -591,11 +591,12 @@ const SERVICES = {
591
591
  identity: IDENTITY,
592
592
  records: RECORDS,
593
593
  runtime: API.createRuntimeService(IDENTITY_STORE, RUNTIME, {
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.
598
- runTimes: ['js']
594
+ // `js` is executed here; `sas` is offered so a SAS program can be run at all
595
+ // in the demo, and answered from the recording when one names it. A `.sas`
596
+ // program with no recording is refused in the runner, deliberately - it must
597
+ // not be handed to the JavaScript runtime as source, which is what the
598
+ // resolveProgram guard in BrowserRuntime.run prevents.
599
+ runTimes: ['js', 'sas']
599
600
  }),
600
601
  session: API.createSessionService(IDENTITY_STORE, RUNTIME),
601
602
  authConfig: API.createAuthConfigService({}),
@@ -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;
@@ -872,7 +917,7 @@ function buildMockSite(target) {
872
917
  builtAt: new Date().toISOString()
873
918
  }, null, 2));
874
919
  indexPath = path_1.default.join(outputFolder, 'index.html');
875
- _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;
876
921
  fs_1.default.writeFileSync(indexPath, patched);
877
922
  warnings.forEach(function (warning) { var _a; return (_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn(warning); });
878
923
  return [2 /*return*/, {
@@ -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
  });
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.23.0"
128
+ "version": "6.24.0"
129
129
  }