@sasjs/cli 6.30.0 → 6.30.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.
@@ -37,7 +37,6 @@
37
37
  */
38
38
  importScripts('host/runtime-core.js')
39
39
  importScripts('host/api-browser.js')
40
- importScripts('host/lint-browser.js')
41
40
 
42
41
  const R = self.SASjsMockRuntime
43
42
 
@@ -47,10 +46,6 @@ const R = self.SASjsMockRuntime
47
46
  // and the few capabilities a static origin does not have, which the package
48
47
  // reports as a 501 naming the missing service.
49
48
  const API = self.SASjsApi
50
- // The linter, bundled at build time from the installed @sasjs/api/code and
51
- // @sasjs/lint (see mockSite.ts). It is a separate script because the linter is
52
- // not part of the root package a service worker can import.
53
- const CODE = self.SASjsCode
54
49
 
55
50
  const DRIVE_STORE = R.driveStore
56
51
 
@@ -968,6 +963,53 @@ const homeService = API.createHomeService(async () => {
968
963
  */
969
964
  const LINT_CONFIG_FILE = '.sasjslint'
970
965
 
966
+ /**
967
+ * The linter, bundled at build time from the installed @sasjs/api/code and
968
+ * @sasjs/lint (see mockSite.ts).
969
+ *
970
+ * Imported on the first code route rather than by the worker at boot: it is
971
+ * around 40KB of the worker's payload for two routes (`code/lint` and
972
+ * `code/format`), and the install has to finish before the page may make its
973
+ * first request - so every visit pays it, including the ones that never open the
974
+ * editor. The build stamps the version into the URL below, so the copy fetched
975
+ * here is cache-busted with the rest of the host.
976
+ */
977
+ let codeService
978
+ let linter
979
+
980
+ const code = () => {
981
+ if (codeService) return codeService
982
+ importScripts('host/lint-browser.js')
983
+ linter = self.SASjsCode
984
+ codeService = linter.createCodeService({ lintConfig })
985
+ return codeService
986
+ }
987
+
988
+ /**
989
+ * The package's code service, resolved on first use.
990
+ *
991
+ * A proxy rather than an object listing `lint` and `format`: the package decides
992
+ * which methods a service has, and a facade would answer `undefined` for one a
993
+ * later version adds. Dispatch reads `services.code` once per request and only
994
+ * touches a method on a code route, so nothing below the first method access
995
+ * loads the linter.
996
+ */
997
+ const lazyCode = () =>
998
+ new Proxy(
999
+ {},
1000
+ {
1001
+ get: (target, property) => {
1002
+ // probing for a promise is not a method lookup: answering it must not
1003
+ // pull 40KB
1004
+ if (typeof property === 'symbol' || property === 'then')
1005
+ return undefined
1006
+ const service = code()
1007
+ const value = service[property]
1008
+ return typeof value === 'function' ? value.bind(service) : value
1009
+ }
1010
+ }
1011
+ )
1012
+
971
1013
  const lintConfig = async (filePath) => {
972
1014
  // A path that escapes the drive has no folder to walk from, and a buffer that
973
1015
  // names no file has none either: both take the drive root's rules, which is
@@ -977,9 +1019,14 @@ const lintConfig = async (filePath) => {
977
1019
  for (;;) {
978
1020
  const configPath =
979
1021
  folder === '/' ? `/${LINT_CONFIG_FILE}` : `${folder}/${LINT_CONFIG_FILE}`
980
- const contents = await DRIVE_STORE.read(configPath).catch(() => null)
1022
+ const contents = await DRIVE_STORE.read(configPath).catch((e) => {
1023
+ // a piece that is still on the site is not a folder with no rules file:
1024
+ // swallowing it here would lint against the defaults and never fetch it
1025
+ if (e && e.code === R.PENDING_CODE) throw e
1026
+ return null
1027
+ })
981
1028
  const parsed = parseLintConfig(contents)
982
- if (parsed) return new CODE.LintConfig(parsed)
1029
+ if (parsed) return new linter.LintConfig(parsed)
983
1030
  if (folder === '/') return undefined
984
1031
  folder = API.dirname(folder)
985
1032
  }
@@ -1026,7 +1073,7 @@ const SERVICES = {
1026
1073
  // own rules: a buffer is linted and formatted against the nearest
1027
1074
  // `.sasjslint` at or above its folder, so editing that file changes the
1028
1075
  // editor here exactly as it does on a server
1029
- code: CODE.createCodeService({ lintConfig })
1076
+ code: lazyCode()
1030
1077
  }
1031
1078
 
1032
1079
  /** The routes this host performs itself, because the answer is not JSON. */
@@ -1355,6 +1402,15 @@ const subjectOf = async (request, query) => {
1355
1402
  * `_debug` means - is still the package's decision.
1356
1403
  */
1357
1404
  const runRoute = async (routeId, payload, subject) => {
1405
+ // A program may read any file it likes - the drive is its filesystem - so
1406
+ // every piece of a sharded drive is fetched here rather than guessed from the
1407
+ // request. That is deliberately the whole drive and not just the app being
1408
+ // run: replaying a program because a read failed would run its writes twice,
1409
+ // so the safe choice costs the download of every other app's bundle the first
1410
+ // time anything runs on a cold visit. A page that only lists apps never pays
1411
+ // it; the saving this change is for is on that page.
1412
+ await R.ensureAll()
1413
+
1358
1414
  const query = payload.query
1359
1415
  const body = payload.body
1360
1416
  const tables = payload.files.map((file) => ({
@@ -1446,7 +1502,38 @@ const contentsResponse = (answer, path) => {
1446
1502
  })
1447
1503
  }
1448
1504
 
1449
- const routeRequest = async (request, url) => {
1505
+ /**
1506
+ * The shards a request names, fetched before it is answered.
1507
+ *
1508
+ * The drive of a sharded site is not in memory: the build splits a capture so a
1509
+ * first page does not download every app's bundle, and a route that reads a path
1510
+ * has to ask for the piece holding it. The two query parameters below are where
1511
+ * a request names a path outright, which is the common case and the cheap one -
1512
+ * whatever a route resolves from the drive itself is left to the retry in
1513
+ * `routeRequest`, which knows the path from the read that failed rather than
1514
+ * guessing at it from the URL.
1515
+ */
1516
+ const ensureForRequest = async (url) => {
1517
+ const paths = []
1518
+ for (const name of ['_filePath', '_program']) {
1519
+ const value = url.searchParams.get(name)
1520
+ if (value) paths.push(value)
1521
+ }
1522
+ if (paths.length) await R.ensurePaths(paths)
1523
+ }
1524
+
1525
+ /**
1526
+ * One attempt at a request, with the body source a retry can copy again.
1527
+ *
1528
+ * A Request's body can only be read once, and a retry (see the catch below) is a
1529
+ * second read of the same request - so the first attempt keeps an unread copy
1530
+ * beside it and every attempt after that is made from a copy of the copy. Only a
1531
+ * request with a body needs one, and `source` is what carries it through the
1532
+ * recursion.
1533
+ */
1534
+ const routeRequest = async (request, url, attempts = 0, source = null) => {
1535
+ const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
1536
+ const replay = hasBody ? source || request.clone() : null
1450
1537
  const path = withoutBase(url.pathname).replace(/\/+$/, '') || '/'
1451
1538
  // `/api` is the short spelling of the API, and `/SASjsApi` the canonical one:
1452
1539
  // both are answered, so the alias is stripped once here and everything below -
@@ -1462,6 +1549,7 @@ const routeRequest = async (request, url) => {
1462
1549
  // (a static asset fetch raced by an API call) must not answer as nobody
1463
1550
  await ensureIdentity()
1464
1551
  await R.ensureReady()
1552
+ await ensureForRequest(url)
1465
1553
 
1466
1554
  // The app stream is the server's own web surface rather than part of the
1467
1555
  // API: the listing, the landing page and the redirect from one to the other
@@ -1577,6 +1665,20 @@ const routeRequest = async (request, url) => {
1577
1665
  )
1578
1666
  return json(answer.body, answer.status)
1579
1667
  } catch (e) {
1668
+ // A read of a file that is still on the site: the route reached a path the
1669
+ // request did not name - an app asset, a `.sasjslint` above the edited file,
1670
+ // a join a service resolves from the drive. The failure names the path, so
1671
+ // the first attempt fetches exactly that piece; a second one in the same
1672
+ // request means it wants more than one, so the rest of the drive comes in.
1673
+ // Bounded, because a refusal that keeps repeating is a real one.
1674
+ if (e && e.code === R.PENDING_CODE && attempts < 2) {
1675
+ if (!attempts && e.path) await R.ensurePaths([e.path])
1676
+ else await R.ensureAll()
1677
+ // a fresh copy per attempt: the one this attempt read is spent
1678
+ const next = replay ? replay.clone() : request
1679
+ return await routeRequest(next, url, attempts + 1, replay)
1680
+ }
1681
+
1580
1682
  const message = (e && e.stack) || String(e)
1581
1683
  const status = (e && e.status) || 500
1582
1684
  const refusal = status >= 400 && status < 500
@@ -1681,7 +1783,7 @@ self.addEventListener('message', (event) => {
1681
1783
  case 'sasjs-load':
1682
1784
  event.waitUntil(
1683
1785
  bootIdentity(data)
1684
- .then(() => R.load(data.files || {}))
1786
+ .then(() => R.load(data.files || {}, data.deleted || []))
1685
1787
  .then(async () => {
1686
1788
  await persistChanges()
1687
1789
  reply({
@@ -1844,7 +1946,22 @@ self.addEventListener('message', (event) => {
1844
1946
  }
1845
1947
 
1846
1948
  case 'sasjs-snapshot':
1847
- reply({ type: 'sasjs-snapshot', files: R.dumpAll() })
1949
+ // a sharded drive is not all in memory, and the caller is exporting it -
1950
+ // for persistence after a change, or as `mock-snapshot.json` for the next
1951
+ // build. Whole means whole: the pieces nobody read are fetched first, or
1952
+ // the export would quietly lose every app bundle it had not opened.
1953
+ event.waitUntil(
1954
+ R.ensureReady()
1955
+ .then(() => R.ensureAll())
1956
+ .then(() => reply({ type: 'sasjs-snapshot', files: R.dumpAll() }))
1957
+ .catch((e) =>
1958
+ reply({
1959
+ type: 'sasjs-snapshot',
1960
+ files: R.dumpAll(),
1961
+ error: (e && e.message) || String(e)
1962
+ })
1963
+ )
1964
+ )
1848
1965
  break
1849
1966
 
1850
1967
  case 'sasjs-stats':
@@ -9,6 +9,11 @@ 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
+ /**
13
+ * How many pieces the captured drive ships in: the boot shard plus one per
14
+ * large folder or file. Zero when the site ships no snapshot.
15
+ */
16
+ snapshotShards: number;
12
17
  /** True when a recorded SAS session was shipped, for .sas runs to answer. */
13
18
  sasTranscript: boolean;
14
19
  /**
@@ -64,6 +69,24 @@ export declare const MAX_SEED_BYTES: number;
64
69
  * marker format changes.
65
70
  */
66
71
  export declare const DRIVE_FORMAT = 1;
72
+ /**
73
+ * The size at which a captured file is fetched on its own rather than with its
74
+ * folder.
75
+ *
76
+ * A shard is a download the visitor waits for, and an app's folder holds both
77
+ * its bundles and the small files a listing shows - its logo, a config. Splitting
78
+ * on size keeps one from dragging the other: the tile asks for the folder's small
79
+ * files, the app's own page asks for the bundle.
80
+ */
81
+ export declare const SHARD_FILE_BYTES: number;
82
+ /**
83
+ * The size below which a folder's files ride in the boot shard.
84
+ *
85
+ * The boot shard is fetched before the first page renders, so it is paid by every
86
+ * visit. Folders under this size are cheaper there than as a request of their
87
+ * own; folders over it are only fetched when something reads them.
88
+ */
89
+ export declare const SHARD_GROUP_BYTES: number;
67
90
  /**
68
91
  * Builds a static site that runs the project's JS services (its mocks) in the
69
92
  * browser, so a frontend can be tested with no server and no SAS.
@@ -79,7 +79,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
79
79
  return (mod && mod.__esModule) ? mod : { "default": mod };
80
80
  };
81
81
  Object.defineProperty(exports, "__esModule", { value: true });
82
- exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.buildLintBundle = exports.WEBOUT_SENTINELS = void 0;
82
+ exports.SHARD_GROUP_BYTES = exports.SHARD_FILE_BYTES = exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.buildLintBundle = exports.WEBOUT_SENTINELS = void 0;
83
83
  exports.buildMockSite = buildMockSite;
84
84
  var crypto_1 = require("crypto");
85
85
  var fs_1 = __importDefault(require("fs"));
@@ -512,6 +512,137 @@ var countPrograms = function (files, extension) {
512
512
  return inProgramFolder && key.toLowerCase().endsWith(extension);
513
513
  }).length;
514
514
  };
515
+ /**
516
+ * The size at which a captured file is fetched on its own rather than with its
517
+ * folder.
518
+ *
519
+ * A shard is a download the visitor waits for, and an app's folder holds both
520
+ * its bundles and the small files a listing shows - its logo, a config. Splitting
521
+ * on size keeps one from dragging the other: the tile asks for the folder's small
522
+ * files, the app's own page asks for the bundle.
523
+ */
524
+ exports.SHARD_FILE_BYTES = 256 * 1024;
525
+ /**
526
+ * The size below which a folder's files ride in the boot shard.
527
+ *
528
+ * The boot shard is fetched before the first page renders, so it is paid by every
529
+ * visit. Folders under this size are cheaper there than as a request of their
530
+ * own; folders over it are only fetched when something reads them.
531
+ */
532
+ exports.SHARD_GROUP_BYTES = 64 * 1024;
533
+ /** The folder a captured path is grouped under: its first three segments. */
534
+ var shardFolderFor = function (filePath) {
535
+ var prefix = '/sasjs/drive/files/';
536
+ if (!filePath.startsWith(prefix))
537
+ return path_1.default.dirname(filePath);
538
+ var parts = filePath.slice(prefix.length).split('/');
539
+ // a file at the drive root groups with the other root files, which is where
540
+ // the index, the linter rules and a project's configs live
541
+ return (prefix +
542
+ parts
543
+ .slice(0, Math.max(0, parts.length - 1))
544
+ .slice(0, 3)
545
+ .join('/'));
546
+ };
547
+ /**
548
+ * Splits a captured drive into a boot shard and the pieces fetched on demand.
549
+ *
550
+ * A site that ships its whole drive as one file makes every visitor download all
551
+ * of it before the first page renders - on a demonstration site that is every
552
+ * app's bundle, behind a list of tiles that names them. The split is by what a
553
+ * page reads, not by what the drive holds:
554
+ *
555
+ * - a file at or over `SHARD_FILE_BYTES` gets a shard of its own;
556
+ * - the rest are grouped by folder, and a folder under `SHARD_GROUP_BYTES` joins
557
+ * the boot shard (id 0), so no visit pays a round trip for a logo;
558
+ * - everything over it becomes a shard, fetched when a path in it is read.
559
+ *
560
+ * The index carries every path with its shard and byte count, so a listing, a
561
+ * stat and an existence check answer for the whole drive with nothing fetched.
562
+ */
563
+ var shardSnapshot = function (files) {
564
+ var e_7, _a, e_8, _b, _c;
565
+ var paths = Object.keys(files).sort();
566
+ var bytesOf = function (filePath) {
567
+ return Buffer.byteLength(String(files[filePath]));
568
+ };
569
+ var groups = new Map();
570
+ try {
571
+ for (var paths_1 = __values(paths), paths_1_1 = paths_1.next(); !paths_1_1.done; paths_1_1 = paths_1.next()) {
572
+ var filePath = paths_1_1.value;
573
+ var big = bytesOf(filePath) >= exports.SHARD_FILE_BYTES;
574
+ var key = big ? "file:".concat(filePath) : "dir:".concat(shardFolderFor(filePath));
575
+ groups.set(key, (groups.get(key) || []).concat(filePath));
576
+ }
577
+ }
578
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
579
+ finally {
580
+ try {
581
+ if (paths_1_1 && !paths_1_1.done && (_a = paths_1.return)) _a.call(paths_1);
582
+ }
583
+ finally { if (e_7) throw e_7.error; }
584
+ }
585
+ var shards = [{ paths: [], bytes: 0 }];
586
+ try {
587
+ for (var _d = __values(__spreadArray([], __read(groups.keys()), false).sort()), _e = _d.next(); !_e.done; _e = _d.next()) {
588
+ var key = _e.value;
589
+ var members = groups.get(key);
590
+ var bytes = members.reduce(function (total, filePath) { return total + bytesOf(filePath); }, 0);
591
+ var ridesInBoot = key.startsWith('dir:') && bytes <= exports.SHARD_GROUP_BYTES;
592
+ if (ridesInBoot) {
593
+ (_c = shards[0].paths).push.apply(_c, __spreadArray([], __read(members), false));
594
+ shards[0].bytes += bytes;
595
+ continue;
596
+ }
597
+ shards.push({ paths: members, bytes: bytes });
598
+ }
599
+ }
600
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
601
+ finally {
602
+ try {
603
+ if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
604
+ }
605
+ finally { if (e_8) throw e_8.error; }
606
+ }
607
+ var entries = {};
608
+ shards.forEach(function (shard, id) {
609
+ var e_9, _a;
610
+ try {
611
+ for (var _b = __values(shard.paths), _c = _b.next(); !_c.done; _c = _b.next()) {
612
+ var filePath = _c.value;
613
+ entries[filePath] = { shard: id, bytes: bytesOf(filePath) };
614
+ }
615
+ }
616
+ catch (e_9_1) { e_9 = { error: e_9_1 }; }
617
+ finally {
618
+ try {
619
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
620
+ }
621
+ finally { if (e_9) throw e_9.error; }
622
+ }
623
+ });
624
+ return {
625
+ index: {
626
+ format: exports.DRIVE_FORMAT,
627
+ sharded: true,
628
+ boot: 0,
629
+ files: paths.length,
630
+ entries: entries,
631
+ shards: shards.map(function (shard, id) { return ({
632
+ id: id,
633
+ file: "host/vfs-snapshot-".concat(id, ".json"),
634
+ bytes: shard.bytes,
635
+ files: shard.paths.length
636
+ }); })
637
+ },
638
+ shards: shards.map(function (shard, id) { return ({
639
+ id: id,
640
+ file: "vfs-snapshot-".concat(id, ".json"),
641
+ bytes: shard.bytes,
642
+ files: Object.fromEntries(shard.paths.map(function (filePath) { return [filePath, files[filePath]]; }))
643
+ }); })
644
+ };
645
+ };
515
646
  /** The script-tag attributes that change how an app's script runs. */
516
647
  var REPLAYED_ATTRIBUTES = {
517
648
  type: 'type',
@@ -520,6 +651,20 @@ var REPLAYED_ATTRIBUTES = {
520
651
  integrity: 'integrity',
521
652
  nomodule: 'noModule'
522
653
  };
654
+ /**
655
+ * Script types a preload may name.
656
+ *
657
+ * The loader only re-runs a tag once the worker controls the page, so without a
658
+ * hint the app's bundle is not requested until the install has finished - the
659
+ * largest asset on the site behind the largest prerequisite. A preload link
660
+ * executes nothing, so the download can start while the worker is still
661
+ * installing.
662
+ *
663
+ * `module` is excluded on purpose: a module is fetched with different
664
+ * credentials semantics from a classic script, so `as="script"` does not match
665
+ * the later tag and the file is fetched twice - slower than no hint at all.
666
+ */
667
+ var PRELOADABLE_TYPES = ['', 'text/javascript', 'application/javascript'];
523
668
  var parseAttributes = function (source) {
524
669
  var _a, _b, _c;
525
670
  var attributes = {};
@@ -538,6 +683,45 @@ var escapeAttribute = function (value) {
538
683
  .replace(/</g, '&lt;')
539
684
  .replace(/>/g, '&gt;');
540
685
  };
686
+ /** Puts markup at the end of the head, or leaves the page alone when it has none. */
687
+ var insertIntoHead = function (html, markup) {
688
+ var head = /<head(\s[^>]*)?>/i.exec(html);
689
+ if (!head)
690
+ return html;
691
+ var start = head.index + head[0].length;
692
+ var close = html.toLowerCase().indexOf('</head>', start);
693
+ var at = close < 0 ? start : close;
694
+ return html.slice(0, at) + markup + html.slice(at);
695
+ };
696
+ /**
697
+ * Hints the app scripts the loader will replay, so their download starts while
698
+ * the worker installs rather than after it.
699
+ *
700
+ * Only scripts this build owns are hinted - the file has to be in the output for
701
+ * the version query to be meaningful - and only classic ones. A `type="module"`
702
+ * tag is fetched with different credentials semantics, so an `as="script"`
703
+ * preload does not match it; and a cross-origin src would need `crossorigin` on
704
+ * the hint to match its tag. Either mismatch fetches the file twice, which is
705
+ * worse than no hint.
706
+ */
707
+ var preloadHints = function (scripts, outputFolder) {
708
+ return scripts
709
+ .filter(function (script) {
710
+ if (!script.src)
711
+ return false;
712
+ if (!PRELOADABLE_TYPES.includes((script.type || '').trim().toLowerCase()))
713
+ return false;
714
+ if (script.crossOrigin)
715
+ return false;
716
+ if (/^([a-z]+:)?\/\//i.test(script.src))
717
+ return false;
718
+ if (!outputFolder)
719
+ return true;
720
+ var pathname = script.src.split('?')[0];
721
+ return fs_1.default.existsSync(path_1.default.join(outputFolder, pathname.replace(/^(\.\/|\/)/, '')));
722
+ })
723
+ .map(function (script) { return "<link rel=\"preload\" as=\"script\" href=\"".concat(script.src, "\" />"); });
724
+ };
541
725
  /**
542
726
  * The boot loader is a FILE rather than an inline script: an app with a strict
543
727
  * Content-Security-Policy (`default-src 'self'`, no `unsafe-inline`) refuses an
@@ -714,7 +898,7 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
714
898
  var early = [];
715
899
  var tagPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>[ \t]*\r?\n?/gi;
716
900
  patched = patched.replace(tagPattern, function (tag, source, body) {
717
- var e_7, _a;
901
+ var e_10, _a;
718
902
  var attributes = parseAttributes(source);
719
903
  var type = (attributes.type || '').trim().toLowerCase();
720
904
  var inlined = !attributes.src;
@@ -753,12 +937,12 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
753
937
  spec[name_2] = attribute === 'nomodule' ? 'true' : attributes[attribute];
754
938
  }
755
939
  }
756
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
940
+ catch (e_10_1) { e_10 = { error: e_10_1 }; }
757
941
  finally {
758
942
  try {
759
943
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
760
944
  }
761
- finally { if (e_7) throw e_7.error; }
945
+ finally { if (e_10) throw e_10.error; }
762
946
  }
763
947
  scripts.push(spec);
764
948
  return '';
@@ -767,6 +951,9 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
767
951
  var versioned = outputFolder
768
952
  ? versionScripts(scripts, outputFolder)
769
953
  : scripts;
954
+ var hints = preloadHints(versioned, outputFolder);
955
+ if (hints.length)
956
+ patched = insertIntoHead(patched, hints.join(''));
770
957
  var tag_1 = bootTag(versioned, version, basePath);
771
958
  // a function, not a replacement string: the tag carries the page's own srcs
772
959
  // in `data-scripts`, and a `$&` in one would expand to the matched markup
@@ -837,11 +1024,11 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
837
1024
  */
838
1025
  function buildMockSite(target) {
839
1026
  return __awaiter(this, void 0, void 0, function () {
840
- var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, identity, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, transcriptSource, hasTranscript, indexPath, _d, patched, warnings;
841
- var e_8, _e;
842
- var _f, _g, _h, _j, _k, _l;
843
- return __generator(this, function (_m) {
844
- switch (_m.label) {
1027
+ var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, identity, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, snapshotShards, capture, files, split, _d, _e, shard, transcriptSource, hasTranscript, indexPath, _f, patched, warnings;
1028
+ var e_11, _g, e_12, _h;
1029
+ var _j, _k, _l, _m, _o, _p;
1030
+ return __generator(this, function (_q) {
1031
+ switch (_q.label) {
845
1032
  case 0:
846
1033
  projectDir = process.projectDir || process.cwd();
847
1034
  appLocRaw = target.appLoc;
@@ -853,24 +1040,24 @@ function buildMockSite(target) {
853
1040
  path_1.default.join(projectDir, 'sasjsbuild');
854
1041
  return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
855
1042
  case 1:
856
- if (!(_m.sent())) {
1043
+ if (!(_q.sent())) {
857
1044
  throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
858
1045
  }
859
- basePath = normaliseBasePath((_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.mockWebBasePath);
860
- webSourceRaw = (_g = target.streamConfig) === null || _g === void 0 ? void 0 : _g.webSourcePath;
1046
+ basePath = normaliseBasePath((_j = target.streamConfig) === null || _j === void 0 ? void 0 : _j.mockWebBasePath);
1047
+ webSourceRaw = (_k = target.streamConfig) === null || _k === void 0 ? void 0 : _k.webSourcePath;
861
1048
  if (!webSourceRaw) {
862
1049
  throw new Error('No built web app. Set streamConfig.webSourcePath on the target (for example ./client/dist), and build the frontend first.');
863
1050
  }
864
1051
  webFolder = path_1.default.resolve(projectDir, webSourceRaw);
865
1052
  return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
866
1053
  case 2:
867
- if (!(_m.sent())) {
1054
+ if (!(_q.sent())) {
868
1055
  throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
869
1056
  }
870
1057
  if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
871
1058
  throw new Error("".concat(webFolder, " does not contain an index.html - is streamConfig.webSourcePath pointing at the built app?"));
872
1059
  }
873
- outputFolder = ((_h = target.streamConfig) === null || _h === void 0 ? void 0 : _h.mockWebFolder)
1060
+ outputFolder = ((_l = target.streamConfig) === null || _l === void 0 ? void 0 : _l.mockWebFolder)
874
1061
  ? path_1.default.resolve(projectDir, target.streamConfig.mockWebFolder)
875
1062
  : path_1.default.join(buildFolder, 'mock');
876
1063
  assertSafeOutputFolder(outputFolder, projectDir, buildFolder, webFolder);
@@ -891,14 +1078,14 @@ function buildMockSite(target) {
891
1078
  // without a SAS licence and without a login. What they cannot do is run here,
892
1079
  // so the build says so rather than letting a Run button be the surprise.
893
1080
  if (jsPrograms === 0 && sasPrograms > 0) {
894
- (_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."));
1081
+ (_m = process.logger) === null || _m === void 0 ? void 0 : _m.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."));
895
1082
  }
896
1083
  // No PROGRAMS is a different thing again: a compile that produced nothing, or
897
1084
  // a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
898
1085
  // behind that is not a program folder but does reach the drive, so the count
899
1086
  // that decides this is of programs, not of files.
900
1087
  if (jsPrograms + sasPrograms === 0) {
901
- (_k = process.logger) === null || _k === void 0 ? void 0 : _k.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
1088
+ (_o = process.logger) === null || _o === void 0 ? void 0 : _o.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
902
1089
  'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
903
1090
  }
904
1091
  // The package supplies the drive rules the worker answers with, so an install
@@ -911,7 +1098,7 @@ function buildMockSite(target) {
911
1098
  'or check that @sasjs/api still publishes build/browser.js.');
912
1099
  }
913
1100
  if (skipped.length) {
914
- (_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: ") +
1101
+ (_p = process.logger) === null || _p === void 0 ? void 0 : _p.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: ") +
915
1102
  "".concat(skipped
916
1103
  .slice(0, 3)
917
1104
  .map(function (file) {
@@ -942,12 +1129,12 @@ function buildMockSite(target) {
942
1129
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
943
1130
  }
944
1131
  }
945
- catch (e_8_1) { e_8 = { error: e_8_1 }; }
1132
+ catch (e_11_1) { e_11 = { error: e_11_1 }; }
946
1133
  finally {
947
1134
  try {
948
- if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
1135
+ if (_c && !_c.done && (_g = _b.return)) _g.call(_b);
949
1136
  }
950
- finally { if (e_8) throw e_8.error; }
1137
+ finally { if (e_11) throw e_11.error; }
951
1138
  }
952
1139
  fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
953
1140
  // 2. the linter, bundled from the installed packages so the code routes answer
@@ -959,7 +1146,7 @@ function buildMockSite(target) {
959
1146
  case 3:
960
1147
  // 2. the linter, bundled from the installed packages so the code routes answer
961
1148
  // with the rules this CLI resolved rather than a second implementation
962
- _m.sent();
1149
+ _q.sent();
963
1150
  // sw.js sits at the site root: a worker can only control the folder it is
964
1151
  // served from, and the app has to be controlled from /
965
1152
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -1015,8 +1202,34 @@ function buildMockSite(target) {
1015
1202
  .replace("importScripts('host/lint-browser.js')", "importScripts('host/lint-browser.js?v=".concat(hostVersion, "')")));
1016
1203
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
1017
1204
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
1205
+ snapshotShards = 0;
1018
1206
  if (hasSnapshot) {
1019
- fs_1.default.copyFileSync(snapshotSource, path_1.default.join(hostFolder, 'vfs-snapshot.json'));
1207
+ capture = JSON.parse(fs_1.default.readFileSync(snapshotSource, 'utf8'));
1208
+ if (capture &&
1209
+ capture.format !== undefined &&
1210
+ capture.format !== exports.DRIVE_FORMAT)
1211
+ throw new Error("The captured drive at ".concat(snapshotSource, " is format ") +
1212
+ "".concat(JSON.stringify(capture.format), ", and this build writes format ") +
1213
+ "".concat(exports.DRIVE_FORMAT, ". Seed the drive again with window.SASjsMock.download()."));
1214
+ files = capture && capture.files ? capture.files : capture;
1215
+ if (!files || typeof files !== 'object')
1216
+ throw new Error("The captured drive at ".concat(snapshotSource, " carries no files - the export is a JSON object of path to contents."));
1217
+ split = shardSnapshot(files);
1218
+ try {
1219
+ for (_d = __values(split.shards), _e = _d.next(); !_e.done; _e = _d.next()) {
1220
+ shard = _e.value;
1221
+ fs_1.default.writeFileSync(path_1.default.join(hostFolder, shard.file), JSON.stringify({ format: exports.DRIVE_FORMAT, files: shard.files }));
1222
+ }
1223
+ }
1224
+ catch (e_12_1) { e_12 = { error: e_12_1 }; }
1225
+ finally {
1226
+ try {
1227
+ if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
1228
+ }
1229
+ finally { if (e_12) throw e_12.error; }
1230
+ }
1231
+ fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'vfs-snapshot.json'), JSON.stringify(split.index, null, 2));
1232
+ snapshotShards = split.shards.length;
1020
1233
  }
1021
1234
  transcriptSource = path_1.default.join(buildFolder, 'sas-transcript.json');
1022
1235
  hasTranscript = fs_1.default.existsSync(transcriptSource);
@@ -1031,13 +1244,14 @@ function buildMockSite(target) {
1031
1244
  jsPrograms: jsPrograms,
1032
1245
  programFolders: PROGRAM_FOLDERS,
1033
1246
  snapshot: hasSnapshot,
1247
+ snapshotShards: snapshotShards,
1034
1248
  sasTranscript: hasTranscript,
1035
1249
  identity: identity.source,
1036
1250
  buildStamp: buildStamp,
1037
1251
  builtAt: new Date().toISOString()
1038
1252
  }, null, 2));
1039
1253
  indexPath = path_1.default.join(outputFolder, 'index.html');
1040
- _d = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion, basePath, outputFolder), patched = _d.patched, warnings = _d.warnings;
1254
+ _f = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion, basePath, outputFolder), patched = _f.patched, warnings = _f.warnings;
1041
1255
  fs_1.default.writeFileSync(indexPath, patched);
1042
1256
  warnings.forEach(function (warning) { var _a; return (_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn(warning); });
1043
1257
  return [2 /*return*/, {
@@ -1047,6 +1261,7 @@ function buildMockSite(target) {
1047
1261
  jsPrograms: jsPrograms,
1048
1262
  webFileCount: webFileCount,
1049
1263
  snapshot: hasSnapshot,
1264
+ snapshotShards: snapshotShards,
1050
1265
  sasTranscript: hasTranscript,
1051
1266
  identitySource: identity.source
1052
1267
  }];