@sasjs/cli 6.15.0 → 6.16.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/sw.js +246 -12
- package/build/commands/build/internal/mockSite/mockSite.d.ts +10 -0
- package/build/commands/build/internal/mockSite/mockSite.js +176 -24
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +24 -1
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +239 -88
- package/package.json +2 -1
|
@@ -23,10 +23,19 @@
|
|
|
23
23
|
* The drive is persisted by the page (host/persist.js) and the identity is not:
|
|
24
24
|
* an account, group, permission rule or client created through these routes
|
|
25
25
|
* lives in this worker's memory, so a worker the browser terminates starts again
|
|
26
|
-
* from the fixture. Edits that must survive a restart belong on a server.
|
|
26
|
+
* from the fixture. Edits that must survive a restart belong on a server. The
|
|
27
|
+
* activity log the Admin routes report is in this worker's memory for the same
|
|
28
|
+
* reason.
|
|
29
|
+
*
|
|
30
|
+
* Every operation in @sasjs/api's route table is answered here: the drive, the
|
|
31
|
+
* sign-in flow, the records, the runs, the sessions, the diagnostics and logs,
|
|
32
|
+
* the directory import, the app-stream delete, the linter and the home page. The
|
|
33
|
+
* services are the package's; this file supplies the transport, the worker's own
|
|
34
|
+
* activity, and the browser's answers to questions a server asks its process.
|
|
27
35
|
*/
|
|
28
36
|
importScripts('/host/runtime-core.js')
|
|
29
37
|
importScripts('/host/api-browser.js')
|
|
38
|
+
importScripts('/host/lint-browser.js')
|
|
30
39
|
|
|
31
40
|
const R = self.SASjsMockRuntime
|
|
32
41
|
|
|
@@ -36,6 +45,10 @@ const R = self.SASjsMockRuntime
|
|
|
36
45
|
// and the few capabilities a static origin does not have, which the package
|
|
37
46
|
// reports as a 501 naming the missing service.
|
|
38
47
|
const API = self.SASjsApi
|
|
48
|
+
// The linter, bundled at build time from the installed @sasjs/api/code and
|
|
49
|
+
// @sasjs/lint (see mockSite.ts). It is a separate script because the linter is
|
|
50
|
+
// not part of the root package a service worker can import.
|
|
51
|
+
const CODE = self.SASjsCode
|
|
39
52
|
|
|
40
53
|
const DRIVE_STORE = R.driveStore
|
|
41
54
|
const IDENTITY_STORE = new API.MemoryIdentityStore(API.DEFAULT_IDENTITY)
|
|
@@ -304,6 +317,175 @@ const tablesOf = (files) => {
|
|
|
304
317
|
|
|
305
318
|
const RUNTIME = new BrowserRuntime()
|
|
306
319
|
|
|
320
|
+
// ------------------------------------------------------------- host reports
|
|
321
|
+
// The Admin routes ask a host what it is doing. A browser has no process, but it
|
|
322
|
+
// does have a page, a heap and this worker's own activity, so the reports are
|
|
323
|
+
// built from those rather than refused: the request log below is the log file,
|
|
324
|
+
// and the memory reading is `performance.memory` where the browser exposes it.
|
|
325
|
+
const STARTED = Date.now()
|
|
326
|
+
const HOST_LOG_LIMIT = 2000
|
|
327
|
+
const HOST_LOG = []
|
|
328
|
+
const LOG_FILE = 'app.log'
|
|
329
|
+
// the moment the last line was written, which is the file's mtime - not the
|
|
330
|
+
// moment a report read it
|
|
331
|
+
let LOG_UPDATED = null
|
|
332
|
+
|
|
333
|
+
const logEntry = (line) => {
|
|
334
|
+
LOG_UPDATED = new Date().toISOString()
|
|
335
|
+
HOST_LOG.push(`${LOG_UPDATED} ${line}`)
|
|
336
|
+
if (HOST_LOG.length > HOST_LOG_LIMIT)
|
|
337
|
+
HOST_LOG.splice(0, HOST_LOG.length - HOST_LOG_LIMIT)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const logText = () => HOST_LOG.join('\n')
|
|
341
|
+
const bytesOf = (text) => new TextEncoder().encode(String(text)).length
|
|
342
|
+
|
|
343
|
+
const adminReports = {
|
|
344
|
+
diagnostics: async () => {
|
|
345
|
+
const perf = self.performance || {}
|
|
346
|
+
const heap = perf.memory || {}
|
|
347
|
+
const deviceBytes = Number(navigator.deviceMemory || 0) * 1024 ** 3
|
|
348
|
+
const total = Number(heap.jsHeapSizeLimit || deviceBytes || 0)
|
|
349
|
+
const used = Number(heap.usedJSHeapSize || 0)
|
|
350
|
+
return {
|
|
351
|
+
build: { version: 'dev', gitHash: 'unknown', buildDate: null },
|
|
352
|
+
server: {
|
|
353
|
+
// a page is not a process: the fields exist so a diagnostics screen
|
|
354
|
+
// renders, and their values describe this worker rather than inventing
|
|
355
|
+
// one behind it
|
|
356
|
+
mode: 'browser',
|
|
357
|
+
uptimeSeconds: (Date.now() - STARTED) / 1000,
|
|
358
|
+
startedAt: new Date(STARTED).toISOString(),
|
|
359
|
+
pid: 0,
|
|
360
|
+
nodeVersion: 'n/a',
|
|
361
|
+
platform: navigator.platform || 'browser',
|
|
362
|
+
arch: 'browser',
|
|
363
|
+
cpuCount: navigator.hardwareConcurrency || 1,
|
|
364
|
+
cpuModel: 'unknown',
|
|
365
|
+
loadAverage: [],
|
|
366
|
+
runTimes: ['js'],
|
|
367
|
+
sasjsRoot: '/'
|
|
368
|
+
},
|
|
369
|
+
memory: {
|
|
370
|
+
rssBytes: used,
|
|
371
|
+
heapTotalBytes: total,
|
|
372
|
+
heapUsedBytes: used,
|
|
373
|
+
externalBytes: 0,
|
|
374
|
+
systemTotalBytes: deviceBytes || total,
|
|
375
|
+
systemFreeBytes: Math.max(0, (deviceBytes || total) - used)
|
|
376
|
+
},
|
|
377
|
+
// a static origin cannot read the device's filesystem, which the document
|
|
378
|
+
// allows the host to state
|
|
379
|
+
filesystem: null,
|
|
380
|
+
folders: [],
|
|
381
|
+
logs: {
|
|
382
|
+
folder: 'worker memory',
|
|
383
|
+
// no file until a request has written one, and the stamp is the last
|
|
384
|
+
// write rather than the moment this report was assembled
|
|
385
|
+
fileCount: HOST_LOG.length ? 1 : 0,
|
|
386
|
+
totalBytes: bytesOf(logText()),
|
|
387
|
+
newestFile: HOST_LOG.length ? LOG_FILE : null,
|
|
388
|
+
newestModified: LOG_UPDATED
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
logs: async () =>
|
|
393
|
+
HOST_LOG.length
|
|
394
|
+
? [
|
|
395
|
+
{
|
|
396
|
+
name: LOG_FILE,
|
|
397
|
+
sizeBytes: bytesOf(logText()),
|
|
398
|
+
modified: LOG_UPDATED
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
: [],
|
|
402
|
+
logTail: async (fileName, lines) => {
|
|
403
|
+
if (fileName !== LOG_FILE || !HOST_LOG.length) return undefined
|
|
404
|
+
const tail = HOST_LOG.slice(-lines)
|
|
405
|
+
const content = tail.join('\n')
|
|
406
|
+
return {
|
|
407
|
+
fileName,
|
|
408
|
+
lines: tail.length,
|
|
409
|
+
bytesRead: bytesOf(content),
|
|
410
|
+
truncated: tail.length < HOST_LOG.length,
|
|
411
|
+
content
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The directory the LDAP import reads.
|
|
418
|
+
*
|
|
419
|
+
* A browser has no bind to make, so the source is the fixture this host already
|
|
420
|
+
* signs in with: every account and group it names is present, so the import
|
|
421
|
+
* answers the route (the package owns the idempotency) and creates nothing. A
|
|
422
|
+
* host with a real directory supplies its own source.
|
|
423
|
+
*/
|
|
424
|
+
const directorySource = {
|
|
425
|
+
users: async () =>
|
|
426
|
+
API.DEFAULT_IDENTITY.users.map((user) => ({
|
|
427
|
+
username: user.username,
|
|
428
|
+
displayName: user.displayName
|
|
429
|
+
})),
|
|
430
|
+
groups: async () =>
|
|
431
|
+
API.DEFAULT_IDENTITY.groups.map((group) => ({
|
|
432
|
+
name: group.name,
|
|
433
|
+
members: group.users || []
|
|
434
|
+
}))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The app streams this host published: the site itself.
|
|
439
|
+
*
|
|
440
|
+
* The folder is the identity, so unpublishing the app the site was built for
|
|
441
|
+
* answers `success`; any other folder is the documented 404. The static files
|
|
442
|
+
* stay where they are - a worker cannot delete the site it is served from - so
|
|
443
|
+
* this is a record of the publication, not a filesystem operation.
|
|
444
|
+
*/
|
|
445
|
+
let registryPromise
|
|
446
|
+
const registry = () => {
|
|
447
|
+
if (!registryPromise)
|
|
448
|
+
registryPromise = (async () => {
|
|
449
|
+
let appLoc
|
|
450
|
+
try {
|
|
451
|
+
const config = await (await fetch('/host/mock-config.json')).json()
|
|
452
|
+
appLoc = config && config.appLoc
|
|
453
|
+
} catch (e) {
|
|
454
|
+
appLoc = undefined
|
|
455
|
+
}
|
|
456
|
+
return new API.MemoryAppStreamRegistry(appLoc ? [appLoc] : [])
|
|
457
|
+
})()
|
|
458
|
+
return registryPromise
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* The page at `/`.
|
|
463
|
+
*
|
|
464
|
+
* The site's own index.html is already in the build, so the home route reads it
|
|
465
|
+
* from the static host and hands the package the page and a token of its
|
|
466
|
+
* contents. The package decides what to do with the token; these builds do not
|
|
467
|
+
* reference `index.bundle.js`, so in practice it is returned unchanged.
|
|
468
|
+
*/
|
|
469
|
+
const contentVersion = (text) => {
|
|
470
|
+
let hash = 0x811c9dc5
|
|
471
|
+
for (let i = 0; i < text.length; i++) {
|
|
472
|
+
hash ^= text.charCodeAt(i)
|
|
473
|
+
hash = Math.imul(hash, 0x01000193)
|
|
474
|
+
}
|
|
475
|
+
return (hash >>> 0).toString(16)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const homeService = API.createHomeService(async () => {
|
|
479
|
+
try {
|
|
480
|
+
const response = await fetch('/index.html', { cache: 'no-store' })
|
|
481
|
+
if (!response.ok) return undefined
|
|
482
|
+
const html = await response.text()
|
|
483
|
+
return { html, version: contentVersion(html) }
|
|
484
|
+
} catch (e) {
|
|
485
|
+
return undefined
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
|
|
307
489
|
const SERVICES = {
|
|
308
490
|
drive: API.createDriveService(DRIVE_STORE),
|
|
309
491
|
info: API.createInfoService(API.BROWSER_INFO),
|
|
@@ -315,11 +497,17 @@ const SERVICES = {
|
|
|
315
497
|
runTimes: ['js']
|
|
316
498
|
}),
|
|
317
499
|
session: API.createSessionService(IDENTITY_STORE, RUNTIME),
|
|
318
|
-
authConfig: API.createAuthConfigService({})
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
500
|
+
authConfig: API.createAuthConfigService({}),
|
|
501
|
+
admin: API.createAdminService(adminReports),
|
|
502
|
+
directory: API.createDirectoryService(IDENTITY_STORE, directorySource),
|
|
503
|
+
registry: {
|
|
504
|
+
unpublish: async (appLoc) => (await registry()).unpublish(appLoc)
|
|
505
|
+
},
|
|
506
|
+
home: homeService,
|
|
507
|
+
// the package's own code service over the bundled linter; the host passes no
|
|
508
|
+
// lint config, so a buffer is linted against the linter's defaults - a mock
|
|
509
|
+
// has no `.sasjslint` tree to resolve one from
|
|
510
|
+
code: CODE.createCodeService({})
|
|
323
511
|
}
|
|
324
512
|
|
|
325
513
|
/** The routes this host performs itself, because the answer is not JSON. */
|
|
@@ -663,7 +851,7 @@ const contentsResponse = (answer, path) => {
|
|
|
663
851
|
})
|
|
664
852
|
}
|
|
665
853
|
|
|
666
|
-
const
|
|
854
|
+
const routeRequest = async (request, url) => {
|
|
667
855
|
const path = url.pathname.replace(/\/+$/, '') || '/'
|
|
668
856
|
const query = {}
|
|
669
857
|
for (const [k, v] of url.searchParams.entries()) query[k] = v
|
|
@@ -710,6 +898,14 @@ const handle = async (request, url) => {
|
|
|
710
898
|
const answer = await API.dispatch(SERVICES, routeId, apiRequest)
|
|
711
899
|
if (request.method !== 'GET') await persistChanges()
|
|
712
900
|
|
|
901
|
+
// the shell is a page, not a file the caller named: the document states
|
|
902
|
+
// text/html for it, and the extension of `/` says nothing
|
|
903
|
+
if (routeId === 'GET /')
|
|
904
|
+
return new Response(answer.contents, {
|
|
905
|
+
status: answer.status,
|
|
906
|
+
headers: { 'content-type': 'text/html; charset=utf-8' }
|
|
907
|
+
})
|
|
908
|
+
|
|
713
909
|
if (answer.contents !== undefined)
|
|
714
910
|
return contentsResponse(
|
|
715
911
|
answer,
|
|
@@ -718,6 +914,7 @@ const handle = async (request, url) => {
|
|
|
718
914
|
return json(answer.body, answer.status)
|
|
719
915
|
} catch (e) {
|
|
720
916
|
const message = (e && e.stack) || String(e)
|
|
917
|
+
logEntry(`${request.method} ${path} ERROR ${(e && e.message) || message}`)
|
|
721
918
|
broadcast({ type: 'sasjs-host-error', path, message })
|
|
722
919
|
// A refusal the API decided on carries its status and, where the route
|
|
723
920
|
// answers an envelope, the body to send. The drive's routes send the message
|
|
@@ -736,14 +933,51 @@ const handle = async (request, url) => {
|
|
|
736
933
|
}
|
|
737
934
|
}
|
|
738
935
|
|
|
936
|
+
/**
|
|
937
|
+
* One request, answered and written to the host's activity log.
|
|
938
|
+
*
|
|
939
|
+
* The log is what the Admin routes report: a browser has no server log file, so
|
|
940
|
+
* the worker keeps its own - method, path, status and how long it took - and the
|
|
941
|
+
* three admin routes read it back.
|
|
942
|
+
*/
|
|
943
|
+
const handle = async (request, url) => {
|
|
944
|
+
const started = Date.now()
|
|
945
|
+
const response = await routeRequest(request, url)
|
|
946
|
+
logEntry(
|
|
947
|
+
`${request.method} ${url.pathname} ${response.status} ${
|
|
948
|
+
Date.now() - started
|
|
949
|
+
}ms`
|
|
950
|
+
)
|
|
951
|
+
return response
|
|
952
|
+
}
|
|
953
|
+
|
|
739
954
|
self.addEventListener('fetch', (event) => {
|
|
740
955
|
const url = new URL(event.request.url)
|
|
741
956
|
if (url.origin !== self.location.origin) return
|
|
742
|
-
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
957
|
+
|
|
958
|
+
// The site's own page is answered by the home route, but a navigation must
|
|
959
|
+
// never be at the API's mercy: if that route throws or answers an error, the
|
|
960
|
+
// request falls back to the static host, which holds the same index.html. A
|
|
961
|
+
// broken API route can cost a client its data; it must not cost the app its
|
|
962
|
+
// page.
|
|
963
|
+
if (url.pathname === '/') {
|
|
964
|
+
event.respondWith(
|
|
965
|
+
(async () => {
|
|
966
|
+
try {
|
|
967
|
+
const response = await handle(event.request, url)
|
|
968
|
+
if (response && response.status < 400) return response
|
|
969
|
+
} catch (e) {
|
|
970
|
+
/* fall through to the static host */
|
|
971
|
+
}
|
|
972
|
+
return fetch(event.request)
|
|
973
|
+
})()
|
|
974
|
+
)
|
|
975
|
+
return
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// The API's own paths are answered here, including one the table does not
|
|
979
|
+
// have - falling through to the static host would put the SPA's index.html
|
|
980
|
+
// behind a typo in a caller's URL.
|
|
747
981
|
if (!/^\/(SASjsApi|SASLogon)(\/|$)/i.test(url.pathname)) return
|
|
748
982
|
event.respondWith(handle(event.request, url))
|
|
749
983
|
})
|
|
@@ -28,6 +28,16 @@ export declare const WEBOUT_SENTINELS: {
|
|
|
28
28
|
userId: string;
|
|
29
29
|
apiServerUrl: string;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Builds `lint-browser.js` from the installed packages.
|
|
33
|
+
*
|
|
34
|
+
* The shim sources are written to the system temp folder, never beside this
|
|
35
|
+
* module: an installed package can be read-only (a global install, a pnpm store
|
|
36
|
+
* symlink, a container that builds as another user), and a build that writes
|
|
37
|
+
* inside it fails with EACCES for exactly the users least able to diagnose it.
|
|
38
|
+
* Resolution is kept by naming the CLI's `node_modules` through `nodePaths`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const buildLintBundle: (destination: string) => void;
|
|
31
41
|
/**
|
|
32
42
|
* The bounds on what the browser drive seeds. A byte entry is carried as base64
|
|
33
43
|
* in vfs-seed.json and held in the worker's memory, so a large artefact - an
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
|
|
3
|
+
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
|
|
4
|
+
return cooked;
|
|
5
|
+
};
|
|
2
6
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
7
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
8
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -75,11 +79,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
75
79
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
76
80
|
};
|
|
77
81
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
78
|
-
exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.WEBOUT_SENTINELS = void 0;
|
|
82
|
+
exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.buildLintBundle = exports.WEBOUT_SENTINELS = void 0;
|
|
79
83
|
exports.buildMockSite = buildMockSite;
|
|
80
84
|
var crypto_1 = require("crypto");
|
|
81
85
|
var fs_1 = __importDefault(require("fs"));
|
|
86
|
+
var os_1 = __importDefault(require("os"));
|
|
82
87
|
var path_1 = __importDefault(require("path"));
|
|
88
|
+
var esbuild_1 = require("esbuild");
|
|
83
89
|
var utils_1 = require("@sasjs/utils");
|
|
84
90
|
/**
|
|
85
91
|
* The webout() block is the one @sasjs/utils generates for the server, emitted
|
|
@@ -116,8 +122,141 @@ var ASSETS_FOLDER = path_1.default.join(__dirname, 'assets');
|
|
|
116
122
|
*/
|
|
117
123
|
var API_BROWSER_BUILD = path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
|
|
118
124
|
var toPosix = function (value) { return value.split(path_1.default.sep).join('/'); };
|
|
119
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The linter, as a service worker can run it.
|
|
127
|
+
*
|
|
128
|
+
* `@sasjs/api/code` is a node-only entry because `@sasjs/lint` resolves its
|
|
129
|
+
* configuration by walking a filesystem - but the linter's core (`lintText`,
|
|
130
|
+
* `formatText`) reads only the text it is given and the configuration it is
|
|
131
|
+
* passed, so the two can run anywhere the module graph can be built. This
|
|
132
|
+
* bundle is that graph: the package's own code service, the linter it wraps, and
|
|
133
|
+
* shims for the four node modules reached at import time (`path` and `os` for
|
|
134
|
+
* resolution, `fs` and `@sasjs/utils/file` for the config lookup that a mock
|
|
135
|
+
* never makes, because it hands the linter the default configuration).
|
|
136
|
+
*
|
|
137
|
+
* It is built from the installed @sasjs/api and @sasjs/lint on every mock
|
|
138
|
+
* build, so the rules a client sees are the version the CLI resolved - a
|
|
139
|
+
* committed bundle would be a second copy to drift.
|
|
140
|
+
*/
|
|
141
|
+
var LINT_BUNDLE_FILES = {
|
|
142
|
+
'entry.js': [
|
|
143
|
+
"import './process.js'",
|
|
144
|
+
"import { createCodeService } from '@sasjs/api/code'",
|
|
145
|
+
'globalThis.SASjsCode = { createCodeService }'
|
|
146
|
+
].join('\n'),
|
|
147
|
+
'process.js': [
|
|
148
|
+
'// the linter reads process.env for a default and process.cwd() for a root;',
|
|
149
|
+
'// neither is a real lookup in a worker, and both are answered in one place',
|
|
150
|
+
'if (typeof globalThis.process === "undefined")',
|
|
151
|
+
' globalThis.process = {',
|
|
152
|
+
' env: {},',
|
|
153
|
+
' platform: "browser",',
|
|
154
|
+
' cwd: () => "/",',
|
|
155
|
+
' versions: {},',
|
|
156
|
+
' argv: [],',
|
|
157
|
+
' version: "v0"',
|
|
158
|
+
' }'
|
|
159
|
+
].join('\n'),
|
|
160
|
+
'fs.js': [
|
|
161
|
+
'const unavailable = (name) => () => {',
|
|
162
|
+
' throw new Error(name + " is not available in the browser host")',
|
|
163
|
+
'}',
|
|
164
|
+
'module.exports = {',
|
|
165
|
+
' readFileSync: unavailable("readFileSync"),',
|
|
166
|
+
' writeFileSync: unavailable("writeFileSync"),',
|
|
167
|
+
' existsSync: () => false,',
|
|
168
|
+
' promises: {}',
|
|
169
|
+
'}'
|
|
170
|
+
].join('\n'),
|
|
171
|
+
'os.js': [
|
|
172
|
+
'module.exports = {',
|
|
173
|
+
' homedir: () => "/",',
|
|
174
|
+
' platform: () => "browser",',
|
|
175
|
+
' tmpdir: () => "/tmp",',
|
|
176
|
+
' EOL: "\\n"',
|
|
177
|
+
'}'
|
|
178
|
+
].join('\n'),
|
|
179
|
+
'utils-file.js': [
|
|
180
|
+
'const unavailable = (name) => async () => {',
|
|
181
|
+
' throw new Error(name + " is not available in the browser host")',
|
|
182
|
+
'}',
|
|
183
|
+
'module.exports = {',
|
|
184
|
+
' readFile: unavailable("readFile"),',
|
|
185
|
+
' writeFile: unavailable("writeFile"),',
|
|
186
|
+
' fileExists: async () => false,',
|
|
187
|
+
' listFilesInFolder: async () => []',
|
|
188
|
+
'}'
|
|
189
|
+
].join('\n'),
|
|
190
|
+
'path.js': String.raw(templateObject_1 || (templateObject_1 = __makeTemplateObject(["// the posix half of path, which is all the linter uses: the rules name files,\n// and a mock hands the linter no folder to walk\nconst SEP = String.fromCharCode(92)\nconst clean = (value) =>\n String(value).split(SEP).join(\"/\").replace(//+/g, \"/\")\nconst strip = (value) => clean(value).replace(//+$/, \"\")\nconst basename = (value) => strip(value).split(\"/\").pop() || \"\"\nconst extname = (value) => {\n const base = basename(value)\n const dot = base.lastIndexOf(\".\")\n return dot > 0 ? base.slice(dot) : \"\"\n}\nconst dirname = (value) => {\n const parts = clean(value).split(\"/\")\n parts.pop()\n return parts.join(\"/\") || \".\"\n}\nconst join = (...parts) => clean(parts.filter(Boolean).join(\"/\"))\nmodule.exports = {\n sep: \"/\",\n delimiter: \":\",\n basename,\n extname,\n dirname,\n join,\n resolve: (...parts) => clean(parts.filter(Boolean).join(\"/\")),\n relative: (from, to) => clean(to).replace(clean(from) + \"/\", \"\"),\n isAbsolute: (value) => String(value).startsWith(\"/\"),\n normalize: clean,\n parse: (value) => ({\n root: \"\",\n dir: dirname(value),\n base: basename(value),\n ext: extname(value),\n name: basename(value).slice(0, basename(value).length - extname(value).length)\n })\n}"], ["// the posix half of path, which is all the linter uses: the rules name files,\n// and a mock hands the linter no folder to walk\nconst SEP = String.fromCharCode(92)\nconst clean = (value) =>\n String(value).split(SEP).join(\"/\").replace(/\\/+/g, \"/\")\nconst strip = (value) => clean(value).replace(/\\/+$/, \"\")\nconst basename = (value) => strip(value).split(\"/\").pop() || \"\"\nconst extname = (value) => {\n const base = basename(value)\n const dot = base.lastIndexOf(\".\")\n return dot > 0 ? base.slice(dot) : \"\"\n}\nconst dirname = (value) => {\n const parts = clean(value).split(\"/\")\n parts.pop()\n return parts.join(\"/\") || \".\"\n}\nconst join = (...parts) => clean(parts.filter(Boolean).join(\"/\"))\nmodule.exports = {\n sep: \"/\",\n delimiter: \":\",\n basename,\n extname,\n dirname,\n join,\n resolve: (...parts) => clean(parts.filter(Boolean).join(\"/\")),\n relative: (from, to) => clean(to).replace(clean(from) + \"/\", \"\"),\n isAbsolute: (value) => String(value).startsWith(\"/\"),\n normalize: clean,\n parse: (value) => ({\n root: \"\",\n dir: dirname(value),\n base: basename(value),\n ext: extname(value),\n name: basename(value).slice(0, basename(value).length - extname(value).length)\n })\n}"])))
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* The `node_modules` the CLI's own dependency lives in.
|
|
194
|
+
*
|
|
195
|
+
* esbuild resolves a bare import from the entry file upwards, which is wrong for
|
|
196
|
+
* a scratch entry outside the install: the walk would miss the installed
|
|
197
|
+
* packages entirely. `nodePaths` states where to look instead, and it is taken
|
|
198
|
+
* from `require.resolve` so it is the tree the CLI actually loaded from - a
|
|
199
|
+
* hoisted install, a nested one, and a pnpm store symlink all answer with their
|
|
200
|
+
* own location.
|
|
201
|
+
*/
|
|
202
|
+
var NODE_MODULES = (function () {
|
|
203
|
+
var resolved = require.resolve('@sasjs/api');
|
|
204
|
+
var marker = "".concat(path_1.default.sep, "node_modules").concat(path_1.default.sep);
|
|
205
|
+
var index = resolved.lastIndexOf(marker);
|
|
206
|
+
return index < 0 ? undefined : resolved.slice(0, index + marker.length - 1);
|
|
207
|
+
})();
|
|
208
|
+
/**
|
|
209
|
+
* Builds `lint-browser.js` from the installed packages.
|
|
210
|
+
*
|
|
211
|
+
* The shim sources are written to the system temp folder, never beside this
|
|
212
|
+
* module: an installed package can be read-only (a global install, a pnpm store
|
|
213
|
+
* symlink, a container that builds as another user), and a build that writes
|
|
214
|
+
* inside it fails with EACCES for exactly the users least able to diagnose it.
|
|
215
|
+
* Resolution is kept by naming the CLI's `node_modules` through `nodePaths`.
|
|
216
|
+
*/
|
|
217
|
+
var buildLintBundle = function (destination) {
|
|
120
218
|
var e_1, _a;
|
|
219
|
+
var work = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'sasjs-lint-'));
|
|
220
|
+
try {
|
|
221
|
+
try {
|
|
222
|
+
for (var _b = __values(Object.entries(LINT_BUNDLE_FILES)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
223
|
+
var _d = __read(_c.value, 2), name_1 = _d[0], contents = _d[1];
|
|
224
|
+
fs_1.default.writeFileSync(path_1.default.join(work, name_1), contents);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
228
|
+
finally {
|
|
229
|
+
try {
|
|
230
|
+
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
231
|
+
}
|
|
232
|
+
finally { if (e_1) throw e_1.error; }
|
|
233
|
+
}
|
|
234
|
+
(0, esbuild_1.buildSync)({
|
|
235
|
+
entryPoints: [path_1.default.join(work, 'entry.js')],
|
|
236
|
+
bundle: true,
|
|
237
|
+
platform: 'browser',
|
|
238
|
+
format: 'iife',
|
|
239
|
+
outfile: destination,
|
|
240
|
+
nodePaths: NODE_MODULES ? [NODE_MODULES] : [],
|
|
241
|
+
alias: {
|
|
242
|
+
fs: path_1.default.join(work, 'fs.js'),
|
|
243
|
+
path: path_1.default.join(work, 'path.js'),
|
|
244
|
+
os: path_1.default.join(work, 'os.js'),
|
|
245
|
+
'@sasjs/utils/file': path_1.default.join(work, 'utils-file.js')
|
|
246
|
+
},
|
|
247
|
+
logLevel: 'silent'
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
throw new Error("The browser lint bundle could not be built from @sasjs/api/code: ".concat((error && error.message) || error));
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
fs_1.default.rmSync(work, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
exports.buildLintBundle = buildLintBundle;
|
|
258
|
+
var listFiles = function (folder, base) {
|
|
259
|
+
var e_2, _a;
|
|
121
260
|
if (base === void 0) { base = folder; }
|
|
122
261
|
var entries = fs_1.default.readdirSync(folder, { withFileTypes: true });
|
|
123
262
|
var files = [];
|
|
@@ -131,17 +270,17 @@ var listFiles = function (folder, base) {
|
|
|
131
270
|
files.push(toPosix(path_1.default.relative(base, absolute)));
|
|
132
271
|
}
|
|
133
272
|
}
|
|
134
|
-
catch (
|
|
273
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
135
274
|
finally {
|
|
136
275
|
try {
|
|
137
276
|
if (entries_1_1 && !entries_1_1.done && (_a = entries_1.return)) _a.call(entries_1);
|
|
138
277
|
}
|
|
139
|
-
finally { if (
|
|
278
|
+
finally { if (e_2) throw e_2.error; }
|
|
140
279
|
}
|
|
141
280
|
return files;
|
|
142
281
|
};
|
|
143
282
|
var copyFolder = function (from, to) {
|
|
144
|
-
var
|
|
283
|
+
var e_3, _a;
|
|
145
284
|
fs_1.default.mkdirSync(to, { recursive: true });
|
|
146
285
|
try {
|
|
147
286
|
for (var _b = __values(fs_1.default.readdirSync(from, { withFileTypes: true })), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
@@ -154,12 +293,12 @@ var copyFolder = function (from, to) {
|
|
|
154
293
|
fs_1.default.copyFileSync(source, target);
|
|
155
294
|
}
|
|
156
295
|
}
|
|
157
|
-
catch (
|
|
296
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
158
297
|
finally {
|
|
159
298
|
try {
|
|
160
299
|
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
161
300
|
}
|
|
162
|
-
finally { if (
|
|
301
|
+
finally { if (e_3) throw e_3.error; }
|
|
163
302
|
}
|
|
164
303
|
};
|
|
165
304
|
/**
|
|
@@ -221,7 +360,7 @@ var readDriveFile = function (absolute) {
|
|
|
221
360
|
};
|
|
222
361
|
};
|
|
223
362
|
var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
|
|
224
|
-
var
|
|
363
|
+
var e_4, _a, e_5, _b, e_6, _c;
|
|
225
364
|
if (exclude === void 0) { exclude = []; }
|
|
226
365
|
if (excludeFolders === void 0) { excludeFolders = []; }
|
|
227
366
|
var files = {};
|
|
@@ -247,26 +386,26 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
|
|
|
247
386
|
candidates.push({ key: key, absolute: path_1.default.join(folder, relative) });
|
|
248
387
|
};
|
|
249
388
|
try {
|
|
250
|
-
for (var _d = (
|
|
389
|
+
for (var _d = (e_5 = void 0, __values(listFiles(folder))), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
251
390
|
var relative = _e.value;
|
|
252
391
|
_loop_1(relative);
|
|
253
392
|
}
|
|
254
393
|
}
|
|
255
|
-
catch (
|
|
394
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
256
395
|
finally {
|
|
257
396
|
try {
|
|
258
397
|
if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
|
|
259
398
|
}
|
|
260
|
-
finally { if (
|
|
399
|
+
finally { if (e_5) throw e_5.error; }
|
|
261
400
|
}
|
|
262
401
|
}
|
|
263
402
|
}
|
|
264
|
-
catch (
|
|
403
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
265
404
|
finally {
|
|
266
405
|
try {
|
|
267
406
|
if (directories_1_1 && !directories_1_1.done && (_a = directories_1.return)) _a.call(directories_1);
|
|
268
407
|
}
|
|
269
|
-
finally { if (
|
|
408
|
+
finally { if (e_4) throw e_4.error; }
|
|
270
409
|
}
|
|
271
410
|
// The budget is spent in path order, not in whatever order the folder listing
|
|
272
411
|
// returns, so the same project yields the same drive - and the same warning -
|
|
@@ -294,12 +433,12 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
|
|
|
294
433
|
files[candidate.key] = file.contents;
|
|
295
434
|
}
|
|
296
435
|
}
|
|
297
|
-
catch (
|
|
436
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
298
437
|
finally {
|
|
299
438
|
try {
|
|
300
439
|
if (candidates_1_1 && !candidates_1_1.done && (_c = candidates_1.return)) _c.call(candidates_1);
|
|
301
440
|
}
|
|
302
|
-
finally { if (
|
|
441
|
+
finally { if (e_6) throw e_6.error; }
|
|
303
442
|
}
|
|
304
443
|
return { files: files, skipped: skipped };
|
|
305
444
|
};
|
|
@@ -441,7 +580,7 @@ var patchIndexHtml = function (html, version) {
|
|
|
441
580
|
var scripts = [];
|
|
442
581
|
var tagPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>[ \t]*\r?\n?/gi;
|
|
443
582
|
patched = patched.replace(tagPattern, function (tag, source, body) {
|
|
444
|
-
var
|
|
583
|
+
var e_7, _a;
|
|
445
584
|
var attributes = parseAttributes(source);
|
|
446
585
|
var type = (attributes.type || '').trim().toLowerCase();
|
|
447
586
|
var inlined = !attributes.src;
|
|
@@ -466,16 +605,16 @@ var patchIndexHtml = function (html, version) {
|
|
|
466
605
|
continue;
|
|
467
606
|
if (attribute === 'type' && !attributes.type)
|
|
468
607
|
continue;
|
|
469
|
-
var
|
|
470
|
-
spec[
|
|
608
|
+
var name_2 = REPLAYED_ATTRIBUTES[attribute];
|
|
609
|
+
spec[name_2] = attribute === 'nomodule' ? 'true' : attributes[attribute];
|
|
471
610
|
}
|
|
472
611
|
}
|
|
473
|
-
catch (
|
|
612
|
+
catch (e_7_1) { e_7 = { error: e_7_1 }; }
|
|
474
613
|
finally {
|
|
475
614
|
try {
|
|
476
615
|
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
477
616
|
}
|
|
478
|
-
finally { if (
|
|
617
|
+
finally { if (e_7) throw e_7.error; }
|
|
479
618
|
}
|
|
480
619
|
scripts.push(spec);
|
|
481
620
|
return '';
|
|
@@ -508,7 +647,7 @@ var patchIndexHtml = function (html, version) {
|
|
|
508
647
|
function buildMockSite(target) {
|
|
509
648
|
return __awaiter(this, void 0, void 0, function () {
|
|
510
649
|
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;
|
|
511
|
-
var
|
|
650
|
+
var e_8, _e;
|
|
512
651
|
var _f, _g, _h;
|
|
513
652
|
return __generator(this, function (_j) {
|
|
514
653
|
switch (_j.label) {
|
|
@@ -597,14 +736,24 @@ function buildMockSite(target) {
|
|
|
597
736
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
|
|
598
737
|
}
|
|
599
738
|
}
|
|
600
|
-
catch (
|
|
739
|
+
catch (e_8_1) { e_8 = { error: e_8_1 }; }
|
|
601
740
|
finally {
|
|
602
741
|
try {
|
|
603
742
|
if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
|
|
604
743
|
}
|
|
605
|
-
finally { if (
|
|
744
|
+
finally { if (e_8) throw e_8.error; }
|
|
606
745
|
}
|
|
607
746
|
fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
|
|
747
|
+
// 2. the linter, bundled from the installed packages so the code routes answer
|
|
748
|
+
// with the rules this CLI resolved rather than a second implementation
|
|
749
|
+
return [4 /*yield*/, (0, exports.buildLintBundle)(path_1.default.join(hostFolder, 'lint-browser.js'))
|
|
750
|
+
// sw.js sits at the site root: a worker can only control the folder it is
|
|
751
|
+
// served from, and the app has to be controlled from /
|
|
752
|
+
];
|
|
753
|
+
case 3:
|
|
754
|
+
// 2. the linter, bundled from the installed packages so the code routes answer
|
|
755
|
+
// with the rules this CLI resolved rather than a second implementation
|
|
756
|
+
_j.sent();
|
|
608
757
|
// sw.js sits at the site root: a worker can only control the folder it is
|
|
609
758
|
// served from, and the app has to be controlled from /
|
|
610
759
|
fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
|
|
@@ -630,6 +779,7 @@ function buildMockSite(target) {
|
|
|
630
779
|
'intercept.js',
|
|
631
780
|
'boot.js',
|
|
632
781
|
'api-browser.js',
|
|
782
|
+
'lint-browser.js',
|
|
633
783
|
'webout-prelude.json'
|
|
634
784
|
]
|
|
635
785
|
.map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
|
|
@@ -640,7 +790,8 @@ function buildMockSite(target) {
|
|
|
640
790
|
fs_1.default.writeFileSync(swPath, fs_1.default
|
|
641
791
|
.readFileSync(swPath, 'utf8')
|
|
642
792
|
.replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
|
|
643
|
-
.replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')"))
|
|
793
|
+
.replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')"))
|
|
794
|
+
.replace("importScripts('/host/lint-browser.js')", "importScripts('/host/lint-browser.js?v=".concat(hostVersion, "')")));
|
|
644
795
|
snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
|
|
645
796
|
hasSnapshot = fs_1.default.existsSync(snapshotSource);
|
|
646
797
|
if (hasSnapshot) {
|
|
@@ -673,3 +824,4 @@ function buildMockSite(target) {
|
|
|
673
824
|
});
|
|
674
825
|
});
|
|
675
826
|
}
|
|
827
|
+
var templateObject_1;
|
|
@@ -169,7 +169,8 @@ describe('browser-hosted build', function () {
|
|
|
169
169
|
'persist.js',
|
|
170
170
|
'intercept.js',
|
|
171
171
|
'boot.js',
|
|
172
|
-
'api-browser.js'
|
|
172
|
+
'api-browser.js',
|
|
173
|
+
'lint-browser.js'
|
|
173
174
|
]), _b = _a.next(); !_b.done; _b = _a.next()) {
|
|
174
175
|
asset = _b.value;
|
|
175
176
|
expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', asset))).toBe(true);
|
|
@@ -209,11 +210,33 @@ describe('browser-hosted build', function () {
|
|
|
209
210
|
// a rebuild that changes either file has to change the URL
|
|
210
211
|
expect(sw).toMatch(/importScripts\('\/host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
|
|
211
212
|
expect(sw).toMatch(/importScripts\('\/host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
|
|
213
|
+
expect(sw).toMatch(/importScripts\('\/host\/lint-browser\.js\?v=[0-9a-f]{10}'\)/);
|
|
212
214
|
expect(sw).not.toContain("importScripts('/host/runtime-core.js')");
|
|
213
215
|
return [2 /*return*/];
|
|
214
216
|
}
|
|
215
217
|
});
|
|
216
218
|
}); });
|
|
219
|
+
it('bundles the linter from the installed packages, ready for a worker', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
220
|
+
var bundle, vm, realm;
|
|
221
|
+
return __generator(this, function (_a) {
|
|
222
|
+
switch (_a.label) {
|
|
223
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
224
|
+
case 1:
|
|
225
|
+
_a.sent();
|
|
226
|
+
bundle = fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'lint-browser.js'), 'utf8');
|
|
227
|
+
// the bundle carries the package's code service, not a second linter
|
|
228
|
+
expect(bundle).toContain('SASjsCode');
|
|
229
|
+
expect(bundle).toContain('createCodeService');
|
|
230
|
+
vm = require('vm');
|
|
231
|
+
realm = { globalThis: null, console: console };
|
|
232
|
+
realm.globalThis = realm;
|
|
233
|
+
vm.createContext(realm);
|
|
234
|
+
vm.runInContext(bundle, realm);
|
|
235
|
+
expect(typeof realm.SASjsCode.createCodeService).toBe('function');
|
|
236
|
+
return [2 /*return*/];
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}); });
|
|
217
240
|
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 () {
|
|
218
241
|
var browserBuild, exists;
|
|
219
242
|
return __generator(this, function (_a) {
|
|
@@ -35,17 +35,6 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
35
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
-
var __values = (this && this.__values) || function(o) {
|
|
39
|
-
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
40
|
-
if (m) return m.call(o);
|
|
41
|
-
if (o && typeof o.length === "number") return {
|
|
42
|
-
next: function () {
|
|
43
|
-
if (o && i >= o.length) o = void 0;
|
|
44
|
-
return { value: o && o[i++], done: !o };
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
48
|
-
};
|
|
49
38
|
var __read = (this && this.__read) || function (o, n) {
|
|
50
39
|
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
51
40
|
if (!m) return o;
|
|
@@ -62,13 +51,26 @@ var __read = (this && this.__read) || function (o, n) {
|
|
|
62
51
|
}
|
|
63
52
|
return ar;
|
|
64
53
|
};
|
|
54
|
+
var __values = (this && this.__values) || function(o) {
|
|
55
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
56
|
+
if (m) return m.call(o);
|
|
57
|
+
if (o && typeof o.length === "number") return {
|
|
58
|
+
next: function () {
|
|
59
|
+
if (o && i >= o.length) o = void 0;
|
|
60
|
+
return { value: o && o[i++], done: !o };
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
64
|
+
};
|
|
65
65
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
66
66
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
67
67
|
};
|
|
68
68
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
69
|
var path_1 = __importDefault(require("path"));
|
|
70
70
|
var api_1 = require("@sasjs/api");
|
|
71
|
+
var os_1 = __importDefault(require("os"));
|
|
71
72
|
var yazl_1 = __importDefault(require("yazl"));
|
|
73
|
+
var mockSite_1 = require("../mockSite");
|
|
72
74
|
/**
|
|
73
75
|
* The other half of the parity claim: the service worker is what turns an
|
|
74
76
|
* /SASjsApi/ request into a response, so its framing (webout body, the _debug
|
|
@@ -87,6 +89,19 @@ var drivePath = function (relative) {
|
|
|
87
89
|
var apiBrowserBuild = function () {
|
|
88
90
|
return path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
|
|
89
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* The lint bundle the site ships, built once for the suite from the installed
|
|
94
|
+
* packages - the same call `sasjs build` makes, so the worker under test answers
|
|
95
|
+
* the code routes with the code the site carries.
|
|
96
|
+
*/
|
|
97
|
+
var lintBundlePath;
|
|
98
|
+
var lintBundle = function () {
|
|
99
|
+
if (!lintBundlePath) {
|
|
100
|
+
lintBundlePath = path_1.default.join(os_1.default.tmpdir(), "sasjs-lint-".concat(process.pid, ".js"));
|
|
101
|
+
(0, mockSite_1.buildLintBundle)(lintBundlePath);
|
|
102
|
+
}
|
|
103
|
+
return lintBundlePath;
|
|
104
|
+
};
|
|
90
105
|
var loadWorker = function () {
|
|
91
106
|
jest.resetModules();
|
|
92
107
|
var handlers = {};
|
|
@@ -108,6 +123,13 @@ var loadWorker = function () {
|
|
|
108
123
|
return;
|
|
109
124
|
if (path.includes('api-browser'))
|
|
110
125
|
return require(apiBrowserBuild());
|
|
126
|
+
if (path.includes('lint-browser')) {
|
|
127
|
+
// the bundle assigns globalThis.SASjsCode, and this harness keeps the
|
|
128
|
+
// worker's `self` apart from the module global
|
|
129
|
+
require(lintBundle());
|
|
130
|
+
self.SASjsCode = global.SASjsCode;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
111
133
|
require('../assets/runtime-core.js');
|
|
112
134
|
// The corpus needs the same drive with its writes refused, without building
|
|
113
135
|
// a second site. The flag is flipped after the case has seeded, because a
|
|
@@ -133,10 +155,20 @@ var loadWorker = function () {
|
|
|
133
155
|
};
|
|
134
156
|
};
|
|
135
157
|
require('../assets/sw.js');
|
|
136
|
-
global.fetch = jest.fn(function (
|
|
158
|
+
global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
|
|
159
|
+
var target;
|
|
137
160
|
var _a;
|
|
138
161
|
return __generator(this, function (_b) {
|
|
139
|
-
|
|
162
|
+
target = typeof input === 'string' ? input : (input && input.url) || String(input);
|
|
163
|
+
// a navigation back to the site root: the static host's copy of the page
|
|
164
|
+
if (new URL(target, origin).pathname === '/')
|
|
165
|
+
return [2 /*return*/, new Response('<html><body>static page</body></html>', {
|
|
166
|
+
status: 200,
|
|
167
|
+
headers: { 'content-type': 'text/html' }
|
|
168
|
+
})];
|
|
169
|
+
if (target.includes('index.html'))
|
|
170
|
+
return [2 /*return*/, new Response('<!doctype html><html><head><title>App</title></head><body>home</body></html>', { status: 200, headers: { 'content-type': 'text/html' } })];
|
|
171
|
+
return [2 /*return*/, target.includes('vfs-seed')
|
|
140
172
|
? jsonResponse({
|
|
141
173
|
files: (_a = {},
|
|
142
174
|
_a[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
|
|
@@ -699,85 +731,204 @@ describe('service worker host', function () {
|
|
|
699
731
|
}
|
|
700
732
|
});
|
|
701
733
|
}); });
|
|
702
|
-
it('answers the
|
|
703
|
-
var dispatch,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
switch (_h.label) {
|
|
734
|
+
it('answers the Admin reports from the worker activity log', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
735
|
+
var dispatch, empty, _a, afterFirst, _b, listed, lines, infoLines, diagnostics, report, logs, _c, tail, tailBody, wrongName, missing;
|
|
736
|
+
return __generator(this, function (_d) {
|
|
737
|
+
switch (_d.label) {
|
|
707
738
|
case 0:
|
|
708
739
|
dispatch = seeded().dispatch;
|
|
709
|
-
|
|
710
|
-
['GET', '/SASjsApi/admin/diagnostics', 'admin service'],
|
|
711
|
-
['GET', '/SASjsApi/admin/logs', 'admin service'],
|
|
712
|
-
['GET', '/SASjsApi/admin/logs/app.log', 'admin service'],
|
|
713
|
-
[
|
|
714
|
-
'POST',
|
|
715
|
-
'/SASjsApi/authConfig/synchroniseWithLDAP',
|
|
716
|
-
'directory service'
|
|
717
|
-
]
|
|
718
|
-
];
|
|
719
|
-
_h.label = 1;
|
|
740
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/diagnostics')];
|
|
720
741
|
case 1:
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
742
|
+
empty = _d.sent();
|
|
743
|
+
_a = expect;
|
|
744
|
+
return [4 /*yield*/, empty.json()];
|
|
724
745
|
case 2:
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
746
|
+
_a.apply(void 0, [(_d.sent()).logs]).toEqual({
|
|
747
|
+
folder: 'worker memory',
|
|
748
|
+
fileCount: 0,
|
|
749
|
+
totalBytes: 0,
|
|
750
|
+
newestFile: null,
|
|
751
|
+
newestModified: null
|
|
752
|
+
});
|
|
753
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
|
|
728
754
|
case 3:
|
|
729
|
-
|
|
730
|
-
expect(response.status).toBe(501);
|
|
755
|
+
afterFirst = _d.sent();
|
|
731
756
|
_b = expect;
|
|
732
|
-
return [4 /*yield*/,
|
|
757
|
+
return [4 /*yield*/, afterFirst.json()];
|
|
733
758
|
case 4:
|
|
734
|
-
_b.apply(void 0, [(
|
|
735
|
-
|
|
759
|
+
_b.apply(void 0, [(_d.sent()).files.length]).toBe(1);
|
|
760
|
+
// the file's mtime is the last write, not the moment a report was read:
|
|
761
|
+
// /info was the request immediately before this listing
|
|
762
|
+
return [4 /*yield*/, dispatch('/SASjsApi/info')];
|
|
736
763
|
case 5:
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
764
|
+
// the file's mtime is the last write, not the moment a report was read:
|
|
765
|
+
// /info was the request immediately before this listing
|
|
766
|
+
_d.sent();
|
|
767
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
|
|
768
|
+
case 6: return [4 /*yield*/, (_d.sent()).json()];
|
|
740
769
|
case 7:
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
return [
|
|
744
|
-
case 8:
|
|
745
|
-
try {
|
|
746
|
-
if (cases_1_1 && !cases_1_1.done && (_f = cases_1.return)) _f.call(cases_1);
|
|
747
|
-
}
|
|
748
|
-
finally { if (e_1) throw e_1.error; }
|
|
749
|
-
return [7 /*endfinally*/];
|
|
770
|
+
listed = (_d.sent())
|
|
771
|
+
.files[0];
|
|
772
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/app.log?lines=20')];
|
|
773
|
+
case 8: return [4 /*yield*/, (_d.sent()).json()];
|
|
750
774
|
case 9:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
775
|
+
lines = (_d.sent()).content.split('\n');
|
|
776
|
+
infoLines = lines.filter(function (line) {
|
|
777
|
+
return line.includes('GET /SASjsApi/info 200');
|
|
778
|
+
});
|
|
779
|
+
expect(infoLines.length).toBe(1);
|
|
780
|
+
expect(infoLines[0].startsWith(listed.modified)).toBe(true);
|
|
781
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/diagnostics')];
|
|
754
782
|
case 10:
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
return [4 /*yield*/,
|
|
783
|
+
diagnostics = _d.sent();
|
|
784
|
+
expect(diagnostics.status).toBe(200);
|
|
785
|
+
return [4 /*yield*/, diagnostics.json()];
|
|
758
786
|
case 11:
|
|
759
|
-
|
|
760
|
-
expect(
|
|
761
|
-
|
|
762
|
-
|
|
787
|
+
report = _d.sent();
|
|
788
|
+
expect(report.server.mode).toBe('browser');
|
|
789
|
+
expect(report.filesystem).toBeNull();
|
|
790
|
+
expect(report.logs.fileCount).toBe(1);
|
|
791
|
+
expect(typeof report.memory.heapTotalBytes).toBe('number');
|
|
792
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
|
|
763
793
|
case 12:
|
|
764
|
-
|
|
765
|
-
|
|
794
|
+
logs = _d.sent();
|
|
795
|
+
_c = expect;
|
|
796
|
+
return [4 /*yield*/, logs.json()];
|
|
766
797
|
case 13:
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
798
|
+
_c.apply(void 0, [(_d.sent()).files.map(function (file) { return file.name; })]).toEqual([
|
|
799
|
+
'app.log'
|
|
800
|
+
]);
|
|
801
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/app.log?lines=5')];
|
|
802
|
+
case 14:
|
|
803
|
+
tail = _d.sent();
|
|
804
|
+
return [4 /*yield*/, tail.json()];
|
|
770
805
|
case 15:
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
806
|
+
tailBody = _d.sent();
|
|
807
|
+
expect(tail.status).toBe(200);
|
|
808
|
+
expect(tailBody.content).toContain('GET /SASjsApi/info 200');
|
|
809
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/secret.txt')];
|
|
774
810
|
case 16:
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
811
|
+
wrongName = _d.sent();
|
|
812
|
+
expect(wrongName.status).toBe(400);
|
|
813
|
+
return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/other.log')];
|
|
814
|
+
case 17:
|
|
815
|
+
missing = _d.sent();
|
|
816
|
+
expect(missing.status).toBe(404);
|
|
817
|
+
return [2 /*return*/];
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
}); });
|
|
821
|
+
it('imports the fixture directory, and unpublishes the site itself', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
822
|
+
var dispatch, synced, _a, unpublished, _b, other;
|
|
823
|
+
return __generator(this, function (_c) {
|
|
824
|
+
switch (_c.label) {
|
|
825
|
+
case 0:
|
|
826
|
+
dispatch = seeded().dispatch;
|
|
827
|
+
return [4 /*yield*/, dispatch('/SASjsApi/authConfig/synchroniseWithLDAP', { method: 'POST' })];
|
|
828
|
+
case 1:
|
|
829
|
+
synced = _c.sent();
|
|
830
|
+
expect(synced.status).toBe(200);
|
|
831
|
+
_a = expect;
|
|
832
|
+
return [4 /*yield*/, synced.json()];
|
|
833
|
+
case 2:
|
|
834
|
+
_a.apply(void 0, [_c.sent()]).toEqual({ userCount: 0, groupCount: 0 });
|
|
835
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy', {
|
|
836
|
+
method: 'DELETE',
|
|
837
|
+
headers: { 'content-type': 'application/json' },
|
|
838
|
+
body: JSON.stringify({
|
|
839
|
+
streamServiceName: 'Test',
|
|
840
|
+
appLoc: appLoc
|
|
841
|
+
})
|
|
842
|
+
})];
|
|
843
|
+
case 3:
|
|
844
|
+
unpublished = _c.sent();
|
|
845
|
+
expect(unpublished.status).toBe(200);
|
|
846
|
+
_b = expect;
|
|
847
|
+
return [4 /*yield*/, unpublished.json()];
|
|
848
|
+
case 4:
|
|
849
|
+
_b.apply(void 0, [_c.sent()]).toEqual({ status: 'success' });
|
|
850
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy', {
|
|
851
|
+
method: 'DELETE',
|
|
852
|
+
headers: { 'content-type': 'application/json' },
|
|
853
|
+
body: JSON.stringify({
|
|
854
|
+
streamServiceName: 'Other',
|
|
855
|
+
appLoc: '/Public/app/other'
|
|
856
|
+
})
|
|
857
|
+
})];
|
|
858
|
+
case 5:
|
|
859
|
+
other = _c.sent();
|
|
860
|
+
expect(other.status).toBe(404);
|
|
861
|
+
return [2 /*return*/];
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}); });
|
|
865
|
+
it('lints and formats a buffer with the bundled linter', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
866
|
+
var dispatch, linted, diagnostics, formatted, _a;
|
|
867
|
+
return __generator(this, function (_b) {
|
|
868
|
+
switch (_b.label) {
|
|
869
|
+
case 0:
|
|
870
|
+
dispatch = seeded().dispatch;
|
|
871
|
+
return [4 /*yield*/, dispatch('/SASjsApi/code/lint', runRequest({ code: 'data a;\nrun;\n' }))];
|
|
872
|
+
case 1:
|
|
873
|
+
linted = _b.sent();
|
|
874
|
+
expect(linted.status).toBe(200);
|
|
875
|
+
return [4 /*yield*/, linted.json()];
|
|
876
|
+
case 2:
|
|
877
|
+
diagnostics = _b.sent();
|
|
878
|
+
expect(Array.isArray(diagnostics)).toBe(true);
|
|
879
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
880
|
+
expect(diagnostics[0].message).toContain('Doxygen');
|
|
881
|
+
return [4 /*yield*/, dispatch('/SASjsApi/code/format', runRequest({ code: 'data a;set b;run;' }))];
|
|
882
|
+
case 3:
|
|
883
|
+
formatted = _b.sent();
|
|
884
|
+
expect(formatted.status).toBe(200);
|
|
885
|
+
_a = expect;
|
|
886
|
+
return [4 /*yield*/, formatted.text()];
|
|
887
|
+
case 4:
|
|
888
|
+
_a.apply(void 0, [_b.sent()]).toContain('@brief');
|
|
889
|
+
return [2 /*return*/];
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}); });
|
|
893
|
+
it('serves the build page at the home route, and falls back when it fails', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
894
|
+
var worker, response, _a, runtime, original, fallback, body;
|
|
895
|
+
return __generator(this, function (_b) {
|
|
896
|
+
switch (_b.label) {
|
|
897
|
+
case 0:
|
|
898
|
+
worker = seeded();
|
|
899
|
+
return [4 /*yield*/, worker.dispatch('/')];
|
|
900
|
+
case 1:
|
|
901
|
+
response = _b.sent();
|
|
902
|
+
expect(response.status).toBe(200);
|
|
903
|
+
expect(response.headers.get('content-type')).toContain('text/html');
|
|
904
|
+
_a = expect;
|
|
905
|
+
return [4 /*yield*/, response.text()];
|
|
906
|
+
case 2:
|
|
907
|
+
_a.apply(void 0, [_b.sent()]).toContain('<body>home</body>');
|
|
908
|
+
runtime = worker.self.SASjsMockRuntime;
|
|
909
|
+
original = runtime.ensureReady;
|
|
910
|
+
runtime.ensureReady = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
911
|
+
return __generator(this, function (_a) {
|
|
912
|
+
throw new Error('home route exploded');
|
|
913
|
+
});
|
|
914
|
+
}); };
|
|
915
|
+
_b.label = 3;
|
|
916
|
+
case 3:
|
|
917
|
+
_b.trys.push([3, , 6, 7]);
|
|
918
|
+
return [4 /*yield*/, worker.dispatch('/')];
|
|
919
|
+
case 4:
|
|
920
|
+
fallback = _b.sent();
|
|
921
|
+
expect(fallback.status).toBe(200);
|
|
922
|
+
return [4 /*yield*/, fallback.text()];
|
|
923
|
+
case 5:
|
|
924
|
+
body = _b.sent();
|
|
925
|
+
expect(body).toContain('static page');
|
|
926
|
+
expect(body).not.toContain('home route exploded');
|
|
927
|
+
return [3 /*break*/, 7];
|
|
928
|
+
case 6:
|
|
929
|
+
runtime.ensureReady = original;
|
|
779
930
|
return [7 /*endfinally*/];
|
|
780
|
-
case
|
|
931
|
+
case 7: return [2 /*return*/];
|
|
781
932
|
}
|
|
782
933
|
});
|
|
783
934
|
}); });
|
|
@@ -1856,7 +2007,7 @@ describe('service worker host', function () {
|
|
|
1856
2007
|
}); });
|
|
1857
2008
|
});
|
|
1858
2009
|
describe('conformance corpus', function () {
|
|
1859
|
-
var
|
|
2010
|
+
var e_1, _a;
|
|
1860
2011
|
/**
|
|
1861
2012
|
* The corpus is what "the same API" means: the cases @sasjs/api runs against
|
|
1862
2013
|
* its own services, and against a live sasjs/server, run here against this
|
|
@@ -1865,7 +2016,7 @@ describe('service worker host', function () {
|
|
|
1865
2016
|
* used to have - and it is a test rather than a review.
|
|
1866
2017
|
*/
|
|
1867
2018
|
var httpFor = function (testCase) {
|
|
1868
|
-
var
|
|
2019
|
+
var e_2, _a;
|
|
1869
2020
|
var _b;
|
|
1870
2021
|
var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
|
|
1871
2022
|
var query = new URLSearchParams();
|
|
@@ -1876,12 +2027,12 @@ describe('service worker host', function () {
|
|
|
1876
2027
|
query.set(key, String(value));
|
|
1877
2028
|
}
|
|
1878
2029
|
}
|
|
1879
|
-
catch (
|
|
2030
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
1880
2031
|
finally {
|
|
1881
2032
|
try {
|
|
1882
2033
|
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
1883
2034
|
}
|
|
1884
|
-
finally { if (
|
|
2035
|
+
finally { if (e_2) throw e_2.error; }
|
|
1885
2036
|
}
|
|
1886
2037
|
var suffix = query.toString() ? "?".concat(query) : '';
|
|
1887
2038
|
var init = { method: method };
|
|
@@ -1912,8 +2063,8 @@ describe('service worker host', function () {
|
|
|
1912
2063
|
* does.
|
|
1913
2064
|
*/
|
|
1914
2065
|
var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
1915
|
-
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents,
|
|
1916
|
-
var
|
|
2066
|
+
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_3_1;
|
|
2067
|
+
var e_4, _g, e_3, _h;
|
|
1917
2068
|
return __generator(this, function (_j) {
|
|
1918
2069
|
switch (_j.label) {
|
|
1919
2070
|
case 0:
|
|
@@ -1925,12 +2076,12 @@ describe('service worker host', function () {
|
|
|
1925
2076
|
text["/sasjs/drive/files".concat(file)] = contents;
|
|
1926
2077
|
}
|
|
1927
2078
|
}
|
|
1928
|
-
catch (
|
|
2079
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
1929
2080
|
finally {
|
|
1930
2081
|
try {
|
|
1931
2082
|
if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
|
|
1932
2083
|
}
|
|
1933
|
-
finally { if (
|
|
2084
|
+
finally { if (e_4) throw e_4.error; }
|
|
1934
2085
|
}
|
|
1935
2086
|
return [4 /*yield*/, runtime.load(text)];
|
|
1936
2087
|
case 1:
|
|
@@ -1953,14 +2104,14 @@ describe('service worker host', function () {
|
|
|
1953
2104
|
return [3 /*break*/, 3];
|
|
1954
2105
|
case 6: return [3 /*break*/, 9];
|
|
1955
2106
|
case 7:
|
|
1956
|
-
|
|
1957
|
-
|
|
2107
|
+
e_3_1 = _j.sent();
|
|
2108
|
+
e_3 = { error: e_3_1 };
|
|
1958
2109
|
return [3 /*break*/, 9];
|
|
1959
2110
|
case 8:
|
|
1960
2111
|
try {
|
|
1961
2112
|
if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
|
|
1962
2113
|
}
|
|
1963
|
-
finally { if (
|
|
2114
|
+
finally { if (e_3) throw e_3.error; }
|
|
1964
2115
|
return [7 /*endfinally*/];
|
|
1965
2116
|
case 9: return [2 /*return*/];
|
|
1966
2117
|
}
|
|
@@ -2028,12 +2179,12 @@ describe('service worker host', function () {
|
|
|
2028
2179
|
_loop_1(testCase);
|
|
2029
2180
|
}
|
|
2030
2181
|
}
|
|
2031
|
-
catch (
|
|
2182
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
2032
2183
|
finally {
|
|
2033
2184
|
try {
|
|
2034
2185
|
if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
|
|
2035
2186
|
}
|
|
2036
|
-
finally { if (
|
|
2187
|
+
finally { if (e_1) throw e_1.error; }
|
|
2037
2188
|
}
|
|
2038
2189
|
});
|
|
2039
2190
|
});
|
package/package.json
CHANGED
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"axios": "1.20.0",
|
|
79
79
|
"chalk": "4.1.2",
|
|
80
80
|
"dotenv": "18.0.8",
|
|
81
|
+
"esbuild": "0.28.2",
|
|
81
82
|
"js-base64": "3.9.4",
|
|
82
83
|
"jsdom": "26.0.0",
|
|
83
84
|
"lodash.uniqby": "4.7.0",
|
|
@@ -124,5 +125,5 @@
|
|
|
124
125
|
"ws": "8.21.0",
|
|
125
126
|
"nanoid": "3.3.18"
|
|
126
127
|
},
|
|
127
|
-
"version": "6.
|
|
128
|
+
"version": "6.16.0"
|
|
128
129
|
}
|