@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.
- package/build/commands/build/build.js +20 -10
- package/build/commands/build/internal/mockSite/assets/intercept.js +172 -0
- package/build/commands/build/internal/mockSite/assets/md5.js +75 -0
- package/build/commands/build/internal/mockSite/assets/persist.js +215 -0
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +616 -0
- package/build/commands/build/internal/mockSite/assets/sw.js +228 -0
- package/build/commands/build/internal/mockSite/mockSite.d.ts +27 -0
- package/build/commands/build/internal/mockSite/mockSite.js +408 -0
- package/build/commands/build/internal/mockSite/spec/intercept.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/intercept.spec.js +267 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +405 -0
- package/build/commands/build/internal/mockSite/spec/persist.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +418 -0
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +296 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.d.ts +1 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +361 -0
- package/build/commands/index.d.ts +1 -0
- package/build/commands/index.js +3 -1
- package/package.json +8 -7
|
@@ -0,0 +1,296 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
/**
|
|
40
|
+
* The parity claim of a browser-hosted build is that a service runs in the
|
|
41
|
+
* browser exactly as it runs as a stored program. That claim lives in
|
|
42
|
+
* runtime-core.js (parameter emission, WEBIN refs, program resolution, webout
|
|
43
|
+
* precedence, the error shape), so it is tested directly against the file the
|
|
44
|
+
* site ships.
|
|
45
|
+
*/
|
|
46
|
+
var appLoc = '/Public/app/test';
|
|
47
|
+
var drivePath = function (relative) {
|
|
48
|
+
return "/sasjs/drive/files".concat(appLoc, "/").concat(relative);
|
|
49
|
+
};
|
|
50
|
+
var loadRuntime = function () {
|
|
51
|
+
;
|
|
52
|
+
global.self = {};
|
|
53
|
+
jest.resetModules();
|
|
54
|
+
return require('../assets/runtime-core.js');
|
|
55
|
+
};
|
|
56
|
+
var jsonResponse = function (body) { return ({
|
|
57
|
+
ok: true,
|
|
58
|
+
json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
59
|
+
return [2 /*return*/, body];
|
|
60
|
+
}); }); }
|
|
61
|
+
}); };
|
|
62
|
+
var seedDrive = function (runtime, files) { return __awaiter(void 0, void 0, void 0, function () {
|
|
63
|
+
return __generator(this, function (_a) {
|
|
64
|
+
switch (_a.label) {
|
|
65
|
+
case 0:
|
|
66
|
+
;
|
|
67
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
68
|
+
return __generator(this, function (_a) {
|
|
69
|
+
if (url.includes('mock-config'))
|
|
70
|
+
return [2 /*return*/, jsonResponse({ appLoc: appLoc })];
|
|
71
|
+
if (url.includes('vfs-seed'))
|
|
72
|
+
return [2 /*return*/, jsonResponse({ files: files })];
|
|
73
|
+
return [2 /*return*/, { ok: false, json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
74
|
+
return [2 /*return*/, ({})];
|
|
75
|
+
}); }); } }];
|
|
76
|
+
});
|
|
77
|
+
}); });
|
|
78
|
+
return [4 /*yield*/, runtime.seed()];
|
|
79
|
+
case 1:
|
|
80
|
+
_a.sent();
|
|
81
|
+
return [2 /*return*/];
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}); };
|
|
85
|
+
describe('browser runtime', function () {
|
|
86
|
+
afterEach(function () {
|
|
87
|
+
delete global.self;
|
|
88
|
+
delete global.fetch;
|
|
89
|
+
});
|
|
90
|
+
it('exposes request parameters as variables, with JSON escaping', function () {
|
|
91
|
+
var runtime = loadRuntime();
|
|
92
|
+
var result = runtime.run({
|
|
93
|
+
code: '_webout = typeof said + "|" + said + "|" + count;',
|
|
94
|
+
params: { said: 'he said "hi"\nback\\slash', count: '7' }
|
|
95
|
+
});
|
|
96
|
+
expect(result.error).toBeNull();
|
|
97
|
+
expect(result.webout).toBe('string|he said "hi"\nback\\slash|7');
|
|
98
|
+
});
|
|
99
|
+
it('leaves a parameter name that is not an identifier out of the prelude', function () {
|
|
100
|
+
var runtime = loadRuntime();
|
|
101
|
+
// an emitted `const not-an-identifier` would be a syntax error for every
|
|
102
|
+
// service on the drive, so such names are filtered out
|
|
103
|
+
var result = runtime.run({
|
|
104
|
+
code: '_webout = "ran";',
|
|
105
|
+
params: { 'not-an-identifier': 'x', '1leading': 'y' }
|
|
106
|
+
});
|
|
107
|
+
expect(result.error).toBeNull();
|
|
108
|
+
expect(result.webout).toBe('ran');
|
|
109
|
+
});
|
|
110
|
+
it('exposes uploaded tables as WEBIN refs', function () {
|
|
111
|
+
var runtime = loadRuntime();
|
|
112
|
+
var result = runtime.run({
|
|
113
|
+
code: '_webout = [_WEBIN_FILE_COUNT, _WEBIN_NAME1, _WEBIN_FILENAME1, _WEBIN_FILEREF1].join(",");',
|
|
114
|
+
tables: [{ name: 'table1', filename: 'data.csv', csv: 'a,b\n1,2\n' }]
|
|
115
|
+
});
|
|
116
|
+
expect(result.error).toBeNull();
|
|
117
|
+
expect(result.webout).toBe('1,table1,data.csv,a,b\n1,2\n');
|
|
118
|
+
// the fileref carries the CSV verbatim, as the fileref does on the server
|
|
119
|
+
expect(result.webout).toContain('a,b\n1,2');
|
|
120
|
+
});
|
|
121
|
+
it('reads a webout the service wrote to the fileref directly', function () {
|
|
122
|
+
var runtime = loadRuntime();
|
|
123
|
+
var result = runtime.run({
|
|
124
|
+
code: "require('fs').writeFileSync(weboutPath, 'from-file');"
|
|
125
|
+
});
|
|
126
|
+
expect(result.webout).toBe('from-file');
|
|
127
|
+
});
|
|
128
|
+
it('writes an assigned webout to the fileref, as the generated program does', function () {
|
|
129
|
+
var runtime = loadRuntime();
|
|
130
|
+
var result = runtime.run({ code: "_webout = 'inline';" });
|
|
131
|
+
expect(result.webout).toBe('inline');
|
|
132
|
+
});
|
|
133
|
+
it('reports a thrown error without failing the run', function () {
|
|
134
|
+
var runtime = loadRuntime();
|
|
135
|
+
var result = runtime.run({ code: 'throw new Error("boom")' });
|
|
136
|
+
expect(result.error).toContain('boom');
|
|
137
|
+
expect(result.log).toContain('boom');
|
|
138
|
+
expect(result.webout).toBe('');
|
|
139
|
+
});
|
|
140
|
+
it('collects service console output into the log', function () {
|
|
141
|
+
var runtime = loadRuntime();
|
|
142
|
+
var result = runtime.run({
|
|
143
|
+
code: 'console.log("hello", { a: 1 }); _webout = "ok";'
|
|
144
|
+
});
|
|
145
|
+
expect(result.log).toContain('hello {"a":1}');
|
|
146
|
+
expect(result.webout).toBe('ok');
|
|
147
|
+
});
|
|
148
|
+
it('resolves the full path the adapter sends', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
149
|
+
var runtime, result;
|
|
150
|
+
var _a;
|
|
151
|
+
return __generator(this, function (_b) {
|
|
152
|
+
switch (_b.label) {
|
|
153
|
+
case 0:
|
|
154
|
+
runtime = loadRuntime();
|
|
155
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {},
|
|
156
|
+
_a[drivePath('services/public/getdata.js')] = '_webout = "full";',
|
|
157
|
+
_a))];
|
|
158
|
+
case 1:
|
|
159
|
+
_b.sent();
|
|
160
|
+
result = runtime.run({ program: "".concat(appLoc, "/services/public/getdata") });
|
|
161
|
+
expect(result.error).toBeNull();
|
|
162
|
+
expect(result.webout).toBe('full');
|
|
163
|
+
return [2 /*return*/];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}); });
|
|
167
|
+
it('resolves an appLoc-relative reference under services, jobs or tests', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
168
|
+
var runtime;
|
|
169
|
+
var _a;
|
|
170
|
+
return __generator(this, function (_b) {
|
|
171
|
+
switch (_b.label) {
|
|
172
|
+
case 0:
|
|
173
|
+
runtime = loadRuntime();
|
|
174
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {},
|
|
175
|
+
_a[drivePath('services/public/getdata.js')] = '_webout = "apploc-relative";',
|
|
176
|
+
_a[drivePath('jobs/public/loaddata.js')] = '_webout = "job";',
|
|
177
|
+
_a))
|
|
178
|
+
// the adapter prepends the appLoc when the app passes no leading slash
|
|
179
|
+
];
|
|
180
|
+
case 1:
|
|
181
|
+
_b.sent();
|
|
182
|
+
// the adapter prepends the appLoc when the app passes no leading slash
|
|
183
|
+
expect(runtime.run({ program: "".concat(appLoc, "/public/getdata") }).webout).toBe('apploc-relative');
|
|
184
|
+
expect(runtime.run({ program: 'public/loaddata' }).webout).toBe('job');
|
|
185
|
+
return [2 /*return*/];
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}); });
|
|
189
|
+
it('gives a service the virtual drive to work on', function () {
|
|
190
|
+
var runtime = loadRuntime();
|
|
191
|
+
var folder = "/sasjs/drive/files".concat(appLoc, "/data");
|
|
192
|
+
var result = runtime.run({
|
|
193
|
+
code: [
|
|
194
|
+
"fs.mkdirSync('".concat(folder, "');"),
|
|
195
|
+
"fs.writeFileSync('".concat(folder, "/a.txt', 'hello');"),
|
|
196
|
+
"fs.copyFileSync('".concat(folder, "/a.txt', '").concat(folder, "/b.txt');"),
|
|
197
|
+
"const seen = [fs.existsSync('".concat(folder, "/a.txt'), fs.readFileSync('").concat(folder, "/b.txt'), fs.statSync('").concat(folder, "/a.txt').size, fs.readdirSync('").concat(folder, "').join()];"),
|
|
198
|
+
"fs.unlinkSync('".concat(folder, "/b.txt');"),
|
|
199
|
+
"_webout = seen.join('|') + '|' + fs.existsSync('".concat(folder, "/b.txt');")
|
|
200
|
+
].join('\n')
|
|
201
|
+
});
|
|
202
|
+
expect(result.error).toBeNull();
|
|
203
|
+
expect(result.webout).toBe('true|hello|5|a.txt,b.txt|false');
|
|
204
|
+
});
|
|
205
|
+
it('reports a missing drive file the way the server does', function () {
|
|
206
|
+
var runtime = loadRuntime();
|
|
207
|
+
var result = runtime.run({
|
|
208
|
+
code: "_webout = String(require('fs').readFileSync('/no/such/file'))"
|
|
209
|
+
});
|
|
210
|
+
expect(result.error).toContain('ENOENT');
|
|
211
|
+
});
|
|
212
|
+
it('hands the headers a service writes back to the host', function () {
|
|
213
|
+
var runtime = loadRuntime();
|
|
214
|
+
var result = runtime.run({
|
|
215
|
+
code: "require('fs').writeFileSync(_SASJS_WEBOUT_HEADERS, 'content-type: application/json');"
|
|
216
|
+
});
|
|
217
|
+
expect(result.headers).toBe('content-type: application/json');
|
|
218
|
+
});
|
|
219
|
+
it('lists folders and builds the drive tree', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
220
|
+
var runtime, listing, tree;
|
|
221
|
+
var _a;
|
|
222
|
+
return __generator(this, function (_b) {
|
|
223
|
+
switch (_b.label) {
|
|
224
|
+
case 0:
|
|
225
|
+
runtime = loadRuntime();
|
|
226
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {},
|
|
227
|
+
_a[drivePath('services/public/getdata.js')] = '_webout = "x";',
|
|
228
|
+
_a[drivePath('data/a.txt')] = 'a',
|
|
229
|
+
_a))];
|
|
230
|
+
case 1:
|
|
231
|
+
_b.sent();
|
|
232
|
+
listing = runtime.list("".concat(appLoc, "/services/public"));
|
|
233
|
+
expect(listing.files).toEqual(['getdata.js']);
|
|
234
|
+
tree = runtime.tree();
|
|
235
|
+
expect(JSON.stringify(tree)).toContain('getdata.js');
|
|
236
|
+
expect(runtime.read("".concat(appLoc, "/data/a.txt")).contents).toBe('a');
|
|
237
|
+
return [2 /*return*/];
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}); });
|
|
241
|
+
it('reports the files a run changed, and the whole drive on demand', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
242
|
+
var runtime;
|
|
243
|
+
var _a;
|
|
244
|
+
return __generator(this, function (_b) {
|
|
245
|
+
switch (_b.label) {
|
|
246
|
+
case 0:
|
|
247
|
+
runtime = loadRuntime();
|
|
248
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {},
|
|
249
|
+
_a[drivePath('services/public/getdata.js')] = '_webout = "x";',
|
|
250
|
+
_a))];
|
|
251
|
+
case 1:
|
|
252
|
+
_b.sent();
|
|
253
|
+
runtime.takeDirty();
|
|
254
|
+
runtime.run({
|
|
255
|
+
code: "require('fs').writeFileSync('/sasjs/drive/files".concat(appLoc, "/data/new.txt', 'n');")
|
|
256
|
+
});
|
|
257
|
+
expect(Object.keys(runtime.takeDirty())).toEqual([
|
|
258
|
+
"/sasjs/drive/files".concat(appLoc, "/data/new.txt")
|
|
259
|
+
]);
|
|
260
|
+
expect(Object.keys(runtime.dumpAll()).some(function (k) { return k.endsWith('new.txt'); })).toBe(true);
|
|
261
|
+
// the seed, the three session files and the one the service wrote
|
|
262
|
+
expect(runtime.stats().files).toBe(5);
|
|
263
|
+
return [2 /*return*/];
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}); });
|
|
267
|
+
it('clears the drive and re-seeds on reset', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
268
|
+
var runtime;
|
|
269
|
+
var _a;
|
|
270
|
+
return __generator(this, function (_b) {
|
|
271
|
+
switch (_b.label) {
|
|
272
|
+
case 0:
|
|
273
|
+
runtime = loadRuntime();
|
|
274
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {},
|
|
275
|
+
_a[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
|
|
276
|
+
_a))];
|
|
277
|
+
case 1:
|
|
278
|
+
_b.sent();
|
|
279
|
+
runtime.run({
|
|
280
|
+
code: "require('fs').writeFileSync('/sasjs/drive/files".concat(appLoc, "/temp.txt', 't');")
|
|
281
|
+
});
|
|
282
|
+
expect(runtime.dumpAll()["/sasjs/drive/files".concat(appLoc, "/temp.txt")]).toBe('t');
|
|
283
|
+
return [4 /*yield*/, runtime.reset()];
|
|
284
|
+
case 2:
|
|
285
|
+
_b.sent();
|
|
286
|
+
expect(runtime.dumpAll()["/sasjs/drive/files".concat(appLoc, "/temp.txt")]).toBe(undefined);
|
|
287
|
+
expect(runtime.run({ program: "".concat(appLoc, "/services/public/getdata") }).webout).toBe('seeded');
|
|
288
|
+
return [2 /*return*/];
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}); });
|
|
292
|
+
it('fails a run that names a program the drive does not hold', function () {
|
|
293
|
+
var runtime = loadRuntime();
|
|
294
|
+
expect(function () { return runtime.run({ program: 'services/public/missing' }); }).toThrow(/Program not found in drive/);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
/**
|
|
40
|
+
* The other half of the parity claim: the service worker is what turns an
|
|
41
|
+
* /SASjsApi/ request into a response, so its framing (webout body, the _debug
|
|
42
|
+
* log shape, the 400 and the 500) is tested against the file the site ships.
|
|
43
|
+
*/
|
|
44
|
+
var appLoc = '/Public/app/test';
|
|
45
|
+
var origin = 'https://mock.example.com';
|
|
46
|
+
var LOGS_SEPARATOR = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784';
|
|
47
|
+
var drivePath = function (relative) {
|
|
48
|
+
return "/sasjs/drive/files".concat(appLoc, "/").concat(relative);
|
|
49
|
+
};
|
|
50
|
+
var loadWorker = function () {
|
|
51
|
+
jest.resetModules();
|
|
52
|
+
var handlers = {};
|
|
53
|
+
var self = {
|
|
54
|
+
location: { origin: origin },
|
|
55
|
+
addEventListener: function (type, fn) {
|
|
56
|
+
handlers[type] = handlers[type] || [];
|
|
57
|
+
handlers[type].push(fn);
|
|
58
|
+
},
|
|
59
|
+
skipWaiting: function () { },
|
|
60
|
+
clients: { claim: function () { }, matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
61
|
+
return [2 /*return*/, []];
|
|
62
|
+
}); }); } }
|
|
63
|
+
};
|
|
64
|
+
global.self = self;
|
|
65
|
+
global.importScripts = function (path) {
|
|
66
|
+
if (path.includes('md5'))
|
|
67
|
+
return;
|
|
68
|
+
require('../assets/runtime-core.js');
|
|
69
|
+
};
|
|
70
|
+
require('../assets/sw.js');
|
|
71
|
+
global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
|
|
72
|
+
var _a;
|
|
73
|
+
return __generator(this, function (_b) {
|
|
74
|
+
return [2 /*return*/, String(url).includes('vfs-seed')
|
|
75
|
+
? jsonResponse({
|
|
76
|
+
files: (_a = {},
|
|
77
|
+
_a[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
|
|
78
|
+
_a)
|
|
79
|
+
})
|
|
80
|
+
: jsonResponse({ appLoc: appLoc })];
|
|
81
|
+
});
|
|
82
|
+
}); });
|
|
83
|
+
var runtime = self.SASjsMockRuntime;
|
|
84
|
+
var dispatch = function (path, init) {
|
|
85
|
+
if (init === void 0) { init = {}; }
|
|
86
|
+
var request = new Request(origin + path, init);
|
|
87
|
+
var event = {
|
|
88
|
+
request: request,
|
|
89
|
+
respondWith: function (response) {
|
|
90
|
+
event.response = response;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
(handlers.fetch || [])[0](event);
|
|
94
|
+
return event.response;
|
|
95
|
+
};
|
|
96
|
+
var message = function (data) {
|
|
97
|
+
var replies = [];
|
|
98
|
+
var event = {
|
|
99
|
+
data: data,
|
|
100
|
+
source: { postMessage: function (m) { return replies.push(m); } },
|
|
101
|
+
waitUntil: function (promise) {
|
|
102
|
+
event.pending = promise;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
(handlers.message || []).forEach(function (fn) { return fn(event); });
|
|
106
|
+
return { replies: replies, pending: event.pending };
|
|
107
|
+
};
|
|
108
|
+
var lifecycle = function (type) {
|
|
109
|
+
var event = { waitUntil: function () { } };
|
|
110
|
+
(handlers[type] || []).forEach(function (fn) { return fn(event); });
|
|
111
|
+
};
|
|
112
|
+
return { runtime: runtime, dispatch: dispatch, message: message, lifecycle: lifecycle, self: self };
|
|
113
|
+
};
|
|
114
|
+
var jsonResponse = function (body) { return ({
|
|
115
|
+
ok: true,
|
|
116
|
+
json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
117
|
+
return [2 /*return*/, body];
|
|
118
|
+
}); }); }
|
|
119
|
+
}); };
|
|
120
|
+
var runRequest = function (body) { return ({
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'content-type': 'application/json' },
|
|
123
|
+
body: JSON.stringify(body)
|
|
124
|
+
}); };
|
|
125
|
+
describe('service worker host', function () {
|
|
126
|
+
afterEach(function () {
|
|
127
|
+
delete global.self;
|
|
128
|
+
delete global.importScripts;
|
|
129
|
+
delete global.fetch;
|
|
130
|
+
});
|
|
131
|
+
var seeded = function () {
|
|
132
|
+
var _a;
|
|
133
|
+
var worker = loadWorker();
|
|
134
|
+
worker.runtime.load((_a = {},
|
|
135
|
+
_a[drivePath('services/public/getdata.js')] = 'console.log("hello"); _webout = "seeded";',
|
|
136
|
+
_a));
|
|
137
|
+
return worker;
|
|
138
|
+
};
|
|
139
|
+
it('returns the webout as the response body', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
140
|
+
var dispatch, response, _a;
|
|
141
|
+
return __generator(this, function (_b) {
|
|
142
|
+
switch (_b.label) {
|
|
143
|
+
case 0:
|
|
144
|
+
dispatch = seeded().dispatch;
|
|
145
|
+
return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
|
|
146
|
+
encodeURIComponent("".concat(appLoc, "/services/public/getdata")))];
|
|
147
|
+
case 1:
|
|
148
|
+
response = _b.sent();
|
|
149
|
+
expect(response.status).toBe(200);
|
|
150
|
+
_a = expect;
|
|
151
|
+
return [4 /*yield*/, response.text()];
|
|
152
|
+
case 2:
|
|
153
|
+
_a.apply(void 0, [_b.sent()]).toBe('seeded');
|
|
154
|
+
return [2 /*return*/];
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}); });
|
|
158
|
+
it('frames the log behind the separator when _debug is set', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
159
|
+
var dispatch, response, _a;
|
|
160
|
+
return __generator(this, function (_b) {
|
|
161
|
+
switch (_b.label) {
|
|
162
|
+
case 0:
|
|
163
|
+
dispatch = seeded().dispatch;
|
|
164
|
+
return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
|
|
165
|
+
encodeURIComponent("".concat(appLoc, "/services/public/getdata")) +
|
|
166
|
+
'&_debug=log')
|
|
167
|
+
// the log lands between the separators, not the empty string the framing
|
|
168
|
+
// would produce if the lines were dropped
|
|
169
|
+
];
|
|
170
|
+
case 1:
|
|
171
|
+
response = _b.sent();
|
|
172
|
+
// the log lands between the separators, not the empty string the framing
|
|
173
|
+
// would produce if the lines were dropped
|
|
174
|
+
_a = expect;
|
|
175
|
+
return [4 /*yield*/, response.text()];
|
|
176
|
+
case 2:
|
|
177
|
+
// the log lands between the separators, not the empty string the framing
|
|
178
|
+
// would produce if the lines were dropped
|
|
179
|
+
_a.apply(void 0, [_b.sent()]).toBe(['seeded', LOGS_SEPARATOR, 'hello', LOGS_SEPARATOR].join('\n'));
|
|
180
|
+
return [2 /*return*/];
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}); });
|
|
184
|
+
it('answers 400 when a request names neither a program nor code', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
185
|
+
var dispatch, response, _a;
|
|
186
|
+
return __generator(this, function (_b) {
|
|
187
|
+
switch (_b.label) {
|
|
188
|
+
case 0:
|
|
189
|
+
dispatch = seeded().dispatch;
|
|
190
|
+
return [4 /*yield*/, dispatch('/SASjsApi/stp/execute')];
|
|
191
|
+
case 1:
|
|
192
|
+
response = _b.sent();
|
|
193
|
+
expect(response.status).toBe(400);
|
|
194
|
+
_a = expect;
|
|
195
|
+
return [4 /*yield*/, response.json()];
|
|
196
|
+
case 2:
|
|
197
|
+
_a.apply(void 0, [_b.sent()]).toEqual({ message: '_program is required' });
|
|
198
|
+
return [2 /*return*/];
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}); });
|
|
202
|
+
it('answers 500 with the failure shape when the service throws', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
203
|
+
var _a, runtime, dispatch, response, body;
|
|
204
|
+
var _b;
|
|
205
|
+
return __generator(this, function (_c) {
|
|
206
|
+
switch (_c.label) {
|
|
207
|
+
case 0:
|
|
208
|
+
_a = seeded(), runtime = _a.runtime, dispatch = _a.dispatch;
|
|
209
|
+
runtime.load((_b = {},
|
|
210
|
+
_b[drivePath('services/public/broken.js')] = 'throw new Error("boom")',
|
|
211
|
+
_b));
|
|
212
|
+
return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
|
|
213
|
+
encodeURIComponent("".concat(appLoc, "/services/public/broken")))];
|
|
214
|
+
case 1:
|
|
215
|
+
response = _c.sent();
|
|
216
|
+
return [4 /*yield*/, response.json()];
|
|
217
|
+
case 2:
|
|
218
|
+
body = _c.sent();
|
|
219
|
+
expect(response.status).toBe(500);
|
|
220
|
+
expect(body.status).toBe('failure');
|
|
221
|
+
expect(body.error).toContain('boom');
|
|
222
|
+
return [2 /*return*/];
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}); });
|
|
226
|
+
it('runs code carried by a code/execute request', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
227
|
+
var dispatch, response, _a;
|
|
228
|
+
return __generator(this, function (_b) {
|
|
229
|
+
switch (_b.label) {
|
|
230
|
+
case 0:
|
|
231
|
+
dispatch = seeded().dispatch;
|
|
232
|
+
return [4 /*yield*/, dispatch('/SASjsApi/code/execute', runRequest({ code: '_webout = "inline";', params: {} }))];
|
|
233
|
+
case 1:
|
|
234
|
+
response = _b.sent();
|
|
235
|
+
_a = expect;
|
|
236
|
+
return [4 /*yield*/, response.text()];
|
|
237
|
+
case 2:
|
|
238
|
+
_a.apply(void 0, [_b.sent()]).toBe('inline');
|
|
239
|
+
return [2 /*return*/];
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}); });
|
|
243
|
+
it('serves a file off the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
244
|
+
var dispatch, response, _a;
|
|
245
|
+
return __generator(this, function (_b) {
|
|
246
|
+
switch (_b.label) {
|
|
247
|
+
case 0:
|
|
248
|
+
dispatch = seeded().dispatch;
|
|
249
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
250
|
+
encodeURIComponent("".concat(appLoc, "/services/public/getdata.js")))];
|
|
251
|
+
case 1:
|
|
252
|
+
response = _b.sent();
|
|
253
|
+
_a = expect;
|
|
254
|
+
return [4 /*yield*/, response.text()];
|
|
255
|
+
case 2:
|
|
256
|
+
_a.apply(void 0, [_b.sent()]).toBe('console.log("hello"); _webout = "seeded";');
|
|
257
|
+
return [2 /*return*/];
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}); });
|
|
261
|
+
it('answers the API info and drive routes', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
262
|
+
var dispatch, info, _a, folder, _b, tree, _c, _d, _e;
|
|
263
|
+
return __generator(this, function (_f) {
|
|
264
|
+
switch (_f.label) {
|
|
265
|
+
case 0:
|
|
266
|
+
dispatch = seeded().dispatch;
|
|
267
|
+
return [4 /*yield*/, dispatch('/SASjsApi/info')];
|
|
268
|
+
case 1:
|
|
269
|
+
info = _f.sent();
|
|
270
|
+
_a = expect;
|
|
271
|
+
return [4 /*yield*/, info.json()];
|
|
272
|
+
case 2:
|
|
273
|
+
_a.apply(void 0, [_f.sent()]).toEqual(expect.objectContaining({ protocol: 'browser', runTimes: ['js'] }));
|
|
274
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/folder?_folderPath=' +
|
|
275
|
+
encodeURIComponent("".concat(appLoc, "/services/public")))];
|
|
276
|
+
case 3:
|
|
277
|
+
folder = _f.sent();
|
|
278
|
+
_b = expect;
|
|
279
|
+
return [4 /*yield*/, folder.json()];
|
|
280
|
+
case 4:
|
|
281
|
+
_b.apply(void 0, [(_f.sent()).files]).toContain('getdata.js');
|
|
282
|
+
return [4 /*yield*/, dispatch('/SASjsApi/drive/filetree')];
|
|
283
|
+
case 5:
|
|
284
|
+
tree = _f.sent();
|
|
285
|
+
_c = expect;
|
|
286
|
+
_e = (_d = JSON).stringify;
|
|
287
|
+
return [4 /*yield*/, tree.json()];
|
|
288
|
+
case 6:
|
|
289
|
+
_c.apply(void 0, [_e.apply(_d, [_f.sent()])]).toContain('getdata.js');
|
|
290
|
+
return [2 /*return*/];
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}); });
|
|
294
|
+
it('accepts a multipart request body', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
295
|
+
var dispatch, form, response, _a;
|
|
296
|
+
return __generator(this, function (_b) {
|
|
297
|
+
switch (_b.label) {
|
|
298
|
+
case 0:
|
|
299
|
+
dispatch = seeded().dispatch;
|
|
300
|
+
form = new FormData();
|
|
301
|
+
form.append('_program', "".concat(appLoc, "/services/public/getdata"));
|
|
302
|
+
return [4 /*yield*/, dispatch('/SASjsApi/stp/execute', {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
body: form
|
|
305
|
+
})];
|
|
306
|
+
case 1:
|
|
307
|
+
response = _b.sent();
|
|
308
|
+
_a = expect;
|
|
309
|
+
return [4 /*yield*/, response.text()];
|
|
310
|
+
case 2:
|
|
311
|
+
_a.apply(void 0, [_b.sent()]).toBe('seeded');
|
|
312
|
+
return [2 /*return*/];
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}); });
|
|
316
|
+
it('acknowledges a load, a seed, a reset and a snapshot from the page', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
317
|
+
var _a, message, runtime, files, load, seed, snapshot, reset, stats;
|
|
318
|
+
var _b;
|
|
319
|
+
return __generator(this, function (_c) {
|
|
320
|
+
switch (_c.label) {
|
|
321
|
+
case 0:
|
|
322
|
+
_a = seeded(), message = _a.message, runtime = _a.runtime;
|
|
323
|
+
files = (_b = {}, _b[drivePath('services/public/other.js')] = '_webout = "x";', _b);
|
|
324
|
+
load = message({ type: 'sasjs-load', files: files });
|
|
325
|
+
return [4 /*yield*/, load.pending];
|
|
326
|
+
case 1:
|
|
327
|
+
_c.sent();
|
|
328
|
+
expect(load.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-load-ack' }));
|
|
329
|
+
seed = message({ type: 'sasjs-seed' });
|
|
330
|
+
return [4 /*yield*/, seed.pending];
|
|
331
|
+
case 2:
|
|
332
|
+
_c.sent();
|
|
333
|
+
expect(seed.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-seed-ack' }));
|
|
334
|
+
runtime.load(files);
|
|
335
|
+
snapshot = message({ type: 'sasjs-snapshot' });
|
|
336
|
+
expect(snapshot.replies[0].type).toBe('sasjs-snapshot');
|
|
337
|
+
expect(Object.keys(snapshot.replies[0].files).length).toBe(1);
|
|
338
|
+
reset = message({ type: 'sasjs-reset' });
|
|
339
|
+
return [4 /*yield*/, reset.pending];
|
|
340
|
+
case 3:
|
|
341
|
+
_c.sent();
|
|
342
|
+
expect(reset.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-reset-ack' }));
|
|
343
|
+
stats = message({ type: 'sasjs-stats' });
|
|
344
|
+
return [4 /*yield*/, stats.pending];
|
|
345
|
+
case 4:
|
|
346
|
+
_c.sent();
|
|
347
|
+
expect(stats.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-stats' }));
|
|
348
|
+
return [2 /*return*/];
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}); });
|
|
352
|
+
it('claims the clients on install and activate', function () {
|
|
353
|
+
var lifecycle = seeded().lifecycle;
|
|
354
|
+
expect(function () { return lifecycle('install'); }).not.toThrow();
|
|
355
|
+
expect(function () { return lifecycle('activate'); }).not.toThrow();
|
|
356
|
+
});
|
|
357
|
+
it('leaves requests outside /SASjsApi/ to the static host', function () {
|
|
358
|
+
var dispatch = seeded().dispatch;
|
|
359
|
+
expect(dispatch('/index.html')).toBeUndefined();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -3,6 +3,7 @@ export { addCredential } from './add/addCredential';
|
|
|
3
3
|
export { compile } from './compile/compile';
|
|
4
4
|
export { compileSingleFile } from './compile/compileSingleFile';
|
|
5
5
|
export { build, getBuildVars } from './build/build';
|
|
6
|
+
export { buildMockSite } from './build/internal/mockSite/mockSite';
|
|
6
7
|
export { init } from './init/init';
|
|
7
8
|
export { create } from './create/create';
|
|
8
9
|
export { generateDocs } from './docs/generateDocs';
|