@sasjs/cli 6.30.1 → 6.31.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.
@@ -0,0 +1,83 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>API console - browser demo</title>
7
+ <meta name="sasjs-mock-version" content="__MOCK_VERSION__" />
8
+ <!--
9
+ The API console for a browser-hosted build.
10
+
11
+ Static, and deliberately so: no bundler, no framework, no network of its
12
+ own beyond the API it is documenting. It is served from <base>/api/ and
13
+ every request it makes is the same request an application makes, so the
14
+ service worker answers it and the answer is the mock's real answer.
15
+
16
+ Nothing is inline: a project that sets `default-src 'self'` without
17
+ 'unsafe-inline' would refuse an inline script or style, and this page
18
+ cannot add a hash to a policy it does not own.
19
+ -->
20
+ <link rel="stylesheet" href="console.css" />
21
+ </head>
22
+ <body>
23
+ <header class="bar">
24
+ <h1>API console</h1>
25
+ <span class="badge">browser demo</span>
26
+ <span class="spacer"></span>
27
+ <span id="who" class="who" title="The account this host answers as"
28
+ >connected</span
29
+ >
30
+ </header>
31
+
32
+ <main class="layout">
33
+ <aside class="list">
34
+ <input
35
+ id="filter"
36
+ type="search"
37
+ placeholder="Filter routes (user, group, drive...)"
38
+ autocomplete="off"
39
+ />
40
+ <div id="routes" class="routes"></div>
41
+ </aside>
42
+
43
+ <section class="detail">
44
+ <div id="empty" class="empty">
45
+ <p>
46
+ This is a <strong>browser-hosted</strong> SASjs server: the API you
47
+ are about to call runs in a service worker in this tab, and its
48
+ drive lives in this browser.
49
+ </p>
50
+ <p>
51
+ Pick a route on the left. Writes are allowed on purpose, and they
52
+ survive a refresh - add <code>?reset=1</code> to this page's URL to
53
+ clear the drive and start again from the shipped state.
54
+ </p>
55
+ </div>
56
+
57
+ <!-- outside #empty: the accounts are most needed while a route is open,
58
+ and #empty is hidden as soon as one is selected -->
59
+ <div id="accounts"></div>
60
+
61
+ <div id="route" hidden>
62
+ <h2 id="title"></h2>
63
+ <p id="summary" class="summary"></p>
64
+ <form id="form" class="form"></form>
65
+ <div class="actions">
66
+ <button id="send" type="button">Send</button>
67
+ <span id="timing" class="timing"></span>
68
+ </div>
69
+ <div id="response" class="response" hidden>
70
+ <div class="responsebar">
71
+ <span id="status" class="status"></span>
72
+ <span class="spacer"></span>
73
+ <button id="copy" type="button" class="link">Copy</button>
74
+ </div>
75
+ <pre id="body"></pre>
76
+ </div>
77
+ </div>
78
+ </section>
79
+ </main>
80
+
81
+ <script src="console.js"></script>
82
+ </body>
83
+ </html>
@@ -1732,6 +1732,22 @@ const handle = async (request, url) => {
1732
1732
  return response
1733
1733
  }
1734
1734
 
