@sasjs/cli 6.21.0 → 6.23.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/cli.d.ts +18 -0
- package/build/cli.js +82 -8
- package/build/cli.spec.js +103 -0
- package/build/commands/build/internal/mockSite/assets/runtime-core.js +3 -0
- package/build/commands/build/internal/mockSite/assets/sw.js +113 -9
- package/build/commands/build/internal/mockSite/mockSite.d.ts +2 -0
- package/build/commands/build/internal/mockSite/mockSite.js +8 -2
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +37 -0
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +138 -1
- package/build/commands/help/help.js +27 -6
- package/build/commands/logcheck/logcheckCommand.js +1 -1
- package/build/commands/logcheck/types.d.ts +3 -1
- package/build/types/command/parse.d.ts +24 -3
- package/build/types/command/parse.js +40 -12
- package/build/types/command/spec/parse.spec.js +28 -0
- package/package.json +1 -1
package/build/cli.d.ts
CHANGED
|
@@ -1,2 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line printed when the arguments name no command the CLI registers.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the printing so it can be asserted without standing up a
|
|
5
|
+
* logger, and so the two cases - a typo, and no argument at all - stay
|
|
6
|
+
* distinguishable in a script's output.
|
|
7
|
+
*
|
|
8
|
+
* @param name - the command as typed, empty when none was given
|
|
9
|
+
*/
|
|
10
|
+
export declare const unknownCommandMessage: (name: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* The registered command nearest to what was typed, when one is close enough to
|
|
13
|
+
* be worth naming - so `sasjs lc` finds `logcheck` rather than only saying no.
|
|
14
|
+
*
|
|
15
|
+
* @param name - the command as typed
|
|
16
|
+
* @returns the suggestion, or `undefined` when nothing is close
|
|
17
|
+
*/
|
|
18
|
+
export declare const closestCommand: (name: string) => string | undefined;
|
|
1
19
|
export declare function cli(args: string[]): Promise<void>;
|
|
2
20
|
export declare function instantiateLogger(): Promise<void>;
|
package/build/cli.js
CHANGED
|
@@ -36,29 +36,103 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.closestCommand = exports.unknownCommandMessage = void 0;
|
|
39
40
|
exports.cli = cli;
|
|
40
41
|
exports.instantiateLogger = instantiateLogger;
|
|
41
42
|
var logger_1 = require("@sasjs/utils/logger");
|
|
42
43
|
var utils_1 = require("./utils");
|
|
43
44
|
var parse_1 = require("./types/command/parse");
|
|
45
|
+
var help_1 = require("./commands/help/help");
|
|
46
|
+
var command_1 = require("./types/command");
|
|
47
|
+
/**
|
|
48
|
+
* The line printed when the arguments name no command the CLI registers.
|
|
49
|
+
*
|
|
50
|
+
* Kept apart from the printing so it can be asserted without standing up a
|
|
51
|
+
* logger, and so the two cases - a typo, and no argument at all - stay
|
|
52
|
+
* distinguishable in a script's output.
|
|
53
|
+
*
|
|
54
|
+
* @param name - the command as typed, empty when none was given
|
|
55
|
+
*/
|
|
56
|
+
var unknownCommandMessage = function (name) {
|
|
57
|
+
return name
|
|
58
|
+
? "Invalid SASjs command: ".concat(name, ".")
|
|
59
|
+
: 'No command given - here is what this CLI can do.';
|
|
60
|
+
};
|
|
61
|
+
exports.unknownCommandMessage = unknownCommandMessage;
|
|
62
|
+
/**
|
|
63
|
+
* The registered command nearest to what was typed, when one is close enough to
|
|
64
|
+
* be worth naming - so `sasjs lc` finds `logcheck` rather than only saying no.
|
|
65
|
+
*
|
|
66
|
+
* @param name - the command as typed
|
|
67
|
+
* @returns the suggestion, or `undefined` when nothing is close
|
|
68
|
+
*/
|
|
69
|
+
var closestCommand = function (name) {
|
|
70
|
+
var _a;
|
|
71
|
+
var typed = name.toLowerCase();
|
|
72
|
+
var names = (0, parse_1.commandNames)();
|
|
73
|
+
return (_a = names
|
|
74
|
+
.map(function (candidate) { return ({
|
|
75
|
+
candidate: candidate,
|
|
76
|
+
// A prefix is what the user was reaching for, so it outranks a short
|
|
77
|
+
// edit distance: `logc` means `logcheck`, not `doc`.
|
|
78
|
+
prefix: candidate.toLowerCase().startsWith(typed),
|
|
79
|
+
distance: editDistance(typed, candidate.toLowerCase())
|
|
80
|
+
}); })
|
|
81
|
+
.filter(function (_a) {
|
|
82
|
+
var prefix = _a.prefix, distance = _a.distance;
|
|
83
|
+
return prefix || distance <= 2;
|
|
84
|
+
})
|
|
85
|
+
.sort(function (a, b) { return Number(b.prefix) - Number(a.prefix) || a.distance - b.distance; })[0]) === null || _a === void 0 ? void 0 : _a.candidate;
|
|
86
|
+
};
|
|
87
|
+
exports.closestCommand = closestCommand;
|
|
88
|
+
var editDistance = function (a, b) {
|
|
89
|
+
var rows = Array.from({ length: a.length + 1 }, function (_, i) { return [i]; });
|
|
90
|
+
for (var j = 1; j <= b.length; j++)
|
|
91
|
+
rows[0][j] = j;
|
|
92
|
+
for (var i = 1; i <= a.length; i++) {
|
|
93
|
+
for (var j = 1; j <= b.length; j++) {
|
|
94
|
+
rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return rows[a.length][b.length];
|
|
98
|
+
};
|
|
44
99
|
function cli(args) {
|
|
45
100
|
return __awaiter(this, void 0, void 0, function () {
|
|
46
|
-
var command, returnCode;
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
var command, name_1, suggestion, returnCode;
|
|
102
|
+
var _a, _b, _c, _d;
|
|
103
|
+
return __generator(this, function (_e) {
|
|
104
|
+
switch (_e.label) {
|
|
49
105
|
case 0: return [4 /*yield*/, (0, utils_1.setProjectDir)(args)];
|
|
50
106
|
case 1:
|
|
51
|
-
|
|
107
|
+
_e.sent();
|
|
52
108
|
return [4 /*yield*/, instantiateLogger()];
|
|
53
109
|
case 2:
|
|
54
|
-
|
|
110
|
+
_e.sent();
|
|
55
111
|
return [4 /*yield*/, (0, utils_1.loadProjectEnvVariables)()];
|
|
56
112
|
case 3:
|
|
57
|
-
|
|
113
|
+
_e.sent();
|
|
58
114
|
command = (0, parse_1.parse)(args);
|
|
59
|
-
return [
|
|
115
|
+
if (!!command) return [3 /*break*/, 6];
|
|
116
|
+
name_1 = (0, parse_1.requestedCommandName)(args);
|
|
117
|
+
(_a = process.logger) === null || _a === void 0 ? void 0 : _a.error((0, exports.unknownCommandMessage)(name_1));
|
|
118
|
+
if (!!name_1) return [3 /*break*/, 5];
|
|
119
|
+
return [4 /*yield*/, (0, help_1.printHelpText)()];
|
|
60
120
|
case 4:
|
|
61
|
-
|
|
121
|
+
_e.sent();
|
|
122
|
+
process.exit(command_1.ReturnCode.InvalidCommand);
|
|
123
|
+
return [2 /*return*/];
|
|
124
|
+
case 5:
|
|
125
|
+
suggestion = (0, exports.closestCommand)(name_1);
|
|
126
|
+
if (suggestion) {
|
|
127
|
+
(_b = process.logger) === null || _b === void 0 ? void 0 : _b.info("Did you mean `sasjs ".concat(suggestion, "`?"));
|
|
128
|
+
}
|
|
129
|
+
(_c = process.logger) === null || _c === void 0 ? void 0 : _c.info("Commands: ".concat((0, parse_1.commandNames)().sort().join(', ')));
|
|
130
|
+
(_d = process.logger) === null || _d === void 0 ? void 0 : _d.info('Run `sasjs help` for the full text, or `sasjs <command> --help` for one command.');
|
|
131
|
+
process.exit(command_1.ReturnCode.InvalidCommand);
|
|
132
|
+
return [2 /*return*/];
|
|
133
|
+
case 6: return [4 /*yield*/, command.execute()];
|
|
134
|
+
case 7:
|
|
135
|
+
returnCode = _e.sent();
|
|
62
136
|
process.exit(returnCode);
|
|
63
137
|
return [2 /*return*/];
|
|
64
138
|
}
|
package/build/cli.spec.js
CHANGED
|
@@ -89,6 +89,7 @@ var test_1 = require("./utils/test");
|
|
|
89
89
|
var envVariablesModule = __importStar(require("./utils/loadEnvVariables"));
|
|
90
90
|
var projectDirModule = __importStar(require("./utils/setProjectDir"));
|
|
91
91
|
var parseModule = __importStar(require("./types/command/parse"));
|
|
92
|
+
var helpModule = __importStar(require("./commands/help/help"));
|
|
92
93
|
var utils_1 = require("@sasjs/utils");
|
|
93
94
|
var commandBase_1 = require("./types/command/commandBase");
|
|
94
95
|
var command_1 = require("./types/command");
|
|
@@ -163,6 +164,81 @@ describe('CLI command parsing', function () {
|
|
|
163
164
|
}
|
|
164
165
|
});
|
|
165
166
|
}); });
|
|
167
|
+
describe('when the arguments name no command', function () {
|
|
168
|
+
var helpSpy = function () {
|
|
169
|
+
return jest
|
|
170
|
+
.spyOn(helpModule, 'printHelpText')
|
|
171
|
+
.mockImplementation(function () {
|
|
172
|
+
return Promise.resolve({ outputCommands: '', outputAliases: '' });
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
it('should print the whole help when no command is given', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
176
|
+
var help;
|
|
177
|
+
return __generator(this, function (_a) {
|
|
178
|
+
switch (_a.label) {
|
|
179
|
+
case 0:
|
|
180
|
+
jest.spyOn(parseModule, 'parse').mockImplementation(function () { return undefined; });
|
|
181
|
+
jest
|
|
182
|
+
.spyOn(parseModule, 'requestedCommandName')
|
|
183
|
+
.mockImplementation(function () { return ''; });
|
|
184
|
+
jest.spyOn(parseModule, 'commandNames').mockImplementation(function () { return ['run']; });
|
|
185
|
+
help = helpSpy();
|
|
186
|
+
return [4 /*yield*/, (0, cli_1.cli)(['node', 'sasjs'])];
|
|
187
|
+
case 1:
|
|
188
|
+
_a.sent();
|
|
189
|
+
expect(help).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect(process.exit).toHaveBeenCalledWith(command_1.ReturnCode.InvalidCommand);
|
|
191
|
+
return [2 /*return*/];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}); });
|
|
195
|
+
it('should list the commands when the name is unknown', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
196
|
+
var help;
|
|
197
|
+
return __generator(this, function (_a) {
|
|
198
|
+
switch (_a.label) {
|
|
199
|
+
case 0:
|
|
200
|
+
jest.spyOn(parseModule, 'parse').mockImplementation(function () { return undefined; });
|
|
201
|
+
jest
|
|
202
|
+
.spyOn(parseModule, 'requestedCommandName')
|
|
203
|
+
.mockImplementation(function () { return 'frobnicate'; });
|
|
204
|
+
jest
|
|
205
|
+
.spyOn(parseModule, 'commandNames')
|
|
206
|
+
.mockImplementation(function () { return ['run', 'lint']; });
|
|
207
|
+
help = helpSpy();
|
|
208
|
+
return [4 /*yield*/, (0, cli_1.cli)(['node', 'sasjs', 'frobnicate'])
|
|
209
|
+
// The full text is for `sasjs` on its own; a typo gets the list, which is
|
|
210
|
+
// the part that answers it, rather than two hundred lines.
|
|
211
|
+
];
|
|
212
|
+
case 1:
|
|
213
|
+
_a.sent();
|
|
214
|
+
// The full text is for `sasjs` on its own; a typo gets the list, which is
|
|
215
|
+
// the part that answers it, rather than two hundred lines.
|
|
216
|
+
expect(help).not.toHaveBeenCalled();
|
|
217
|
+
expect(parseModule.commandNames).toHaveBeenCalled();
|
|
218
|
+
expect(process.exit).toHaveBeenCalledWith(command_1.ReturnCode.InvalidCommand);
|
|
219
|
+
return [2 /*return*/];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}); });
|
|
223
|
+
it('should not execute anything', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
224
|
+
return __generator(this, function (_a) {
|
|
225
|
+
switch (_a.label) {
|
|
226
|
+
case 0:
|
|
227
|
+
jest.spyOn(parseModule, 'parse').mockImplementation(function () { return undefined; });
|
|
228
|
+
jest
|
|
229
|
+
.spyOn(parseModule, 'requestedCommandName')
|
|
230
|
+
.mockImplementation(function () { return 'x'; });
|
|
231
|
+
jest.spyOn(parseModule, 'commandNames').mockImplementation(function () { return []; });
|
|
232
|
+
helpSpy();
|
|
233
|
+
return [4 /*yield*/, (0, cli_1.cli)(['node', 'sasjs', 'x'])];
|
|
234
|
+
case 1:
|
|
235
|
+
_a.sent();
|
|
236
|
+
expect(mockCommand.execute).not.toHaveBeenCalled();
|
|
237
|
+
return [2 /*return*/];
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}); });
|
|
241
|
+
});
|
|
166
242
|
it('should return with the correct exit code on errors', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
167
243
|
return __generator(this, function (_a) {
|
|
168
244
|
switch (_a.label) {
|
|
@@ -179,6 +255,25 @@ describe('CLI command parsing', function () {
|
|
|
179
255
|
});
|
|
180
256
|
}); });
|
|
181
257
|
});
|
|
258
|
+
describe('closestCommand', function () {
|
|
259
|
+
it('suggests a command one letter away', function () {
|
|
260
|
+
expect((0, cli_1.closestCommand)('logchek')).toEqual('logcheck');
|
|
261
|
+
});
|
|
262
|
+
it('suggests the command a prefix belongs to', function () {
|
|
263
|
+
expect((0, cli_1.closestCommand)('logc')).toEqual('logcheck');
|
|
264
|
+
});
|
|
265
|
+
it('suggests nothing when the name is unrelated', function () {
|
|
266
|
+
expect((0, cli_1.closestCommand)('frobnicate')).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe('unknownCommandMessage', function () {
|
|
270
|
+
it('names the command that was typed', function () {
|
|
271
|
+
expect((0, cli_1.unknownCommandMessage)('frobnicate')).toEqual('Invalid SASjs command: frobnicate.');
|
|
272
|
+
});
|
|
273
|
+
it('says no command was given when there is none', function () {
|
|
274
|
+
expect((0, cli_1.unknownCommandMessage)('')).toEqual('No command given - here is what this CLI can do.');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
182
277
|
var setupMocks = function () {
|
|
183
278
|
jest.resetAllMocks();
|
|
184
279
|
(0, test_1.mockProcessExit)();
|
|
@@ -191,5 +286,13 @@ var setupMocks = function () {
|
|
|
191
286
|
.spyOn(projectDirModule, 'setProjectDir')
|
|
192
287
|
.mockImplementation(function () { return Promise.resolve(); });
|
|
193
288
|
jest.spyOn(parseModule, 'parse').mockImplementation(function () { return mockCommand; });
|
|
289
|
+
jest
|
|
290
|
+
.spyOn(parseModule, 'requestedCommandName')
|
|
291
|
+
.mockImplementation(function () { return 'mock'; });
|
|
292
|
+
jest
|
|
293
|
+
.spyOn(helpModule, 'printHelpText')
|
|
294
|
+
.mockImplementation(function () {
|
|
295
|
+
return Promise.resolve({ outputCommands: '', outputAliases: '' });
|
|
296
|
+
});
|
|
194
297
|
jest.spyOn(mockCommand, 'execute');
|
|
195
298
|
};
|
|
@@ -1525,6 +1525,9 @@ const api = {
|
|
|
1525
1525
|
bytes: [...FILES.keys()].reduce((total, abs) => total + entrySizeAt(abs), 0)
|
|
1526
1526
|
}),
|
|
1527
1527
|
run: (payload) => runProgram(payload),
|
|
1528
|
+
// the file a program names, so the host can tell a .sas program from a .js one
|
|
1529
|
+
// and refuse the former instead of compiling SAS source as JavaScript
|
|
1530
|
+
resolveProgram: (program) => resolveProgram(program),
|
|
1528
1531
|
// the drive's bytes, for @sasjs/api's services: the rules that decide a
|
|
1529
1532
|
// status are the package's, so this host hands over storage and nothing else
|
|
1530
1533
|
driveStore,
|
|
@@ -181,19 +181,21 @@ const SEEDED_HEADER_LINE = String(R.HEADERS_SEED)
|
|
|
181
181
|
* it.
|
|
182
182
|
*/
|
|
183
183
|
const artifactOf = (result, debug) => {
|
|
184
|
-
if (debug)
|
|
184
|
+
if (debug) {
|
|
185
|
+
// webout SEP log SEP [listing] - the framing @sasjs/adapter and the Studio's
|
|
186
|
+
// parser both split on, so a recorded session reads the same as a real one.
|
|
187
|
+
const framed = [result.webout, LOGS_SEPARATOR, result.log, LOGS_SEPARATOR]
|
|
188
|
+
|
|
189
|
+
if (result.print) framed.push(result.print)
|
|
190
|
+
|
|
185
191
|
return {
|
|
186
|
-
contents:
|
|
187
|
-
result.webout,
|
|
188
|
-
LOGS_SEPARATOR,
|
|
189
|
-
result.log,
|
|
190
|
-
LOGS_SEPARATOR
|
|
191
|
-
].join('\n'),
|
|
192
|
+
contents: framed.join('\n'),
|
|
192
193
|
// the frame is text, and the service's own type and disposition are not
|
|
193
194
|
// applied to it - a download there would put the frame in the file
|
|
194
195
|
headers: { 'content-type': 'text/plain' },
|
|
195
196
|
log: result.log
|
|
196
197
|
}
|
|
198
|
+
}
|
|
197
199
|
|
|
198
200
|
const headers = parseHeaders(result.headers)
|
|
199
201
|
let contents = result.webout
|
|
@@ -231,6 +233,66 @@ const persistChanges = async () => {
|
|
|
231
233
|
await broadcast({ type: 'sasjs-persist', files })
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
// ----------------------------------------------------------- the SAS transcript
|
|
237
|
+
/**
|
|
238
|
+
* A recorded SAS session, for a project that ships one.
|
|
239
|
+
*
|
|
240
|
+
* A browser has no SAS binary. Rather than let a Run on a .sas program be a
|
|
241
|
+
* surprise, a project may record a session once and commit it as
|
|
242
|
+
* `sas-transcript.json`: the build copies it into the site and this answers a
|
|
243
|
+
* run of a program it names with that session's log, listing and webout. The
|
|
244
|
+
* recording is the demo's own data, so nothing here is invented for a project
|
|
245
|
+
* that does not ship one - those runs reach the runner and are refused exactly
|
|
246
|
+
* as before.
|
|
247
|
+
*
|
|
248
|
+
* Shape: `{ "<program path>": {"log": "...", "print": "...", "webout": "..."} }`,
|
|
249
|
+
* all three strings. The path is matched as written apart from the
|
|
250
|
+
* `/sasjs/drive/files` prefix and the `.sas` suffix, so a recording keeps the
|
|
251
|
+
* path a person sees; a bare file name is not enough, because that would answer
|
|
252
|
+
* every file of that name in the drive.
|
|
253
|
+
*/
|
|
254
|
+
const SAS_TRANSCRIPT = 'host/sas-transcript.json'
|
|
255
|
+
let TRANSCRIPT = null
|
|
256
|
+
|
|
257
|
+
const transcript = async () => {
|
|
258
|
+
if (TRANSCRIPT) return TRANSCRIPT
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(SAS_TRANSCRIPT, { cache: 'no-cache' })
|
|
261
|
+
TRANSCRIPT = response.ok ? await response.json() : {}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
TRANSCRIPT = {}
|
|
264
|
+
}
|
|
265
|
+
return TRANSCRIPT
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The drive path without its mount prefix or extension.
|
|
270
|
+
*
|
|
271
|
+
* A recording is written with the path a person sees (`/sasjs/drive/files/...`,
|
|
272
|
+
* ending in `.sas`); the route hands the request the path relative to the app,
|
|
273
|
+
* and Studio asks for it without the extension. Comparing the logical path makes
|
|
274
|
+
* those the same program without widening the match to the file name alone,
|
|
275
|
+
* which would answer every `report.sas` in the drive with a session recorded for
|
|
276
|
+
* some other file.
|
|
277
|
+
*/
|
|
278
|
+
const logicalPath = (value) =>
|
|
279
|
+
String(value || '')
|
|
280
|
+
.replace(/^\/sasjs\/drive\/files/i, '')
|
|
281
|
+
.replace(/\.sas$/i, '')
|
|
282
|
+
|
|
283
|
+
/** The recording naming this program, if the project shipped one. */
|
|
284
|
+
const transcriptFor = async (programPath) => {
|
|
285
|
+
const requested = logicalPath(programPath)
|
|
286
|
+
if (!requested) return undefined
|
|
287
|
+
|
|
288
|
+
const recorded = await transcript()
|
|
289
|
+
|
|
290
|
+
for (const key of Object.keys(recorded))
|
|
291
|
+
if (logicalPath(key) === requested) return recorded[key]
|
|
292
|
+
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
234
296
|
// ---------------------------------------------------------------- the runner
|
|
235
297
|
/**
|
|
236
298
|
* The work a run does, over the browser's drive.
|
|
@@ -250,6 +312,19 @@ class BrowserRuntime extends API.MemorySessionHost {
|
|
|
250
312
|
async run(request) {
|
|
251
313
|
const program = request.program || {}
|
|
252
314
|
const params = Object.assign({}, request.vars || {})
|
|
315
|
+
|
|
316
|
+
// A SAS program the recording does not name. This host has no SAS binary, so
|
|
317
|
+
// the answer is the refusal a client already understands - not a JavaScript
|
|
318
|
+
// SyntaxError quoting the SAS source, which is what handing it to the runner
|
|
319
|
+
// would produce. The file is resolved because the request may name the
|
|
320
|
+
// program without its extension.
|
|
321
|
+
if (program.code === undefined) {
|
|
322
|
+
const resolved = R.resolveProgram(program.path)
|
|
323
|
+
|
|
324
|
+
if (resolved && /\.sas$/i.test(resolved))
|
|
325
|
+
throw runFailure('"runTime" must be one of [js]')
|
|
326
|
+
}
|
|
327
|
+
|
|
253
328
|
const payload = { params, tables: tablesOf(request.files) }
|
|
254
329
|
if (program.code !== undefined) payload.code = program.code
|
|
255
330
|
else payload.program = program.path
|
|
@@ -516,8 +591,10 @@ const SERVICES = {
|
|
|
516
591
|
identity: IDENTITY,
|
|
517
592
|
records: RECORDS,
|
|
518
593
|
runtime: API.createRuntimeService(IDENTITY_STORE, RUNTIME, {
|
|
519
|
-
//
|
|
520
|
-
//
|
|
594
|
+
// The one run time this host executes. A recorded SAS session is answered
|
|
595
|
+
// before the runner (see SAS_TRANSCRIPT below), not by claiming a `sas` run
|
|
596
|
+
// time - a .sas program with no recording must still be refused, and a route
|
|
597
|
+
// that accepted `sas` would compile SAS source as JavaScript instead.
|
|
521
598
|
runTimes: ['js']
|
|
522
599
|
}),
|
|
523
600
|
session: API.createSessionService(IDENTITY_STORE, RUNTIME),
|
|
@@ -860,6 +937,33 @@ const runRoute = async (routeId, payload, subject) => {
|
|
|
860
937
|
runRequest.program = query._program || body._program
|
|
861
938
|
}
|
|
862
939
|
|
|
940
|
+
// A recorded SAS session is answered here, ahead of the run time check: a
|
|
941
|
+
// transcript is data, not a run time, and this host holds only `js`. Accepting
|
|
942
|
+
// `sas` instead would make the package compile SAS source as JavaScript and
|
|
943
|
+
// hand back a SyntaxError where every other .sas program gets a refusal.
|
|
944
|
+
// Neither a code run nor a trigger: a trigger has to answer with the session
|
|
945
|
+
// record the route builds, which a client parses as JSON, so a recording is not
|
|
946
|
+
// an answer there.
|
|
947
|
+
if (!code && !trigger) {
|
|
948
|
+
const recording = await transcriptFor(runRequest.program)
|
|
949
|
+
|
|
950
|
+
if (recording)
|
|
951
|
+
return artifactResponse(
|
|
952
|
+
artifactOf(
|
|
953
|
+
{
|
|
954
|
+
webout: recording.webout || '',
|
|
955
|
+
log: recording.log || '',
|
|
956
|
+
print: recording.print || '',
|
|
957
|
+
// the seed every session starts with, so a run without `_debug`
|
|
958
|
+
// answers text/html rather than leaving the type to the browser -
|
|
959
|
+
// the same rule runtime-core applies to a service that declares none
|
|
960
|
+
headers: SEEDED_HEADER_LINE
|
|
961
|
+
},
|
|
962
|
+
vars._debug !== undefined
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
}
|
|
966
|
+
|
|
863
967
|
if (trigger) {
|
|
864
968
|
runRequest.expiresAfterMins =
|
|
865
969
|
query.expiresAfterMins !== undefined
|
|
@@ -9,6 +9,8 @@ export interface MockSiteResult {
|
|
|
9
9
|
webFileCount: number;
|
|
10
10
|
/** True when a captured drive snapshot was shipped with the site. */
|
|
11
11
|
snapshot: boolean;
|
|
12
|
+
/** True when a recorded SAS session was shipped, for .sas runs to answer. */
|
|
13
|
+
sasTranscript: boolean;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* The webout() block is the one @sasjs/utils generates for the server, emitted
|
|
@@ -683,7 +683,7 @@ var patchIndexHtml = function (html, version, basePath) {
|
|
|
683
683
|
*/
|
|
684
684
|
function buildMockSite(target) {
|
|
685
685
|
return __awaiter(this, void 0, void 0, function () {
|
|
686
|
-
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
|
|
686
|
+
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, transcriptSource, hasTranscript, indexPath, _d, patched, warnings;
|
|
687
687
|
var e_8, _e;
|
|
688
688
|
var _f, _g, _h, _j, _k, _l;
|
|
689
689
|
return __generator(this, function (_m) {
|
|
@@ -854,6 +854,10 @@ function buildMockSite(target) {
|
|
|
854
854
|
if (hasSnapshot) {
|
|
855
855
|
fs_1.default.copyFileSync(snapshotSource, path_1.default.join(hostFolder, 'vfs-snapshot.json'));
|
|
856
856
|
}
|
|
857
|
+
transcriptSource = path_1.default.join(buildFolder, 'sas-transcript.json');
|
|
858
|
+
hasTranscript = fs_1.default.existsSync(transcriptSource);
|
|
859
|
+
if (hasTranscript)
|
|
860
|
+
fs_1.default.copyFileSync(transcriptSource, path_1.default.join(hostFolder, 'sas-transcript.json'));
|
|
857
861
|
// 3. the drive, laid out from the compile output
|
|
858
862
|
fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'vfs-seed.json'), JSON.stringify({ format: exports.DRIVE_FORMAT, files: seed }));
|
|
859
863
|
fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'mock-config.json'), JSON.stringify({
|
|
@@ -863,6 +867,7 @@ function buildMockSite(target) {
|
|
|
863
867
|
jsPrograms: jsPrograms,
|
|
864
868
|
programFolders: PROGRAM_FOLDERS,
|
|
865
869
|
snapshot: hasSnapshot,
|
|
870
|
+
sasTranscript: hasTranscript,
|
|
866
871
|
buildStamp: buildStamp,
|
|
867
872
|
builtAt: new Date().toISOString()
|
|
868
873
|
}, null, 2));
|
|
@@ -876,7 +881,8 @@ function buildMockSite(target) {
|
|
|
876
881
|
driveEntries: driveEntries,
|
|
877
882
|
jsPrograms: jsPrograms,
|
|
878
883
|
webFileCount: webFileCount,
|
|
879
|
-
snapshot: hasSnapshot
|
|
884
|
+
snapshot: hasSnapshot,
|
|
885
|
+
sasTranscript: hasTranscript
|
|
880
886
|
}];
|
|
881
887
|
}
|
|
882
888
|
});
|
|
@@ -642,6 +642,43 @@ describe('browser-hosted build', function () {
|
|
|
642
642
|
}
|
|
643
643
|
});
|
|
644
644
|
}); });
|
|
645
|
+
it('ships a recorded SAS session when the project has one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
646
|
+
var recording, result;
|
|
647
|
+
var _a;
|
|
648
|
+
return __generator(this, function (_b) {
|
|
649
|
+
switch (_b.label) {
|
|
650
|
+
case 0:
|
|
651
|
+
recording = (_a = {},
|
|
652
|
+
_a["/sasjs/drive/files".concat(appLoc, "/services/public/report.sas")] = {
|
|
653
|
+
log: 'NOTE: Copyright (c) 2016 by SAS Institute Inc., Cary, NC, USA.',
|
|
654
|
+
print: 'The SAS System',
|
|
655
|
+
webout: '{"SYSSITE":"00000000"}'
|
|
656
|
+
},
|
|
657
|
+
_a);
|
|
658
|
+
write(path_1.default.join(root, 'sasjsbuild', 'sas-transcript.json'), JSON.stringify(recording));
|
|
659
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
660
|
+
case 1:
|
|
661
|
+
result = _b.sent();
|
|
662
|
+
expect(result.sasTranscript).toBe(true);
|
|
663
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'sas-transcript.json'), 'utf8'))).toEqual(recording);
|
|
664
|
+
expect(readConfig().sasTranscript).toBe(true);
|
|
665
|
+
return [2 /*return*/];
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}); });
|
|
669
|
+
it('ships no recording when the project has none', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
670
|
+
var result;
|
|
671
|
+
return __generator(this, function (_a) {
|
|
672
|
+
switch (_a.label) {
|
|
673
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
674
|
+
case 1:
|
|
675
|
+
result = _a.sent();
|
|
676
|
+
expect(result.sasTranscript).toBe(false);
|
|
677
|
+
expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', 'sas-transcript.json'))).toBe(false);
|
|
678
|
+
return [2 /*return*/];
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}); });
|
|
645
682
|
it('ships no snapshot when the project has none', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
646
683
|
var result;
|
|
647
684
|
return __generator(this, function (_a) {
|
|
@@ -163,7 +163,7 @@ var loadWorker = function (base) {
|
|
|
163
163
|
};
|
|
164
164
|
require('../assets/sw.js');
|
|
165
165
|
global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
|
|
166
|
-
var target;
|
|
166
|
+
var target, fixture;
|
|
167
167
|
var _a;
|
|
168
168
|
return __generator(this, function (_b) {
|
|
169
169
|
target = typeof input === 'string' ? input : (input && input.url) || String(input);
|
|
@@ -175,6 +175,10 @@ var loadWorker = function (base) {
|
|
|
175
175
|
})];
|
|
176
176
|
if (target.includes('index.html'))
|
|
177
177
|
return [2 /*return*/, new Response('<!doctype html><html><head><title>App</title></head><body>home</body></html>', { status: 200, headers: { 'content-type': 'text/html' } })];
|
|
178
|
+
if (target.includes('sas-transcript')) {
|
|
179
|
+
fixture = global.__sasTranscript;
|
|
180
|
+
return [2 /*return*/, fixture ? jsonResponse(fixture) : new Response('', { status: 404 })];
|
|
181
|
+
}
|
|
178
182
|
return [2 /*return*/, target.includes('vfs-seed')
|
|
179
183
|
? jsonResponse({
|
|
180
184
|
files: (_a = {},
|
|
@@ -286,6 +290,104 @@ describe('service worker host', function () {
|
|
|
286
290
|
}
|
|
287
291
|
});
|
|
288
292
|
}); });
|
|
293
|
+
it('answers a recorded SAS session with its log, listing and webout', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
294
|
+
var worker, response, body;
|
|
295
|
+
var _a, _b;
|
|
296
|
+
return __generator(this, function (_c) {
|
|
297
|
+
switch (_c.label) {
|
|
298
|
+
case 0:
|
|
299
|
+
// A browser has no SAS binary. A project may ship the transcript of one real
|
|
300
|
+
// session, and a program it names is answered with it - framed the way the
|
|
301
|
+
// adapter and the Studio split the three parts: webout SEP log SEP listing.
|
|
302
|
+
;
|
|
303
|
+
global.__sasTranscript = (_a = {},
|
|
304
|
+
_a["".concat(appLoc, "/services/public/report.sas")] = {
|
|
305
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
306
|
+
log: 'NOTE: Copyright (c) 2016 by SAS Institute Inc., Cary, NC, USA.',
|
|
307
|
+
print: 'The SAS System\n\n Obs customer\n 1 Northwind Ltd'
|
|
308
|
+
},
|
|
309
|
+
_a);
|
|
310
|
+
worker = loadWorker();
|
|
311
|
+
worker.runtime.load((_b = {},
|
|
312
|
+
_b[drivePath('services/public/report.sas')] = 'data work.class; run;',
|
|
313
|
+
_b));
|
|
314
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
315
|
+
encodeURIComponent("".concat(appLoc, "/services/public/report")) +
|
|
316
|
+
'&_debug=131')];
|
|
317
|
+
case 1:
|
|
318
|
+
response = _c.sent();
|
|
319
|
+
expect(response.status).toBe(200);
|
|
320
|
+
return [4 /*yield*/, response.text()];
|
|
321
|
+
case 2:
|
|
322
|
+
body = _c.sent();
|
|
323
|
+
expect(body).toContain('NOTE: Copyright');
|
|
324
|
+
expect(body).toContain('Northwind Ltd');
|
|
325
|
+
expect(body).toContain('"SYSSITE":"00000000"');
|
|
326
|
+
delete global.__sasTranscript;
|
|
327
|
+
return [2 /*return*/];
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}); });
|
|
331
|
+
it('gives a recorded run the seeded content type when not debugging', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
332
|
+
var worker, response, _a;
|
|
333
|
+
var _b, _c;
|
|
334
|
+
return __generator(this, function (_d) {
|
|
335
|
+
switch (_d.label) {
|
|
336
|
+
case 0:
|
|
337
|
+
// Without `_debug` the body is the webout alone, and it still has to carry
|
|
338
|
+
// the type a session is seeded with - an empty header set leaves the choice
|
|
339
|
+
// to the browser, which is the case the seed exists to prevent.
|
|
340
|
+
;
|
|
341
|
+
global.__sasTranscript = (_b = {},
|
|
342
|
+
_b["".concat(appLoc, "/services/public/report.sas")] = {
|
|
343
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
344
|
+
log: 'NOTE: recorded',
|
|
345
|
+
print: 'The SAS System'
|
|
346
|
+
},
|
|
347
|
+
_b);
|
|
348
|
+
worker = loadWorker();
|
|
349
|
+
worker.runtime.load((_c = {},
|
|
350
|
+
_c[drivePath('services/public/report.sas')] = 'data work.class; run;',
|
|
351
|
+
_c));
|
|
352
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
353
|
+
encodeURIComponent("".concat(appLoc, "/services/public/report")))];
|
|
354
|
+
case 1:
|
|
355
|
+
response = _d.sent();
|
|
356
|
+
expect(response.status).toBe(200);
|
|
357
|
+
expect(response.headers.get('content-type')).toContain('text/html');
|
|
358
|
+
_a = expect;
|
|
359
|
+
return [4 /*yield*/, response.text()];
|
|
360
|
+
case 2:
|
|
361
|
+
_a.apply(void 0, [_d.sent()]).toBe('{"SYSSITE":"00000000"}');
|
|
362
|
+
delete global.__sasTranscript;
|
|
363
|
+
return [2 /*return*/];
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}); });
|
|
367
|
+
it('refuses a .sas program no recording names, as it always did', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
368
|
+
var worker, response, body;
|
|
369
|
+
var _a;
|
|
370
|
+
return __generator(this, function (_b) {
|
|
371
|
+
switch (_b.label) {
|
|
372
|
+
case 0:
|
|
373
|
+
worker = loadWorker();
|
|
374
|
+
worker.runtime.load((_a = {},
|
|
375
|
+
_a[drivePath('services/public/other.sas')] = 'data work.class; run;',
|
|
376
|
+
_a));
|
|
377
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
378
|
+
encodeURIComponent("".concat(appLoc, "/services/public/other")))];
|
|
379
|
+
case 1:
|
|
380
|
+
response = _b.sent();
|
|
381
|
+
expect(response.status).toBe(400);
|
|
382
|
+
return [4 /*yield*/, response.text()];
|
|
383
|
+
case 2:
|
|
384
|
+
body = _b.sent();
|
|
385
|
+
expect(body).toContain('runTime');
|
|
386
|
+
expect(body).not.toContain('SyntaxError');
|
|
387
|
+
return [2 /*return*/];
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}); });
|
|
289
391
|
/**
|
|
290
392
|
* A worker whose drive holds one service that answers with a download, the
|
|
291
393
|
* way a streaming service does on the server: the body goes to the webout
|
|
@@ -1059,6 +1161,41 @@ describe('service worker host', function () {
|
|
|
1059
1161
|
}
|
|
1060
1162
|
});
|
|
1061
1163
|
}); });
|
|
1164
|
+
it('keeps a trigger a JSON answer, not the recorded transcript', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1165
|
+
var worker, response, body;
|
|
1166
|
+
var _a, _b;
|
|
1167
|
+
return __generator(this, function (_c) {
|
|
1168
|
+
switch (_c.label) {
|
|
1169
|
+
case 0:
|
|
1170
|
+
// A recording answers a run, not a trigger: the trigger route answers with
|
|
1171
|
+
// the session record a client parses as JSON, so returning the framed
|
|
1172
|
+
// webout there would break triggerProgram and the Studio.
|
|
1173
|
+
;
|
|
1174
|
+
global.__sasTranscript = (_a = {},
|
|
1175
|
+
_a["".concat(appLoc, "/services/public/report.sas")] = {
|
|
1176
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
1177
|
+
log: 'NOTE: recorded',
|
|
1178
|
+
print: 'The SAS System'
|
|
1179
|
+
},
|
|
1180
|
+
_a);
|
|
1181
|
+
worker = loadWorker();
|
|
1182
|
+
worker.runtime.load((_b = {},
|
|
1183
|
+
_b[drivePath('services/public/report.sas')] = 'data work.class; run;',
|
|
1184
|
+
_b));
|
|
1185
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/trigger?_program=' +
|
|
1186
|
+
encodeURIComponent("".concat(appLoc, "/services/public/report")), runRequest({ params: {} }))];
|
|
1187
|
+
case 1:
|
|
1188
|
+
response = _c.sent();
|
|
1189
|
+
expect(response.headers.get('content-type')).toContain('application/json');
|
|
1190
|
+
return [4 /*yield*/, response.json()];
|
|
1191
|
+
case 2:
|
|
1192
|
+
body = _c.sent();
|
|
1193
|
+
expect(body).not.toHaveProperty('webout');
|
|
1194
|
+
delete global.__sasTranscript;
|
|
1195
|
+
return [2 /*return*/];
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
}); });
|
|
1062
1199
|
it('refuses a deploy upload with no archive, and one that is not a zip', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1063
1200
|
var dispatch, empty, _a, form, notZip, _b;
|
|
1064
1201
|
return __generator(this, function (_c) {
|
|
@@ -209,6 +209,8 @@ function printHelpText() {
|
|
|
209
209
|
description: [
|
|
210
210
|
"lets the user add a build target to either the local project configuration, or to the global .sasjsrc file.",
|
|
211
211
|
"",
|
|
212
|
+
"[2spaces]NOTE: Providing insecure flag (--insecure or -i) is optional. It allows the command to bypass the HTTPS requirement, and is not recommended.",
|
|
213
|
+
"",
|
|
212
214
|
"NOTE: This command requires the user to input all the required parameters. More information available in the online documentation: ".concat(chalk_1.default.cyanBright('https://sasjs.io/sasjs-cli-add'))
|
|
213
215
|
]
|
|
214
216
|
},
|
|
@@ -220,6 +222,9 @@ function printHelpText() {
|
|
|
220
222
|
"With the 'login' sub command (SASVIYA targets only), authentication is performed with a SAS username and password - no OAuth client/secret required - and the resulting tokens are saved.",
|
|
221
223
|
"Without a sub command, behaves like 'sasjs add cred' (client/secret based authentication).",
|
|
222
224
|
"",
|
|
225
|
+
"[2spaces]NOTE: Providing insecure flag (--insecure or -i) is optional. It allows the command to bypass the HTTPS requirement, and is not recommended.",
|
|
226
|
+
"[2spaces]NOTE: Providing password-stdin flag (--passwordStdin) is optional. The password is read from stdin instead of being prompted for, which is how a pipeline passes it without a terminal.",
|
|
227
|
+
"",
|
|
223
228
|
"NOTE: This command requires the user to input all the required parameters. More information available in the online documentation: ".concat(chalk_1.default.cyanBright('https://cli.sasjs.io/auth/'))
|
|
224
229
|
]
|
|
225
230
|
},
|
|
@@ -231,7 +236,8 @@ function printHelpText() {
|
|
|
231
236
|
"The target can exist either in the local project configuration or in the global .sasjsrc file.",
|
|
232
237
|
"Providing log flag (--log or -l) is optional. If not present, the log is stored locally with a timestamp. If present, CLI will fetch and save the log to the specified location. If a relative location, it will be relative to the directory in which the command is invoked.",
|
|
233
238
|
"Providing source flag (--source or -s) is optional. It should point to a JSON file with the following structure: {\"macroVars\": {\"var1\": \"val1\", \"var2\": \"val2\"}}. These will be parsed into SAS \"%let\" statements and pre-append to the SAS code being run.",
|
|
234
|
-
"Providing compile flag (--compile or -c) is optional. It is used to compile the program prior to execution. Useful for including dependent macros and includes. More info here: https://cli.sasjs.io/compile"
|
|
239
|
+
"Providing compile flag (--compile or -c) is optional. It is used to compile the program prior to execution. Useful for including dependent macros and includes. More info here: https://cli.sasjs.io/compile",
|
|
240
|
+
"Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log this run returned and fails when the logCheck setting in sasjsconfig.json says so. --no-logcheck switches both the report and the verdict off for this run."
|
|
235
241
|
]
|
|
236
242
|
},
|
|
237
243
|
{
|
|
@@ -243,7 +249,9 @@ function printHelpText() {
|
|
|
243
249
|
"The target can exist either in the local project configuration or in the global .sasjsrc file.",
|
|
244
250
|
"<sasProgramPath> - if this has a leading slash (eg /Public/app/folder/servicename) then it must be the full path. If it is a relative path (eg path/servicename) then it will be pre-pended with the appLoc - which must then be defined in the sasjs config.",
|
|
245
251
|
"<path/to/log> - Location in which to store the log file. If not provided AND current directory is a sasjs project, it will be saved in sasjsresults else in the current directory",
|
|
246
|
-
"<path/to/output> - Location to store the output file. If not provided AND current directory is a sasjs project, an output file will be saved in the sasjsresults folder else in current directory."
|
|
252
|
+
"<path/to/output> - Location to store the output file. If not provided AND current directory is a sasjs project, an output file will be saved in the sasjsresults folder else in current directory.",
|
|
253
|
+
"<path/to/datafile> - (--data or -d) the json file holding the input data passed to the request. --config (-c) takes the json config for the execution itself.",
|
|
254
|
+
"Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log this request returned and fails when the logCheck setting in sasjsconfig.json says so."
|
|
247
255
|
]
|
|
248
256
|
},
|
|
249
257
|
{
|
|
@@ -274,7 +282,8 @@ function printHelpText() {
|
|
|
274
282
|
"* ".concat(chalk_1.default.cyanBright('compile'), " - Compiles a SAS program with the contents of a local directory."),
|
|
275
283
|
"[2spaces]command example: sasjs fs compile /Public/folder --output ./outputProgram",
|
|
276
284
|
"",
|
|
277
|
-
"[2spaces]NOTE: If output flag (-o or --output) is not provided, output will be stored in './fs-compile/{timestamp}' directory relative to working directory."
|
|
285
|
+
"[2spaces]NOTE: If output flag (-o or --output) is not provided, output will be stored in './fs-compile/{timestamp}' directory relative to working directory.",
|
|
286
|
+
"[2spaces]NOTE: Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log the compile returned, and fails when the logCheck setting in sasjsconfig.json says so."
|
|
278
287
|
]
|
|
279
288
|
},
|
|
280
289
|
{
|
|
@@ -290,7 +299,12 @@ function printHelpText() {
|
|
|
290
299
|
"[2spaces]NOTE: Providing output flag (--output or -o) is optional. If present, CLI will immediately print out the response JSON. If value is provided, it will be treated as file path to save the response JSON.",
|
|
291
300
|
"[2spaces]NOTE: Providing log flag (--log or -l) is optional. If present, CLI will fetch and save job log to local file.",
|
|
292
301
|
"[2spaces]NOTE: Providing ignore warnings (--ignoreWarnings or -i) flag is optional. If present, CLI will return status '0', when the job state is warning.",
|
|
293
|
-
"[2spaces]NOTE: Providing verbose (--verbose or -v) flag is optional. If present, CLI will log summary of every HTTP response. If set to 'bleached', CLI will log summary of every HTTP response without extra colors."
|
|
302
|
+
"[2spaces]NOTE: Providing verbose (--verbose or -v) flag is optional. If present, CLI will log summary of every HTTP response. If set to 'bleached', CLI will log summary of every HTTP response without extra colors.",
|
|
303
|
+
"[2spaces]NOTE: Providing source flag (--source or -s) is optional. It points to a JSON file of job variables, of the form {\"macroVars\": {\"var1\": \"val1\"}}.",
|
|
304
|
+
"[2spaces]NOTE: Providing returnStatusOnly (--returnStatusOnly or -r) is deprecated and has no effect: passing it prints a deprecation warning. The job status is returned with --verbose.",
|
|
305
|
+
"[2spaces]NOTE: Providing statusFile flag (--statusFile) is optional. The job status is fetched and saved to the given path; with no path it is printed to the console.",
|
|
306
|
+
"[2spaces]NOTE: Providing streamLog flag (--streamLog) is optional. As the job runs, its log is streamed into the local file given here, rather than fetched once at the end.",
|
|
307
|
+
"[2spaces]NOTE: Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log the job returned, and fails when the logCheck setting in sasjsconfig.json says so."
|
|
294
308
|
]
|
|
295
309
|
},
|
|
296
310
|
{
|
|
@@ -299,7 +313,10 @@ function printHelpText() {
|
|
|
299
313
|
description: [
|
|
300
314
|
"Performs operations on flows of jobs.",
|
|
301
315
|
"[2spaces]* ".concat(chalk_1.default.cyanBright('execute'), " - triggers flow for execution."),
|
|
302
|
-
"[2spaces]command example: sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv"
|
|
316
|
+
"[2spaces]command example: sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv",
|
|
317
|
+
"",
|
|
318
|
+
"[2spaces]NOTE: Providing streamLog flag (--streamLog) is optional. As each job in the flow runs, its log is streamed into the log folder, rather than fetched once the job finishes.",
|
|
319
|
+
"[2spaces]NOTE: Providing logcheck flag (--logcheck or --no-logcheck) is optional. It checks the log of every job in the flow, and fails when the logCheck setting in sasjsconfig.json says so. A log streamed with --streamLog goes straight to its file and is not checked."
|
|
303
320
|
]
|
|
304
321
|
},
|
|
305
322
|
{
|
|
@@ -333,7 +350,11 @@ function printHelpText() {
|
|
|
333
350
|
description: [
|
|
334
351
|
"Checks SAS logs for the messages SAS printed - ERRORs, WARNINGs and the notes that flag a problem - and fails when the failure policy is broken.",
|
|
335
352
|
"[2spaces]With no path, the project's results folder is checked. No target, no login and no project are needed, so a log produced elsewhere checks the same way.",
|
|
336
|
-
"[2spaces]command example: sasjs logcheck --fail-on warning --format json"
|
|
353
|
+
"[2spaces]command example: sasjs logcheck --fail-on warning --format json",
|
|
354
|
+
"",
|
|
355
|
+
"[2spaces]Flags: --fail-on error|warning|note|never (defaults to logCheck.failOn in sasjsconfig.json, else error), --max-warnings <n>, --ignore-code <49-169>, --ignore-path <path segment|glob>, --format human|json|annotations, --json, --verbose.",
|
|
356
|
+
"[2spaces]The same settings live in the logCheck section of sasjsconfig.json: --ignore-code and --ignore-path add to what the file lists, while --fail-on and --max-warnings replace the file's value when they are given.",
|
|
357
|
+
"[2spaces]The exit code is 5 when the policy is broken, which is distinct from lint's 3, so a pipeline can tell a run SAS complained about from code-quality failures."
|
|
337
358
|
]
|
|
338
359
|
},
|
|
339
360
|
{
|
|
@@ -115,7 +115,7 @@ var LogCheckCommand = /** @class */ (function (_super) {
|
|
|
115
115
|
},
|
|
116
116
|
'ignore-path': {
|
|
117
117
|
type: 'array',
|
|
118
|
-
description: 'Log path to leave out, as a
|
|
118
|
+
description: 'Log path to leave out, as a path segment or a glob. A non-glob entry matches whole segments only, so `test` covers `test/run.log` but not `latest.log`. Repeatable.'
|
|
119
119
|
},
|
|
120
120
|
format: {
|
|
121
121
|
type: 'string',
|
|
@@ -32,8 +32,10 @@ export interface LogCheckConfig {
|
|
|
32
32
|
*/
|
|
33
33
|
ignoreCodes: string[];
|
|
34
34
|
/**
|
|
35
|
-
* Log paths to leave out, as a
|
|
35
|
+
* Log paths to leave out, as a path segment (`sasjsresults/legacy`) or a
|
|
36
36
|
* glob (`**/build*.log`). Matched against the path shown in the report.
|
|
37
|
+
* A non-glob entry matches whole segments, so `test` covers `test/run.log`
|
|
38
|
+
* and not `latest.log`.
|
|
37
39
|
*/
|
|
38
40
|
ignorePaths: string[];
|
|
39
41
|
/**
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { CommandBase } from './commandBase';
|
|
2
2
|
/**
|
|
3
|
-
* Rewrites a version flag in the command position to the
|
|
3
|
+
* Rewrites a version or help flag in the command position to the command it
|
|
4
|
+
* stands for.
|
|
4
5
|
*
|
|
5
|
-
* @param args - the process arguments, as `[node, sasjs, ...]
|
|
6
|
+
* @param args - the process arguments, as `[node, sasjs, ...]`
|
|
6
7
|
*/
|
|
7
8
|
export declare const normaliseArgs: (args: string[]) => string[];
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the arguments to the command that will run them.
|
|
11
|
+
*
|
|
12
|
+
* @param args - the process arguments, as `[node, sasjs, ...]`
|
|
13
|
+
* @returns the command, or `undefined` when the arguments name no command the
|
|
14
|
+
* CLI registers. Printing the help and choosing the exit code is the caller's
|
|
15
|
+
* job, so this stays a pure lookup.
|
|
16
|
+
*/
|
|
17
|
+
export declare const parse: (args: string[]) => CommandBase | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The command name the arguments asked for, as typed - so a message can repeat
|
|
20
|
+
* it back. Empty when no command was given at all.
|
|
21
|
+
*
|
|
22
|
+
* @param args - the process arguments, as `[node, sasjs, ...]`
|
|
23
|
+
*/
|
|
24
|
+
export declare const requestedCommandName: (args: string[]) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Every command the CLI registers, so a misspelling can be answered with the
|
|
27
|
+
* list rather than a pointer to another command.
|
|
28
|
+
*/
|
|
29
|
+
export declare const commandNames: () => string[];
|
|
@@ -25,33 +25,61 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
25
25
|
return to.concat(ar || Array.prototype.slice.call(from));
|
|
26
26
|
};
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
-
exports.parse = exports.normaliseArgs = void 0;
|
|
28
|
+
exports.commandNames = exports.requestedCommandName = exports.parse = exports.normaliseArgs = void 0;
|
|
29
29
|
var getBaseCommand_1 = require("../../utils/getBaseCommand");
|
|
30
30
|
var commandFactory_1 = require("./commandFactory");
|
|
31
31
|
// `sasjs version`, `sasjs v`, `sasjs -v` and `sasjs --version` all mean the
|
|
32
32
|
// same thing. yargs reads a leading `-v`/`--version` as an option rather than
|
|
33
33
|
// a command, and its own version handling is switched off, so the token is
|
|
34
|
-
// rewritten to the command name before dispatch.
|
|
34
|
+
// rewritten to the command name before dispatch. The help flags get the same
|
|
35
|
+
// treatment: `sasjs --help` is the first thing most people try, and letting it
|
|
36
|
+
// fall through made the root answer "Invalid SASjs command: --help".
|
|
35
37
|
var versionFlags = ['-v', '--version'];
|
|
38
|
+
var helpFlags = ['-h', '--help'];
|
|
36
39
|
/**
|
|
37
|
-
* Rewrites a version flag in the command position to the
|
|
40
|
+
* Rewrites a version or help flag in the command position to the command it
|
|
41
|
+
* stands for.
|
|
38
42
|
*
|
|
39
|
-
* @param args - the process arguments, as `[node, sasjs, ...]
|
|
43
|
+
* @param args - the process arguments, as `[node, sasjs, ...]`
|
|
40
44
|
*/
|
|
41
45
|
var normaliseArgs = function (args) {
|
|
42
46
|
var _a = __read(args, 3), command = _a[2];
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if (versionFlags.includes(command)) {
|
|
48
|
+
return __spreadArray(__spreadArray(__spreadArray([], __read(args.slice(0, 2)), false), ['version'], false), __read(args.slice(3)), false);
|
|
49
|
+
}
|
|
50
|
+
if (helpFlags.includes(command)) {
|
|
51
|
+
return __spreadArray(__spreadArray(__spreadArray([], __read(args.slice(0, 2)), false), ['help'], false), __read(args.slice(3)), false);
|
|
52
|
+
}
|
|
53
|
+
return args;
|
|
45
54
|
};
|
|
46
55
|
exports.normaliseArgs = normaliseArgs;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the arguments to the command that will run them.
|
|
58
|
+
*
|
|
59
|
+
* @param args - the process arguments, as `[node, sasjs, ...]`
|
|
60
|
+
* @returns the command, or `undefined` when the arguments name no command the
|
|
61
|
+
* CLI registers. Printing the help and choosing the exit code is the caller's
|
|
62
|
+
* job, so this stays a pure lookup.
|
|
63
|
+
*/
|
|
47
64
|
var parse = function (args) {
|
|
48
|
-
var _a;
|
|
49
65
|
var commandArgs = (0, exports.normaliseArgs)(args);
|
|
50
66
|
var baseCommand = (0, getBaseCommand_1.getBaseCommand)(commandArgs);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return commandFactory_1.commandFactory.get(baseCommand.name)(commandArgs);
|
|
67
|
+
return commandFactory_1.commandFactory.has(baseCommand.name)
|
|
68
|
+
? commandFactory_1.commandFactory.get(baseCommand.name)(commandArgs)
|
|
69
|
+
: undefined;
|
|
56
70
|
};
|
|
57
71
|
exports.parse = parse;
|
|
72
|
+
/**
|
|
73
|
+
* The command name the arguments asked for, as typed - so a message can repeat
|
|
74
|
+
* it back. Empty when no command was given at all.
|
|
75
|
+
*
|
|
76
|
+
* @param args - the process arguments, as `[node, sasjs, ...]`
|
|
77
|
+
*/
|
|
78
|
+
var requestedCommandName = function (args) { var _a; return (_a = (0, exports.normaliseArgs)(args)[2]) !== null && _a !== void 0 ? _a : ''; };
|
|
79
|
+
exports.requestedCommandName = requestedCommandName;
|
|
80
|
+
/**
|
|
81
|
+
* Every command the CLI registers, so a misspelling can be answered with the
|
|
82
|
+
* list rather than a pointer to another command.
|
|
83
|
+
*/
|
|
84
|
+
var commandNames = function () { return Array.from(commandFactory_1.commandFactory.keys()); };
|
|
85
|
+
exports.commandNames = commandNames;
|
|
@@ -8,10 +8,31 @@ describe('parse', function () {
|
|
|
8
8
|
expect(command).toBeInstanceOf(versionCommand_1.VersionCommand);
|
|
9
9
|
expect(command.name).toEqual('version');
|
|
10
10
|
});
|
|
11
|
+
it.each(['help', '-h', '--help'])('resolves `sasjs %s` to the help command', function (token) {
|
|
12
|
+
var command = (0, parse_1.parse)(['node', 'sasjs', token]);
|
|
13
|
+
expect(command.name).toEqual('help');
|
|
14
|
+
});
|
|
11
15
|
it('leaves another command as it is', function () {
|
|
12
16
|
var command = (0, parse_1.parse)(['node', 'sasjs', 'help']);
|
|
13
17
|
expect(command.name).toEqual('help');
|
|
14
18
|
});
|
|
19
|
+
it('returns undefined for a command it does not register', function () {
|
|
20
|
+
expect((0, parse_1.parse)(['node', 'sasjs', 'frobnicate'])).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
it('returns undefined when no command is given', function () {
|
|
23
|
+
expect((0, parse_1.parse)(['node', 'sasjs'])).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('requestedCommandName', function () {
|
|
27
|
+
it('reports the command as typed', function () {
|
|
28
|
+
expect((0, parse_1.requestedCommandName)(['node', 'sasjs', 'frobnicate'])).toEqual('frobnicate');
|
|
29
|
+
});
|
|
30
|
+
it('reports the version command for a version flag', function () {
|
|
31
|
+
expect((0, parse_1.requestedCommandName)(['node', 'sasjs', '--version'])).toEqual('version');
|
|
32
|
+
});
|
|
33
|
+
it('reports an empty name when there is no command', function () {
|
|
34
|
+
expect((0, parse_1.requestedCommandName)(['node', 'sasjs'])).toEqual('');
|
|
35
|
+
});
|
|
15
36
|
});
|
|
16
37
|
describe('normaliseArgs', function () {
|
|
17
38
|
it.each(['-v', '--version'])('rewrites %s in the command position', function (flag) {
|
|
@@ -21,6 +42,13 @@ describe('normaliseArgs', function () {
|
|
|
21
42
|
'version'
|
|
22
43
|
]);
|
|
23
44
|
});
|
|
45
|
+
it.each(['-h', '--help'])('rewrites %s in the command position to the help command', function (flag) {
|
|
46
|
+
expect((0, parse_1.normaliseArgs)(['node', 'sasjs', flag])).toEqual([
|
|
47
|
+
'node',
|
|
48
|
+
'sasjs',
|
|
49
|
+
'help'
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
24
52
|
it('keeps the arguments that follow the flag', function () {
|
|
25
53
|
expect((0, parse_1.normaliseArgs)(['node', 'sasjs', '--version', 'extra'])).toEqual([
|
|
26
54
|
'node',
|
package/package.json
CHANGED