@sasjs/cli 6.12.0 → 6.13.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 +149 -0
- package/build/commands/build/internal/mockSite/mockSite.js +107 -12
- package/build/commands/build/internal/mockSite/spec/boot.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +462 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +46 -4
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The mock host's boot loader. It ships as a file rather than an inline
|
|
3
|
+
* <script> because an inline one is refused by any app that sets a strict
|
|
4
|
+
* Content-Security-Policy (default-src 'self' with no 'unsafe-inline' is the
|
|
5
|
+
* SASjs norm), and the builder cannot add a hash or nonce to a policy it does
|
|
6
|
+
* not own.
|
|
7
|
+
*
|
|
8
|
+
* Order: register the service worker that hosts the mock backend, wait until it
|
|
9
|
+
* controls this page, hand it the persisted drive (persist.js), install the XHR
|
|
10
|
+
* bridge (intercept.js), and only then replay the application's own script tags
|
|
11
|
+
* - in the order the page listed them. Nothing about the app changes: it is the
|
|
12
|
+
* same tags, the same order, and the same globals, just later than the parser
|
|
13
|
+
* would have run them.
|
|
14
|
+
*
|
|
15
|
+
* The tag list arrives in the `data-scripts` attribute as JSON, written by
|
|
16
|
+
* buildMockSite. `defer` and `async` are not replayed: execution is sequenced
|
|
17
|
+
* on purpose here, because the host has to be ready before the first request
|
|
18
|
+
* the app makes.
|
|
19
|
+
*
|
|
20
|
+
* The fallback ladder, because a site whose worker never claims the page fails
|
|
21
|
+
* every request: an unreadable config stops before the app boots; a script that
|
|
22
|
+
* fails to load is reported and the next one still runs; a worker that takes
|
|
23
|
+
* control late is waited for; one that never does gets a single reload, then the
|
|
24
|
+
* app boots anyway with the failure in the console; and no service worker
|
|
25
|
+
* support at all boots the app in localStorage-only mode rather than a blank
|
|
26
|
+
* page.
|
|
27
|
+
*/
|
|
28
|
+
;(function () {
|
|
29
|
+
'use strict'
|
|
30
|
+
|
|
31
|
+
var script = document.currentScript
|
|
32
|
+
var specs = []
|
|
33
|
+
try {
|
|
34
|
+
specs = JSON.parse((script && script.getAttribute('data-scripts')) || '[]')
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('[mock] the boot config could not be read: ' + err)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function load(src, next) {
|
|
41
|
+
var s = document.createElement('script')
|
|
42
|
+
s.src = src
|
|
43
|
+
s.onload = next
|
|
44
|
+
s.onerror = function () {
|
|
45
|
+
console.error('[mock] failed to load ' + src)
|
|
46
|
+
next()
|
|
47
|
+
}
|
|
48
|
+
document.body.appendChild(s)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Replay one original tag: same src, and the attributes an app can rely on. */
|
|
52
|
+
function replay(spec, next) {
|
|
53
|
+
var s = document.createElement('script')
|
|
54
|
+
if (spec.type) s.setAttribute('type', spec.type)
|
|
55
|
+
if (spec.id) s.id = spec.id
|
|
56
|
+
if (spec.crossOrigin) s.crossOrigin = spec.crossOrigin
|
|
57
|
+
if (spec.integrity) s.integrity = spec.integrity
|
|
58
|
+
if (spec.noModule) s.noModule = true
|
|
59
|
+
|
|
60
|
+
// an inline snippet runs synchronously on insertion, exactly as it did in
|
|
61
|
+
// the page - there is no load event to wait for
|
|
62
|
+
if (spec.code !== undefined && spec.src === undefined) {
|
|
63
|
+
s.textContent = spec.code
|
|
64
|
+
document.body.appendChild(s)
|
|
65
|
+
return next()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
s.src = spec.src
|
|
69
|
+
s.onload = next
|
|
70
|
+
s.onerror = function () {
|
|
71
|
+
console.error('[mock] failed to load ' + spec.src)
|
|
72
|
+
next()
|
|
73
|
+
}
|
|
74
|
+
document.body.appendChild(s)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function replayAll(list, done) {
|
|
78
|
+
if (!list.length) return done()
|
|
79
|
+
replay(list[0], function () {
|
|
80
|
+
replayAll(list.slice(1), done)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function start() {
|
|
85
|
+
load('host/persist.js', function () {
|
|
86
|
+
Promise.resolve(window.__sasjsMockReady).then(function (boot) {
|
|
87
|
+
if (boot && boot.error) console.error('[mock] boot: ' + boot.error)
|
|
88
|
+
load('host/intercept.js', function () {
|
|
89
|
+
replayAll(specs, function () {})
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!('serviceWorker' in navigator)) {
|
|
96
|
+
console.error(
|
|
97
|
+
'[mock] service workers are unavailable - the mock backend cannot run'
|
|
98
|
+
)
|
|
99
|
+
start()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/*
|
|
104
|
+
* The worker script's own URL is the only thing a returning visitor cannot
|
|
105
|
+
* bypass: the host files behind it are cache-busted with a version query, but
|
|
106
|
+
* a cached sw.js keeps serving the previous build's worker - and through it
|
|
107
|
+
* the previous runtime. The boot tag already carries the host version, so its
|
|
108
|
+
* query string is reused rather than passing the version twice.
|
|
109
|
+
*/
|
|
110
|
+
var stamp = ''
|
|
111
|
+
try {
|
|
112
|
+
stamp = new URL(script.src, location.href).search
|
|
113
|
+
} catch (err) {
|
|
114
|
+
stamp = ''
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
navigator.serviceWorker
|
|
118
|
+
.register('/sw.js' + stamp, { scope: '/', updateViaCache: 'none' })
|
|
119
|
+
.then(function () {
|
|
120
|
+
return navigator.serviceWorker.ready
|
|
121
|
+
})
|
|
122
|
+
.then(function () {
|
|
123
|
+
if (navigator.serviceWorker.controller) return
|
|
124
|
+
return new Promise(function (resolve) {
|
|
125
|
+
var timer = setTimeout(resolve, 4000)
|
|
126
|
+
navigator.serviceWorker.addEventListener(
|
|
127
|
+
'controllerchange',
|
|
128
|
+
function () {
|
|
129
|
+
clearTimeout(timer)
|
|
130
|
+
resolve()
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
.then(function () {
|
|
136
|
+
if (navigator.serviceWorker.controller) return start()
|
|
137
|
+
if (!sessionStorage.getItem('mock-sw-reloaded')) {
|
|
138
|
+
sessionStorage.setItem('mock-sw-reloaded', '1')
|
|
139
|
+
location.reload()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
console.error('[mock] no service worker controller - requests will fail')
|
|
143
|
+
start()
|
|
144
|
+
})
|
|
145
|
+
.catch(function (err) {
|
|
146
|
+
console.error('[mock] service worker registration failed: ' + err)
|
|
147
|
+
start()
|
|
148
|
+
})
|
|
149
|
+
})()
|
|
@@ -341,11 +341,60 @@ var countJsPrograms = function (files) {
|
|
|
341
341
|
return inProgramFolder && key.toLowerCase().endsWith('.js');
|
|
342
342
|
}).length;
|
|
343
343
|
};
|
|
344
|
-
|
|
344
|
+
/** The script-tag attributes that change how an app's script runs. */
|
|
345
|
+
var REPLAYED_ATTRIBUTES = {
|
|
346
|
+
type: 'type',
|
|
347
|
+
id: 'id',
|
|
348
|
+
crossorigin: 'crossOrigin',
|
|
349
|
+
integrity: 'integrity',
|
|
350
|
+
nomodule: 'noModule'
|
|
351
|
+
};
|
|
352
|
+
var parseAttributes = function (source) {
|
|
353
|
+
var _a, _b, _c;
|
|
354
|
+
var attributes = {};
|
|
355
|
+
var pattern = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
|
|
356
|
+
var match;
|
|
357
|
+
while ((match = pattern.exec(source))) {
|
|
358
|
+
var value = (_c = (_b = (_a = match[2]) !== null && _a !== void 0 ? _a : match[3]) !== null && _b !== void 0 ? _b : match[4]) !== null && _c !== void 0 ? _c : '';
|
|
359
|
+
attributes[match[1].toLowerCase()] = value;
|
|
360
|
+
}
|
|
361
|
+
return attributes;
|
|
362
|
+
};
|
|
363
|
+
var escapeAttribute = function (value) {
|
|
364
|
+
return value
|
|
365
|
+
.replace(/&/g, '&')
|
|
366
|
+
.replace(/'/g, ''')
|
|
367
|
+
.replace(/</g, '<')
|
|
368
|
+
.replace(/>/g, '>');
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* The boot loader is a FILE rather than an inline script: an app with a strict
|
|
372
|
+
* Content-Security-Policy (`default-src 'self'`, no `unsafe-inline`) refuses an
|
|
373
|
+
* inline one, and the builder cannot add a hash or nonce to a policy it does
|
|
374
|
+
* not own.
|
|
375
|
+
*
|
|
376
|
+
* The app's script tags travel to it in `data-scripts`, in document order. Any
|
|
377
|
+
* number of tags works - one bundle, or several plain scripts a page loads in
|
|
378
|
+
* order, plus the inline snippets some pages use to set up a global before the
|
|
379
|
+
* bundle runs - so the host does not dictate how the app is built.
|
|
380
|
+
*/
|
|
381
|
+
var bootTag = function (scripts, version) {
|
|
382
|
+
return "<script src=\"host/boot.js?v=".concat(version, "\" data-scripts='").concat(escapeAttribute(JSON.stringify(scripts)), "'></script>");
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Script types the browser executes. Anything else (`application/ld+json`, a
|
|
386
|
+
* template) is data the page keeps for later, so it stays where it is.
|
|
387
|
+
*/
|
|
388
|
+
var EXECUTABLE_TYPES = [
|
|
389
|
+
'',
|
|
390
|
+
'module',
|
|
391
|
+
'text/javascript',
|
|
392
|
+
'application/javascript'
|
|
393
|
+
];
|
|
345
394
|
/**
|
|
346
|
-
* Points the app at its own origin and replaces its
|
|
347
|
-
*
|
|
348
|
-
*
|
|
395
|
+
* Points the app at its own origin and replaces its script tags with the boot
|
|
396
|
+
* loader. Both changes are reported when the expected markup is absent, rather
|
|
397
|
+
* than producing a site that silently talks to a real server.
|
|
349
398
|
*/
|
|
350
399
|
var patchIndexHtml = function (html, version) {
|
|
351
400
|
var warnings = [];
|
|
@@ -357,12 +406,56 @@ var patchIndexHtml = function (html, version) {
|
|
|
357
406
|
else {
|
|
358
407
|
warnings.push('no <sasjs> element found - check the adapter configuration manually');
|
|
359
408
|
}
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
409
|
+
var scripts = [];
|
|
410
|
+
var tagPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>[ \t]*\r?\n?/gi;
|
|
411
|
+
patched = patched.replace(tagPattern, function (tag, source, body) {
|
|
412
|
+
var e_6, _a;
|
|
413
|
+
var attributes = parseAttributes(source);
|
|
414
|
+
var type = (attributes.type || '').trim().toLowerCase();
|
|
415
|
+
var inlined = !attributes.src;
|
|
416
|
+
// data blocks stay in the page; they are not scripts to replay
|
|
417
|
+
if (!EXECUTABLE_TYPES.includes(type))
|
|
418
|
+
return tag;
|
|
419
|
+
var spec = inlined
|
|
420
|
+
? {}
|
|
421
|
+
: { src: attributes.src };
|
|
422
|
+
if (inlined) {
|
|
423
|
+
var code = body.trim();
|
|
424
|
+
// an empty inline tag (a placeholder, or a commented-out block) is not
|
|
425
|
+
// worth a round trip
|
|
426
|
+
if (!code)
|
|
427
|
+
return '';
|
|
428
|
+
spec.code = code;
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
for (var _b = __values(Object.keys(REPLAYED_ATTRIBUTES)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
432
|
+
var attribute = _c.value;
|
|
433
|
+
if (attributes[attribute] === undefined)
|
|
434
|
+
continue;
|
|
435
|
+
if (attribute === 'type' && !attributes.type)
|
|
436
|
+
continue;
|
|
437
|
+
var name_1 = REPLAYED_ATTRIBUTES[attribute];
|
|
438
|
+
spec[name_1] = attribute === 'nomodule' ? 'true' : attributes[attribute];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
442
|
+
finally {
|
|
443
|
+
try {
|
|
444
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
445
|
+
}
|
|
446
|
+
finally { if (e_6) throw e_6.error; }
|
|
447
|
+
}
|
|
448
|
+
scripts.push(spec);
|
|
449
|
+
return '';
|
|
450
|
+
});
|
|
451
|
+
if (scripts.length) {
|
|
452
|
+
var tag = bootTag(scripts, version);
|
|
453
|
+
patched = /<\/body>/i.test(patched)
|
|
454
|
+
? patched.replace(/<\/body>/i, "".concat(tag, "\n</body>"))
|
|
455
|
+
: patched + tag;
|
|
363
456
|
}
|
|
364
457
|
else {
|
|
365
|
-
warnings.push('no
|
|
458
|
+
warnings.push('no script tags found - the mock loader was not installed');
|
|
366
459
|
}
|
|
367
460
|
return { patched: patched, warnings: warnings };
|
|
368
461
|
};
|
|
@@ -383,7 +476,7 @@ var patchIndexHtml = function (html, version) {
|
|
|
383
476
|
function buildMockSite(target) {
|
|
384
477
|
return __awaiter(this, void 0, void 0, function () {
|
|
385
478
|
var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
386
|
-
var
|
|
479
|
+
var e_7, _e;
|
|
387
480
|
var _f, _g, _h;
|
|
388
481
|
return __generator(this, function (_j) {
|
|
389
482
|
switch (_j.label) {
|
|
@@ -465,18 +558,19 @@ function buildMockSite(target) {
|
|
|
465
558
|
'md5.js',
|
|
466
559
|
'runtime-core.js',
|
|
467
560
|
'persist.js',
|
|
468
|
-
'intercept.js'
|
|
561
|
+
'intercept.js',
|
|
562
|
+
'boot.js'
|
|
469
563
|
]), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
470
564
|
asset = _c.value;
|
|
471
565
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
|
|
472
566
|
}
|
|
473
567
|
}
|
|
474
|
-
catch (
|
|
568
|
+
catch (e_7_1) { e_7 = { error: e_7_1 }; }
|
|
475
569
|
finally {
|
|
476
570
|
try {
|
|
477
571
|
if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
|
|
478
572
|
}
|
|
479
|
-
finally { if (
|
|
573
|
+
finally { if (e_7) throw e_7.error; }
|
|
480
574
|
}
|
|
481
575
|
fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
|
|
482
576
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
@@ -502,6 +596,7 @@ function buildMockSite(target) {
|
|
|
502
596
|
'runtime-core.js',
|
|
503
597
|
'persist.js',
|
|
504
598
|
'intercept.js',
|
|
599
|
+
'boot.js',
|
|
505
600
|
'api-browser.js',
|
|
506
601
|
'webout-prelude.json'
|
|
507
602
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
var loadBoot = function (specs, options) {
|
|
40
|
+
if (options === void 0) { options = {}; }
|
|
41
|
+
jest.resetModules();
|
|
42
|
+
var appended = [];
|
|
43
|
+
var timeline = [];
|
|
44
|
+
var registered = [];
|
|
45
|
+
var stored = {};
|
|
46
|
+
if (options.guard)
|
|
47
|
+
stored['mock-sw-reloaded'] = options.guard;
|
|
48
|
+
var createElement = function () {
|
|
49
|
+
var element = {
|
|
50
|
+
attributes: {},
|
|
51
|
+
setAttribute: function (name, value) {
|
|
52
|
+
element.attributes[name] = value;
|
|
53
|
+
if (name === 'type')
|
|
54
|
+
element.type = value;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return element;
|
|
58
|
+
};
|
|
59
|
+
var document = {
|
|
60
|
+
currentScript: options.noScript
|
|
61
|
+
? null
|
|
62
|
+
: {
|
|
63
|
+
// the builder stamps the boot tag with the host version, and the
|
|
64
|
+
// loader reuses that query for the worker's own URL
|
|
65
|
+
src: options.noStamp
|
|
66
|
+
? 'http://localhost:8123/host/boot.js'
|
|
67
|
+
: 'http://localhost:8123/host/boot.js?v=abc123',
|
|
68
|
+
getAttribute: function (name) {
|
|
69
|
+
if (name !== 'data-scripts')
|
|
70
|
+
return null;
|
|
71
|
+
return options.brokenConfig ? 'not json' : JSON.stringify(specs);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
createElement: createElement,
|
|
75
|
+
body: {
|
|
76
|
+
appendChild: function (element) {
|
|
77
|
+
appended.push(element);
|
|
78
|
+
timeline.push(element.src || '[' + element.textContent + ']');
|
|
79
|
+
if (!element.src)
|
|
80
|
+
return element;
|
|
81
|
+
setTimeout(function () {
|
|
82
|
+
var failed = (options.failSrcs || []).includes(element.src);
|
|
83
|
+
if (failed && element.onerror)
|
|
84
|
+
element.onerror();
|
|
85
|
+
else if (element.onload)
|
|
86
|
+
element.onload();
|
|
87
|
+
}, 0);
|
|
88
|
+
return element;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var listeners = {};
|
|
93
|
+
var controller = options.controller || 'present';
|
|
94
|
+
var navigator = {
|
|
95
|
+
serviceWorker: {
|
|
96
|
+
register: function (url, init) {
|
|
97
|
+
registered.push(url + ' ' + JSON.stringify(init));
|
|
98
|
+
return options.registerFails
|
|
99
|
+
? Promise.reject(new Error('nope'))
|
|
100
|
+
: Promise.resolve();
|
|
101
|
+
},
|
|
102
|
+
ready: Promise.resolve(),
|
|
103
|
+
get controller() {
|
|
104
|
+
if (controller === 'present')
|
|
105
|
+
return {};
|
|
106
|
+
if (controller === 'late' && late)
|
|
107
|
+
return {};
|
|
108
|
+
return null;
|
|
109
|
+
},
|
|
110
|
+
addEventListener: function (type, fn) {
|
|
111
|
+
listeners[type] = listeners[type] || [];
|
|
112
|
+
listeners[type].push(fn);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var late = false;
|
|
117
|
+
global.document = document;
|
|
118
|
+
global.navigator = navigator;
|
|
119
|
+
global.window = options.bootError
|
|
120
|
+
? { __sasjsMockReady: Promise.resolve({ error: options.bootError }) }
|
|
121
|
+
: {};
|
|
122
|
+
global.sessionStorage = {
|
|
123
|
+
getItem: function (key) { return (key in stored ? stored[key] : null); },
|
|
124
|
+
setItem: function (key, value) {
|
|
125
|
+
stored[key] = value;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
global.location = { reload: function () { return timeline.push('reload'); } };
|
|
129
|
+
jest.spyOn(console, 'error').mockImplementation(function () { });
|
|
130
|
+
require('../assets/boot.js');
|
|
131
|
+
var claim = function () {
|
|
132
|
+
late = true;
|
|
133
|
+
(listeners['controllerchange'] || []).forEach(function (fn) { return fn(); });
|
|
134
|
+
};
|
|
135
|
+
return { appended: appended, timeline: timeline, registered: registered, stored: stored, claim: claim };
|
|
136
|
+
};
|
|
137
|
+
var settle = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
138
|
+
var i;
|
|
139
|
+
return __generator(this, function (_a) {
|
|
140
|
+
switch (_a.label) {
|
|
141
|
+
case 0:
|
|
142
|
+
i = 0;
|
|
143
|
+
_a.label = 1;
|
|
144
|
+
case 1:
|
|
145
|
+
if (!(i < 20)) return [3 /*break*/, 4];
|
|
146
|
+
return [4 /*yield*/, new Promise(function (r) { return setTimeout(r, 1); })];
|
|
147
|
+
case 2:
|
|
148
|
+
_a.sent();
|
|
149
|
+
_a.label = 3;
|
|
150
|
+
case 3:
|
|
151
|
+
i++;
|
|
152
|
+
return [3 /*break*/, 1];
|
|
153
|
+
case 4: return [2 /*return*/];
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}); };
|
|
157
|
+
describe('the boot loader', function () {
|
|
158
|
+
afterEach(function () {
|
|
159
|
+
delete global.document;
|
|
160
|
+
delete global.navigator;
|
|
161
|
+
delete global.window;
|
|
162
|
+
delete global.sessionStorage;
|
|
163
|
+
delete global.location;
|
|
164
|
+
});
|
|
165
|
+
it("registers the worker at the site root, at the build's version", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
166
|
+
var registered;
|
|
167
|
+
return __generator(this, function (_a) {
|
|
168
|
+
switch (_a.label) {
|
|
169
|
+
case 0:
|
|
170
|
+
registered = loadBoot([]).registered;
|
|
171
|
+
return [4 /*yield*/, settle()
|
|
172
|
+
// 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
|
|
174
|
+
];
|
|
175
|
+
case 1:
|
|
176
|
+
_a.sent();
|
|
177
|
+
// 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
|
|
179
|
+
expect(registered).toEqual([
|
|
180
|
+
'/sw.js?v=abc123 ' +
|
|
181
|
+
JSON.stringify({ scope: '/', updateViaCache: 'none' })
|
|
182
|
+
]);
|
|
183
|
+
return [2 /*return*/];
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}); });
|
|
187
|
+
it('boots the host before the app, then replays every tag in order', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
188
|
+
var timeline;
|
|
189
|
+
return __generator(this, function (_a) {
|
|
190
|
+
switch (_a.label) {
|
|
191
|
+
case 0:
|
|
192
|
+
timeline = loadBoot([
|
|
193
|
+
{ src: 'sasjs.js', id: 'sasjs-adapter' },
|
|
194
|
+
{ code: 'window.__config = 1' },
|
|
195
|
+
{ src: 'js/backend.js' },
|
|
196
|
+
{ src: 'js/game.js' }
|
|
197
|
+
]).timeline;
|
|
198
|
+
return [4 /*yield*/, settle()];
|
|
199
|
+
case 1:
|
|
200
|
+
_a.sent();
|
|
201
|
+
expect(timeline).toEqual([
|
|
202
|
+
'host/persist.js',
|
|
203
|
+
'host/intercept.js',
|
|
204
|
+
'sasjs.js',
|
|
205
|
+
'[window.__config = 1]',
|
|
206
|
+
'js/backend.js',
|
|
207
|
+
'js/game.js'
|
|
208
|
+
]);
|
|
209
|
+
return [2 /*return*/];
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}); });
|
|
213
|
+
it('keeps the attributes an app relies on', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
214
|
+
var appended, app;
|
|
215
|
+
return __generator(this, function (_a) {
|
|
216
|
+
switch (_a.label) {
|
|
217
|
+
case 0:
|
|
218
|
+
appended = loadBoot([
|
|
219
|
+
{ src: 'sasjs.js', id: 'sasjs-adapter' },
|
|
220
|
+
{ src: 'main.js', type: 'module' },
|
|
221
|
+
{ src: 'cdn.js', crossOrigin: 'anonymous', integrity: 'sha384-x' },
|
|
222
|
+
{ src: 'legacy.js', noModule: 'true' }
|
|
223
|
+
]).appended;
|
|
224
|
+
return [4 /*yield*/, settle()
|
|
225
|
+
// the first two are the host's own: persist.js and intercept.js
|
|
226
|
+
];
|
|
227
|
+
case 1:
|
|
228
|
+
_a.sent();
|
|
229
|
+
app = appended.slice(2);
|
|
230
|
+
expect(app.map(function (element) { return element.src; })).toEqual([
|
|
231
|
+
'sasjs.js',
|
|
232
|
+
'main.js',
|
|
233
|
+
'cdn.js',
|
|
234
|
+
'legacy.js'
|
|
235
|
+
]);
|
|
236
|
+
expect(app[0].id).toEqual('sasjs-adapter');
|
|
237
|
+
expect(app[1].type).toEqual('module');
|
|
238
|
+
expect(app[2].crossOrigin).toEqual('anonymous');
|
|
239
|
+
expect(app[2].integrity).toEqual('sha384-x');
|
|
240
|
+
expect(app[3].noModule).toBe(true);
|
|
241
|
+
return [2 /*return*/];
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}); });
|
|
245
|
+
it('runs an inline snippet without waiting for a load event', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
246
|
+
var appended, inline;
|
|
247
|
+
return __generator(this, function (_a) {
|
|
248
|
+
switch (_a.label) {
|
|
249
|
+
case 0:
|
|
250
|
+
appended = loadBoot([{ code: 'window.__boot = 1' }]).appended;
|
|
251
|
+
return [4 /*yield*/, settle()];
|
|
252
|
+
case 1:
|
|
253
|
+
_a.sent();
|
|
254
|
+
inline = appended.find(function (element) { return element.textContent; });
|
|
255
|
+
expect(inline).toBeDefined();
|
|
256
|
+
expect(inline.src).toBeUndefined();
|
|
257
|
+
return [2 /*return*/];
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}); });
|
|
261
|
+
it('moves on when a script fails to load', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
262
|
+
var timeline;
|
|
263
|
+
return __generator(this, function (_a) {
|
|
264
|
+
switch (_a.label) {
|
|
265
|
+
case 0:
|
|
266
|
+
timeline = loadBoot([{ src: 'missing.js' }, { src: 'js/game.js' }], { failSrcs: ['missing.js'] }).timeline;
|
|
267
|
+
return [4 /*yield*/, settle()];
|
|
268
|
+
case 1:
|
|
269
|
+
_a.sent();
|
|
270
|
+
expect(timeline.slice(-2)).toEqual(['missing.js', 'js/game.js']);
|
|
271
|
+
return [2 /*return*/];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}); });
|
|
275
|
+
it('moves on when a host script fails to load', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
276
|
+
var timeline;
|
|
277
|
+
return __generator(this, function (_a) {
|
|
278
|
+
switch (_a.label) {
|
|
279
|
+
case 0:
|
|
280
|
+
timeline = loadBoot([{ src: 'js/game.js' }], {
|
|
281
|
+
failSrcs: ['host/persist.js']
|
|
282
|
+
}).timeline;
|
|
283
|
+
return [4 /*yield*/, settle()
|
|
284
|
+
// a dead persist.js still reaches the app rather than hanging the page
|
|
285
|
+
];
|
|
286
|
+
case 1:
|
|
287
|
+
_a.sent();
|
|
288
|
+
// a dead persist.js still reaches the app rather than hanging the page
|
|
289
|
+
expect(timeline).toContain('js/game.js');
|
|
290
|
+
return [2 /*return*/];
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}); });
|
|
294
|
+
it('reports an error the host booted with', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
295
|
+
return __generator(this, function (_a) {
|
|
296
|
+
switch (_a.label) {
|
|
297
|
+
case 0:
|
|
298
|
+
loadBoot([], { bootError: 'seed missing' });
|
|
299
|
+
return [4 /*yield*/, settle()];
|
|
300
|
+
case 1:
|
|
301
|
+
_a.sent();
|
|
302
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('seed missing'));
|
|
303
|
+
return [2 /*return*/];
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}); });
|
|
307
|
+
it('refuses a boot config it cannot read instead of guessing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
308
|
+
var timeline;
|
|
309
|
+
return __generator(this, function (_a) {
|
|
310
|
+
switch (_a.label) {
|
|
311
|
+
case 0:
|
|
312
|
+
timeline = loadBoot([], { brokenConfig: true }).timeline;
|
|
313
|
+
return [4 /*yield*/, settle()];
|
|
314
|
+
case 1:
|
|
315
|
+
_a.sent();
|
|
316
|
+
expect(timeline).toEqual([]);
|
|
317
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('boot config could not be read'));
|
|
318
|
+
return [2 /*return*/];
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}); });
|
|
322
|
+
it('registers the plain worker URL when the tag carries no version', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
323
|
+
var registered;
|
|
324
|
+
return __generator(this, function (_a) {
|
|
325
|
+
switch (_a.label) {
|
|
326
|
+
case 0:
|
|
327
|
+
registered = loadBoot([], { noStamp: true }).registered;
|
|
328
|
+
return [4 /*yield*/, settle()];
|
|
329
|
+
case 1:
|
|
330
|
+
_a.sent();
|
|
331
|
+
expect(registered[0]).toMatch(/^\/sw\.js /);
|
|
332
|
+
return [2 /*return*/];
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}); });
|
|
336
|
+
it('boots the host with no app scripts when the tag carries no config', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
337
|
+
var timeline;
|
|
338
|
+
return __generator(this, function (_a) {
|
|
339
|
+
switch (_a.label) {
|
|
340
|
+
case 0:
|
|
341
|
+
timeline = loadBoot([], { noScript: true }).timeline;
|
|
342
|
+
return [4 /*yield*/, settle()];
|
|
343
|
+
case 1:
|
|
344
|
+
_a.sent();
|
|
345
|
+
expect(timeline).toEqual(['host/persist.js', 'host/intercept.js']);
|
|
346
|
+
return [2 /*return*/];
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}); });
|
|
350
|
+
it('waits for a worker that takes control a moment later', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
351
|
+
var _a, timeline, claim;
|
|
352
|
+
return __generator(this, function (_b) {
|
|
353
|
+
switch (_b.label) {
|
|
354
|
+
case 0:
|
|
355
|
+
_a = loadBoot([{ src: 'app.js' }], {
|
|
356
|
+
controller: 'late'
|
|
357
|
+
}), timeline = _a.timeline, claim = _a.claim;
|
|
358
|
+
return [4 /*yield*/, settle()];
|
|
359
|
+
case 1:
|
|
360
|
+
_b.sent();
|
|
361
|
+
claim();
|
|
362
|
+
return [4 /*yield*/, settle()];
|
|
363
|
+
case 2:
|
|
364
|
+
_b.sent();
|
|
365
|
+
expect(timeline).toEqual(['host/persist.js', 'host/intercept.js', 'app.js']);
|
|
366
|
+
return [2 /*return*/];
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}); });
|
|
370
|
+
it('boots anyway when the worker cannot be registered at all', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
371
|
+
var timeline;
|
|
372
|
+
return __generator(this, function (_a) {
|
|
373
|
+
switch (_a.label) {
|
|
374
|
+
case 0:
|
|
375
|
+
timeline = loadBoot([{ src: 'app.js' }], {
|
|
376
|
+
registerFails: true,
|
|
377
|
+
controller: 'never'
|
|
378
|
+
}).timeline;
|
|
379
|
+
return [4 /*yield*/, settle()];
|
|
380
|
+
case 1:
|
|
381
|
+
_a.sent();
|
|
382
|
+
expect(timeline).toEqual(['host/persist.js', 'host/intercept.js', 'app.js']);
|
|
383
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('service worker registration failed'));
|
|
384
|
+
return [2 /*return*/];
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}); });
|
|
388
|
+
it('reloads once when the worker never claims the page', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
389
|
+
var _a, timeline, stored;
|
|
390
|
+
return __generator(this, function (_b) {
|
|
391
|
+
switch (_b.label) {
|
|
392
|
+
case 0:
|
|
393
|
+
_a = loadBoot([{ src: 'app.js' }], {
|
|
394
|
+
controller: 'never'
|
|
395
|
+
}), timeline = _a.timeline, stored = _a.stored;
|
|
396
|
+
// the loader's grace period is 4s: wait past it so its timer cannot fire
|
|
397
|
+
// into the next test's globals
|
|
398
|
+
return [4 /*yield*/, new Promise(function (r) { return setTimeout(r, 4200); })];
|
|
399
|
+
case 1:
|
|
400
|
+
// the loader's grace period is 4s: wait past it so its timer cannot fire
|
|
401
|
+
// into the next test's globals
|
|
402
|
+
_b.sent();
|
|
403
|
+
expect(stored['mock-sw-reloaded']).toEqual('1');
|
|
404
|
+
expect(timeline).toEqual(['reload']);
|
|
405
|
+
return [2 /*return*/];
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}); }, 10000);
|
|
409
|
+
it('boots anyway once the reload has already been tried', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
410
|
+
var timeline;
|
|
411
|
+
return __generator(this, function (_a) {
|
|
412
|
+
switch (_a.label) {
|
|
413
|
+
case 0:
|
|
414
|
+
timeline = loadBoot([{ src: 'app.js' }], {
|
|
415
|
+
controller: 'never',
|
|
416
|
+
guard: '1'
|
|
417
|
+
}).timeline;
|
|
418
|
+
return [4 /*yield*/, new Promise(function (r) { return setTimeout(r, 4200); })];
|
|
419
|
+
case 1:
|
|
420
|
+
_a.sent();
|
|
421
|
+
expect(timeline).toEqual(['host/persist.js', 'host/intercept.js', 'app.js']);
|
|
422
|
+
return [2 /*return*/];
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}); }, 10000);
|
|
426
|
+
it('starts without a worker when service workers are unavailable', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
427
|
+
var timeline, document;
|
|
428
|
+
return __generator(this, function (_a) {
|
|
429
|
+
switch (_a.label) {
|
|
430
|
+
case 0:
|
|
431
|
+
jest.resetModules();
|
|
432
|
+
timeline = [];
|
|
433
|
+
document = {
|
|
434
|
+
currentScript: {
|
|
435
|
+
getAttribute: function () { return JSON.stringify([{ src: 'app.js' }]); }
|
|
436
|
+
},
|
|
437
|
+
createElement: function () { return ({}); },
|
|
438
|
+
body: {
|
|
439
|
+
appendChild: function (element) {
|
|
440
|
+
timeline.push(element.src);
|
|
441
|
+
setTimeout(function () { return element.onload && element.onload(); }, 0);
|
|
442
|
+
return element;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
global.document = document;
|
|
447
|
+
global.navigator = {};
|
|
448
|
+
global.window = {};
|
|
449
|
+
global.sessionStorage = { getItem: function () { return null; }, setItem: function () { } };
|
|
450
|
+
global.location = { reload: function () { } };
|
|
451
|
+
jest.spyOn(console, 'error').mockImplementation(function () { });
|
|
452
|
+
require('../assets/boot.js');
|
|
453
|
+
return [4 /*yield*/, settle()];
|
|
454
|
+
case 1:
|
|
455
|
+
_a.sent();
|
|
456
|
+
expect(timeline).toEqual(['host/persist.js', 'host/intercept.js', 'app.js']);
|
|
457
|
+
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('service workers are unavailable'));
|
|
458
|
+
return [2 /*return*/];
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}); });
|
|
462
|
+
});
|
|
@@ -168,6 +168,7 @@ describe('browser-hosted build', function () {
|
|
|
168
168
|
'runtime-core.js',
|
|
169
169
|
'persist.js',
|
|
170
170
|
'intercept.js',
|
|
171
|
+
'boot.js',
|
|
171
172
|
'api-browser.js'
|
|
172
173
|
]), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
173
174
|
asset = _b.value;
|
|
@@ -353,9 +354,50 @@ describe('browser-hosted build', function () {
|
|
|
353
354
|
expect(html).toContain('serverurl=""');
|
|
354
355
|
expect(html).toContain('loginmechanism="Default"');
|
|
355
356
|
expect(html).not.toContain('https://sas.example.com');
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
expect(html).toContain('
|
|
357
|
+
// the loader is a file, never an inline script: an app with a strict
|
|
358
|
+
// Content-Security-Policy would refuse an inline one
|
|
359
|
+
expect(html).toContain('<script src="host/boot.js?v=');
|
|
360
|
+
expect(html).not.toContain('<script>');
|
|
361
|
+
expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', 'boot.js'))).toBe(true);
|
|
362
|
+
// the app's own tags are replayed by the loader, in the order the page has
|
|
363
|
+
// them, and are gone from the page itself
|
|
364
|
+
expect(html).not.toContain('<script src="main.abc123.js"');
|
|
365
|
+
expect(html).toContain('"src":"main.abc123.js","type":"module"');
|
|
366
|
+
return [2 /*return*/];
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}); });
|
|
370
|
+
it('replays every script tag a page loads, not just a bundled entry point', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
371
|
+
var html, tag, specs;
|
|
372
|
+
return __generator(this, function (_a) {
|
|
373
|
+
switch (_a.label) {
|
|
374
|
+
case 0:
|
|
375
|
+
write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
|
|
376
|
+
'<!doctype html><html><body>',
|
|
377
|
+
'<sasjs serverurl="https://sas.example.com" apploc="/Public/app/test"></sasjs>',
|
|
378
|
+
'<script type="application/ld+json">{"name":"app"}</script>',
|
|
379
|
+
'<script defer id="sasjs-adapter" src="sasjs.js"></script>',
|
|
380
|
+
'<script>window.__boot = {ready: false}</script>',
|
|
381
|
+
'<script src="js/backend.js"></script>',
|
|
382
|
+
'<script src="js/game.js" crossorigin="anonymous"></script>',
|
|
383
|
+
'</body></html>'
|
|
384
|
+
].join('\n'));
|
|
385
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
386
|
+
case 1:
|
|
387
|
+
_a.sent();
|
|
388
|
+
html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
|
|
389
|
+
tag = html.match(/data-scripts='([^']*)'/);
|
|
390
|
+
expect(tag).toBeTruthy();
|
|
391
|
+
specs = JSON.parse(tag[1].replace(/'/g, "'").replace(/&/g, '&'));
|
|
392
|
+
expect(specs).toEqual([
|
|
393
|
+
{ src: 'sasjs.js', id: 'sasjs-adapter' },
|
|
394
|
+
{ code: 'window.__boot = {ready: false}' },
|
|
395
|
+
{ src: 'js/backend.js' },
|
|
396
|
+
{ src: 'js/game.js', crossOrigin: 'anonymous' }
|
|
397
|
+
]);
|
|
398
|
+
expect(html).not.toContain('<script src="js/backend.js"');
|
|
399
|
+
// a data block is not a script: it stays in the page
|
|
400
|
+
expect(html).toContain('<script type="application/ld+json">');
|
|
359
401
|
return [2 /*return*/];
|
|
360
402
|
}
|
|
361
403
|
});
|
|
@@ -580,7 +622,7 @@ describe('browser-hosted build', function () {
|
|
|
580
622
|
result = _c.sent();
|
|
581
623
|
expect(result.webFileCount).toBeGreaterThan(0);
|
|
582
624
|
expect((_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn).toHaveBeenCalledWith(expect.stringContaining('no <sasjs> element found'));
|
|
583
|
-
expect((_b = process.logger) === null || _b === void 0 ? void 0 : _b.warn).toHaveBeenCalledWith(expect.stringContaining('no
|
|
625
|
+
expect((_b = process.logger) === null || _b === void 0 ? void 0 : _b.warn).toHaveBeenCalledWith(expect.stringContaining('no script tags found'));
|
|
584
626
|
return [2 /*return*/];
|
|
585
627
|
}
|
|
586
628
|
});
|
package/package.json
CHANGED