@sasjs/cli 6.18.0 → 6.19.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.
- package/build/commands/build/internal/mockSite/assets/boot.js +8 -1
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +10 -5
- package/build/commands/build/internal/mockSite/assets/sw.js +28 -11
- package/build/commands/build/internal/mockSite/mockSite.js +48 -19
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +35 -8
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +32 -3
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +56 -2
- package/package.json +1 -1
|
@@ -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('
|
|
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('
|
|
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
|
-
|
|
845
|
-
const SNAPSHOT_PATH = '
|
|
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('
|
|
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('
|
|
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') {
|
|
@@ -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('
|
|
37
|
-
importScripts('
|
|
38
|
-
importScripts('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
653
|
-
switch (
|
|
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 (!(
|
|
693
|
+
if (!(_m.sent())) {
|
|
666
694
|
throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
|
|
667
695
|
}
|
|
668
|
-
|
|
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 (!(
|
|
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 = ((
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
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('
|
|
806
|
-
.replace("importScripts('
|
|
807
|
-
.replace("importScripts('
|
|
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: '
|
|
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 = {
|
|
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
|
|
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: '
|
|
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(
|
|
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 = {
|
|
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\('
|
|
212
|
-
expect(sw).toMatch(/importScripts\('
|
|
213
|
-
expect(sw).toMatch(/importScripts\('
|
|
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) {
|
|
@@ -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
|
|
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