1735
+ // The console's own files under `/api/`, replaced at build time from the list the
1736
+ // builder writes the folder from, so the two cannot drift apart.
1737
+ const API_CONSOLE_FILES = [
1738
+ 'index.html',
1739
+ 'console.js',
1740
+ 'console.css',
1741
+ 'operations.json'
1742
+ ]
1743
+
1744
+ const CONSOLE_FILE = new RegExp(
1745
+ '^/api/(' +
1746
+ API_CONSOLE_FILES.map((name) => name.replace(/\./g, '\\.')).join('|') +
1747
+ ')$',
1748
+ 'i'
1749
+ )
1750
+
1735
1751
  self.addEventListener('fetch', (event) => {
1736
1752
  const url = new URL(event.request.url)
1737
1753
  if (url.origin !== self.location.origin) return
@@ -1764,12 +1780,20 @@ self.addEventListener('fetch', (event) => {
1764
1780
  // listing is the server's web route, and an interface that lists apps asks for
1765
1781
  // it on every visit.
1766
1782
  //
1767
- // `/api` and `/api/` on their own are the API's documentation page - a file
1768
- // the build copies into the site - so the worker stays out of the way there
1769
- // and the static host answers it. Every other `/api/...` is an API call on the
1770
- // short spelling, which routeRequest canonicalises to `/SASjsApi`.
1783
+ // `/api` and `/api/` on their own are the console page - `api/index.html`,
1784
+ // which this build writes - so the worker stays out of the way there and the
1785
+ // static host answers it. Every other `/api/...` is an API call on the short
1786
+ // spelling, which routeRequest canonicalises to `/SASjsApi`.
1771
1787
  if (/^\/api\/?$/i.test(path)) return
1772
1788
 
1789
+ // And the console's own files, which live under that prefix: a path naming one
1790
+ // of them is the page's, not an API call, so the static host answers it. Named
1791
+ // one by one on purpose - an extension test would also pass through
1792
+ // `/api/admin/logs/foo.log`, which is a real call on the short spelling. The
1793
+ // list is replaced at build time with the one the builder writes the folder
1794
+ // from, so a file added there cannot arrive here as an API path.
1795
+ if (CONSOLE_FILE.test(path)) return
1796
+
1773
1797
  if (!/^\/(SASjsApi|SASLogon|AppStream|api|app)(\/|$)/i.test(path)) return
1774
1798
  event.respondWith(handle(event.request, url))
1775
1799
  })
@@ -14,6 +14,8 @@ export interface MockSiteResult {
14
14
  * large folder or file. Zero when the site ships no snapshot.
15
15
  */
16
16
  snapshotShards: number;
17
+ /** True when the site carries the API console at <base>/api/. */
18
+ apiConsole: boolean;
17
19
  /** True when a recorded SAS session was shipped, for .sas runs to answer. */
18
20
  sasTranscript: boolean;
19
21
  /**
@@ -1008,6 +1008,67 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
1008
1008
  }
1009
1009
  return { patched: patched, warnings: warnings };
1010
1010
  };
1011
+ /** The files the console consists of, under `<output>/api`. */
1012
+ var API_CONSOLE_FILES = [
1013
+ 'index.html',
1014
+ 'console.js',
1015
+ 'console.css',
1016
+ 'operations.json'
1017
+ ];
1018
+ /**
1019
+ * The API console: a page at `<base>/api/` that lists every operation the API
1020
+ * document declares and sends them against this host.
1021
+ *
1022
+ * The route table is the same one the worker answers from (`ROUTES` from
1023
+ * @sasjs/api), so the console cannot drift from the API: a route the package
1024
+ * adds appears here on the next build, with the parameters it declares.
1025
+ *
1026
+ * Three files and the data, all static. It is a developer surface rather than a
1027
+ * screen of the app, so nothing in the page links to it and nothing preloads it -
1028
+ * a visit costs the demo nothing until someone types the URL.
1029
+ */
1030
+ var writeApiConsole = function (outputFolder, version) {
1031
+ var folder = path_1.default.join(outputFolder, 'api');
1032
+ // `<base>/api/` is this build's, not the project's: a site that ships its own
1033
+ // files there would have them replaced without a word, so it keeps them and
1034
+ // the console is left out
1035
+ var taken = API_CONSOLE_FILES.filter(function (file) {
1036
+ return fs_1.default.existsSync(path_1.default.join(folder, file));
1037
+ });
1038
+ if (taken.length)
1039
+ return {
1040
+ written: false,
1041
+ operations: 0,
1042
+ warning: "The site already carries ".concat(taken
1043
+ .map(function (file) { return 'api/' + file; })
1044
+ .join(', '), ", so the API console was not written. ") +
1045
+ 'Move those files, or set streamConfig.mockApi to false to silence this.'
1046
+ };
1047
+ fs_1.default.mkdirSync(folder, { recursive: true });
1048
+ // The console starts the host itself, so it has to ask for the same worker the
1049
+ // app's loader asks for: a registration is identified by its script URL, and a
1050
+ // bare `sw.js` here would replace the versioned one on every visit to either
1051
+ // page - an install nobody asked for, which is what the version exists to avoid.
1052
+ fs_1.default.writeFileSync(path_1.default.join(folder, 'index.html'), fs_1.default
1053
+ .readFileSync(path_1.default.join(ASSETS_FOLDER, 'api-index.html'), 'utf8')
1054
+ .replace('__MOCK_VERSION__', version));
1055
+ fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'api-console.js'), path_1.default.join(folder, 'console.js'));
1056
+ fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'api-console.css'), path_1.default.join(folder, 'console.css'));
1057
+ var operations = api_1.ROUTES.map(function (route) { return ({
1058
+ id: route.id,
1059
+ method: route.method,
1060
+ path: route.path,
1061
+ tag: route.tag,
1062
+ summary: route.summary,
1063
+ params: route.params,
1064
+ requestBodies: route.requestBodies,
1065
+ responses: route.responses,
1066
+ implemented: route.implemented,
1067
+ reason: route.reason
1068
+ }); });
1069
+ fs_1.default.writeFileSync(path_1.default.join(folder, 'operations.json'), JSON.stringify(operations, null, 2));
1070
+ return { written: true, operations: operations.length };
1071
+ };
1011
1072
  /**
1012
1073
  * Builds a static site that runs the project's JS services (its mocks) in the
1013
1074
  * browser, so a frontend can be tested with no server and no SAS.
@@ -1024,11 +1085,11 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
1024
1085
  */
