@sasjs/cli 6.29.0 → 6.30.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/boot.js +23 -1
- package/build/commands/build/internal/mockSite/assets/persist.js +76 -5
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +413 -50
- package/build/commands/build/internal/mockSite/assets/sw.js +182 -12
- package/build/commands/build/internal/mockSite/mockSite.d.ts +23 -0
- package/build/commands/build/internal/mockSite/mockSite.js +245 -29
- package/build/commands/build/internal/mockSite/spec/boot.spec.js +50 -3
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +132 -3
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +179 -1
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +546 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +587 -4
- package/build/commands/create/spec/create.spec.js +22 -18
- package/build/commands/deploy/spec/cbd.spec.js +149 -75
- package/build/utils/test.d.ts +12 -0
- package/build/utils/test.js +66 -1
- package/build/utils/utils.d.ts +8 -0
- package/build/utils/utils.js +16 -4
- package/package.json +2 -2
- package/build/commands/create/spec/angularAppFiles.d.ts +0 -2
- package/build/commands/create/spec/angularAppFiles.js +0 -117
- package/build/commands/create/spec/minimalAppFiles.d.ts +0 -2
- package/build/commands/create/spec/minimalAppFiles.js +0 -44
- package/build/commands/create/spec/reactAppFiles.d.ts +0 -2
- package/build/commands/create/spec/reactAppFiles.js +0 -93
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
*/
|
|
38
38
|
importScripts('host/runtime-core.js')
|
|
39
39
|
importScripts('host/api-browser.js')
|
|
40
|
-
importScripts('host/lint-browser.js')
|
|
41
40
|
|
|
42
41
|
const R = self.SASjsMockRuntime
|
|
43
42
|
|
|
@@ -47,10 +46,6 @@ const R = self.SASjsMockRuntime
|
|
|
47
46
|
// and the few capabilities a static origin does not have, which the package
|
|
48
47
|
// reports as a 501 naming the missing service.
|
|
49
48
|
const API = self.SASjsApi
|
|
50
|
-
// The linter, bundled at build time from the installed @sasjs/api/code and
|
|
51
|
-
// @sasjs/lint (see mockSite.ts). It is a separate script because the linter is
|
|
52
|
-
// not part of the root package a service worker can import.
|
|
53
|
-
const CODE = self.SASjsCode
|
|
54
49
|
|
|
55
50
|
const DRIVE_STORE = R.driveStore
|
|
56
51
|
|
|
@@ -946,6 +941,110 @@ const homeService = API.createHomeService(async () => {
|
|
|
946
941
|
}
|
|
947
942
|
})
|
|
948
943
|
|
|
944
|
+
/**
|
|
945
|
+
* The lint rules a SAS file is linted against: the nearest `.sasjslint` at or
|
|
946
|
+
* above its folder on the drive.
|
|
947
|
+
*
|
|
948
|
+
* This is the server's rule, applied here so the demo behaves like the
|
|
949
|
+
* installation it demonstrates: a folder in the tree carries rules for
|
|
950
|
+
* everything beneath it, and changing a rule is a plain edit of that file. It
|
|
951
|
+
* is why the build seeds a `.sasjslint` at the drive root - a file that is
|
|
952
|
+
* displayed but never read would be worse than absent.
|
|
953
|
+
*
|
|
954
|
+
* Paths go through the package's own normaliser, so a backslashed or escaping
|
|
955
|
+
* path resolves the way the drive routes resolve it rather than by a second set
|
|
956
|
+
* of string rules that could disagree with them.
|
|
957
|
+
*
|
|
958
|
+
* A file that is missing, is not JSON, or parses to something that is not an
|
|
959
|
+
* object is skipped and the walk continues upwards - a broken rules file must
|
|
960
|
+
* never break linting or formatting. A walk that reaches the root with nothing
|
|
961
|
+
* to show returns undefined, which lints against the linter's own defaults: the
|
|
962
|
+
* answer for a drive with no rules file at all.
|
|
963
|
+
*/
|
|
964
|
+
const LINT_CONFIG_FILE = '.sasjslint'
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* The linter, bundled at build time from the installed @sasjs/api/code and
|
|
968
|
+
* @sasjs/lint (see mockSite.ts).
|
|
969
|
+
*
|
|
970
|
+
* Imported on the first code route rather than by the worker at boot: it is
|
|
971
|
+
* around 40KB of the worker's payload for two routes (`code/lint` and
|
|
972
|
+
* `code/format`), and the install has to finish before the page may make its
|
|
973
|
+
* first request - so every visit pays it, including the ones that never open the
|
|
974
|
+
* editor. The build stamps the version into the URL below, so the copy fetched
|
|
975
|
+
* here is cache-busted with the rest of the host.
|
|
976
|
+
*/
|
|
977
|
+
let codeService
|
|
978
|
+
let linter
|
|
979
|
+
|
|
980
|
+
const code = () => {
|
|
981
|
+
if (codeService) return codeService
|
|
982
|
+
importScripts('host/lint-browser.js')
|
|
983
|
+
linter = self.SASjsCode
|
|
984
|
+
codeService = linter.createCodeService({ lintConfig })
|
|
985
|
+
return codeService
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* The package's code service, resolved on first use.
|
|
990
|
+
*
|
|
991
|
+
* A proxy rather than an object listing `lint` and `format`: the package decides
|
|
992
|
+
* which methods a service has, and a facade would answer `undefined` for one a
|
|
993
|
+
* later version adds. Dispatch reads `services.code` once per request and only
|
|
994
|
+
* touches a method on a code route, so nothing below the first method access
|
|
995
|
+
* loads the linter.
|
|
996
|
+
*/
|
|
997
|
+
const lazyCode = () =>
|
|
998
|
+
new Proxy(
|
|
999
|
+
{},
|
|
1000
|
+
{
|
|
1001
|
+
get: (target, property) => {
|
|
1002
|
+
// probing for a promise is not a method lookup: answering it must not
|
|
1003
|
+
// pull 40KB
|
|
1004
|
+
if (typeof property === 'symbol' || property === 'then')
|
|
1005
|
+
return undefined
|
|
1006
|
+
const service = code()
|
|
1007
|
+
const value = service[property]
|
|
1008
|
+
return typeof value === 'function' ? value.bind(service) : value
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
const lintConfig = async (filePath) => {
|
|
1014
|
+
// A path that escapes the drive has no folder to walk from, and a buffer that
|
|
1015
|
+
// names no file has none either: both take the drive root's rules, which is
|
|
1016
|
+
// what a server answers for each - a refused path there falls back to the
|
|
1017
|
+
// root file rather than to the linter's defaults.
|
|
1018
|
+
let folder = API.dirname(API.normaliseDrivePath(filePath) ?? '/')
|
|
1019
|
+
for (;;) {
|
|
1020
|
+
const configPath =
|
|
1021
|
+
folder === '/' ? `/${LINT_CONFIG_FILE}` : `${folder}/${LINT_CONFIG_FILE}`
|
|
1022
|
+
const contents = await DRIVE_STORE.read(configPath).catch((e) => {
|
|
1023
|
+
// a piece that is still on the site is not a folder with no rules file:
|
|
1024
|
+
// swallowing it here would lint against the defaults and never fetch it
|
|
1025
|
+
if (e && e.code === R.PENDING_CODE) throw e
|
|
1026
|
+
return null
|
|
1027
|
+
})
|
|
1028
|
+
const parsed = parseLintConfig(contents)
|
|
1029
|
+
if (parsed) return new linter.LintConfig(parsed)
|
|
1030
|
+
if (folder === '/') return undefined
|
|
1031
|
+
folder = API.dirname(folder)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** A `.sasjslint` as the object a configuration is built from, or null. */
|
|
1036
|
+
const parseLintConfig = (contents) => {
|
|
1037
|
+
if (contents === null || contents === undefined) return null
|
|
1038
|
+
const text =
|
|
1039
|
+
typeof contents === 'string' ? contents : new TextDecoder().decode(contents)
|
|
1040
|
+
try {
|
|
1041
|
+
const parsed = JSON.parse(text)
|
|
1042
|
+
return parsed && typeof parsed === 'object' ? parsed : null
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
return null
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
949
1048
|
const SERVICES = {
|
|
950
1049
|
drive: API.createDriveService(DRIVE_STORE),
|
|
951
1050
|
// The package's browser fixture states one run time; this host serves two, and
|
|
@@ -970,10 +1069,11 @@ const SERVICES = {
|
|
|
970
1069
|
directory: API.createDirectoryService(IDENTITY_STORE, directorySource),
|
|
971
1070
|
appStream: APP_STREAM,
|
|
972
1071
|
home: homeService,
|
|
973
|
-
// the package's own code service over the bundled linter
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
|
|
1072
|
+
// the package's own code service over the bundled linter, with the drive's
|
|
1073
|
+
// own rules: a buffer is linted and formatted against the nearest
|
|
1074
|
+
// `.sasjslint` at or above its folder, so editing that file changes the
|
|
1075
|
+
// editor here exactly as it does on a server
|
|
1076
|
+
code: lazyCode()
|
|
977
1077
|
}
|
|
978
1078
|
|
|
979
1079
|
/** The routes this host performs itself, because the answer is not JSON. */
|
|
@@ -1302,6 +1402,15 @@ const subjectOf = async (request, query) => {
|
|
|
1302
1402
|
* `_debug` means - is still the package's decision.
|
|
1303
1403
|
*/
|
|
1304
1404
|
const runRoute = async (routeId, payload, subject) => {
|
|
1405
|
+
// A program may read any file it likes - the drive is its filesystem - so
|
|
1406
|
+
// every piece of a sharded drive is fetched here rather than guessed from the
|
|
1407
|
+
// request. That is deliberately the whole drive and not just the app being
|
|
1408
|
+
// run: replaying a program because a read failed would run its writes twice,
|
|
1409
|
+
// so the safe choice costs the download of every other app's bundle the first
|
|
1410
|
+
// time anything runs on a cold visit. A page that only lists apps never pays
|
|
1411
|
+
// it; the saving this change is for is on that page.
|
|
1412
|
+
await R.ensureAll()
|
|
1413
|
+
|
|
1305
1414
|
const query = payload.query
|
|
1306
1415
|
const body = payload.body
|
|
1307
1416
|
const tables = payload.files.map((file) => ({
|
|
@@ -1393,7 +1502,38 @@ const contentsResponse = (answer, path) => {
|
|
|
1393
1502
|
})
|
|
1394
1503
|
}
|
|
1395
1504
|
|
|
1396
|
-
|
|
1505
|
+
/**
|
|
1506
|
+
* The shards a request names, fetched before it is answered.
|
|
1507
|
+
*
|
|
1508
|
+
* The drive of a sharded site is not in memory: the build splits a capture so a
|
|
1509
|
+
* first page does not download every app's bundle, and a route that reads a path
|
|
1510
|
+
* has to ask for the piece holding it. The two query parameters below are where
|
|
1511
|
+
* a request names a path outright, which is the common case and the cheap one -
|
|
1512
|
+
* whatever a route resolves from the drive itself is left to the retry in
|
|
1513
|
+
* `routeRequest`, which knows the path from the read that failed rather than
|
|
1514
|
+
* guessing at it from the URL.
|
|
1515
|
+
*/
|
|
1516
|
+
const ensureForRequest = async (url) => {
|
|
1517
|
+
const paths = []
|
|
1518
|
+
for (const name of ['_filePath', '_program']) {
|
|
1519
|
+
const value = url.searchParams.get(name)
|
|
1520
|
+
if (value) paths.push(value)
|
|
1521
|
+
}
|
|
1522
|
+
if (paths.length) await R.ensurePaths(paths)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* One attempt at a request, with the body source a retry can copy again.
|
|
1527
|
+
*
|
|
1528
|
+
* A Request's body can only be read once, and a retry (see the catch below) is a
|
|
1529
|
+
* second read of the same request - so the first attempt keeps an unread copy
|
|
1530
|
+
* beside it and every attempt after that is made from a copy of the copy. Only a
|
|
1531
|
+
* request with a body needs one, and `source` is what carries it through the
|
|
1532
|
+
* recursion.
|
|
1533
|
+
*/
|
|
1534
|
+
const routeRequest = async (request, url, attempts = 0, source = null) => {
|
|
1535
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
|
1536
|
+
const replay = hasBody ? source || request.clone() : null
|
|
1397
1537
|
const path = withoutBase(url.pathname).replace(/\/+$/, '') || '/'
|
|
1398
1538
|
// `/api` is the short spelling of the API, and `/SASjsApi` the canonical one:
|
|
1399
1539
|
// both are answered, so the alias is stripped once here and everything below -
|
|
@@ -1409,6 +1549,7 @@ const routeRequest = async (request, url) => {
|
|
|
1409
1549
|
// (a static asset fetch raced by an API call) must not answer as nobody
|
|
1410
1550
|
await ensureIdentity()
|
|
1411
1551
|
await R.ensureReady()
|
|
1552
|
+
await ensureForRequest(url)
|
|
1412
1553
|
|
|
1413
1554
|
// The app stream is the server's own web surface rather than part of the
|
|
1414
1555
|
// API: the listing, the landing page and the redirect from one to the other
|
|
@@ -1524,6 +1665,20 @@ const routeRequest = async (request, url) => {
|
|
|
1524
1665
|
)
|
|
1525
1666
|
return json(answer.body, answer.status)
|
|
1526
1667
|
} catch (e) {
|
|
1668
|
+
// A read of a file that is still on the site: the route reached a path the
|
|
1669
|
+
// request did not name - an app asset, a `.sasjslint` above the edited file,
|
|
1670
|
+
// a join a service resolves from the drive. The failure names the path, so
|
|
1671
|
+
// the first attempt fetches exactly that piece; a second one in the same
|
|
1672
|
+
// request means it wants more than one, so the rest of the drive comes in.
|
|
1673
|
+
// Bounded, because a refusal that keeps repeating is a real one.
|
|
1674
|
+
if (e && e.code === R.PENDING_CODE && attempts < 2) {
|
|
1675
|
+
if (!attempts && e.path) await R.ensurePaths([e.path])
|
|
1676
|
+
else await R.ensureAll()
|
|
1677
|
+
// a fresh copy per attempt: the one this attempt read is spent
|
|
1678
|
+
const next = replay ? replay.clone() : request
|
|
1679
|
+
return await routeRequest(next, url, attempts + 1, replay)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1527
1682
|
const message = (e && e.stack) || String(e)
|
|
1528
1683
|
const status = (e && e.status) || 500
|
|
1529
1684
|
const refusal = status >= 400 && status < 500
|
|
@@ -1628,7 +1783,7 @@ self.addEventListener('message', (event) => {
|
|
|
1628
1783
|
case 'sasjs-load':
|
|
1629
1784
|
event.waitUntil(
|
|
1630
1785
|
bootIdentity(data)
|
|
1631
|
-
.then(() => R.load(data.files || {}))
|
|
1786
|
+
.then(() => R.load(data.files || {}, data.deleted || []))
|
|
1632
1787
|
.then(async () => {
|
|
1633
1788
|
await persistChanges()
|
|
1634
1789
|
reply({
|
|
@@ -1791,7 +1946,22 @@ self.addEventListener('message', (event) => {
|
|
|
1791
1946
|
}
|
|
1792
1947
|
|
|
1793
1948
|
case 'sasjs-snapshot':
|
|
1794
|
-
|
|
1949
|
+
// a sharded drive is not all in memory, and the caller is exporting it -
|
|
1950
|
+
// for persistence after a change, or as `mock-snapshot.json` for the next
|
|
1951
|
+
// build. Whole means whole: the pieces nobody read are fetched first, or
|
|
1952
|
+
// the export would quietly lose every app bundle it had not opened.
|
|
1953
|
+
event.waitUntil(
|
|
1954
|
+
R.ensureReady()
|
|
1955
|
+
.then(() => R.ensureAll())
|
|
1956
|
+
.then(() => reply({ type: 'sasjs-snapshot', files: R.dumpAll() }))
|
|
1957
|
+
.catch((e) =>
|
|
1958
|
+
reply({
|
|
1959
|
+
type: 'sasjs-snapshot',
|
|
1960
|
+
files: R.dumpAll(),
|
|
1961
|
+
error: (e && e.message) || String(e)
|
|
1962
|
+
})
|
|
1963
|
+
)
|
|
1964
|
+
)
|
|
1795
1965
|
break
|
|
1796
1966
|
|
|
1797
1967
|
case 'sasjs-stats':
|
|
@@ -9,6 +9,11 @@ export interface MockSiteResult {
|
|
|
9
9
|
webFileCount: number;
|
|
10
10
|
/** True when a captured drive snapshot was shipped with the site. */
|
|
11
11
|
snapshot: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* How many pieces the captured drive ships in: the boot shard plus one per
|
|
14
|
+
* large folder or file. Zero when the site ships no snapshot.
|
|
15
|
+
*/
|
|
16
|
+
snapshotShards: number;
|
|
12
17
|
/** True when a recorded SAS session was shipped, for .sas runs to answer. */
|
|
13
18
|
sasTranscript: boolean;
|
|
14
19
|
/**
|
|
@@ -64,6 +69,24 @@ export declare const MAX_SEED_BYTES: number;
|
|
|
64
69
|
* marker format changes.
|
|
65
70
|
*/
|
|
66
71
|
export declare const DRIVE_FORMAT = 1;
|
|
72
|
+
/**
|
|
73
|
+
* The size at which a captured file is fetched on its own rather than with its
|
|
74
|
+
* folder.
|
|
75
|
+
*
|
|
76
|
+
* A shard is a download the visitor waits for, and an app's folder holds both
|
|
77
|
+
* its bundles and the small files a listing shows - its logo, a config. Splitting
|
|
78
|
+
* on size keeps one from dragging the other: the tile asks for the folder's small
|
|
79
|
+
* files, the app's own page asks for the bundle.
|
|
80
|
+
*/
|
|
81
|
+
export declare const SHARD_FILE_BYTES: number;
|
|
82
|
+
/**
|
|
83
|
+
* The size below which a folder's files ride in the boot shard.
|
|
84
|
+
*
|
|
85
|
+
* The boot shard is fetched before the first page renders, so it is paid by every
|
|
86
|
+
* visit. Folders under this size are cheaper there than as a request of their
|
|
87
|
+
* own; folders over it are only fetched when something reads them.
|
|
88
|
+
*/
|
|
89
|
+
export declare const SHARD_GROUP_BYTES: number;
|
|
67
90
|
/**
|
|
68
91
|
* Builds a static site that runs the project's JS services (its mocks) in the
|
|
69
92
|
* browser, so a frontend can be tested with no server and no SAS.
|