@sasjs/cli 6.19.1 → 6.19.3
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 +34 -6
- package/build/commands/build/internal/mockSite/assets/persist.js +85 -0
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +17 -1
- package/build/commands/build/internal/mockSite/mockSite.js +29 -13
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +109 -7
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +89 -17
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +204 -1
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +50 -0
- package/build/types/index.d.ts +1 -0
- package/build/types/index.js +1 -0
- package/build/types/mockWebBasePath.d.ts +24 -0
- package/build/types/mockWebBasePath.js +2 -0
- package/package.json +1 -1
|
@@ -114,13 +114,41 @@
|
|
|
114
114
|
stamp = ''
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// Registered
|
|
118
|
-
// the site is served from the origin root on
|
|
119
|
-
// another (a demonstration site beside its own
|
|
120
|
-
//
|
|
117
|
+
// Registered against the loader's own URL, which sits in <base>/host/, and
|
|
118
|
+
// scoped to the folder above it: the site is served from the origin root on
|
|
119
|
+
// one host and from a folder on another (a demonstration site beside its own
|
|
120
|
+
// docs). The page cannot be the reference - at `<base>` with no trailing
|
|
121
|
+
// slash a page-relative URL names the origin root, and './' resolves to '/',
|
|
122
|
+
// which is the mis-registration this is here to avoid.
|
|
123
|
+
//
|
|
124
|
+
// The tag the build emits is absolute (`<base>/host/boot.js`) when the target
|
|
125
|
+
// names a folder, and relative (`host/boot.js`) when it does not - so a site
|
|
126
|
+
// published in a folder without naming it depends on its own URL ending in a
|
|
127
|
+
// slash - and in both cases the loader's own URL then carries `<base>`. The
|
|
128
|
+
// fallback below is for the case where there is no currentScript to read.
|
|
129
|
+
var loaderSrc = (script && script.src) || loaderUrl()
|
|
130
|
+
|
|
131
|
+
// The loader always sits in <base>/host/, so without a currentScript the page
|
|
132
|
+
// still names the base. A path that does not end in a slash is ambiguous: it
|
|
133
|
+
// is the folder when its last segment has no dot (`/demo`), and a file when it
|
|
134
|
+
// does (`/demo/index.html`) - only the second has a segment to drop.
|
|
135
|
+
// `location.href` cannot do this: one level above `/demo` is the origin root.
|
|
136
|
+
function loaderUrl() {
|
|
137
|
+
var page = location.pathname
|
|
138
|
+
var lastSegment = page.slice(page.lastIndexOf('/') + 1)
|
|
139
|
+
var base
|
|
140
|
+
|
|
141
|
+
if (lastSegment.indexOf('.') === -1) {
|
|
142
|
+
base = page.charAt(page.length - 1) === '/' ? page : page + '/'
|
|
143
|
+
} else {
|
|
144
|
+
base = page.slice(0, page.lastIndexOf('/') + 1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return location.origin + base + 'host/boot.js'
|
|
148
|
+
}
|
|
121
149
|
navigator.serviceWorker
|
|
122
|
-
.register(new URL('sw.js' + stamp,
|
|
123
|
-
scope: '
|
|
150
|
+
.register(new URL('../sw.js' + stamp, loaderSrc).href, {
|
|
151
|
+
scope: new URL('../', loaderSrc).pathname,
|
|
124
152
|
updateViaCache: 'none'
|
|
125
153
|
})
|
|
126
154
|
.then(function () {
|
|
@@ -89,6 +89,63 @@
|
|
|
89
89
|
|
|
90
90
|
const clearAll = () => withStore('readwrite', (store) => store.clear())
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The build this site is, as the page can see it: mock-config.json carries the
|
|
94
|
+
* stamp the build wrote over its host code and the seed it lays out. Read here
|
|
95
|
+
* rather than passed down, because the page decides whether to keep a stored
|
|
96
|
+
* drive before the worker is asked anything.
|
|
97
|
+
*
|
|
98
|
+
* The base is the worker's script URL, not the page's: the site can be served
|
|
99
|
+
* from a folder, and a page reached at `/demo` (no trailing slash) has a
|
|
100
|
+
* baseURI of `https://host/demo`, which resolves `host/mock-config.json` one
|
|
101
|
+
* level too high. The worker sits at `<base>/sw.js` and fetches the same file
|
|
102
|
+
* relative to itself, so its URL is the one definition of the site folder.
|
|
103
|
+
*
|
|
104
|
+
* Empty when the config cannot be read, which leaves the drive alone: a page
|
|
105
|
+
* that cannot tell which build it is has no business dropping one.
|
|
106
|
+
*/
|
|
107
|
+
const readBuildStamp = async (controller) => {
|
|
108
|
+
try {
|
|
109
|
+
const script = controller && controller.scriptURL
|
|
110
|
+
// no controller URL means no way to name the site folder; keep the drive
|
|
111
|
+
if (!script) return ''
|
|
112
|
+
const res = await fetch(new URL('host/mock-config.json', script), {
|
|
113
|
+
cache: 'no-store'
|
|
114
|
+
})
|
|
115
|
+
if (!res.ok) return ''
|
|
116
|
+
const config = await res.json()
|
|
117
|
+
return config && typeof config.buildStamp === 'string'
|
|
118
|
+
? config.buildStamp
|
|
119
|
+
: ''
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return ''
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Where the stamp of the build that wrote the stored drive is kept. */
|
|
126
|
+
const BUILD_STAMP_KEY = 'sasjs-mock-build'
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The stamp recorded for the stored drive, '' when there is none (a drive
|
|
130
|
+
* from a build before stamps existed) and null when storage cannot be read at
|
|
131
|
+
* all - which is not the same answer, and must not drop a drive on every load.
|
|
132
|
+
*/
|
|
133
|
+
const storedBuildStamp = () => {
|
|
134
|
+
try {
|
|
135
|
+
return localStorage.getItem(BUILD_STAMP_KEY) || ''
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const keepBuildStamp = (stamp) => {
|
|
142
|
+
try {
|
|
143
|
+
if (stamp) localStorage.setItem(BUILD_STAMP_KEY, stamp)
|
|
144
|
+
} catch (e) {
|
|
145
|
+
/* a browser that refuses storage still runs the site */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
92
149
|
const waitForController = () =>
|
|
93
150
|
new Promise((resolve) => {
|
|
94
151
|
if (navigator.serviceWorker.controller)
|
|
@@ -210,6 +267,32 @@
|
|
|
210
267
|
console.error('[sasjs-mock] could not read storage: ' + e)
|
|
211
268
|
}
|
|
212
269
|
|
|
270
|
+
// A stored drive outlives the build that wrote it, so a returning visitor
|
|
271
|
+
// would keep running that build's services - including ones the app being
|
|
272
|
+
// loaded no longer matches. This build stamps the seed it ships, and a drive
|
|
273
|
+
// whose stamp is absent or different is dropped rather than merged: a
|
|
274
|
+
// partial drive from two builds is the one thing that cannot be reasoned
|
|
275
|
+
// about. A site whose config carries no stamp keeps its drives, which is the
|
|
276
|
+
// behaviour every build before this one had.
|
|
277
|
+
const stamp = await readBuildStamp(controller)
|
|
278
|
+
const previous = storedBuildStamp()
|
|
279
|
+
if (
|
|
280
|
+
Object.keys(files).length &&
|
|
281
|
+
stamp &&
|
|
282
|
+
previous !== null &&
|
|
283
|
+
previous !== stamp
|
|
284
|
+
) {
|
|
285
|
+
console.info(
|
|
286
|
+
'[sasjs-mock] the stored drive is from another build - seeding this one'
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
await clearAll()
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.error('[sasjs-mock] could not clear storage: ' + e)
|
|
292
|
+
}
|
|
293
|
+
files = {}
|
|
294
|
+
}
|
|
295
|
+
|
|
213
296
|
if (Object.keys(files).length) {
|
|
214
297
|
window.__sasjsMockBoot = await askWorker(
|
|
215
298
|
{ type: 'sasjs-load', files },
|
|
@@ -221,6 +304,8 @@
|
|
|
221
304
|
'sasjs-seed-ack'
|
|
222
305
|
)
|
|
223
306
|
}
|
|
307
|
+
if (window.__sasjsMockBoot && !window.__sasjsMockBoot.error)
|
|
308
|
+
keepBuildStamp(stamp)
|
|
224
309
|
return window.__sasjsMockBoot
|
|
225
310
|
})()
|
|
226
311
|
})()
|
|
@@ -1100,7 +1100,16 @@ return _webout;
|
|
|
1100
1100
|
BUFFERS
|
|
1101
1101
|
)
|
|
1102
1102
|
} catch (e) {
|
|
1103
|
-
|
|
1103
|
+
// The stack alone is not an error message: WebKit's carries frames only, so
|
|
1104
|
+
// a failure there arrives at the app as "anonymous@... > Function:219:1" with
|
|
1105
|
+
// the reason nowhere in it. V8's stack already opens with the message, so it
|
|
1106
|
+
// is prepended only when it is missing rather than repeated.
|
|
1107
|
+
const message = e && e.message ? String(e.message) : ''
|
|
1108
|
+
const stack = e && e.stack ? String(e.stack) : ''
|
|
1109
|
+
thrown =
|
|
1110
|
+
stack && message && stack.indexOf(message) < 0
|
|
1111
|
+
? message + '\n' + stack
|
|
1112
|
+
: stack || String(e)
|
|
1104
1113
|
logLines.push(String(thrown))
|
|
1105
1114
|
}
|
|
1106
1115
|
|
|
@@ -1415,6 +1424,13 @@ const boot = async () => {
|
|
|
1415
1424
|
await loadMockConfig()
|
|
1416
1425
|
await loadWeboutPrelude()
|
|
1417
1426
|
const { source, files: seed } = await fetchDrive()
|
|
1427
|
+
// a seed replaces the drive, it does not merge into one: the worker survives
|
|
1428
|
+
// page loads, so it can still hold the drive it was given last time, and any
|
|
1429
|
+
// file only that drive had would otherwise outlive the build that wrote it
|
|
1430
|
+
FILES.clear()
|
|
1431
|
+
SIZES.clear()
|
|
1432
|
+
DIRS.clear()
|
|
1433
|
+
DIRS.add('/')
|
|
1418
1434
|
mkdirp(DRIVE_APP)
|
|
1419
1435
|
mkdirp(`${DRIVE_ROOT}/files`)
|
|
1420
1436
|
for (const [path, contents] of Object.entries(seed)) {
|
|
@@ -517,8 +517,9 @@ var escapeAttribute = function (value) {
|
|
|
517
517
|
* order, plus the inline snippets some pages use to set up a global before the
|
|
518
518
|
* bundle runs - so the host does not dictate how the app is built.
|
|
519
519
|
*/
|
|
520
|
-
var bootTag = function (scripts, version) {
|
|
521
|
-
|
|
520
|
+
var bootTag = function (scripts, version, basePath) {
|
|
521
|
+
if (basePath === void 0) { basePath = ''; }
|
|
522
|
+
return "<script src=\"".concat(basePath ? "".concat(basePath, "/host/boot.js") : 'host/boot.js', "?v=").concat(version, "\" data-scripts='").concat(escapeAttribute(JSON.stringify(scripts)), "'></script>");
|
|
522
523
|
};
|
|
523
524
|
/**
|
|
524
525
|
* Script types the browser executes. Anything else (`application/ld+json`, a
|
|
@@ -531,28 +532,36 @@ var EXECUTABLE_TYPES = [
|
|
|
531
532
|
'application/javascript'
|
|
532
533
|
];
|
|
533
534
|
/**
|
|
534
|
-
* The folder the site is published under,
|
|
535
|
+
* The folder the site is published under, with a leading slash and no trailing
|
|
536
|
+
* one.
|
|
535
537
|
*
|
|
536
538
|
* Empty for the origin root, which is the ordinary case. A host that serves the
|
|
537
539
|
* demonstration site beside its documentation (a GitHub Pages site whose root is
|
|
538
540
|
* the docs) names the folder here, and every path the build emits follows it.
|
|
541
|
+
*
|
|
542
|
+
* The shapes the released build accepts keep working - a bare `demo`, a leading
|
|
543
|
+
* or trailing slash, surrounding space - and are normalised rather than
|
|
544
|
+
* refused, because a `fix(mock)` release must not fail a build that passes
|
|
545
|
+
* today. The sasjsconfig schema states the canonical form; this only rejects
|
|
546
|
+
* what could never have worked, a segment that is `.`, `..` or empty.
|
|
539
547
|
*/
|
|
540
548
|
var normaliseBasePath = function (value) {
|
|
541
549
|
if (!value)
|
|
542
550
|
return '';
|
|
543
|
-
var
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
551
|
+
var segments = String(value)
|
|
552
|
+
.trim()
|
|
553
|
+
.replace(/^\/+/, '')
|
|
554
|
+
.replace(/\/+$/, '')
|
|
555
|
+
.split('/');
|
|
548
556
|
if (!segments.length ||
|
|
549
557
|
segments.some(function (segment) {
|
|
550
|
-
return
|
|
558
|
+
return !segment ||
|
|
551
559
|
segment === '.' ||
|
|
552
|
-
segment === '..'
|
|
560
|
+
segment === '..' ||
|
|
561
|
+
!/^[A-Za-z0-9._~-]+$/.test(segment);
|
|
553
562
|
}))
|
|
554
563
|
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
|
|
564
|
+
return '/' + segments.join('/');
|
|
556
565
|
};
|
|
557
566
|
/**
|
|
558
567
|
* Tells the interface that this page is answered by the browser host.
|
|
@@ -648,7 +657,7 @@ var patchIndexHtml = function (html, version, basePath) {
|
|
|
648
657
|
return '';
|
|
649
658
|
});
|
|
650
659
|
if (scripts.length) {
|
|
651
|
-
var tag = bootTag(scripts, version);
|
|
660
|
+
var tag = bootTag(scripts, version, basePath);
|
|
652
661
|
patched = /<\/body>/i.test(patched)
|
|
653
662
|
? patched.replace(/<\/body>/i, "".concat(tag, "\n</body>"))
|
|
654
663
|
: patched + tag;
|
|
@@ -674,7 +683,7 @@ var patchIndexHtml = function (html, version, basePath) {
|
|
|
674
683
|
*/
|
|
675
684
|
function buildMockSite(target) {
|
|
676
685
|
return __awaiter(this, void 0, void 0, function () {
|
|
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;
|
|
686
|
+
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
678
687
|
var e_8, _e;
|
|
679
688
|
var _f, _g, _h, _j, _k, _l;
|
|
680
689
|
return __generator(this, function (_m) {
|
|
@@ -828,6 +837,12 @@ function buildMockSite(target) {
|
|
|
828
837
|
.reduce(function (all, buf) { return Buffer.concat([all, buf]); }, Buffer.alloc(0)))
|
|
829
838
|
.digest('hex')
|
|
830
839
|
.slice(0, 10);
|
|
840
|
+
buildStamp = (0, crypto_1.createHash)('md5')
|
|
841
|
+
.update(JSON.stringify(Object.keys(seed)
|
|
842
|
+
.sort()
|
|
843
|
+
.map(function (key) { return [key, seed[key]]; })))
|
|
844
|
+
.digest('hex')
|
|
845
|
+
.slice(0, 10);
|
|
831
846
|
swPath = path_1.default.join(outputFolder, 'sw.js');
|
|
832
847
|
fs_1.default.writeFileSync(swPath, fs_1.default
|
|
833
848
|
.readFileSync(swPath, 'utf8')
|
|
@@ -848,6 +863,7 @@ function buildMockSite(target) {
|
|
|
848
863
|
jsPrograms: jsPrograms,
|
|
849
864
|
programFolders: PROGRAM_FOLDERS,
|
|
850
865
|
snapshot: hasSnapshot,
|
|
866
|
+
buildStamp: buildStamp,
|
|
851
867
|
builtAt: new Date().toISOString()
|
|
852
868
|
}, null, 2));
|
|
853
869
|
indexPath = path_1.default.join(outputFolder, 'index.html');
|
|
@@ -56,15 +56,24 @@ var loadBoot = function (specs, options) {
|
|
|
56
56
|
};
|
|
57
57
|
return element;
|
|
58
58
|
};
|
|
59
|
+
// The page carries an absolute boot tag, `<base>/host/boot.js`, so the
|
|
60
|
+
// loader's own URL names the folder whether or not the page URL ends in a
|
|
61
|
+
// slash - a relative tag would resolve against the page and, at `/demo`, name
|
|
62
|
+
// the origin root.
|
|
63
|
+
var pageHref = options.href || 'http://localhost:8123/';
|
|
64
|
+
var stamp = options.noStamp ? '' : '?v=abc123';
|
|
65
|
+
// the tag the build emits: absolute when the target names a folder, relative
|
|
66
|
+
// to the page when it does not
|
|
67
|
+
var loaderSrc = options.basePath
|
|
68
|
+
? "".concat(new URL(pageHref).origin).concat(options.basePath, "/host/boot.js").concat(stamp)
|
|
69
|
+
: new URL('host/boot.js' + stamp, pageHref).href;
|
|
59
70
|
var document = {
|
|
60
71
|
currentScript: options.noScript
|
|
61
72
|
? null
|
|
62
73
|
: {
|
|
63
74
|
// the builder stamps the boot tag with the host version, and the
|
|
64
75
|
// loader reuses that query for the worker's own URL
|
|
65
|
-
src:
|
|
66
|
-
? 'http://localhost:8123/host/boot.js'
|
|
67
|
-
: 'http://localhost:8123/host/boot.js?v=abc123',
|
|
76
|
+
src: loaderSrc,
|
|
68
77
|
getAttribute: function (name) {
|
|
69
78
|
if (name !== 'data-scripts')
|
|
70
79
|
return null;
|
|
@@ -126,7 +135,9 @@ var loadBoot = function (specs, options) {
|
|
|
126
135
|
}
|
|
127
136
|
};
|
|
128
137
|
global.location = {
|
|
129
|
-
href:
|
|
138
|
+
href: pageHref,
|
|
139
|
+
pathname: new URL(pageHref).pathname,
|
|
140
|
+
origin: new URL(pageHref).origin,
|
|
130
141
|
reload: function () { return timeline.push('reload'); }
|
|
131
142
|
};
|
|
132
143
|
jest.spyOn(console, 'error').mockImplementation(function () { });
|
|
@@ -185,7 +196,7 @@ describe('the boot loader', function () {
|
|
|
185
196
|
// root on one host and from /demo beside its docs on another.
|
|
186
197
|
expect(registered).toEqual([
|
|
187
198
|
'http://localhost:8123/sw.js?v=abc123 ' +
|
|
188
|
-
JSON.stringify({ scope: '
|
|
199
|
+
JSON.stringify({ scope: '/', updateViaCache: 'none' })
|
|
189
200
|
]);
|
|
190
201
|
return [2 /*return*/];
|
|
191
202
|
}
|
|
@@ -196,13 +207,104 @@ describe('the boot loader', function () {
|
|
|
196
207
|
return __generator(this, function (_a) {
|
|
197
208
|
switch (_a.label) {
|
|
198
209
|
case 0:
|
|
199
|
-
registered = loadBoot([], {
|
|
210
|
+
registered = loadBoot([], {
|
|
211
|
+
href: 'http://localhost:8123/demo/',
|
|
212
|
+
basePath: '/demo'
|
|
213
|
+
}).registered;
|
|
200
214
|
return [4 /*yield*/, settle()];
|
|
201
215
|
case 1:
|
|
202
216
|
_a.sent();
|
|
203
217
|
expect(registered).toEqual([
|
|
204
218
|
'http://localhost:8123/demo/sw.js?v=abc123 ' +
|
|
205
|
-
JSON.stringify({ scope: '
|
|
219
|
+
JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
|
|
220
|
+
]);
|
|
221
|
+
return [2 /*return*/];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}); });
|
|
225
|
+
it('registers under the folder when the page has no trailing slash', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
226
|
+
var registered;
|
|
227
|
+
return __generator(this, function (_a) {
|
|
228
|
+
switch (_a.label) {
|
|
229
|
+
case 0:
|
|
230
|
+
registered = loadBoot([], {
|
|
231
|
+
href: 'http://localhost:8123/demo',
|
|
232
|
+
basePath: '/demo'
|
|
233
|
+
}).registered;
|
|
234
|
+
return [4 /*yield*/, settle()];
|
|
235
|
+
case 1:
|
|
236
|
+
_a.sent();
|
|
237
|
+
expect(registered).toEqual([
|
|
238
|
+
'http://localhost:8123/demo/sw.js?v=abc123 ' +
|
|
239
|
+
JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
|
|
240
|
+
]);
|
|
241
|
+
return [2 /*return*/];
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}); });
|
|
245
|
+
it('finds the folder from the page when the document has no currentScript', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
246
|
+
var registered;
|
|
247
|
+
return __generator(this, function (_a) {
|
|
248
|
+
switch (_a.label) {
|
|
249
|
+
case 0:
|
|
250
|
+
registered = loadBoot([], {
|
|
251
|
+
href: 'http://localhost:8123/demo/',
|
|
252
|
+
basePath: '/demo',
|
|
253
|
+
noScript: true
|
|
254
|
+
}).registered;
|
|
255
|
+
return [4 /*yield*/, settle()
|
|
256
|
+
// no tag, no version: the fallback cannot carry the build's stamp, so this
|
|
257
|
+
// path registers the folder's worker without a cache-busting query
|
|
258
|
+
];
|
|
259
|
+
case 1:
|
|
260
|
+
_a.sent();
|
|
261
|
+
// no tag, no version: the fallback cannot carry the build's stamp, so this
|
|
262
|
+
// path registers the folder's worker without a cache-busting query
|
|
263
|
+
expect(registered).toEqual([
|
|
264
|
+
'http://localhost:8123/demo/sw.js ' +
|
|
265
|
+
JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
|
|
266
|
+
]);
|
|
267
|
+
return [2 /*return*/];
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}); });
|
|
271
|
+
it('treats a path with no trailing slash as the folder, not a file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
272
|
+
var registered;
|
|
273
|
+
return __generator(this, function (_a) {
|
|
274
|
+
switch (_a.label) {
|
|
275
|
+
case 0:
|
|
276
|
+
registered = loadBoot([], {
|
|
277
|
+
href: 'http://localhost:8123/demo',
|
|
278
|
+
basePath: '/demo',
|
|
279
|
+
noScript: true
|
|
280
|
+
}).registered;
|
|
281
|
+
return [4 /*yield*/, settle()];
|
|
282
|
+
case 1:
|
|
283
|
+
_a.sent();
|
|
284
|
+
expect(registered).toEqual([
|
|
285
|
+
'http://localhost:8123/demo/sw.js ' +
|
|
286
|
+
JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
|
|
287
|
+
]);
|
|
288
|
+
return [2 /*return*/];
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}); });
|
|
292
|
+
it('drops a file name from the path, and keeps the folder above it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
293
|
+
var registered;
|
|
294
|
+
return __generator(this, function (_a) {
|
|
295
|
+
switch (_a.label) {
|
|
296
|
+
case 0:
|
|
297
|
+
registered = loadBoot([], {
|
|
298
|
+
href: 'http://localhost:8123/demo/index.html',
|
|
299
|
+
basePath: '/demo',
|
|
300
|
+
noScript: true
|
|
301
|
+
}).registered;
|
|
302
|
+
return [4 /*yield*/, settle()];
|
|
303
|
+
case 1:
|
|
304
|
+
_a.sent();
|
|
305
|
+
expect(registered).toEqual([
|
|
306
|
+
'http://localhost:8123/demo/sw.js ' +
|
|
307
|
+
JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
|
|
206
308
|
]);
|
|
207
309
|
return [2 /*return*/];
|
|
208
310
|
}
|
|
@@ -136,6 +136,34 @@ describe('browser-hosted build', function () {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
}); });
|
|
139
|
+
it('stamps the config with the build, so a page can tell one drive from another', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
|
+
var first;
|
|
141
|
+
return __generator(this, function (_a) {
|
|
142
|
+
switch (_a.label) {
|
|
143
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
144
|
+
case 1:
|
|
145
|
+
_a.sent();
|
|
146
|
+
first = readConfig();
|
|
147
|
+
expect(first.buildStamp).toMatch(/^[0-9a-f]{10}$/);
|
|
148
|
+
// an identical build keeps its stamp: a rebuild that changes nothing must
|
|
149
|
+
// not throw a returning visitor's drive away
|
|
150
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
151
|
+
case 2:
|
|
152
|
+
// an identical build keeps its stamp: a rebuild that changes nothing must
|
|
153
|
+
// not throw a returning visitor's drive away
|
|
154
|
+
_a.sent();
|
|
155
|
+
expect(readConfig().buildStamp).toBe(first.buildStamp);
|
|
156
|
+
// a changed service changes the stamp, which is what makes the page re-seed
|
|
157
|
+
// rather than run the previous build's program
|
|
158
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'services', 'public', 'getdata.js'), 'const a = 999\n');
|
|
159
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
160
|
+
case 3:
|
|
161
|
+
_a.sent();
|
|
162
|
+
expect(readConfig().buildStamp).not.toBe(first.buildStamp);
|
|
163
|
+
return [2 /*return*/];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}); });
|
|
139
167
|
it('stamps the seed with the drive format the runtime checks', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
168
|
return __generator(this, function (_a) {
|
|
141
169
|
switch (_a.label) {
|
|
@@ -375,9 +403,9 @@ describe('browser-hosted build', function () {
|
|
|
375
403
|
_a.sent();
|
|
376
404
|
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
377
405
|
expect(html).toContain('serverurl="/demo"');
|
|
378
|
-
// the
|
|
379
|
-
//
|
|
380
|
-
expect(html).toContain('<script src="host/boot.js?v=');
|
|
406
|
+
// the boot tag is absolute and names the folder, so a page at `/demo` with
|
|
407
|
+
// no trailing slash still loads the loader from `/demo/host/`
|
|
408
|
+
expect(html).toContain('<script src="/demo/host/boot.js?v=');
|
|
381
409
|
return [2 /*return*/];
|
|
382
410
|
}
|
|
383
411
|
});
|
|
@@ -395,6 +423,50 @@ describe('browser-hosted build', function () {
|
|
|
395
423
|
}
|
|
396
424
|
});
|
|
397
425
|
}); });
|
|
426
|
+
it('normalises the folder shapes the released build already accepts', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
427
|
+
var _a, _b, value, html_1, e_3_1, html;
|
|
428
|
+
var e_3, _c;
|
|
429
|
+
return __generator(this, function (_d) {
|
|
430
|
+
switch (_d.label) {
|
|
431
|
+
case 0:
|
|
432
|
+
_d.trys.push([0, 5, 6, 7]);
|
|
433
|
+
_a = __values(['/demo/', ' /demo ', 'demo']), _b = _a.next();
|
|
434
|
+
_d.label = 1;
|
|
435
|
+
case 1:
|
|
436
|
+
if (!!_b.done) return [3 /*break*/, 4];
|
|
437
|
+
value = _b.value;
|
|
438
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockWebBasePath: value }))];
|
|
439
|
+
case 2:
|
|
440
|
+
_d.sent();
|
|
441
|
+
html_1 = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
442
|
+
expect(html_1).toContain('serverurl="/demo"');
|
|
443
|
+
_d.label = 3;
|
|
444
|
+
case 3:
|
|
445
|
+
_b = _a.next();
|
|
446
|
+
return [3 /*break*/, 1];
|
|
447
|
+
case 4: return [3 /*break*/, 7];
|
|
448
|
+
case 5:
|
|
449
|
+
e_3_1 = _d.sent();
|
|
450
|
+
e_3 = { error: e_3_1 };
|
|
451
|
+
return [3 /*break*/, 7];
|
|
452
|
+
case 6:
|
|
453
|
+
try {
|
|
454
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
455
|
+
}
|
|
456
|
+
finally { if (e_3) throw e_3.error; }
|
|
457
|
+
return [7 /*endfinally*/];
|
|
458
|
+
case 7:
|
|
459
|
+
// explicit empty is the origin root, as it was before the option existed
|
|
460
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockWebBasePath: '' }))];
|
|
461
|
+
case 8:
|
|
462
|
+
// explicit empty is the origin root, as it was before the option existed
|
|
463
|
+
_d.sent();
|
|
464
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
465
|
+
expect(html).toContain('serverurl=""');
|
|
466
|
+
return [2 /*return*/];
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}); });
|
|
398
470
|
it('points the app at itself and installs the boot loader', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
399
471
|
var html;
|
|
400
472
|
return __generator(this, function (_a) {
|
|
@@ -630,7 +702,7 @@ describe('browser-hosted build', function () {
|
|
|
630
702
|
}); });
|
|
631
703
|
it('stops seeding bytes once the drive budget is spent, and names the file', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
632
704
|
var chunk, _a, _b, name_1, seed, seeded;
|
|
633
|
-
var
|
|
705
|
+
var e_4, _c;
|
|
634
706
|
var _d;
|
|
635
707
|
return __generator(this, function (_e) {
|
|
636
708
|
switch (_e.label) {
|
|
@@ -643,12 +715,12 @@ describe('browser-hosted build', function () {
|
|
|
643
715
|
fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', name_1), chunk);
|
|
644
716
|
}
|
|
645
717
|
}
|
|
646
|
-
catch (
|
|
718
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
647
719
|
finally {
|
|
648
720
|
try {
|
|
649
721
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
650
722
|
}
|
|
651
|
-
finally { if (
|
|
723
|
+
finally { if (e_4) throw e_4.error; }
|
|
652
724
|
}
|
|
653
725
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
654
726
|
// the budget is spent in path order, so the drive is the same on every host:
|
|
@@ -734,7 +806,7 @@ describe('browser-hosted build', function () {
|
|
|
734
806
|
*/
|
|
735
807
|
it('builds an interface that has no services, and says the drive is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
736
808
|
var _a, _b, folder, warn, result;
|
|
737
|
-
var
|
|
809
|
+
var e_5, _c;
|
|
738
810
|
var _d;
|
|
739
811
|
return __generator(this, function (_e) {
|
|
740
812
|
switch (_e.label) {
|
|
@@ -749,12 +821,12 @@ describe('browser-hosted build', function () {
|
|
|
749
821
|
});
|
|
750
822
|
}
|
|
751
823
|
}
|
|
752
|
-
catch (
|
|
824
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
753
825
|
finally {
|
|
754
826
|
try {
|
|
755
827
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
756
828
|
}
|
|
757
|
-
finally { if (
|
|
829
|
+
finally { if (e_5) throw e_5.error; }
|
|
758
830
|
}
|
|
759
831
|
warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
|
|
760
832
|
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
@@ -776,7 +848,7 @@ describe('browser-hosted build', function () {
|
|
|
776
848
|
*/
|
|
777
849
|
it('warns when the build folder holds only folders that are not programs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
778
850
|
var _a, _b, folder, warn, result;
|
|
779
|
-
var
|
|
851
|
+
var e_6, _c;
|
|
780
852
|
var _d;
|
|
781
853
|
return __generator(this, function (_e) {
|
|
782
854
|
switch (_e.label) {
|
|
@@ -790,12 +862,12 @@ describe('browser-hosted build', function () {
|
|
|
790
862
|
});
|
|
791
863
|
}
|
|
792
864
|
}
|
|
793
|
-
catch (
|
|
865
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
794
866
|
finally {
|
|
795
867
|
try {
|
|
796
868
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
797
869
|
}
|
|
798
|
-
finally { if (
|
|
870
|
+
finally { if (e_6) throw e_6.error; }
|
|
799
871
|
}
|
|
800
872
|
write(path_1.default.join(root, 'sasjsbuild', 'docs', 'index.html'), '<html></html>');
|
|
801
873
|
warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
|
|
@@ -898,8 +970,8 @@ describe('browser-hosted build', function () {
|
|
|
898
970
|
});
|
|
899
971
|
}); });
|
|
900
972
|
it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
901
|
-
var _a, _b, mockWebFolder,
|
|
902
|
-
var
|
|
973
|
+
var _a, _b, mockWebFolder, e_7_1;
|
|
974
|
+
var e_7, _c;
|
|
903
975
|
return __generator(this, function (_d) {
|
|
904
976
|
switch (_d.label) {
|
|
905
977
|
case 0:
|
|
@@ -930,14 +1002,14 @@ describe('browser-hosted build', function () {
|
|
|
930
1002
|
return [3 /*break*/, 1];
|
|
931
1003
|
case 4: return [3 /*break*/, 7];
|
|
932
1004
|
case 5:
|
|
933
|
-
|
|
934
|
-
|
|
1005
|
+
e_7_1 = _d.sent();
|
|
1006
|
+
e_7 = { error: e_7_1 };
|
|
935
1007
|
return [3 /*break*/, 7];
|
|
936
1008
|
case 6:
|
|
937
1009
|
try {
|
|
938
1010
|
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
939
1011
|
}
|
|
940
|
-
finally { if (
|
|
1012
|
+
finally { if (e_7) throw e_7.error; }
|
|
941
1013
|
return [7 /*endfinally*/];
|
|
942
1014
|
case 7:
|
|
943
1015
|
expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
|
|
@@ -137,6 +137,10 @@ var loadPersist = function (options) {
|
|
|
137
137
|
var controller = options.controller === undefined
|
|
138
138
|
? { postMessage: function (message) { return posted.push(message); } }
|
|
139
139
|
: options.controller;
|
|
140
|
+
// a real controller names the site folder, which is how the page finds the
|
|
141
|
+
// config file; every case gets one unless it is testing that it is missing
|
|
142
|
+
if (controller && !controller.scriptURL)
|
|
143
|
+
controller.scriptURL = options.scriptURL || 'https://host.test/sw.js';
|
|
140
144
|
var serviceWorker = {
|
|
141
145
|
controller: controller,
|
|
142
146
|
// Never resolves by default: the boot sequence then stops before it touches
|
|
@@ -158,6 +162,36 @@ var loadPersist = function (options) {
|
|
|
158
162
|
var window = {};
|
|
159
163
|
global.window = window;
|
|
160
164
|
global.navigator = { serviceWorker: serviceWorker };
|
|
165
|
+
// the page reads its build stamp and what the stored drive was written by
|
|
166
|
+
var local = new Map();
|
|
167
|
+
if (options.storedStamp !== undefined)
|
|
168
|
+
local.set('sasjs-mock-build', options.storedStamp);
|
|
169
|
+
global.localStorage = options.storageUnavailable
|
|
170
|
+
? {
|
|
171
|
+
getItem: function () {
|
|
172
|
+
throw new Error('storage is denied');
|
|
173
|
+
},
|
|
174
|
+
setItem: function () {
|
|
175
|
+
throw new Error('storage is denied');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
getItem: function (key) { return (local.has(key) ? local.get(key) : null); },
|
|
180
|
+
setItem: function (key, value) { return local.set(key, value); }
|
|
181
|
+
};
|
|
182
|
+
global.document = {
|
|
183
|
+
baseURI: options.baseURI || 'https://host.test/'
|
|
184
|
+
};
|
|
185
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
186
|
+
return __generator(this, function (_a) {
|
|
187
|
+
return [2 /*return*/, ({
|
|
188
|
+
ok: true,
|
|
189
|
+
json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
190
|
+
return [2 /*return*/, ({ buildStamp: options.buildStamp || '' })];
|
|
191
|
+
}); }); }
|
|
192
|
+
})];
|
|
193
|
+
});
|
|
194
|
+
}); });
|
|
161
195
|
var idb = options.seeded
|
|
162
196
|
? (function () {
|
|
163
197
|
var fake = createIndexedDB();
|
|
@@ -171,7 +205,15 @@ var loadPersist = function (options) {
|
|
|
171
205
|
global.indexedDB = idb;
|
|
172
206
|
require('../assets/persist.js');
|
|
173
207
|
var reply = function (data) { return __spreadArray([], __read(listeners), false).forEach(function (fn) { return fn({ data: data }); }); };
|
|
174
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
window: window,
|
|
210
|
+
listeners: listeners,
|
|
211
|
+
removed: removed,
|
|
212
|
+
posted: posted,
|
|
213
|
+
reply: reply,
|
|
214
|
+
rows: idb.rows,
|
|
215
|
+
stored: local
|
|
216
|
+
};
|
|
175
217
|
};
|
|
176
218
|
describe('page-side store', function () {
|
|
177
219
|
afterEach(function () {
|
|
@@ -179,6 +221,9 @@ describe('page-side store', function () {
|
|
|
179
221
|
delete global.window;
|
|
180
222
|
delete global.navigator;
|
|
181
223
|
delete global.indexedDB;
|
|
224
|
+
delete global.document;
|
|
225
|
+
delete global.localStorage;
|
|
226
|
+
delete global.fetch;
|
|
182
227
|
});
|
|
183
228
|
it('asks the worker for a snapshot and drops the listener when it replies', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
184
229
|
var _a, window, listeners, removed, reply, before, pending;
|
|
@@ -473,4 +518,162 @@ describe('page-side store', function () {
|
|
|
473
518
|
}
|
|
474
519
|
});
|
|
475
520
|
}); });
|
|
521
|
+
/**
|
|
522
|
+
* Answers whatever the page asks, so a boot test can assert on which question
|
|
523
|
+
* was asked rather than on the reply.
|
|
524
|
+
*/
|
|
525
|
+
var answerBoot = function (controller, listeners) {
|
|
526
|
+
var answering = setInterval(function () {
|
|
527
|
+
var asked = controller.postMessage.mock.calls;
|
|
528
|
+
if (!asked.length)
|
|
529
|
+
return;
|
|
530
|
+
var type = asked[0][0].type;
|
|
531
|
+
__spreadArray([], __read(listeners), false).forEach(function (fn) { return fn({ data: { type: type + '-ack' } }); });
|
|
532
|
+
}, 5);
|
|
533
|
+
return function () { return clearInterval(answering); };
|
|
534
|
+
};
|
|
535
|
+
it("drops a drive written by another build and takes this build's seed", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
536
|
+
var controller, _a, window, listeners, stored, rows, stop;
|
|
537
|
+
return __generator(this, function (_b) {
|
|
538
|
+
switch (_b.label) {
|
|
539
|
+
case 0:
|
|
540
|
+
controller = { postMessage: jest.fn() };
|
|
541
|
+
_a = loadPersist({
|
|
542
|
+
controller: controller,
|
|
543
|
+
ready: Promise.resolve({}),
|
|
544
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
545
|
+
buildStamp: 'build-b',
|
|
546
|
+
storedStamp: 'build-a'
|
|
547
|
+
}), window = _a.window, listeners = _a.listeners, stored = _a.stored, rows = _a.rows;
|
|
548
|
+
stop = answerBoot(controller, listeners);
|
|
549
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
550
|
+
case 1:
|
|
551
|
+
_b.sent();
|
|
552
|
+
stop();
|
|
553
|
+
expect(controller.postMessage).toHaveBeenCalledWith({ type: 'sasjs-seed' });
|
|
554
|
+
expect(stored.get('sasjs-mock-build')).toBe('build-b');
|
|
555
|
+
// the old drive is gone rather than merged with the new seed
|
|
556
|
+
expect(rows.size).toBe(0);
|
|
557
|
+
return [2 /*return*/];
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}); });
|
|
561
|
+
it('seeds over a drive from a build before the stamp existed', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
562
|
+
var controller, _a, window, listeners, stop;
|
|
563
|
+
return __generator(this, function (_b) {
|
|
564
|
+
switch (_b.label) {
|
|
565
|
+
case 0:
|
|
566
|
+
controller = { postMessage: jest.fn() };
|
|
567
|
+
_a = loadPersist({
|
|
568
|
+
controller: controller,
|
|
569
|
+
ready: Promise.resolve({}),
|
|
570
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
571
|
+
buildStamp: 'build-b'
|
|
572
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
573
|
+
stop = answerBoot(controller, listeners);
|
|
574
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
575
|
+
case 1:
|
|
576
|
+
_b.sent();
|
|
577
|
+
stop();
|
|
578
|
+
expect(controller.postMessage).toHaveBeenCalledWith({ type: 'sasjs-seed' });
|
|
579
|
+
return [2 /*return*/];
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}); });
|
|
583
|
+
it('keeps a drive this build wrote', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
584
|
+
var controller, _a, window, listeners, stop;
|
|
585
|
+
return __generator(this, function (_b) {
|
|
586
|
+
switch (_b.label) {
|
|
587
|
+
case 0:
|
|
588
|
+
controller = { postMessage: jest.fn() };
|
|
589
|
+
_a = loadPersist({
|
|
590
|
+
controller: controller,
|
|
591
|
+
ready: Promise.resolve({}),
|
|
592
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
593
|
+
buildStamp: 'build-b',
|
|
594
|
+
storedStamp: 'build-b'
|
|
595
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
596
|
+
stop = answerBoot(controller, listeners);
|
|
597
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
598
|
+
case 1:
|
|
599
|
+
_b.sent();
|
|
600
|
+
stop();
|
|
601
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'sasjs-load' }));
|
|
602
|
+
return [2 /*return*/];
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}); });
|
|
606
|
+
it('keeps the drive when the site publishes no stamp', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
607
|
+
var controller, _a, window, listeners, stop;
|
|
608
|
+
return __generator(this, function (_b) {
|
|
609
|
+
switch (_b.label) {
|
|
610
|
+
case 0:
|
|
611
|
+
controller = { postMessage: jest.fn() };
|
|
612
|
+
_a = loadPersist({
|
|
613
|
+
controller: controller,
|
|
614
|
+
ready: Promise.resolve({}),
|
|
615
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' }
|
|
616
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
617
|
+
stop = answerBoot(controller, listeners);
|
|
618
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
619
|
+
case 1:
|
|
620
|
+
_b.sent();
|
|
621
|
+
stop();
|
|
622
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'sasjs-load' }));
|
|
623
|
+
return [2 /*return*/];
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}); });
|
|
627
|
+
it('keeps the drive when storage is refused, instead of seeding every load', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
628
|
+
var controller, _a, window, listeners, stop;
|
|
629
|
+
return __generator(this, function (_b) {
|
|
630
|
+
switch (_b.label) {
|
|
631
|
+
case 0:
|
|
632
|
+
controller = { postMessage: jest.fn() };
|
|
633
|
+
_a = loadPersist({
|
|
634
|
+
controller: controller,
|
|
635
|
+
ready: Promise.resolve({}),
|
|
636
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
637
|
+
buildStamp: 'build-b',
|
|
638
|
+
storageUnavailable: true
|
|
639
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
640
|
+
stop = answerBoot(controller, listeners);
|
|
641
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
642
|
+
case 1:
|
|
643
|
+
_b.sent();
|
|
644
|
+
stop();
|
|
645
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'sasjs-load' }));
|
|
646
|
+
return [2 /*return*/];
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}); });
|
|
650
|
+
it('reads the config from the folder the worker sits in, not the page URL', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
651
|
+
var controller, _a, window, listeners, stop, _b, url, init;
|
|
652
|
+
return __generator(this, function (_c) {
|
|
653
|
+
switch (_c.label) {
|
|
654
|
+
case 0:
|
|
655
|
+
controller = { postMessage: jest.fn() };
|
|
656
|
+
_a = loadPersist({
|
|
657
|
+
controller: controller,
|
|
658
|
+
ready: Promise.resolve({}),
|
|
659
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
660
|
+
buildStamp: 'build-b',
|
|
661
|
+
storedStamp: 'build-a',
|
|
662
|
+
scriptURL: 'https://host.test/demo/sw.js',
|
|
663
|
+
baseURI: 'https://host.test/demo'
|
|
664
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
665
|
+
stop = answerBoot(controller, listeners);
|
|
666
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
667
|
+
case 1:
|
|
668
|
+
_c.sent();
|
|
669
|
+
stop();
|
|
670
|
+
_b = __read(global.fetch.mock.calls[0], 2), url = _b[0], init = _b[1];
|
|
671
|
+
expect(String(url)).toBe('https://host.test/demo/host/mock-config.json');
|
|
672
|
+
expect(init).toEqual({ cache: 'no-store' });
|
|
673
|
+
// and the stamp it read is acted on
|
|
674
|
+
expect(controller.postMessage).toHaveBeenCalledWith({ type: 'sasjs-seed' });
|
|
675
|
+
return [2 /*return*/];
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}); });
|
|
476
679
|
});
|
|
@@ -136,6 +136,37 @@ describe('browser runtime', function () {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
}); });
|
|
139
|
+
it('replaces the drive when a seed arrives, rather than merging the two', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
|
+
var runtime, info;
|
|
141
|
+
var _a;
|
|
142
|
+
return __generator(this, function (_b) {
|
|
143
|
+
switch (_b.label) {
|
|
144
|
+
case 0:
|
|
145
|
+
runtime = loadRuntime();
|
|
146
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {}, _a[drivePath('first.txt')] = 'one', _a))
|
|
147
|
+
// a worker outlives the page that seeded it, so a later seed - a page that
|
|
148
|
+
// found a drive from another build, or a reset - arrives with the previous
|
|
149
|
+
// drive still in memory. A file only that drive had must not survive it.
|
|
150
|
+
];
|
|
151
|
+
case 1:
|
|
152
|
+
_b.sent();
|
|
153
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
154
|
+
var _a;
|
|
155
|
+
return __generator(this, function (_b) {
|
|
156
|
+
return [2 /*return*/, url.includes('mock-config')
|
|
157
|
+
? jsonResponse({ appLoc: appLoc })
|
|
158
|
+
: jsonResponse({ files: (_a = {}, _a[drivePath('second.txt')] = 'two', _a) })];
|
|
159
|
+
});
|
|
160
|
+
}); });
|
|
161
|
+
return [4 /*yield*/, runtime.seed()];
|
|
162
|
+
case 2:
|
|
163
|
+
info = _b.sent();
|
|
164
|
+
expect(info.files).toBe(1);
|
|
165
|
+
expect(Object.keys(runtime.dumpAll())).toEqual([drivePath('second.txt')]);
|
|
166
|
+
return [2 /*return*/];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}); });
|
|
139
170
|
it('reads a drive file with no format stamp as the current format', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
171
|
var runtime;
|
|
141
172
|
var _a;
|
|
@@ -294,6 +325,25 @@ describe('browser runtime', function () {
|
|
|
294
325
|
expect(result.log).toContain('boom');
|
|
295
326
|
expect(result.webout).toBe('');
|
|
296
327
|
});
|
|
328
|
+
it('carries the message when the stack does not, as WebKit reports one', function () {
|
|
329
|
+
var runtime = loadRuntime();
|
|
330
|
+
// WebKit's Error.stack is frames only - V8's opens with the message - so the
|
|
331
|
+
// body a service failure returns had the reason nowhere in it there, which is
|
|
332
|
+
// what a stack of "Function:219:1" in an app's error dialog looks like.
|
|
333
|
+
var result = runtime.run({
|
|
334
|
+
code: [
|
|
335
|
+
'const error = new Error("webout is not defined")',
|
|
336
|
+
'error.stack = "global code@https://host.test/sw.js:1:1"',
|
|
337
|
+
'throw error'
|
|
338
|
+
].join('\n')
|
|
339
|
+
});
|
|
340
|
+
expect(result.error).toBe('webout is not defined\nglobal code@https://host.test/sw.js:1:1');
|
|
341
|
+
});
|
|
342
|
+
it('does not repeat a message the stack already carries', function () {
|
|
343
|
+
var runtime = loadRuntime();
|
|
344
|
+
var result = runtime.run({ code: 'throw new Error("boom")' });
|
|
345
|
+
expect(result.error.match(/boom/g)).toHaveLength(1);
|
|
346
|
+
});
|
|
297
347
|
/**
|
|
298
348
|
* The shims a service reaches for are node's, refusals included: a mock that
|
|
299
349
|
* answered a copy from a missing file with an empty file, or accepted
|
package/build/types/index.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { TargetScope } from './targetScope';
|
|
|
5
5
|
export { TestFlow, Coverage, CoverageReport, CoverageSummary, CoverageType, CoverageState, TestResults, TestResultStatus, TestDescription, TestResult, TestResultDescription, TestResultCsv } from './testing';
|
|
6
6
|
export { Flow, FlowWave, FlowWaveJob } from './flow';
|
|
7
7
|
export * from './log';
|
|
8
|
+
export * from './mockWebBasePath';
|
package/build/types/index.js
CHANGED
|
@@ -24,3 +24,4 @@ Object.defineProperty(exports, "CoverageType", { enumerable: true, get: function
|
|
|
24
24
|
Object.defineProperty(exports, "CoverageState", { enumerable: true, get: function () { return testing_1.CoverageState; } });
|
|
25
25
|
Object.defineProperty(exports, "TestResultStatus", { enumerable: true, get: function () { return testing_1.TestResultStatus; } });
|
|
26
26
|
__exportStar(require("./log"), exports);
|
|
27
|
+
__exportStar(require("./mockWebBasePath"), exports);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mockWebBasePath` on a stream config.
|
|
3
|
+
*
|
|
4
|
+
* A `.ts` rather than a `.d.ts` so it compiles into `build/types/` with the
|
|
5
|
+
* rest of the package: tsc does not emit an ambient input file, and this one
|
|
6
|
+
* would otherwise type the CLI's own build and vanish from the published one.
|
|
7
|
+
*
|
|
8
|
+
* The option belongs to the mock target the CLI reads, but `StreamConfig` in
|
|
9
|
+
* @sasjs/utils 4.9.0 predates it, so without this the property is invisible to
|
|
10
|
+
* anyone typing a sasjsconfig and to anything that round-trips a target through
|
|
11
|
+
* the type. The field lands upstream in sasjs/utils#317; when the pin moves past
|
|
12
|
+
* that release, delete this file and the declaration merges from the package
|
|
13
|
+
* itself.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
declare module '@sasjs/utils/types/config' {
|
|
17
|
+
interface StreamConfig {
|
|
18
|
+
/**
|
|
19
|
+
* The folder the mock site is published in, as a path with no trailing
|
|
20
|
+
* slash (`/demo`). Unset means the origin root.
|
|
21
|
+
*/
|
|
22
|
+
mockWebBasePath?: string;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
CHANGED