1025
1086
  function buildMockSite(target) {
1026
1087
  return __awaiter(this, void 0, void 0, function () {
1027
- var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, identity, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, snapshotShards, capture, files, split, _d, _e, shard, transcriptSource, hasTranscript, indexPath, _f, patched, warnings;
1088
+ var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, identity, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, snapshotShards, capture, files, split, _d, _e, shard, transcriptSource, hasTranscript, apiConsole, written, indexPath, _f, patched, warnings;
1028
1089
  var e_11, _g, e_12, _h;
1029
- var _j, _k, _l, _m, _o, _p;
1030
- return __generator(this, function (_q) {
1031
- switch (_q.label) {
1090
+ var _j, _k, _l, _m, _o, _p, _q, _r;
1091
+ return __generator(this, function (_s) {
1092
+ switch (_s.label) {
1032
1093
  case 0:
1033
1094
  projectDir = process.projectDir || process.cwd();
1034
1095
  appLocRaw = target.appLoc;
@@ -1040,7 +1101,7 @@ function buildMockSite(target) {
1040
1101
  path_1.default.join(projectDir, 'sasjsbuild');
1041
1102
  return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
1042
1103
  case 1:
1043
- if (!(_q.sent())) {
1104
+ if (!(_s.sent())) {
1044
1105
  throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
1045
1106
  }
1046
1107
  basePath = normaliseBasePath((_j = target.streamConfig) === null || _j === void 0 ? void 0 : _j.mockWebBasePath);
@@ -1051,7 +1112,7 @@ function buildMockSite(target) {
1051
1112
  webFolder = path_1.default.resolve(projectDir, webSourceRaw);
1052
1113
  return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
1053
1114
  case 2:
1054
- if (!(_q.sent())) {
1115
+ if (!(_s.sent())) {
1055
1116
  throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
1056
1117
  }
1057
1118
  if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
@@ -1146,7 +1207,7 @@ function buildMockSite(target) {
1146
1207
  case 3:
1147
1208
  // 2. the linter, bundled from the installed packages so the code routes answer
1148
1209
  // with the rules this CLI resolved rather than a second implementation
1149
- _q.sent();
1210
+ _s.sent();
1150
1211
  // sw.js sits at the site root: a worker can only control the folder it is
1151
1212
  // served from, and the app has to be controlled from /
1152
1213
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -1199,7 +1260,10 @@ function buildMockSite(target) {
1199
1260
  .readFileSync(swPath, 'utf8')
1200
1261
  .replace("importScripts('host/runtime-core.js')", "importScripts('host/runtime-core.js?v=".concat(hostVersion, "')"))
1201
1262
  .replace("importScripts('host/api-browser.js')", "importScripts('host/api-browser.js?v=".concat(hostVersion, "')"))
1202
- .replace("importScripts('host/lint-browser.js')", "importScripts('host/lint-browser.js?v=".concat(hostVersion, "')")));
1263
+ .replace("importScripts('host/lint-browser.js')", "importScripts('host/lint-browser.js?v=".concat(hostVersion, "')"))
1264
+ // the console's file list, from the one place that writes the folder: a
1265
+ // second copy here would let a new file be served as an API path
1266
+ .replace(/const API_CONSOLE_FILES = \[[^\]]*\]/, "const API_CONSOLE_FILES = ".concat(JSON.stringify(API_CONSOLE_FILES))));
1203
1267
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
1204
1268
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
1205
1269
  snapshotShards = 0;
@@ -1250,6 +1314,13 @@ function buildMockSite(target) {
1250
1314
  buildStamp: buildStamp,
1251
1315
  builtAt: new Date().toISOString()
1252
1316
  }, null, 2));
1317
+ apiConsole = false;
1318
+ if (((_q = target.streamConfig) === null || _q === void 0 ? void 0 : _q.mockApi) !== false) {
1319
+ written = writeApiConsole(outputFolder, hostVersion);
1320
+ apiConsole = written.written;
1321
+ if (written.warning)
1322
+ (_r = process.logger) === null || _r === void 0 ? void 0 : _r.warn(written.warning);
1323
+ }
1253
1324
  indexPath = path_1.default.join(outputFolder, 'index.html');
1254
1325
  _f = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion, basePath, outputFolder), patched = _f.patched, warnings = _f.warnings;
1255
1326
  fs_1.default.writeFileSync(indexPath, patched);
@@ -1262,6 +1333,7 @@ function buildMockSite(target) {
1262
1333
  webFileCount: webFileCount,
1263
1334
  snapshot: hasSnapshot,
1264
1335
  snapshotShards: snapshotShards,
1336
+ apiConsole: apiConsole,
1265
1337
  sasTranscript: hasTranscript,
1266
1338
  identitySource: identity.source
1267
1339
  }];