@sasjs/cli 5.1.1 → 6.1.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,228 @@
1
+ /**
2
+ * Service-worker host for the browser build.
3
+ *
4
+ * Running the backend here (rather than inside the page) means every request is
5
+ * a real network-layer request: DevTools > Network lists it as "(from
6
+ * ServiceWorker)", with the request payload and the response body inspectable.
7
+ *
8
+ * Storage is owned by the page (host/persist.js) because a service worker can
9
+ * be terminated between requests, and some browsers do not share IndexedDB
10
+ * between the page and its worker. The page hands in the drive at boot and
11
+ * receives the changed files back after every run.
12
+ *
13
+ * TRUST MODEL: this is a single-user, no-auth host. It answers every /SASjsApi/
14
+ * path on the site's origin, runs whatever program the request names, and runs
15
+ * the code a code/execute request carries - the same contract a SASjs server
16
+ * offers, minus the server. Nothing here is a boundary: a page at this origin
17
+ * can already do all of it, because a visitor's own browser is the client. So
18
+ * a site holding anything private, or one served from a shared origin, belongs
19
+ * behind a real server rather than a static host.
20
+ */
21
+ importScripts('/host/runtime-core.js')
22
+
23
+ const R = self.SASjsMockRuntime
24
+ const LOGS_SEPARATOR = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
25
+
26
+ self.addEventListener('install', () => self.skipWaiting())
27
+ self.addEventListener('activate', (event) =>
28
+ event.waitUntil(self.clients.claim())
29
+ )
30
+
31
+ const json = (obj, status) =>
32
+ new Response(JSON.stringify(obj, null, 2), {
33
+ status: status || 200,
34
+ headers: { 'content-type': 'application/json' }
35
+ })
36
+
37
+ const text = (body, status) =>
38
+ new Response(body, {
39
+ status: status || 200,
40
+ headers: { 'content-type': 'text/plain' }
41
+ })
42
+
43
+ const broadcast = async (message) => {
44
+ const clients = await self.clients.matchAll({ includeUncontrolled: true })
45
+ clients.forEach((client) => client.postMessage(message))
46
+ }
47
+
48
+ const persistChanges = async () => {
49
+ const files = R.takeDirty()
50
+ if (Object.keys(files).length)
51
+ await broadcast({ type: 'sasjs-persist', files })
52
+ }
53
+
54
+ const readPayload = async (request) => {
55
+ if (request.method === 'GET' || request.method === 'HEAD')
56
+ return { params: {}, tables: [] }
57
+ const type = request.headers.get('content-type') || ''
58
+ if (!type.includes('application/json')) {
59
+ try {
60
+ const form = await request.formData()
61
+ const params = {}
62
+ const tables = []
63
+ for (const [k, v] of form.entries()) {
64
+ if (typeof v === 'string') params[k] = v
65
+ else
66
+ tables.push({
67
+ name: k,
68
+ filename: v.name || k + '.csv',
69
+ csv: await v.text()
70
+ })
71
+ }
72
+ return { params, tables }
73
+ } catch (e) {
74
+ return { params: {}, tables: [] }
75
+ }
76
+ }
77
+ const body = await request.json()
78
+ return { params: body.params || {}, tables: body.tables || [], json: body }
79
+ }
80
+
81
+ const handle = async (request, url) => {
82
+ const path = url.pathname.replace(/\/+$/, '')
83
+ const query = {}
84
+ for (const [k, v] of url.searchParams.entries()) query[k] = v
85
+
86
+ try {
87
+ await R.ensureReady()
88
+
89
+ if (path.endsWith('/SASjsApi/info')) {
90
+ return json({
91
+ mode: 'desktop',
92
+ cors: 'disabled',
93
+ whiteList: [],
94
+ protocol: 'browser',
95
+ runTimes: ['js']
96
+ })
97
+ }
98
+
99
+ if (path.endsWith('/SASjsApi/drive/folder')) {
100
+ return json(await R.list(query._folderPath || ''))
101
+ }
102
+
103
+ if (path.endsWith('/SASjsApi/drive/filetree')) {
104
+ return json(R.tree())
105
+ }
106
+
107
+ if (path.endsWith('/SASjsApi/drive/file') && request.method === 'GET') {
108
+ return text(R.read(query._filePath || '').contents)
109
+ }
110
+
111
+ if (
112
+ path.endsWith('/SASjsApi/stp/execute') ||
113
+ path.endsWith('/SASjsApi/code/execute')
114
+ ) {
115
+ const payload = await readPayload(request)
116
+ const merged = Object.assign({}, query, payload.params)
117
+ const program = merged._program
118
+ const code =
119
+ payload.json && payload.json.code ? payload.json.code : undefined
120
+ if (!program && !code)
121
+ return json({ message: '_program is required' }, 400)
122
+
123
+ const result = R.run({
124
+ program,
125
+ code,
126
+ params: merged,
127
+ tables: payload.tables
128
+ })
129
+ await persistChanges()
130
+ if (result.error) {
131
+ return json(
132
+ {
133
+ status: 'failure',
134
+ message: 'Job execution failed.',
135
+ error: result.error
136
+ },
137
+ 500
138
+ )
139
+ }
140
+ const debug = String(merged._debug || '').length > 0
141
+ return text(
142
+ debug
143
+ ? [result.webout, LOGS_SEPARATOR, result.log, LOGS_SEPARATOR].join(
144
+ '\n'
145
+ )
146
+ : result.webout
147
+ )
148
+ }
149
+
150
+ if (path.includes('/SASjsApi/session')) {
151
+ return json({ state: 'completed' })
152
+ }
153
+
154
+ return json(
155
+ { message: 'Not implemented by the browser host: ' + path },
156
+ 404
157
+ )
158
+ } catch (e) {
159
+ const message = (e && e.stack) || String(e)
160
+ broadcast({ type: 'sasjs-host-error', path, message })
161
+ return json(
162
+ { message: (e && e.message) || String(e) },
163
+ (e && e.status) || 500
164
+ )
165
+ }
166
+ }
167
+
168
+ self.addEventListener('fetch', (event) => {
169
+ const url = new URL(event.request.url)
170
+ if (url.origin !== self.location.origin) return
171
+ if (!url.pathname.includes('/SASjsApi/')) return
172
+ event.respondWith(handle(event.request, url))
173
+ })
174
+
175
+ // ------------------------------------------------------------- host messages
176
+ self.addEventListener('message', (event) => {
177
+ const data = event.data || {}
178
+ const reply = (message) => event.source && event.source.postMessage(message)
179
+
180
+ switch (data.type) {
181
+ case 'sasjs-load':
182
+ event.waitUntil(
183
+ R.load(data.files || {}).then(async () => {
184
+ await persistChanges()
185
+ reply({
186
+ type: 'sasjs-load-ack',
187
+ boot: R.bootInfo(),
188
+ stats: R.stats()
189
+ })
190
+ })
191
+ )
192
+ break
193
+
194
+ case 'sasjs-seed':
195
+ event.waitUntil(
196
+ R.seed().then(async () => {
197
+ await persistChanges()
198
+ reply({
199
+ type: 'sasjs-seed-ack',
200
+ boot: R.bootInfo(),
201
+ stats: R.stats()
202
+ })
203
+ })
204
+ )
205
+ break
206
+
207
+ case 'sasjs-reset':
208
+ event.waitUntil(
209
+ R.reset().then(async () => {
210
+ await persistChanges()
211
+ reply({ type: 'sasjs-reset-ack', boot: R.bootInfo() })
212
+ })
213
+ )
214
+ break
215
+
216
+ case 'sasjs-snapshot':
217
+ reply({ type: 'sasjs-snapshot', files: R.dumpAll() })
218
+ break
219
+
220
+ case 'sasjs-stats':
221
+ event.waitUntil(
222
+ R.ensureReady().then(() =>
223
+ reply({ type: 'sasjs-stats', boot: R.bootInfo(), stats: R.stats() })
224
+ )
225
+ )
226
+ break
227
+ }
228
+ })
@@ -0,0 +1,27 @@
1
+ import { Target } from '@sasjs/utils';
2
+ export interface MockSiteResult {
3
+ outputFolder: string;
4
+ appLoc: string;
5
+ /** Drive entries written into the seed (programs plus any other content). */
6
+ driveEntries: number;
7
+ /** Executable .js programs found under services, jobs or tests. */
8
+ jsPrograms: number;
9
+ webFileCount: number;
10
+ /** True when a captured drive snapshot was shipped with the site. */
11
+ snapshot: boolean;
12
+ }
13
+ /**
14
+ * Builds a static site that runs the project's JS services (its mocks) in the
15
+ * browser, so a frontend can be tested with no server and no SAS.
16
+ *
17
+ * The output is an ordinary folder of static files: serve it over http(s) and
18
+ * the app behaves as though it were talking to a single-user SASjs server. The
19
+ * work happens in a service worker, so every request is still a real request -
20
+ * visible in DevTools > Network, marked "(from ServiceWorker)" - and the drive
21
+ * it writes to is persisted in IndexedDB, so refreshes keep their state.
22
+ *
23
+ * Everything comes from the target: appLoc, streamConfig.mockWebFolder for the
24
+ * output, streamConfig.webSourcePath for the built app, and the compile output
25
+ * for the drive.
26
+ */
27
+ export declare function buildMockSite(target: Target): Promise<MockSiteResult>;
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
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
+ var __read = (this && this.__read) || function (o, n) {
50
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
51
+ if (!m) return o;
52
+ var i = m.call(o), r, ar = [], e;
53
+ try {
54
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
55
+ }
56
+ catch (error) { e = { error: error }; }
57
+ finally {
58
+ try {
59
+ if (r && !r.done && (m = i["return"])) m.call(i);
60
+ }
61
+ finally { if (e) throw e.error; }
62
+ }
63
+ return ar;
64
+ };
65
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
66
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
67
+ if (ar || !(i in from)) {
68
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
69
+ ar[i] = from[i];
70
+ }
71
+ }
72
+ return to.concat(ar || Array.prototype.slice.call(from));
73
+ };
74
+ var __importDefault = (this && this.__importDefault) || function (mod) {
75
+ return (mod && mod.__esModule) ? mod : { "default": mod };
76
+ };
77
+ Object.defineProperty(exports, "__esModule", { value: true });
78
+ exports.buildMockSite = buildMockSite;
79
+ var crypto_1 = require("crypto");
80
+ var fs_1 = __importDefault(require("fs"));
81
+ var path_1 = __importDefault(require("path"));
82
+ var utils_1 = require("@sasjs/utils");
83
+ var ASSETS_FOLDER = path_1.default.join(__dirname, 'assets');
84
+ var toPosix = function (value) { return value.split(path_1.default.sep).join('/'); };
85
+ var listFiles = function (folder, base) {
86
+ var e_1, _a;
87
+ if (base === void 0) { base = folder; }
88
+ var entries = fs_1.default.readdirSync(folder, { withFileTypes: true });
89
+ var files = [];
90
+ try {
91
+ for (var entries_1 = __values(entries), entries_1_1 = entries_1.next(); !entries_1_1.done; entries_1_1 = entries_1.next()) {
92
+ var entry = entries_1_1.value;
93
+ var absolute = path_1.default.join(folder, entry.name);
94
+ if (entry.isDirectory())
95
+ files.push.apply(files, __spreadArray([], __read(listFiles(absolute, base)), false));
96
+ else if (entry.isFile())
97
+ files.push(toPosix(path_1.default.relative(base, absolute)));
98
+ }
99
+ }
100
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
101
+ finally {
102
+ try {
103
+ if (entries_1_1 && !entries_1_1.done && (_a = entries_1.return)) _a.call(entries_1);
104
+ }
105
+ finally { if (e_1) throw e_1.error; }
106
+ }
107
+ return files;
108
+ };
109
+ var copyFolder = function (from, to) {
110
+ var e_2, _a;
111
+ fs_1.default.mkdirSync(to, { recursive: true });
112
+ try {
113
+ for (var _b = __values(fs_1.default.readdirSync(from, { withFileTypes: true })), _c = _b.next(); !_c.done; _c = _b.next()) {
114
+ var entry = _c.value;
115
+ var source = path_1.default.join(from, entry.name);
116
+ var target = path_1.default.join(to, entry.name);
117
+ if (entry.isDirectory())
118
+ copyFolder(source, target);
119
+ else
120
+ fs_1.default.copyFileSync(source, target);
121
+ }
122
+ }
123
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
124
+ finally {
125
+ try {
126
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
127
+ }
128
+ finally { if (e_2) throw e_2.error; }
129
+ }
130
+ };
131
+ /**
132
+ * The drive seed comes from the COMPILE OUTPUT, not the sources: `sasjs
133
+ * compile` copies every service folder into sasjsbuild/services and then
134
+ * copies the sync folder's contents over the build folder root, so sasjsbuild
135
+ * already holds exactly what a SASjs server would receive on its drive.
136
+ *
137
+ * Root-level files are skipped on purpose - those are the service pack (.sas),
138
+ * its manifest (.json), the compile tree and the zip, none of which belong on
139
+ * the drive.
140
+ */
141
+ var PROGRAM_FOLDERS = ['services', 'jobs', 'tests'];
142
+ /**
143
+ * A drive file is a string in the browser runtime, so a file that does not
144
+ * survive a UTF-8 round trip (an image, an archive, a compiled artefact) cannot
145
+ * be carried onto it. Returning null for those keeps the site honest: the app
146
+ * sees a missing file rather than mojibake it would happily write back.
147
+ */
148
+ var readTextFile = function (absolute) {
149
+ var buffer = fs_1.default.readFileSync(absolute);
150
+ var text = buffer.toString('utf8');
151
+ return Buffer.from(text, 'utf8').equals(buffer) ? text : null;
152
+ };
153
+ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
154
+ var e_3, _a, e_4, _b;
155
+ if (exclude === void 0) { exclude = []; }
156
+ if (excludeFolders === void 0) { excludeFolders = []; }
157
+ var files = {};
158
+ var skipped = [];
159
+ var driveBase = "/sasjs/drive/files".concat(appLoc);
160
+ // Every folder of the build goes on the drive, so a sync folder's content (a
161
+ // licence store, for instance) survives as well as the program folders.
162
+ var directories = fs_1.default
163
+ .readdirSync(buildFolder, { withFileTypes: true })
164
+ .filter(function (entry) { return entry.isDirectory(); });
165
+ try {
166
+ for (var directories_1 = __values(directories), directories_1_1 = directories_1.next(); !directories_1_1.done; directories_1_1 = directories_1.next()) {
167
+ var entry = directories_1_1.value;
168
+ var folder = path_1.default.join(buildFolder, entry.name);
169
+ if (excludeFolders.includes(path_1.default.resolve(folder)))
170
+ continue;
171
+ var _loop_1 = function (relative) {
172
+ var key = "".concat(driveBase, "/").concat(entry.name, "/").concat(relative);
173
+ if (exclude.some(function (prefix) { return key.startsWith(prefix); }))
174
+ return "continue";
175
+ var contents = readTextFile(path_1.default.join(folder, relative));
176
+ if (contents === null)
177
+ skipped.push(key);
178
+ else
179
+ files[key] = contents;
180
+ };
181
+ try {
182
+ for (var _c = (e_4 = void 0, __values(listFiles(folder))), _d = _c.next(); !_d.done; _d = _c.next()) {
183
+ var relative = _d.value;
184
+ _loop_1(relative);
185
+ }
186
+ }
187
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
188
+ finally {
189
+ try {
190
+ if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
191
+ }
192
+ finally { if (e_4) throw e_4.error; }
193
+ }
194
+ }
195
+ }
196
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
197
+ finally {
198
+ try {
199
+ if (directories_1_1 && !directories_1_1.done && (_a = directories_1.return)) _a.call(directories_1);
200
+ }
201
+ finally { if (e_3) throw e_3.error; }
202
+ }
203
+ return { files: files, skipped: skipped };
204
+ };
205
+ /**
206
+ * `mockWebFolder` decides a folder that is deleted before it is written, so it
207
+ * has to name a folder of its own. Anything that would delete the project, the
208
+ * build output the drive is seeded from, or the web app it is copied from is
209
+ * refused before a single file is touched.
210
+ */
211
+ var assertSafeOutputFolder = function (outputFolder, projectDir, buildFolder, webFolder) {
212
+ var isSameOrChild = function (parent, child) {
213
+ var relative = path_1.default.relative(parent, child);
214
+ return (relative === '' ||
215
+ (relative !== '..' &&
216
+ !relative.startsWith('..' + path_1.default.sep) &&
217
+ !path_1.default.isAbsolute(relative)));
218
+ };
219
+ var refusal = function (why) {
220
+ return new Error("Refusing to write the mock site to ".concat(outputFolder, ": ").concat(why, ". ") +
221
+ 'Set streamConfig.mockWebFolder to a folder of its own, for example "sasjsbuild/mock".');
222
+ };
223
+ if (isSameOrChild(outputFolder, projectDir))
224
+ throw refusal('that folder is the project folder or contains it');
225
+ if (isSameOrChild(outputFolder, buildFolder))
226
+ throw refusal('that folder holds the build output the drive is seeded from');
227
+ if (isSameOrChild(outputFolder, webFolder))
228
+ throw refusal('that folder holds the web app the site is copied from');
229
+ if (isSameOrChild(webFolder, outputFolder))
230
+ throw refusal('that folder is inside the web app the site is copied from, so copying it would copy itself');
231
+ if (!isSameOrChild(projectDir, outputFolder) &&
232
+ !isSameOrChild(buildFolder, outputFolder))
233
+ throw refusal('that folder is outside the project');
234
+ };
235
+ var countJsPrograms = function (files) {
236
+ return Object.keys(files).filter(function (key) {
237
+ var inProgramFolder = PROGRAM_FOLDERS.some(function (folder) {
238
+ return key.includes("/".concat(folder, "/"));
239
+ });
240
+ return inProgramFolder && key.toLowerCase().endsWith('.js');
241
+ }).length;
242
+ };
243
+ var loaderScript = function (mainScript, version) { return "<script>\n // Boot order: register the service worker that hosts the mock backend, wait\n // until it controls this page, hand it the persisted drive, install the XHR\n // bridge, then start the app.\n (function () {\n function load(src, next, isModule) {\n var s = document.createElement('script')\n if (isModule) s.type = 'module'\n s.src = src\n s.onload = next\n s.onerror = function () { console.error('[mock] failed to load ' + src); next() }\n document.body.appendChild(s)\n }\n function start() {\n load('host/persist.js?v=".concat(version, "', function () {\n Promise.resolve(window.__sasjsMockReady).then(function (boot) {\n if (boot && boot.error) console.error('[mock] boot: ' + boot.error)\n load('host/intercept.js?v=").concat(version, "', function () {\n load('").concat(mainScript, "', function () {}, true)\n })\n })\n })\n }\n if (!('serviceWorker' in navigator)) {\n console.error('[mock] service workers are unavailable - the mock backend cannot run')\n start()\n return\n }\n navigator.serviceWorker\n .register('/sw.js', { scope: '/' })\n .then(function () { return navigator.serviceWorker.ready })\n .then(function () {\n if (navigator.serviceWorker.controller) return\n return new Promise(function (resolve) {\n var timer = setTimeout(resolve, 4000)\n navigator.serviceWorker.addEventListener('controllerchange', function () {\n clearTimeout(timer)\n resolve()\n })\n })\n })\n .then(function () {\n if (navigator.serviceWorker.controller) return start()\n if (!sessionStorage.getItem('mock-sw-reloaded')) {\n sessionStorage.setItem('mock-sw-reloaded', '1')\n location.reload()\n return\n }\n console.error('[mock] no service worker controller - requests will fail')\n start()\n })\n .catch(function (err) {\n console.error('[mock] service worker registration failed: ' + err)\n start()\n })\n })()\n </script>"); };
244
+ /**
245
+ * Points the app at its own origin and replaces its bundle tag with the
246
+ * boot loader. Both changes are reported when the expected markup is absent,
247
+ * rather than producing a site that silently talks to a real server.
248
+ */
249
+ var patchIndexHtml = function (html, version) {
250
+ var warnings = [];
251
+ var patched = html;
252
+ if (/<sasjs[^>]*>/i.test(patched)) {
253
+ patched = patched.replace(/(<sasjs[^>]*?)\sserverurl="[^"]*"/i, '$1 serverurl=""');
254
+ patched = patched.replace(/(<sasjs[^>]*?)\sloginmechanism="[^"]*"/i, '$1 loginmechanism="Default"');
255
+ }
256
+ else {
257
+ warnings.push('no <sasjs> element found - check the adapter configuration manually');
258
+ }
259
+ var mainMatch = patched.match(/<script[^>]*src="([^"]*main[^"]*\.js)"[^>]*><\/script>/i);
260
+ if (mainMatch) {
261
+ patched = patched.replace(mainMatch[0], loaderScript(mainMatch[1], version));
262
+ }
263
+ else {
264
+ warnings.push('no main bundle script tag found - the mock loader was not installed');
265
+ }
266
+ return { patched: patched, warnings: warnings };
267
+ };
268
+ /**
269
+ * Builds a static site that runs the project's JS services (its mocks) in the
270
+ * browser, so a frontend can be tested with no server and no SAS.
271
+ *
272
+ * The output is an ordinary folder of static files: serve it over http(s) and
273
+ * the app behaves as though it were talking to a single-user SASjs server. The
274
+ * work happens in a service worker, so every request is still a real request -
275
+ * visible in DevTools > Network, marked "(from ServiceWorker)" - and the drive
276
+ * it writes to is persisted in IndexedDB, so refreshes keep their state.
277
+ *
278
+ * Everything comes from the target: appLoc, streamConfig.mockWebFolder for the
279
+ * output, streamConfig.webSourcePath for the built app, and the compile output
280
+ * for the drive.
281
+ */
282
+ function buildMockSite(target) {
283
+ return __awaiter(this, void 0, void 0, function () {
284
+ 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;
285
+ var e_5, _e;
286
+ var _f, _g, _h;
287
+ return __generator(this, function (_j) {
288
+ switch (_j.label) {
289
+ case 0:
290
+ projectDir = process.projectDir || process.cwd();
291
+ appLocRaw = target.appLoc;
292
+ if (!appLocRaw) {
293
+ throw new Error('No appLoc. Set appLoc on the target - it has to match the appLoc the app is built with.');
294
+ }
295
+ appLoc = '/' + appLocRaw.replace(/^\/+/, '').replace(/\/+$/, '');
296
+ buildFolder = process.sasjsConstants.buildDestinationFolder ||
297
+ path_1.default.join(projectDir, 'sasjsbuild');
298
+ return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
299
+ case 1:
300
+ if (!(_j.sent())) {
301
+ throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
302
+ }
303
+ webSourceRaw = (_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.webSourcePath;
304
+ if (!webSourceRaw) {
305
+ throw new Error('No built web app. Set streamConfig.webSourcePath on the target (for example ./client/dist), and build the frontend first.');
306
+ }
307
+ webFolder = path_1.default.resolve(projectDir, webSourceRaw);
308
+ return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
309
+ case 2:
310
+ if (!(_j.sent())) {
311
+ throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
312
+ }
313
+ if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
314
+ throw new Error("".concat(webFolder, " does not contain an index.html - is streamConfig.webSourcePath pointing at the built app?"));
315
+ }
316
+ outputFolder = ((_g = target.streamConfig) === null || _g === void 0 ? void 0 : _g.mockWebFolder)
317
+ ? path_1.default.resolve(projectDir, target.streamConfig.mockWebFolder)
318
+ : path_1.default.join(buildFolder, 'mock');
319
+ assertSafeOutputFolder(outputFolder, projectDir, buildFolder, webFolder);
320
+ streamConfig = target.streamConfig;
321
+ excluded = [];
322
+ // streamWebFolder is defaulted by the config validator, so a streamed app
323
+ // always names its folder.
324
+ if ((streamConfig === null || streamConfig === void 0 ? void 0 : streamConfig.streamWeb) && streamConfig.streamWebFolder) {
325
+ excluded.push("/sasjs/drive/files".concat(appLoc, "/services/").concat(streamConfig.streamWebFolder, "/"));
326
+ }
327
+ _a = seedFromBuild(buildFolder, appLoc, excluded, [outputFolder]), seed = _a.files, skipped = _a.skipped;
328
+ jsPrograms = countJsPrograms(seed);
329
+ driveEntries = Object.keys(seed).length;
330
+ if (jsPrograms === 0) {
331
+ throw new Error("No .js programs in ".concat(buildFolder, "/services, /jobs or /tests. This target builds SAS code, which cannot run in a browser. ") +
332
+ 'Build a target whose services are JavaScript (typically a mocks project).');
333
+ }
334
+ if (skipped.length) {
335
+ (_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("Left ".concat(skipped.length, " non-text file(s) off the mock drive, which holds text files only: ") +
336
+ "".concat(skipped.slice(0, 3).join(', ')).concat(skipped.length > 3 ? ', ...' : ''));
337
+ }
338
+ // Only now is the previous site expendable.
339
+ fs_1.default.rmSync(outputFolder, { recursive: true, force: true });
340
+ fs_1.default.mkdirSync(outputFolder, { recursive: true });
341
+ // 1. the app
342
+ copyFolder(webFolder, outputFolder);
343
+ webFileCount = listFiles(outputFolder).length;
344
+ hostFolder = path_1.default.join(outputFolder, 'host');
345
+ fs_1.default.mkdirSync(hostFolder, { recursive: true });
346
+ try {
347
+ for (_b = __values([
348
+ 'md5.js',
349
+ 'runtime-core.js',
350
+ 'persist.js',
351
+ 'intercept.js'
352
+ ]), _c = _b.next(); !_c.done; _c = _b.next()) {
353
+ asset = _c.value;
354
+ fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
355
+ }
356
+ }
357
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
358
+ finally {
359
+ try {
360
+ if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
361
+ }
362
+ finally { if (e_5) throw e_5.error; }
363
+ }
364
+ // sw.js sits at the site root: a worker can only control the folder it is
365
+ // served from, and the app has to be controlled from /
366
+ fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
367
+ hostVersion = (0, crypto_1.createHash)('md5')
368
+ .update(['runtime-core.js', 'persist.js', 'intercept.js']
369
+ .map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
370
+ .reduce(function (all, buf) { return Buffer.concat([all, buf]); }, Buffer.alloc(0)))
371
+ .digest('hex')
372
+ .slice(0, 10);
373
+ swPath = path_1.default.join(outputFolder, 'sw.js');
374
+ fs_1.default.writeFileSync(swPath, fs_1.default
375
+ .readFileSync(swPath, 'utf8')
376
+ .replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')")));
377
+ snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
378
+ hasSnapshot = fs_1.default.existsSync(snapshotSource);
379
+ if (hasSnapshot) {
380
+ fs_1.default.copyFileSync(snapshotSource, path_1.default.join(hostFolder, 'vfs-snapshot.json'));
381
+ }
382
+ // 3. the drive, laid out from the compile output
383
+ fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'vfs-seed.json'), JSON.stringify({ files: seed }));
384
+ fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'mock-config.json'), JSON.stringify({
385
+ appLoc: appLoc,
386
+ seedPath: '/host/vfs-seed.json',
387
+ driveEntries: driveEntries,
388
+ jsPrograms: jsPrograms,
389
+ programFolders: PROGRAM_FOLDERS,
390
+ snapshot: hasSnapshot,
391
+ builtAt: new Date().toISOString()
392
+ }, null, 2));
393
+ indexPath = path_1.default.join(outputFolder, 'index.html');
394
+ _d = patchIndexHtml(fs_1.default.readFileSync(indexPath, 'utf8'), hostVersion), patched = _d.patched, warnings = _d.warnings;
395
+ fs_1.default.writeFileSync(indexPath, patched);
396
+ warnings.forEach(function (warning) { var _a; return (_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn(warning); });
397
+ return [2 /*return*/, {
398
+ outputFolder: outputFolder,
399
+ appLoc: appLoc,
400
+ driveEntries: driveEntries,
401
+ jsPrograms: jsPrograms,
402
+ webFileCount: webFileCount,
403
+ snapshot: hasSnapshot
404
+ }];
405
+ }
406
+ });
407
+ });
408
+ }