@sasjs/cli 6.0.0 → 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.
@@ -100,12 +100,13 @@ var compile_1 = require("../compile/compile");
100
100
  var internal_1 = require("../compile/internal");
101
101
  var config_2 = require("./internal/config");
102
102
  var getLaunchPageCode_1 = require("./internal/getLaunchPageCode");
103
+ var mockSite_1 = require("./internal/mockSite/mockSite");
103
104
  function build(target) {
104
105
  return __awaiter(this, void 0, void 0, function () {
105
106
  var macroCorePath, _a;
106
- var _b;
107
- return __generator(this, function (_c) {
108
- switch (_c.label) {
107
+ var _b, _c;
108
+ return __generator(this, function (_d) {
109
+ switch (_d.label) {
109
110
  case 0:
110
111
  macroCorePath = process.sasjsConstants.macroCorePath;
111
112
  if (macroCorePath === '') {
@@ -120,18 +121,27 @@ function build(target) {
120
121
  if (!_a) return [3 /*break*/, 2];
121
122
  return [4 /*yield*/, (0, utils_1.folderExists)(process.sasjsConstants.buildDestinationFolder)];
122
123
  case 1:
123
- _a = !(_c.sent());
124
- _c.label = 2;
124
+ _a = !(_d.sent());
125
+ _d.label = 2;
125
126
  case 2:
126
127
  if (!_a) return [3 /*break*/, 4];
127
128
  return [4 /*yield*/, (0, compile_1.compile)(target)];
128
129
  case 3:
129
- _c.sent();
130
- _c.label = 4;
131
- case 4: return [4 /*yield*/, createFinalSasFiles(target)];
130
+ _d.sent();
131
+ _d.label = 4;
132
+ case 4: return [4 /*yield*/, createFinalSasFiles(target)
133
+ // streamConfig.mockWeb opts the target into a static site that runs the same
134
+ // services in a browser. It is built from what compile just produced, so it
135
+ // is only meaningful after the tree above exists.
136
+ ];
132
137
  case 5:
133
- _c.sent();
134
- return [2 /*return*/];
138
+ _d.sent();
139
+ if (!((_c = target.streamConfig) === null || _c === void 0 ? void 0 : _c.mockWeb)) return [3 /*break*/, 7];
140
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target)];
141
+ case 6:
142
+ _d.sent();
143
+ _d.label = 7;
144
+ case 7: return [2 /*return*/];
135
145
  }
136
146
  });
137
147
  });
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Page-side bridge for the browser build.
3
+ *
4
+ * The real work happens in the service worker (see /sw.js), so every request
5
+ * still goes over the network layer and is visible in DevTools > Network as
6
+ * "(from ServiceWorker)" - request payload and response body included.
7
+ *
8
+ * Two things are this file's job:
9
+ * 1. XHR bodies are multipart (the adapter always sends them that way).
10
+ * Reading multipart inside a worker is unreliable for XHR-originated
11
+ * requests, so the payload is converted to JSON here, where reading a
12
+ * FormData is trivial.
13
+ * 2. The XHR object the app is holding must behave like a normal one: status,
14
+ * readyState, responseText and real load events.
15
+ */
16
+ ;(function () {
17
+ if (window.__sasjsMockBridgeInstalled) return
18
+ window.__sasjsMockBridgeInstalled = true
19
+
20
+ // Set localStorage sasjsMockDebug='1' and reload to see this bridge's traffic
21
+ // in the console.
22
+ const DEBUG = (() => {
23
+ try {
24
+ return localStorage.getItem('sasjsMockDebug') === '1'
25
+ } catch (e) {
26
+ return false
27
+ }
28
+ })()
29
+
30
+ const dbg = (...args) => {
31
+ if (DEBUG) console.log('[sasjs-mock]', ...args)
32
+ }
33
+
34
+ const readBody = async (body) => {
35
+ const params = {}
36
+ const tables = []
37
+ if (!body) return { params, tables }
38
+
39
+ if (typeof FormData !== 'undefined' && body instanceof FormData) {
40
+ for (const [key, value] of body.entries()) {
41
+ if (typeof value === 'string') params[key] = value
42
+ else
43
+ tables.push({
44
+ name: key,
45
+ filename: value.name || key + '.csv',
46
+ csv: await value.text()
47
+ })
48
+ }
49
+ return { params, tables }
50
+ }
51
+
52
+ if (typeof body === 'string') {
53
+ try {
54
+ const parsed = JSON.parse(body)
55
+ return { params: {}, tables: [], json: parsed }
56
+ } catch (e) {
57
+ return { params: {}, tables: [] }
58
+ }
59
+ }
60
+ return { params, tables }
61
+ }
62
+
63
+ const safeParse = (text) => {
64
+ try {
65
+ return JSON.parse(text)
66
+ } catch (e) {
67
+ return text
68
+ }
69
+ }
70
+
71
+ const respond = (xhr, status, contentType, body) => {
72
+ Object.defineProperty(xhr, 'status', { value: status, configurable: true })
73
+ Object.defineProperty(xhr, 'statusText', {
74
+ value: status === 200 ? 'OK' : 'ERROR',
75
+ configurable: true
76
+ })
77
+ Object.defineProperty(xhr, 'readyState', { value: 4, configurable: true })
78
+ Object.defineProperty(xhr, 'responseText', {
79
+ value: body,
80
+ configurable: true
81
+ })
82
+ Object.defineProperty(xhr, 'responseURL', {
83
+ value: xhr.__sasjsMockUrl,
84
+ configurable: true
85
+ })
86
+ Object.defineProperty(xhr, 'response', {
87
+ value: xhr.responseType === 'json' ? safeParse(body) : body,
88
+ configurable: true
89
+ })
90
+ xhr.getAllResponseHeaders = () => 'content-type: ' + contentType + '\r\n'
91
+ xhr.getResponseHeader = (key) =>
92
+ key && key.toLowerCase() === 'content-type' ? contentType : null
93
+
94
+ xhr.dispatchEvent(new Event('readystatechange'))
95
+ xhr.dispatchEvent(new ProgressEvent('load'))
96
+ xhr.dispatchEvent(new ProgressEvent('loadend'))
97
+ }
98
+
99
+ const isSasjs = (url) =>
100
+ typeof url === 'string' && url.indexOf('/SASjsApi/') >= 0
101
+
102
+ const proto = XMLHttpRequest.prototype
103
+ const nativeOpen = proto.open
104
+ const nativeSend = proto.send
105
+
106
+ proto.open = function (method, url) {
107
+ this.__sasjsMockMethod = method
108
+ this.__sasjsMockUrl = String(url)
109
+ return nativeOpen.apply(this, arguments)
110
+ }
111
+
112
+ proto.send = function (body) {
113
+ if (!isSasjs(this.__sasjsMockUrl)) return nativeSend.apply(this, arguments)
114
+
115
+ const xhr = this
116
+ const method = (this.__sasjsMockMethod || 'GET').toUpperCase()
117
+
118
+ readBody(body)
119
+ .then(async (payload) => {
120
+ const init = {
121
+ method,
122
+ credentials: 'same-origin',
123
+ headers: { accept: '*/*' }
124
+ }
125
+ if (method !== 'GET' && method !== 'HEAD') {
126
+ init.headers['content-type'] = 'application/json'
127
+ init.body = JSON.stringify(payload)
128
+ }
129
+ dbg(
130
+ method,
131
+ xhr.__sasjsMockUrl,
132
+ payload.tables.map((t) => t.name).join('|')
133
+ )
134
+
135
+ const res = await fetch(xhr.__sasjsMockUrl, init)
136
+ const text = await res.text()
137
+ const contentType = res.headers.get('content-type') || 'text/plain'
138
+ if (res.status >= 400)
139
+ console.error(
140
+ '[sasjs-mock] ' +
141
+ xhr.__sasjsMockUrl +
142
+ ' -> ' +
143
+ res.status +
144
+ ' ' +
145
+ text.slice(0, 400)
146
+ )
147
+ respond(xhr, res.status, contentType, text)
148
+ })
149
+ .catch((err) => {
150
+ const message = String((err && err.message) || err)
151
+ console.error(
152
+ '[sasjs-mock] request failed: ' + xhr.__sasjsMockUrl + ' - ' + message
153
+ )
154
+ respond(
155
+ xhr,
156
+ 500,
157
+ 'application/json',
158
+ JSON.stringify({ message: message, error: message })
159
+ )
160
+ })
161
+ }
162
+
163
+ // fetch() is used by nothing in this build, but answering it keeps the
164
+ // bridge honest if a code path switches transport.
165
+ window.__sasjsMockHostStats = async () => {
166
+ const reg = await navigator.serviceWorker.ready
167
+ return {
168
+ active: !!reg.active,
169
+ controller: !!navigator.serviceWorker.controller
170
+ }
171
+ }
172
+ })()
@@ -0,0 +1,75 @@
1
+ // Compact MD5 (RFC 1321) returning lowercase hex. Used by the crypto shim so
2
+ // that dcMockUtils' mp_hashdataset sees the same digests SAS produces.
3
+ const MD5 = (() => {
4
+ const S = [
5
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5,
6
+ 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11,
7
+ 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10,
8
+ 15, 21
9
+ ]
10
+ const K = new Array(64)
11
+ for (let i = 0; i < 64; i++)
12
+ K[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296) | 0
13
+
14
+ const rotl = (x, c) => (x << c) | (x >>> (32 - c))
15
+
16
+ return function md5(input) {
17
+ const bytes = new TextEncoder().encode(String(input))
18
+ const bitLen = bytes.length * 8
19
+ const withPad = new Uint8Array((((bytes.length + 8) >> 6) + 1) * 64)
20
+ withPad.set(bytes)
21
+ withPad[bytes.length] = 0x80
22
+ const dv = new DataView(withPad.buffer)
23
+ dv.setUint32(withPad.length - 8, bitLen >>> 0, true)
24
+ dv.setUint32(withPad.length - 4, Math.floor(bitLen / 4294967296), true)
25
+
26
+ let a0 = 0x67452301,
27
+ b0 = 0xefcdab89,
28
+ c0 = 0x98badcfe,
29
+ d0 = 0x10325476
30
+
31
+ for (let off = 0; off < withPad.length; off += 64) {
32
+ const M = new Array(16)
33
+ for (let i = 0; i < 16; i++) M[i] = dv.getInt32(off + i * 4, true)
34
+ let A = a0,
35
+ B = b0,
36
+ C = c0,
37
+ D = d0
38
+ for (let i = 0; i < 64; i++) {
39
+ let F, g
40
+ if (i < 16) {
41
+ F = (B & C) | (~B & D)
42
+ g = i
43
+ } else if (i < 32) {
44
+ F = (D & B) | (~D & C)
45
+ g = (5 * i + 1) % 16
46
+ } else if (i < 48) {
47
+ F = B ^ C ^ D
48
+ g = (3 * i + 5) % 16
49
+ } else {
50
+ F = C ^ (B | ~D)
51
+ g = (7 * i) % 16
52
+ }
53
+ const tmp = D
54
+ D = C
55
+ C = B
56
+ B = (B + rotl((A + F + K[i] + M[g]) | 0, S[i])) | 0
57
+ A = tmp
58
+ }
59
+ a0 = (a0 + A) | 0
60
+ b0 = (b0 + B) | 0
61
+ c0 = (c0 + C) | 0
62
+ d0 = (d0 + D) | 0
63
+ }
64
+
65
+ const hex = (n) => {
66
+ let out = ''
67
+ for (let i = 0; i < 4; i++)
68
+ out += ((n >>> (i * 8)) & 0xff).toString(16).padStart(2, '0')
69
+ return out
70
+ }
71
+ return hex(a0) + hex(b0) + hex(c0) + hex(d0)
72
+ }
73
+ })()
74
+
75
+ if (typeof module !== 'undefined') module.exports = MD5
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Drive storage for the browser build: IndexedDB, owned by the page.
3
+ *
4
+ * The service worker hosts the runtime but may be terminated between requests,
5
+ * so it holds no durable state. On boot this script reads the persisted drive
6
+ * and hands it to the worker; after every service run the worker sends back the
7
+ * files it changed and they are written here.
8
+ */
9
+ ;(function () {
10
+ const DB_NAME = 'sasjs-mock-host'
11
+ const STORE = 'files'
12
+
13
+ const openDb = () =>
14
+ new Promise((resolve, reject) => {
15
+ const req = indexedDB.open(DB_NAME, 1)
16
+ req.onupgradeneeded = () => {
17
+ const db = req.result
18
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE)
19
+ }
20
+ req.onsuccess = () => resolve(req.result)
21
+ req.onerror = () => reject(req.error)
22
+ })
23
+
24
+ const withStore = async (mode, run) => {
25
+ const db = await openDb()
26
+ return new Promise((resolve, reject) => {
27
+ const tx = db.transaction(STORE, mode)
28
+ const store = tx.objectStore(STORE)
29
+ let result
30
+ try {
31
+ result = run(store)
32
+ } catch (e) {
33
+ reject(e)
34
+ return
35
+ }
36
+ tx.oncomplete = () => resolve(result)
37
+ tx.onerror = () => reject(tx.error)
38
+ tx.onabort = () => reject(tx.error)
39
+ })
40
+ }
41
+
42
+ const readAll = () =>
43
+ new Promise(async (resolve, reject) => {
44
+ try {
45
+ const db = await openDb()
46
+ const keysReq = db
47
+ .transaction(STORE, 'readonly')
48
+ .objectStore(STORE)
49
+ .getAllKeys()
50
+ keysReq.onsuccess = () => {
51
+ const keys = keysReq.result
52
+ if (!keys.length) return resolve({})
53
+ const valsReq = db
54
+ .transaction(STORE, 'readonly')
55
+ .objectStore(STORE)
56
+ .getAll()
57
+ valsReq.onsuccess = () => {
58
+ const files = {}
59
+ keys.forEach((k, i) => {
60
+ if (typeof valsReq.result[i] === 'string')
61
+ files[k] = valsReq.result[i]
62
+ })
63
+ resolve(files)
64
+ }
65
+ valsReq.onerror = () => reject(valsReq.error)
66
+ }
67
+ keysReq.onerror = () => reject(keysReq.error)
68
+ } catch (e) {
69
+ reject(e)
70
+ }
71
+ })
72
+
73
+ const writeFiles = async (files) => {
74
+ const entries = Object.entries(files || {})
75
+ if (!entries.length) return { written: 0, deleted: 0 }
76
+ let deleted = 0
77
+ await withStore('readwrite', (store) => {
78
+ for (const [path, contents] of entries) {
79
+ if (contents === null || contents === undefined) {
80
+ store.delete(path)
81
+ deleted++
82
+ } else {
83
+ store.put(contents, path)
84
+ }
85
+ }
86
+ })
87
+ return { written: entries.length - deleted, deleted }
88
+ }
89
+
90
+ const clearAll = () => withStore('readwrite', (store) => store.clear())
91
+
92
+ const waitForController = () =>
93
+ new Promise((resolve) => {
94
+ if (navigator.serviceWorker.controller)
95
+ return resolve(navigator.serviceWorker.controller)
96
+ let waited = 0
97
+ const tick = () => {
98
+ if (navigator.serviceWorker.controller)
99
+ return resolve(navigator.serviceWorker.controller)
100
+ if (waited > 10000) return resolve(null)
101
+ waited += 200
102
+ setTimeout(tick, 200)
103
+ }
104
+ tick()
105
+ })
106
+
107
+ const askWorker = (message, expect) =>
108
+ new Promise((resolve) => {
109
+ const sw = navigator.serviceWorker
110
+ let settled = false
111
+ let timer
112
+ const finish = (value) => {
113
+ if (settled) return
114
+ settled = true
115
+ clearTimeout(timer)
116
+ sw.removeEventListener('message', handler)
117
+ resolve(value)
118
+ }
119
+ const handler = (ev) => {
120
+ if (ev.data && ev.data.type === expect) finish(ev.data)
121
+ }
122
+ // The listener is removed on every path: a page that asks repeatedly with
123
+ // a slow worker would otherwise stack one listener per call, and a reply
124
+ // arriving after the timeout would land on a listener nobody reads.
125
+ timer = setTimeout(
126
+ () => finish({ error: 'timed out waiting for ' + expect }),
127
+ 20000
128
+ )
129
+ sw.addEventListener('message', handler)
130
+ waitForController().then((controller) => {
131
+ if (!controller)
132
+ return finish({ error: 'no service worker controller' })
133
+ controller.postMessage(message)
134
+ })
135
+ })
136
+
137
+ // Persist whatever the worker reports as changed.
138
+ navigator.serviceWorker.addEventListener('message', (ev) => {
139
+ const data = ev.data || {}
140
+ if (data.type === 'sasjs-persist')
141
+ writeFiles(data.files).catch((e) =>
142
+ console.error('[sasjs-mock] persist failed', e)
143
+ )
144
+ if (data.type === 'sasjs-host-error')
145
+ console.error('[sasjs-mock] ' + data.path + ': ' + data.message)
146
+ })
147
+
148
+ window.SASjsMock = {
149
+ readAll,
150
+ writeFiles,
151
+ clear: clearAll,
152
+ snapshot: () => askWorker({ type: 'sasjs-snapshot' }, 'sasjs-snapshot'),
153
+ stats: async () => {
154
+ const files = await readAll()
155
+ const paths = Object.keys(files)
156
+ return {
157
+ files: paths.length,
158
+ bytes: paths.reduce((t, p) => t + files[p].length, 0)
159
+ }
160
+ },
161
+ /**
162
+ * Downloads the current drive as `mock-snapshot.json`. Drop that file in the
163
+ * build folder as `mock-snapshot.json` and the next build ships it, so every
164
+ * visitor starts from this state instead of from an empty drive.
165
+ */
166
+ download: async () => {
167
+ const snapshot = await askWorker(
168
+ { type: 'sasjs-snapshot' },
169
+ 'sasjs-snapshot'
170
+ )
171
+ const files = (snapshot && snapshot.files) || {}
172
+ const blob = new Blob([JSON.stringify({ files }, null, 2)], {
173
+ type: 'application/json'
174
+ })
175
+ const url = URL.createObjectURL(blob)
176
+ const link = document.createElement('a')
177
+ link.href = url
178
+ link.download = 'mock-snapshot.json'
179
+ link.click()
180
+ setTimeout(() => URL.revokeObjectURL(url), 10000)
181
+ return Object.keys(files).length
182
+ },
183
+ reset: async () => {
184
+ await askWorker({ type: 'sasjs-reset' }, 'sasjs-reset-ack')
185
+ await clearAll()
186
+ return true
187
+ }
188
+ }
189
+
190
+ window.__sasjsMockReady = (async () => {
191
+ await navigator.serviceWorker.ready
192
+ const controller = await waitForController()
193
+ if (!controller) return { error: 'no service worker controller' }
194
+
195
+ let files = {}
196
+ try {
197
+ files = await readAll()
198
+ } catch (e) {
199
+ console.error('[sasjs-mock] could not read storage: ' + e)
200
+ }
201
+
202
+ if (Object.keys(files).length) {
203
+ window.__sasjsMockBoot = await askWorker(
204
+ { type: 'sasjs-load', files },
205
+ 'sasjs-load-ack'
206
+ )
207
+ } else {
208
+ window.__sasjsMockBoot = await askWorker(
209
+ { type: 'sasjs-seed' },
210
+ 'sasjs-seed-ack'
211
+ )
212
+ }
213
+ return window.__sasjsMockBoot
214
+ })()
215
+ })()