@sasjs/cli 6.11.0 → 6.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +163 -241
- package/build/commands/build/internal/mockSite/assets/sw.js +62 -107
- package/build/commands/build/internal/mockSite/mockSite.js +35 -3
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +125 -2
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +380 -159
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +240 -20
- package/package.json +3 -2
|
@@ -100,6 +100,21 @@ exports.WEBOUT_SENTINELS = {
|
|
|
100
100
|
apiServerUrl: '__SASJS_WEBOUT_APIURL__'
|
|
101
101
|
};
|
|
102
102
|
var ASSETS_FOLDER = path_1.default.join(__dirname, 'assets');
|
|
103
|
+
/**
|
|
104
|
+
* The browser build of @sasjs/api, from the installed package rather than a copy
|
|
105
|
+
* in this folder: the worker answers the drive with the same rules sasjs/server
|
|
106
|
+
* does, so the version that ships is the version the CLI resolves - and a stale
|
|
107
|
+
* second copy is exactly the drift the package exists to remove.
|
|
108
|
+
*
|
|
109
|
+
* The path is walked from `require.resolve` because `exports` in @sasjs/api
|
|
110
|
+
* exposes only the package root - `require.resolve('@sasjs/api/browser.js')`
|
|
111
|
+
* fails with ERR_PACKAGE_PATH_NOT_EXPORTED, and no subpath can reach
|
|
112
|
+
* `build/browser.js`. The walk lands on that file only while `main` stays
|
|
113
|
+
* `build/cjs/index.js`; a change to `main` or `exports` in the package repoints
|
|
114
|
+
* it silently, so a missing file is reported by name rather than as a bare
|
|
115
|
+
* ENOENT three levels up.
|
|
116
|
+
*/
|
|
117
|
+
var API_BROWSER_BUILD = path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
|
|
103
118
|
var toPosix = function (value) { return value.split(path_1.default.sep).join('/'); };
|
|
104
119
|
var listFiles = function (folder, base) {
|
|
105
120
|
var e_1, _a;
|
|
@@ -326,7 +341,7 @@ var countJsPrograms = function (files) {
|
|
|
326
341
|
return inProgramFolder && key.toLowerCase().endsWith('.js');
|
|
327
342
|
}).length;
|
|
328
343
|
};
|
|
329
|
-
var loaderScript = function (mainScript, version) { return "<script>\n // Boot order: register the service worker that hosts the mock backend, wait\n // until it controls this page, hand it the persisted drive, install the XHR\n // bridge, then start the app.\n (function () {\n function load(src, next, isModule) {\n var s = document.createElement('script')\n if (isModule) s.type = 'module'\n s.src = src\n s.onload = next\n s.onerror = function () { console.error('[mock] failed to load ' + src); next() }\n document.body.appendChild(s)\n }\n function start() {\n load('host/persist.js?v=".concat(version, "', function () {\n Promise.resolve(window.__sasjsMockReady).then(function (boot) {\n if (boot && boot.error) console.error('[mock] boot: ' + boot.error)\n load('host/intercept.js?v=").concat(version, "', function () {\n load('").concat(mainScript, "', function () {}, true)\n })\n })\n })\n }\n if (!('serviceWorker' in navigator)) {\n console.error('[mock] service workers are unavailable - the mock backend cannot run')\n start()\n return\n }\n navigator.serviceWorker\n .register('/sw.js', { scope: '/' })\n .then(function () { return navigator.serviceWorker.ready })\n .then(function () {\n if (navigator.serviceWorker.controller) return\n return new Promise(function (resolve) {\n var timer = setTimeout(resolve, 4000)\n navigator.serviceWorker.addEventListener('controllerchange', function () {\n clearTimeout(timer)\n resolve()\n })\n })\n })\n .then(function () {\n if (navigator.serviceWorker.controller) return start()\n if (!sessionStorage.getItem('mock-sw-reloaded')) {\n sessionStorage.setItem('mock-sw-reloaded', '1')\n location.reload()\n return\n }\n console.error('[mock] no service worker controller - requests will fail')\n start()\n })\n .catch(function (err) {\n console.error('[mock] service worker registration failed: ' + err)\n start()\n })\n })()\n </script>"); };
|
|
344
|
+
var loaderScript = function (mainScript, version) { return "<script>\n // Boot order: register the service worker that hosts the mock backend, wait\n // until it controls this page, hand it the persisted drive, install the XHR\n // bridge, then start the app.\n (function () {\n function load(src, next, isModule) {\n var s = document.createElement('script')\n if (isModule) s.type = 'module'\n s.src = src\n s.onload = next\n s.onerror = function () { console.error('[mock] failed to load ' + src); next() }\n document.body.appendChild(s)\n }\n function start() {\n load('host/persist.js?v=".concat(version, "', function () {\n Promise.resolve(window.__sasjsMockReady).then(function (boot) {\n if (boot && boot.error) console.error('[mock] boot: ' + boot.error)\n load('host/intercept.js?v=").concat(version, "', function () {\n load('").concat(mainScript, "', function () {}, true)\n })\n })\n })\n }\n if (!('serviceWorker' in navigator)) {\n console.error('[mock] service workers are unavailable - the mock backend cannot run')\n start()\n return\n }\n navigator.serviceWorker\n // The worker script's own URL is the only thing a returning visitor\n // cannot bypass: the host files behind it are cache-busted with a\n // version query, but a cached sw.js keeps serving the previous\n // build's worker until the browser next revalidates it - and that\n // worker imports the previous runtime, which may know nothing of\n // what this build deploys. Stamping the script URL makes the new\n // worker a different script, so it installs and takes over at once;\n // updateViaCache keeps a proxy from answering the check with the\n // old copy.\n .register('/sw.js?v=").concat(version, "', { scope: '/', updateViaCache: 'none' })\n .then(function () { return navigator.serviceWorker.ready })\n .then(function () {\n if (navigator.serviceWorker.controller) return\n return new Promise(function (resolve) {\n var timer = setTimeout(resolve, 4000)\n navigator.serviceWorker.addEventListener('controllerchange', function () {\n clearTimeout(timer)\n resolve()\n })\n })\n })\n .then(function () {\n if (navigator.serviceWorker.controller) return start()\n if (!sessionStorage.getItem('mock-sw-reloaded')) {\n sessionStorage.setItem('mock-sw-reloaded', '1')\n location.reload()\n return\n }\n console.error('[mock] no service worker controller - requests will fail')\n start()\n })\n .catch(function (err) {\n console.error('[mock] service worker registration failed: ' + err)\n start()\n })\n })()\n </script>"); };
|
|
330
345
|
/**
|
|
331
346
|
* Points the app at its own origin and replaces its bundle tag with the
|
|
332
347
|
* boot loader. Both changes are reported when the expected markup is absent,
|
|
@@ -417,6 +432,15 @@ function buildMockSite(target) {
|
|
|
417
432
|
throw new Error("No .js programs in ".concat(buildFolder, "/services, /jobs or /tests. This target builds SAS code, which cannot run in a browser. ") +
|
|
418
433
|
'Build a target whose services are JavaScript (typically a mocks project).');
|
|
419
434
|
}
|
|
435
|
+
// The package supplies the drive rules the worker answers with, so an install
|
|
436
|
+
// without its browser build is a build that cannot run. Checked here, with the
|
|
437
|
+
// other refusals, so a missing dependency leaves the previous site in place -
|
|
438
|
+
// and named, rather than a bare ENOENT from a path three levels up.
|
|
439
|
+
if (!fs_1.default.existsSync(API_BROWSER_BUILD)) {
|
|
440
|
+
throw new Error("The @sasjs/api browser build is missing: expected it at ".concat(API_BROWSER_BUILD, ". ") +
|
|
441
|
+
'Reinstall dependencies (npm ci) so the package that ships the drive rules is present, ' +
|
|
442
|
+
'or check that @sasjs/api still publishes build/browser.js.');
|
|
443
|
+
}
|
|
420
444
|
if (skipped.length) {
|
|
421
445
|
(_h = process.logger) === null || _h === void 0 ? void 0 : _h.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: ") +
|
|
422
446
|
"".concat(skipped
|
|
@@ -454,6 +478,7 @@ function buildMockSite(target) {
|
|
|
454
478
|
}
|
|
455
479
|
finally { if (e_6) throw e_6.error; }
|
|
456
480
|
}
|
|
481
|
+
fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
|
|
457
482
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
458
483
|
// served from, and the app has to be controlled from /
|
|
459
484
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
|
|
@@ -473,7 +498,13 @@ function buildMockSite(target) {
|
|
|
473
498
|
})
|
|
474
499
|
}));
|
|
475
500
|
hostVersion = (0, crypto_1.createHash)('md5')
|
|
476
|
-
.update([
|
|
501
|
+
.update([
|
|
502
|
+
'runtime-core.js',
|
|
503
|
+
'persist.js',
|
|
504
|
+
'intercept.js',
|
|
505
|
+
'api-browser.js',
|
|
506
|
+
'webout-prelude.json'
|
|
507
|
+
]
|
|
477
508
|
.map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
|
|
478
509
|
.reduce(function (all, buf) { return Buffer.concat([all, buf]); }, Buffer.alloc(0)))
|
|
479
510
|
.digest('hex')
|
|
@@ -481,7 +512,8 @@ function buildMockSite(target) {
|
|
|
481
512
|
swPath = path_1.default.join(outputFolder, 'sw.js');
|
|
482
513
|
fs_1.default.writeFileSync(swPath, fs_1.default
|
|
483
514
|
.readFileSync(swPath, 'utf8')
|
|
484
|
-
.replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
|
|
515
|
+
.replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
|
|
516
|
+
.replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')")));
|
|
485
517
|
snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
|
|
486
518
|
hasSnapshot = fs_1.default.existsSync(snapshotSource);
|
|
487
519
|
if (hasSnapshot) {
|
|
@@ -167,7 +167,8 @@ describe('browser-hosted build', function () {
|
|
|
167
167
|
'md5.js',
|
|
168
168
|
'runtime-core.js',
|
|
169
169
|
'persist.js',
|
|
170
|
-
'intercept.js'
|
|
170
|
+
'intercept.js',
|
|
171
|
+
'api-browser.js'
|
|
171
172
|
]), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
172
173
|
asset = _b.value;
|
|
173
174
|
expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', asset))).toBe(true);
|
|
@@ -189,6 +190,124 @@ describe('browser-hosted build', function () {
|
|
|
189
190
|
}
|
|
190
191
|
});
|
|
191
192
|
}); });
|
|
193
|
+
it('ships the @sasjs/api build the CLI resolves, and versions both imports', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
194
|
+
var shipped, installed, sw;
|
|
195
|
+
return __generator(this, function (_a) {
|
|
196
|
+
switch (_a.label) {
|
|
197
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
|
|
198
|
+
// the same file the installed package carries: a vendored copy is exactly
|
|
199
|
+
// how the worker and the server drift apart
|
|
200
|
+
];
|
|
201
|
+
case 1:
|
|
202
|
+
_a.sent();
|
|
203
|
+
shipped = fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'api-browser.js'));
|
|
204
|
+
installed = fs_1.default.readFileSync(path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js'));
|
|
205
|
+
expect(shipped.equals(installed)).toBe(true);
|
|
206
|
+
sw = fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8');
|
|
207
|
+
// both imports carry the host version: a worker caches what it imports, so
|
|
208
|
+
// a rebuild that changes either file has to change the URL
|
|
209
|
+
expect(sw).toMatch(/importScripts\('\/host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
|
|
210
|
+
expect(sw).toMatch(/importScripts\('\/host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
|
|
211
|
+
expect(sw).not.toContain("importScripts('/host/runtime-core.js')");
|
|
212
|
+
return [2 /*return*/];
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}); });
|
|
216
|
+
it('names the package when its browser build is missing, and keeps the previous site', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
217
|
+
var browserBuild, exists;
|
|
218
|
+
return __generator(this, function (_a) {
|
|
219
|
+
switch (_a.label) {
|
|
220
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
221
|
+
case 1:
|
|
222
|
+
_a.sent();
|
|
223
|
+
write(path_1.default.join(out(), 'stale.txt'), 'previous');
|
|
224
|
+
browserBuild = path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
|
|
225
|
+
exists = jest.spyOn(fs_1.default, 'existsSync');
|
|
226
|
+
exists.mockImplementation(function (checked) { return String(checked) !== browserBuild; });
|
|
227
|
+
_a.label = 2;
|
|
228
|
+
case 2:
|
|
229
|
+
_a.trys.push([2, , 4, 5]);
|
|
230
|
+
return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target())).rejects.toThrow(/@sasjs\/api browser build is missing/)];
|
|
231
|
+
case 3:
|
|
232
|
+
_a.sent();
|
|
233
|
+
return [3 /*break*/, 5];
|
|
234
|
+
case 4:
|
|
235
|
+
exists.mockRestore();
|
|
236
|
+
return [7 /*endfinally*/];
|
|
237
|
+
case 5:
|
|
238
|
+
// and the refusal lands before anything is written, so the site that was
|
|
239
|
+
// there is still there
|
|
240
|
+
expect(fs_1.default.existsSync(path_1.default.join(out(), 'stale.txt'))).toBe(true);
|
|
241
|
+
expect(fs_1.default.readFileSync(path_1.default.join(out(), 'stale.txt'), 'utf8')).toBe('previous');
|
|
242
|
+
return [2 /*return*/];
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}); });
|
|
246
|
+
/**
|
|
247
|
+
* The worker's scripts share one global scope, so a name declared at the top
|
|
248
|
+
* level of two of them is a SyntaxError in the browser - and invisible to this
|
|
249
|
+
* suite, where each file is a module of its own. `importScripts` is the
|
|
250
|
+
* browser's rule: evaluate the files the build wrote, in one context, once.
|
|
251
|
+
*/
|
|
252
|
+
it('evaluates the shipped scripts in one global scope, the way a worker does', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
253
|
+
var vm, sandbox, context, imported;
|
|
254
|
+
return __generator(this, function (_a) {
|
|
255
|
+
switch (_a.label) {
|
|
256
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
257
|
+
case 1:
|
|
258
|
+
_a.sent();
|
|
259
|
+
vm = require('vm');
|
|
260
|
+
sandbox = {
|
|
261
|
+
console: console,
|
|
262
|
+
Request: Request,
|
|
263
|
+
Response: Response,
|
|
264
|
+
Headers: Headers,
|
|
265
|
+
URL: URL,
|
|
266
|
+
FormData: FormData,
|
|
267
|
+
Blob: Blob,
|
|
268
|
+
TextEncoder: TextEncoder,
|
|
269
|
+
TextDecoder: TextDecoder,
|
|
270
|
+
Uint8Array: Uint8Array,
|
|
271
|
+
ArrayBuffer: ArrayBuffer,
|
|
272
|
+
atob: atob,
|
|
273
|
+
btoa: btoa,
|
|
274
|
+
setTimeout: setTimeout,
|
|
275
|
+
clearTimeout: clearTimeout,
|
|
276
|
+
location: { origin: 'https://mock.example.com' },
|
|
277
|
+
clients: { claim: function () { }, matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
278
|
+
return [2 /*return*/, []];
|
|
279
|
+
}); }); } },
|
|
280
|
+
skipWaiting: function () { },
|
|
281
|
+
addEventListener: function () { },
|
|
282
|
+
fetch: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
283
|
+
return [2 /*return*/, new Response('{}', { headers: { 'content-type': 'application/json' } })];
|
|
284
|
+
}); }); }
|
|
285
|
+
};
|
|
286
|
+
sandbox.self = sandbox;
|
|
287
|
+
context = vm.createContext(sandbox);
|
|
288
|
+
imported = new Set();
|
|
289
|
+
sandbox.importScripts = function (url) {
|
|
290
|
+
if (imported.has(url))
|
|
291
|
+
return;
|
|
292
|
+
imported.add(url);
|
|
293
|
+
var file = path_1.default.join(out(), url.replace(/^\//, '').split('?')[0]);
|
|
294
|
+
vm.runInContext(fs_1.default.readFileSync(file, 'utf8'), context, {
|
|
295
|
+
filename: file
|
|
296
|
+
});
|
|
297
|
+
};
|
|
298
|
+
expect(function () {
|
|
299
|
+
return vm.runInContext(fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8'), context, {
|
|
300
|
+
filename: 'sw.js'
|
|
301
|
+
});
|
|
302
|
+
}).not.toThrow();
|
|
303
|
+
// both scripts are loaded, and the drive the API is handed came from the
|
|
304
|
+
// runtime rather than a second implementation
|
|
305
|
+
expect(Object.keys(sandbox.SASjsMockRuntime)).toContain('driveStore');
|
|
306
|
+
expect(typeof sandbox.SASjsApi.createDriveService).toBe('function');
|
|
307
|
+
return [2 /*return*/];
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}); });
|
|
192
311
|
it('ships the webout() block with sentinels the runtime fills in', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
193
312
|
var prelude, _a, _b, sentinel;
|
|
194
313
|
var e_2, _c;
|
|
@@ -234,7 +353,11 @@ describe('browser-hosted build', function () {
|
|
|
234
353
|
expect(html).toContain('serverurl=""');
|
|
235
354
|
expect(html).toContain('loginmechanism="Default"');
|
|
236
355
|
expect(html).not.toContain('https://sas.example.com');
|
|
237
|
-
expect(html).toContain("register('/sw.js
|
|
356
|
+
expect(html).toContain("register('/sw.js?v=");
|
|
357
|
+
// A cached worker script must not keep a returning visitor on the previous
|
|
358
|
+
// build's runtime: the URL carries the host version, and the check skips
|
|
359
|
+
// the HTTP cache.
|
|
360
|
+
expect(html).toContain("updateViaCache: 'none'");
|
|
238
361
|
expect(html).toContain('host/persist.js');
|
|
239
362
|
expect(html).toContain('main.abc123.js');
|
|
240
363
|
return [2 /*return*/];
|