@sasjs/cli 6.11.0 → 6.12.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/runtime-core.js +163 -241
- package/build/commands/build/internal/mockSite/assets/sw.js +62 -107
- package/build/commands/build/internal/mockSite/mockSite.js +34 -2
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +120 -1
- 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;
|
|
@@ -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;
|