@sasjs/cli 6.18.0 → 6.19.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.
@@ -114,8 +114,15 @@
114
114
  stamp = ''
115
115
  }
116
116
 
117
+ // Registered relative to the page, and scoped to the directory it sits in:
118
+ // the site is served from the origin root on one host and from a folder on
119
+ // another (a demonstration site beside its own docs), and the worker's scope
120
+ // has to follow the page rather than assume the root.
117
121
  navigator.serviceWorker
118
- .register('/sw.js' + stamp, { scope: '/', updateViaCache: 'none' })
122
+ .register(new URL('sw.js' + stamp, location.href).href, {
123
+ scope: './',
124
+ updateViaCache: 'none'
125
+ })
119
126
  .then(function () {
120
127
  return navigator.serviceWorker.ready
121
128
  })
@@ -32,7 +32,7 @@
32
32
  * encoding returns a string for either, as node does.
33
33
  */
34
34
 
35
- if (typeof importScripts === 'function') importScripts('/host/md5.js')
35
+ if (typeof importScripts === 'function') importScripts('host/md5.js')
36
36
 
37
37
  // ---------------------------------------------------------------- posix path
38
38
  const P = {
@@ -841,8 +841,8 @@ const DRIVE_ROOT = '/sasjs/drive'
841
841
  // Rewritten at boot from host/mock-config.json, which the mock build writes.
842
842
  let APP_LOC = '/Public/app'
843
843
  let DRIVE_APP = `${DRIVE_ROOT}/files${APP_LOC}`
844
- const SEED_PATH = '/host/vfs-seed.json'
845
- const SNAPSHOT_PATH = '/host/vfs-snapshot.json'
844
+ let SEED_PATH = 'host/vfs-seed.json'
845
+ const SNAPSHOT_PATH = 'host/vfs-snapshot.json'
846
846
 
847
847
  /**
848
848
  * The drive storage format, stamped into the seed and the snapshot. The markers
@@ -864,13 +864,18 @@ const DRIVE_FORMAT = 1
864
864
  */
865
865
  const loadMockConfig = async () => {
866
866
  try {
867
- const res = await fetch('/host/mock-config.json', { cache: 'no-store' })
867
+ const res = await fetch('host/mock-config.json', { cache: 'no-store' })
868
868
  if (!res.ok) return
869
869
  const cfg = await res.json()
870
870
  if (cfg && typeof cfg.appLoc === 'string' && cfg.appLoc.trim()) {
871
871
  APP_LOC = '/' + cfg.appLoc.replace(/^\/+/, '').replace(/\/+$/, '')
872
872
  DRIVE_APP = `${DRIVE_ROOT}/files${APP_LOC}`
873
873
  }
874
+ // the build names the seed it wrote; the default above covers a site built
875
+ // before the config carried it
876
+ if (cfg && typeof cfg.seedPath === 'string' && cfg.seedPath.trim()) {
877
+ SEED_PATH = cfg.seedPath
878
+ }
874
879
  } catch (e) {
875
880
  /* keep the defaults */
876
881
  }
@@ -887,7 +892,7 @@ let weboutSentinels = null
887
892
 
888
893
  const loadWeboutPrelude = async () => {
889
894
  try {
890
- const res = await fetch('/host/webout-prelude.json', { cache: 'no-store' })
895
+ const res = await fetch('host/webout-prelude.json', { cache: 'no-store' })
891
896
  if (!res.ok) return
892
897
  const cfg = await res.json()
893
898
  if (cfg && typeof cfg.template === 'string') {
@@ -1470,9 +1475,16 @@ const api = {
1470
1475
  // Called by the host with the persisted drive; skips seeding entirely.
1471
1476
  load: (files) => {
1472
1477
  started = true
1478
+ // The site's configuration and the webout() block are host files, not drive
1479
+ // contents, so a restore needs them exactly as a seed does: only the seed
1480
+ // used to load them, and every service written with webout() then failed
1481
+ // with "webout is not defined" for a visitor the drive had been restored
1482
+ // for. The drive itself is loaded synchronously, so callers that read it
1483
+ // straight away still can; the promise is what a worker awaits.
1484
+ const hostFiles = Promise.all([loadMockConfig(), loadWeboutPrelude()])
1473
1485
  const info = loadSnapshot(files)
1474
- ready = Promise.resolve(info)
1475
- if (resolveBoot) resolveBoot(info)
1486
+ ready = hostFiles.then(() => info)
1487
+ if (resolveBoot) resolveBoot(ready)
1476
1488
  return ready
1477
1489
  },
1478
1490
  seed: () => {
@@ -33,9 +33,9 @@
33
33
  * services are the package's; this file supplies the transport, the worker's own
34
34
  * activity, and the browser's answers to questions a server asks its process.
35
35
  */
36
- importScripts('/host/runtime-core.js')
37
- importScripts('/host/api-browser.js')
38
- importScripts('/host/lint-browser.js')
36
+ importScripts('host/runtime-core.js')
37
+ importScripts('host/api-browser.js')
38
+ importScripts('host/lint-browser.js')
39
39
 
40
40
  const R = self.SASjsMockRuntime
41
41
 
@@ -317,6 +317,21 @@ const tablesOf = (files) => {
317
317
 
318
318
  const RUNTIME = new BrowserRuntime()
319
319
 
320
+ /**
321
+ * The folder this site is served from.
322
+ *
323
+ * Empty at the origin root, `/demo` where a host publishes the demonstration
324
+ * site beside its own docs. The worker sits at `<base>/sw.js`, so its own
325
+ * location names the base and the route table can keep matching the paths the
326
+ * document states.
327
+ */
328
+ const BASE = (self.location.pathname || '').replace(/\/sw\.js.*$/, '')
329
+
330
+ const withoutBase = (pathname) =>
331
+ BASE && pathname.startsWith(BASE)
332
+ ? pathname.slice(BASE.length) || '/'
333
+ : pathname
334
+
320
335
  // ------------------------------------------------------------- host reports
321
336
  // The Admin routes ask a host what it is doing. A browser has no process, but it
322
337
  // does have a page, a heap and this worker's own activity, so the reports are
@@ -448,7 +463,7 @@ const registry = () => {
448
463
  registryPromise = (async () => {
449
464
  let appLoc
450
465
  try {
451
- const config = await (await fetch('/host/mock-config.json')).json()
466
+ const config = await (await fetch('host/mock-config.json')).json()
452
467
  appLoc = config && config.appLoc
453
468
  } catch (e) {
454
469
  appLoc = undefined
@@ -477,7 +492,7 @@ const contentVersion = (text) => {
477
492
 
478
493
  const homeService = API.createHomeService(async () => {
479
494
  try {
480
- const response = await fetch('/index.html', { cache: 'no-store' })
495
+ const response = await fetch('index.html', { cache: 'no-store' })
481
496
  if (!response.ok) return undefined
482
497
  const html = await response.text()
483
498
  return { html, version: contentVersion(html) }
@@ -860,7 +875,7 @@ const contentsResponse = (answer, path) => {
860
875
  }
861
876
 
862
877
  const routeRequest = async (request, url) => {
863
- const path = url.pathname.replace(/\/+$/, '') || '/'
878
+ const path = withoutBase(url.pathname).replace(/\/+$/, '') || '/'
864
879
  const query = {}
865
880
  for (const [k, v] of url.searchParams.entries()) query[k] = v
866
881
 
@@ -881,13 +896,14 @@ const routeRequest = async (request, url) => {
881
896
  // serve nothing at `/AppStream/<name>/` here, and a listing that named it
882
897
  // would offer a tile that 404s.
883
898
  // `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
899
+ // exactly that, so they are decided on the request's own pathname - minus
900
+ // the folder the site is served from, which the four patterns do not name.
901
+ const stream = withoutBase(url.pathname)
886
902
 
887
903
  if (/^\/AppStream$/i.test(stream))
888
904
  return new Response(null, {
889
905
  status: 301,
890
- headers: { location: '/AppStream/' }
906
+ headers: { location: `${BASE}/AppStream/` }
891
907
  })
892
908
 
893
909
  if (
@@ -1012,13 +1028,14 @@ const handle = async (request, url) => {
1012
1028
  self.addEventListener('fetch', (event) => {
1013
1029
  const url = new URL(event.request.url)
1014
1030
  if (url.origin !== self.location.origin) return
1031
+ const path = withoutBase(url.pathname)
1015
1032
 
1016
1033
  // The site's own page is answered by the home route, but a navigation must
1017
1034
  // never be at the API's mercy: if that route throws or answers an error, the
1018
1035
  // request falls back to the static host, which holds the same index.html. A
1019
1036
  // broken API route can cost a client its data; it must not cost the app its
1020
1037
  // page.
1021
- if (url.pathname === '/') {
1038
+ if (path === '/') {
1022
1039
  event.respondWith(
1023
1040
  (async () => {
1024
1041
  try {
@@ -1039,7 +1056,7 @@ self.addEventListener('fetch', (event) => {
1039
1056
  // JSON. `/AppStream` is one of those: the package serves app deployments, but
1040
1057
  // the listing is the server's web route, and an interface that lists apps asks
1041
1058
  // for it on every visit.
1042
- if (!/^\/(SASjsApi|SASLogon|AppStream)(\/|$)/i.test(url.pathname)) return
1059
+ if (!/^\/(SASjsApi|SASLogon|AppStream)(\/|$)/i.test(path)) return
1043
1060
  event.respondWith(handle(event.request, url))
1044
1061
  })
1045
1062
 
@@ -530,6 +530,30 @@ var EXECUTABLE_TYPES = [
530
530
  'text/javascript',
531
531
  'application/javascript'
532
532
  ];
533
+ /**
534
+ * The folder the site is published under, as a path with no trailing slash.
535
+ *
536
+ * Empty for the origin root, which is the ordinary case. A host that serves the
537
+ * demonstration site beside its documentation (a GitHub Pages site whose root is
538
+ * the docs) names the folder here, and every path the build emits follows it.
539
+ */
540
+ var normaliseBasePath = function (value) {
541
+ if (!value)
542
+ return '';
543
+ var base = String(value).trim();
544
+ if (!base.startsWith('/'))
545
+ base = '/' + base;
546
+ base = base.replace(/\/+$/, '');
547
+ var segments = base.split('/').slice(1);
548
+ if (!segments.length ||
549
+ segments.some(function (segment) {
550
+ return !/^[A-Za-z0-9._~-]+$/.test(segment) ||
551
+ segment === '.' ||
552
+ segment === '..';
553
+ }))
554
+ throw new Error("mockWebBasePath is not a folder path: ".concat(JSON.stringify(value), ". Use a path such as \"/demo\", or leave it unset for the origin root."));
555
+ return base;
556
+ };
533
557
  /**
534
558
  * Tells the interface that this page is answered by the browser host.
535
559
  *
@@ -566,12 +590,16 @@ var markBrowserHost = function (html, warnings) {
566
590
  * loader. Both changes are reported when the expected markup is absent, rather
567
591
  * than producing a site that silently talks to a real server.
568
592
  */
569
- var patchIndexHtml = function (html, version) {
593
+ var patchIndexHtml = function (html, version, basePath) {
594
+ if (basePath === void 0) { basePath = ''; }
570
595
  var warnings = [];
571
596
  var patched = html;
572
597
  patched = markBrowserHost(patched, warnings);
573
598
  if (/<sasjs[^>]*>/i.test(patched)) {
574
- patched = patched.replace(/(<sasjs[^>]*?)\sserverurl="[^"]*"/i, '$1 serverurl=""');
599
+ // the app talks to the origin root by default; on a site published under a
600
+ // folder it has to name that folder, or its API calls leave the worker's
601
+ // scope and meet the static host
602
+ patched = patched.replace(/(<sasjs[^>]*?)\sserverurl="[^"]*"/i, "$1 serverurl=\"".concat(basePath, "\""));
575
603
  patched = patched.replace(/(<sasjs[^>]*?)\sloginmechanism="[^"]*"/i, '$1 loginmechanism="Default"');
576
604
  }
577
605
  else {
@@ -646,11 +674,11 @@ var patchIndexHtml = function (html, version) {
646
674
  */
647
675
  function buildMockSite(target) {
648
676
  return __awaiter(this, void 0, void 0, function () {
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;
677
+ var projectDir, appLocRaw, appLoc, buildFolder, basePath, 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
678
  var e_8, _e;
651
- var _f, _g, _h, _j, _k;
652
- return __generator(this, function (_l) {
653
- switch (_l.label) {
679
+ var _f, _g, _h, _j, _k, _l;
680
+ return __generator(this, function (_m) {
681
+ switch (_m.label) {
654
682
  case 0:
655
683
  projectDir = process.projectDir || process.cwd();
656
684
  appLocRaw = target.appLoc;
@@ -662,23 +690,24 @@ function buildMockSite(target) {
662
690
  path_1.default.join(projectDir, 'sasjsbuild');
663
691
  return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
664
692
  case 1:
665
- if (!(_l.sent())) {
693
+ if (!(_m.sent())) {
666
694
  throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
667
695
  }
668
- webSourceRaw = (_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.webSourcePath;
696
+ basePath = normaliseBasePath((_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.mockWebBasePath);
697
+ webSourceRaw = (_g = target.streamConfig) === null || _g === void 0 ? void 0 : _g.webSourcePath;
669
698
  if (!webSourceRaw) {
670
699
  throw new Error('No built web app. Set streamConfig.webSourcePath on the target (for example ./client/dist), and build the frontend first.');
671
700
  }
672
701
  webFolder = path_1.default.resolve(projectDir, webSourceRaw);
673
702
  return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
674
703
  case 2:
675
- if (!(_l.sent())) {
704
+ if (!(_m.sent())) {
676
705
  throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
677
706
  }
678
707
  if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
679
708
  throw new Error("".concat(webFolder, " does not contain an index.html - is streamConfig.webSourcePath pointing at the built app?"));
680
709
  }
681
- outputFolder = ((_g = target.streamConfig) === null || _g === void 0 ? void 0 : _g.mockWebFolder)
710
+ outputFolder = ((_h = target.streamConfig) === null || _h === void 0 ? void 0 : _h.mockWebFolder)
682
711
  ? path_1.default.resolve(projectDir, target.streamConfig.mockWebFolder)
683
712
  : path_1.default.join(buildFolder, 'mock');
684
713
  assertSafeOutputFolder(outputFolder, projectDir, buildFolder, webFolder);
@@ -699,14 +728,14 @@ function buildMockSite(target) {
699
728
  // without a SAS licence and without a login. What they cannot do is run here,
700
729
  // so the build says so rather than letting a Run button be the surprise.
701
730
  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."));
731
+ (_j = process.logger) === null || _j === void 0 ? void 0 : _j.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
732
  }
704
733
  // No PROGRAMS is a different thing again: a compile that produced nothing, or
705
734
  // a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
706
735
  // behind that is not a program folder but does reach the drive, so the count
707
736
  // that decides this is of programs, not of files.
708
737
  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. ") +
738
+ (_k = process.logger) === null || _k === void 0 ? void 0 : _k.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
710
739
  'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
711
740
  }
712
741
  // The package supplies the drive rules the worker answers with, so an install
@@ -719,7 +748,7 @@ function buildMockSite(target) {
719
748
  'or check that @sasjs/api still publishes build/browser.js.');
720
749
  }
721
750
  if (skipped.length) {
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: ") +
751
+ (_l = process.logger) === null || _l === void 0 ? void 0 : _l.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: ") +
723
752
  "".concat(skipped
724
753
  .slice(0, 3)
725
754
  .map(function (file) {
@@ -766,7 +795,7 @@ function buildMockSite(target) {
766
795
  case 3:
767
796
  // 2. the linter, bundled from the installed packages so the code routes answer
768
797
  // with the rules this CLI resolved rather than a second implementation
769
- _l.sent();
798
+ _m.sent();
770
799
  // sw.js sits at the site root: a worker can only control the folder it is
771
800
  // served from, and the app has to be controlled from /
772
801
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -802,9 +831,9 @@ function buildMockSite(target) {
802
831
  swPath = path_1.default.join(outputFolder, 'sw.js');
803
832
  fs_1.default.writeFileSync(swPath, fs_1.default
804
833
  .readFileSync(swPath, 'utf8')
805
- .replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
806
- .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')"))
807
- .replace("importScripts('/host/lint-browser.js')", "importScripts('/host/lint-browser.js?v=".concat(hostVersion, "')")));
834
+ .replace("importScripts('host/runtime-core.js')", "importScripts('host/runtime-core.js?v=".concat(hostVersion, "')"))
835
+ .replace("importScripts('host/api-browser.js')", "importScripts('host/api-browser.js?v=".concat(hostVersion, "')"))
836
+ .replace("importScripts('host/lint-browser.js')", "importScripts('host/lint-browser.js?v=".concat(hostVersion, "')")));
808
837
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
809
838
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
810
839
  if (hasSnapshot) {
@@ -814,7 +843,7 @@ function buildMockSite(target) {
814
843
  fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'vfs-seed.json'), JSON.stringify({ format: exports.DRIVE_FORMAT, files: seed }));
815
844
  fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'mock-config.json'), JSON.stringify({
816
845
  appLoc: appLoc,
817
- seedPath: '/host/vfs-seed.json',
846
+ seedPath: 'host/vfs-seed.json',
818
847
  driveEntries: driveEntries,
819
848
  jsPrograms: jsPrograms,
820
849
  programFolders: PROGRAM_FOLDERS,
@@ -822,7 +851,7 @@ function buildMockSite(target) {
822
851
  builtAt: new Date().toISOString()
823
852
  }, null, 2));
824
853
  indexPath = path_1.default.join(outputFolder, 'index.html');
825
- _d = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion), patched = _d.patched, warnings = _d.warnings;
854
+ _d = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion, basePath), patched = _d.patched, warnings = _d.warnings;
826
855
  fs_1.default.writeFileSync(indexPath, patched);
827
856
  warnings.forEach(function (warning) { var _a; return (_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn(warning); });
828
857
  return [2 /*return*/, {
@@ -125,7 +125,10 @@ var loadBoot = function (specs, options) {
125
125
  stored[key] = value;
126
126
  }
127
127
  };
128
- global.location = { reload: function () { return timeline.push('reload'); } };
128
+ global.location = {
129
+ href: options.href || 'http://localhost:8123/',
130
+ reload: function () { return timeline.push('reload'); }
131
+ };
129
132
  jest.spyOn(console, 'error').mockImplementation(function () { });
130
133
  require('../assets/boot.js');
131
134
  var claim = function () {
@@ -162,7 +165,7 @@ describe('the boot loader', function () {
162
165
  delete global.sessionStorage;
163
166
  delete global.location;
164
167
  });
165
- it("registers the worker at the site root, at the build's version", function () { return __awaiter(void 0, void 0, void 0, function () {
168
+ it("registers the worker beside the page, at the build's version", function () { return __awaiter(void 0, void 0, void 0, function () {
166
169
  var registered;
167
170
  return __generator(this, function (_a) {
168
171
  switch (_a.label) {
@@ -170,15 +173,36 @@ describe('the boot loader', function () {
170
173
  registered = loadBoot([]).registered;
171
174
  return [4 /*yield*/, settle()
172
175
  // a cached sw.js would keep a returning visitor on the previous build's
173
- // worker, so the URL carries the host version and the check skips the cache
176
+ // worker, so the URL carries the host version and the check skips the cache.
177
+ // The scope is the page's own folder: the site is served from the origin
178
+ // root on one host and from /demo beside its docs on another.
174
179
  ];
175
180
  case 1:
176
181
  _a.sent();
177
182
  // a cached sw.js would keep a returning visitor on the previous build's
178
- // worker, so the URL carries the host version and the check skips the cache
183
+ // worker, so the URL carries the host version and the check skips the cache.
184
+ // The scope is the page's own folder: the site is served from the origin
185
+ // root on one host and from /demo beside its docs on another.
179
186
  expect(registered).toEqual([
180
- '/sw.js?v=abc123 ' +
181
- JSON.stringify({ scope: '/', updateViaCache: 'none' })
187
+ 'http://localhost:8123/sw.js?v=abc123 ' +
188
+ JSON.stringify({ scope: './', updateViaCache: 'none' })
189
+ ]);
190
+ return [2 /*return*/];
191
+ }
192
+ });
193
+ }); });
194
+ it('registers the worker under the folder the page is served from', function () { return __awaiter(void 0, void 0, void 0, function () {
195
+ var registered;
196
+ return __generator(this, function (_a) {
197
+ switch (_a.label) {
198
+ case 0:
199
+ registered = loadBoot([], { href: 'http://localhost:8123/demo/' }).registered;
200
+ return [4 /*yield*/, settle()];
201
+ case 1:
202
+ _a.sent();
203
+ expect(registered).toEqual([
204
+ 'http://localhost:8123/demo/sw.js?v=abc123 ' +
205
+ JSON.stringify({ scope: './', updateViaCache: 'none' })
182
206
  ]);
183
207
  return [2 /*return*/];
184
208
  }
@@ -328,7 +352,7 @@ describe('the boot loader', function () {
328
352
  return [4 /*yield*/, settle()];
329
353
  case 1:
330
354
  _a.sent();
331
- expect(registered[0]).toMatch(/^\/sw\.js /);
355
+ expect(registered[0]).toMatch(/\/sw\.js \{/);
332
356
  return [2 /*return*/];
333
357
  }
334
358
  });
@@ -447,7 +471,10 @@ describe('the boot loader', function () {
447
471
  global.navigator = {};
448
472
  global.window = {};
449
473
  global.sessionStorage = { getItem: function () { return null; }, setItem: function () { } };
450
- global.location = { reload: function () { } };
474
+ global.location = {
475
+ href: 'http://localhost:8123/',
476
+ reload: function () { }
477
+ };
451
478
  jest.spyOn(console, 'error').mockImplementation(function () { });
452
479
  require('../assets/boot.js');
453
480
  return [4 /*yield*/, settle()];
@@ -208,9 +208,9 @@ describe('browser-hosted build', function () {
208
208
  sw = fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8');
209
209
  // both imports carry the host version: a worker caches what it imports, so
210
210
  // a rebuild that changes either file has to change the URL
211
- expect(sw).toMatch(/importScripts\('\/host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
212
- expect(sw).toMatch(/importScripts\('\/host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
213
- expect(sw).toMatch(/importScripts\('\/host\/lint-browser\.js\?v=[0-9a-f]{10}'\)/);
211
+ expect(sw).toMatch(/importScripts\('host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
212
+ expect(sw).toMatch(/importScripts\('host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
213
+ expect(sw).toMatch(/importScripts\('host\/lint-browser\.js\?v=[0-9a-f]{10}'\)/);
214
214
  expect(sw).not.toContain("importScripts('/host/runtime-core.js')");
215
215
  return [2 /*return*/];
216
216
  }
@@ -366,6 +366,35 @@ describe('browser-hosted build', function () {
366
366
  }
367
367
  });
368
368
  }); });
369
+ it('serves the site under a folder when the target names one', function () { return __awaiter(void 0, void 0, void 0, function () {
370
+ var html;
371
+ return __generator(this, function (_a) {
372
+ switch (_a.label) {
373
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockWebBasePath: '/demo' }))];
374
+ case 1:
375
+ _a.sent();
376
+ html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
377
+ expect(html).toContain('serverurl="/demo"');
378
+ // the loader and the host's files stay relative to the page, so the same
379
+ // build works at the origin root and under the folder
380
+ expect(html).toContain('<script src="host/boot.js?v=');
381
+ return [2 /*return*/];
382
+ }
383
+ });
384
+ }); });
385
+ it('refuses a base path that is not a folder path', function () { return __awaiter(void 0, void 0, void 0, function () {
386
+ return __generator(this, function (_a) {
387
+ switch (_a.label) {
388
+ case 0: return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target({ mockWebBasePath: '/demo?x=1' }))).rejects.toThrow(/mockWebBasePath/)];
389
+ case 1:
390
+ _a.sent();
391
+ return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target({ mockWebBasePath: '../escape' }))).rejects.toThrow(/mockWebBasePath/)];
392
+ case 2:
393
+ _a.sent();
394
+ return [2 /*return*/];
395
+ }
396
+ });
397
+ }); });
369
398
  it('points the app at itself and installs the boot loader', function () { return __awaiter(void 0, void 0, void 0, function () {
370
399
  var html;
371
400
  return __generator(this, function (_a) {
@@ -66,6 +66,19 @@ var jsonResponse = function (body) { return ({
66
66
  return [2 /*return*/, body];
67
67
  }); }); }
68
68
  }); };
69
+ /** The webout() block the build ships, with its sentinels still in place. */
70
+ var weboutPrelude = function () { return ({
71
+ sentinels: mockSite_1.WEBOUT_SENTINELS,
72
+ template: (0, utils_1.getWeboutPrelude)({
73
+ weboutPath: mockSite_1.WEBOUT_SENTINELS.weboutPath,
74
+ headersPath: mockSite_1.WEBOUT_SENTINELS.headersPath,
75
+ program: mockSite_1.WEBOUT_SENTINELS.program,
76
+ debug: mockSite_1.WEBOUT_SENTINELS.debug,
77
+ username: mockSite_1.WEBOUT_SENTINELS.username,
78
+ userId: mockSite_1.WEBOUT_SENTINELS.userId,
79
+ apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
80
+ })
81
+ }); };
69
82
  var seedDrive = function (runtime, files, prelude) { return __awaiter(void 0, void 0, void 0, function () {
70
83
  return __generator(this, function (_a) {
71
84
  switch (_a.label) {
@@ -151,18 +164,7 @@ describe('browser runtime', function () {
151
164
  // values runtime-core fills per run. Without it a service written with
152
165
  // webout() fails in a mock build with "webout is not defined", which is the
153
166
  // gap this closes.
154
- return [4 /*yield*/, seedDrive(runtime, {}, {
155
- sentinels: mockSite_1.WEBOUT_SENTINELS,
156
- template: (0, utils_1.getWeboutPrelude)({
157
- weboutPath: mockSite_1.WEBOUT_SENTINELS.weboutPath,
158
- headersPath: mockSite_1.WEBOUT_SENTINELS.headersPath,
159
- program: mockSite_1.WEBOUT_SENTINELS.program,
160
- debug: mockSite_1.WEBOUT_SENTINELS.debug,
161
- username: mockSite_1.WEBOUT_SENTINELS.username,
162
- userId: mockSite_1.WEBOUT_SENTINELS.userId,
163
- apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
164
- })
165
- })];
167
+ return [4 /*yield*/, seedDrive(runtime, {}, weboutPrelude())];
166
168
  case 1:
167
169
  // A mock build ships the same block the server generates, with sentinel
168
170
  // values runtime-core fills per run. Without it a service written with
@@ -189,6 +191,43 @@ describe('browser runtime', function () {
189
191
  }
190
192
  });
191
193
  }); });
194
+ it('restores a persisted drive with the host files, so webout() still runs', function () { return __awaiter(void 0, void 0, void 0, function () {
195
+ var runtime, result, byName;
196
+ var _a;
197
+ return __generator(this, function (_b) {
198
+ switch (_b.label) {
199
+ case 0:
200
+ runtime = loadRuntime();
201
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
202
+ return __generator(this, function (_a) {
203
+ if (url.includes('mock-config'))
204
+ return [2 /*return*/, jsonResponse({ appLoc: appLoc })];
205
+ if (url.includes('webout-prelude'))
206
+ return [2 /*return*/, jsonResponse(weboutPrelude())];
207
+ return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
208
+ return [2 /*return*/, ({})];
209
+ }); }); } }];
210
+ });
211
+ }); });
212
+ return [4 /*yield*/, runtime.load((_a = {},
213
+ _a[drivePath('services/public/svc.js')] = "\nwebout('OPEN')\nwebout('OBJ', 'users', [{ name: 'ann' }])\nwebout('CLOSE')",
214
+ _a))
215
+ // the first failure on a restored drive was this one: the block is a host
216
+ // file, so webout() was not defined at all
217
+ ];
218
+ case 1:
219
+ _b.sent();
220
+ result = runtime.run({
221
+ program: "".concat(appLoc, "/services/public/svc.js")
222
+ });
223
+ expect(result.error).toBeNull();
224
+ expect(JSON.parse(result.webout).users).toEqual([{ name: 'ann' }]);
225
+ byName = runtime.run({ program: 'public/svc' });
226
+ expect(byName.error).toBeNull();
227
+ return [2 /*return*/];
228
+ }
229
+ });
230
+ }); });
192
231
  it('seeds the headers the way a JS session does, for a service that only assigns _webout', function () { return __awaiter(void 0, void 0, void 0, function () {
193
232
  var runtime, result;
194
233
  return __generator(this, function (_a) {
@@ -102,12 +102,14 @@ var lintBundle = function () {
102
102
  }
103
103
  return lintBundlePath;
104
104
  };
105
- var loadWorker = function () {
105
+ var loadWorker = function (base) {
106
+ if (base === void 0) { base = ''; }
106
107
  jest.resetModules();
107
108
  var handlers = {};
108
109
  var refusingWrites = false;
109
110
  var self = {
110
- location: { origin: origin },
111
+ // the worker's own location names the folder the site is served from
112
+ location: { origin: origin, pathname: "".concat(base, "/sw.js") },
111
113
  addEventListener: function (type, fn) {
112
114
  handlers[type] = handlers[type] || [];
113
115
  handlers[type].push(fn);
@@ -890,6 +892,58 @@ describe('service worker host', function () {
890
892
  }
891
893
  });
892
894
  }); });
895
+ it('answers every route under the folder the site is published in', function () { return __awaiter(void 0, void 0, void 0, function () {
896
+ var worker, info, _a, page, folder, stream, landing, apps, _b;
897
+ var _c;
898
+ return __generator(this, function (_d) {
899
+ switch (_d.label) {
900
+ case 0:
901
+ worker = loadWorker('/demo');
902
+ worker.runtime.load((_c = {},
903
+ _c[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
904
+ _c));
905
+ return [4 /*yield*/, worker.dispatch('/demo/SASjsApi/info')];
906
+ case 1:
907
+ info = _d.sent();
908
+ expect(info.status).toBe(200);
909
+ _a = expect;
910
+ return [4 /*yield*/, info.json()];
911
+ case 2:
912
+ _a.apply(void 0, [(_d.sent()).mode]).toBe('desktop');
913
+ return [4 /*yield*/, worker.dispatch('/demo/')];
914
+ case 3:
915
+ page = _d.sent();
916
+ expect(page.status).toBe(200);
917
+ expect(page.headers.get('content-type')).toContain('text/html');
918
+ return [4 /*yield*/, worker.dispatch('/demo')];
919
+ case 4:
920
+ folder = _d.sent();
921
+ expect(folder.status).toBe(200);
922
+ expect(folder.headers.get('content-type')).toContain('text/html');
923
+ return [4 /*yield*/, worker.dispatch('/demo/AppStream')];
924
+ case 5:
925
+ stream = _d.sent();
926
+ expect(stream.status).toBe(301);
927
+ expect(stream.headers.get('location')).toBe('/demo/AppStream/');
928
+ return [4 /*yield*/, worker.dispatch('/demo/AppStream/')];
929
+ case 6:
930
+ landing = _d.sent();
931
+ expect(landing.status).toBe(200);
932
+ expect(landing.headers.get('content-type')).toContain('text/html');
933
+ return [4 /*yield*/, worker.dispatch('/demo/AppStream/apps.json')];
934
+ case 7:
935
+ apps = _d.sent();
936
+ expect(apps.status).toBe(200);
937
+ _b = expect;
938
+ return [4 /*yield*/, apps.json()];
939
+ case 8:
940
+ _b.apply(void 0, [_d.sent()]).toEqual({ apps: [] });
941
+ // a path outside the folder is not the site's to answer
942
+ expect(worker.dispatch('/other/SASjsApi/info')).toBeUndefined();
943
+ return [2 /*return*/];
944
+ }
945
+ });
946
+ }); });
893
947
  it('serves the build page at the home route, and falls back when it fails', function () { return __awaiter(void 0, void 0, void 0, function () {
894
948
  var worker, response, _a, runtime, original, fallback, body;
895
949
  return __generator(this, function (_b) {
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.18.0"
128
+ "version": "6.19.1"
129
129
  }