@sasjs/cli 4.18.1 → 4.18.2

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.
@@ -1,5 +1,23 @@
1
1
  import { CompileTree, Target } from '@sasjs/utils';
2
2
  import { TestFlow } from '../../../types';
3
+ /**
4
+ * Resolves the relative destination fragment for a compiled test file inside
5
+ * `sasjsbuild/tests` - either just the file's basename (saveToRoot, used for
6
+ * testSetUp/testTearDown), or the path fragment following the build
7
+ * destination folder name (used for service/job test files).
8
+ *
9
+ * `filePath` for testSetUp/testTearDown comes straight from a user's
10
+ * sasjsconfig.json, which conventionally uses forward slashes for
11
+ * portability - it is not guaranteed to match the current OS's separator.
12
+ * Splitting on both `/` and `\` (instead of only `path.sep`) means this
13
+ * works whether the config was authored with POSIX-style paths and the CLI
14
+ * is running on Windows (`path.sep === '\\'`) or vice versa. Splitting on
15
+ * `path.sep` alone silently no-ops when the string contains none of that
16
+ * separator, leaving the whole config path intact and producing a nested
17
+ * `sasjsbuild/tests/sasjs/tests/testsetup.sas` instead of a flat
18
+ * `sasjsbuild/tests/testsetup.sas`.
19
+ */
20
+ export declare const getTestFileDestinationFragment: (filePath: string, buildDestinationFolderName: string | undefined, saveToRoot: boolean) => string;
3
21
  export declare function compileTestFile(target: Target, filePath: string, testVar: string | undefined, saveToRoot: boolean | undefined, removeOriginalFile: boolean | undefined, compileTree: CompileTree, destinationPath?: string): Promise<void>;
4
22
  export declare function copyTestMacroFiles(folderAbsolutePath: string): Promise<void>;
5
23
  export declare const compileTestFlow: (target: Target) => Promise<TestFlow | undefined>;
@@ -64,7 +64,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
64
64
  return (mod && mod.__esModule) ? mod : { "default": mod };
65
65
  };
66
66
  Object.defineProperty(exports, "__esModule", { value: true });
67
- exports.compileTestFlow = exports.copyTestMacroFiles = exports.compileTestFile = void 0;
67
+ exports.compileTestFlow = exports.copyTestMacroFiles = exports.compileTestFile = exports.getTestFileDestinationFragment = void 0;
68
68
  var utils_1 = require("@sasjs/utils");
69
69
  var chalk_1 = __importDefault(require("chalk"));
70
70
  var path_1 = __importDefault(require("path"));
@@ -73,6 +73,37 @@ var config_1 = require("../../../utils/config");
73
73
  var file_1 = require("../../../utils/file");
74
74
  var _1 = require("./");
75
75
  var getFileName = function (filePath) { return path_1.default.parse(filePath).base; };
76
+ /**
77
+ * Resolves the relative destination fragment for a compiled test file inside
78
+ * `sasjsbuild/tests` - either just the file's basename (saveToRoot, used for
79
+ * testSetUp/testTearDown), or the path fragment following the build
80
+ * destination folder name (used for service/job test files).
81
+ *
82
+ * `filePath` for testSetUp/testTearDown comes straight from a user's
83
+ * sasjsconfig.json, which conventionally uses forward slashes for
84
+ * portability - it is not guaranteed to match the current OS's separator.
85
+ * Splitting on both `/` and `\` (instead of only `path.sep`) means this
86
+ * works whether the config was authored with POSIX-style paths and the CLI
87
+ * is running on Windows (`path.sep === '\\'`) or vice versa. Splitting on
88
+ * `path.sep` alone silently no-ops when the string contains none of that
89
+ * separator, leaving the whole config path intact and producing a nested
90
+ * `sasjsbuild/tests/sasjs/tests/testsetup.sas` instead of a flat
91
+ * `sasjsbuild/tests/testsetup.sas`.
92
+ */
93
+ var getTestFileDestinationFragment = function (filePath, buildDestinationFolderName, saveToRoot) {
94
+ var segments = filePath.split(/[\\/]/);
95
+ var destinationFragment = saveToRoot
96
+ ? segments.pop() || ''
97
+ : segments
98
+ .reduce(function (acc, item, i, arr) {
99
+ return acc.length
100
+ ? __spreadArray(__spreadArray([], __read(acc), false), [item], false) : arr[i - 1] === buildDestinationFolderName
101
+ ? __spreadArray(__spreadArray([], __read(acc), false), [item], false) : acc;
102
+ }, [])
103
+ .join(path_1.default.sep);
104
+ return destinationFragment;
105
+ };
106
+ exports.getTestFileDestinationFragment = getTestFileDestinationFragment;
76
107
  function compileTestFile(target, filePath, testVar, saveToRoot, removeOriginalFile, compileTree, destinationPath) {
77
108
  if (testVar === void 0) { testVar = ''; }
78
109
  if (saveToRoot === void 0) { saveToRoot = true; }
@@ -100,16 +131,7 @@ function compileTestFile(target, filePath, testVar, saveToRoot, removeOriginalFi
100
131
  .pop();
101
132
  destinationPath =
102
133
  destinationPath ||
103
- path_1.default.join(buildDestinationTestFolder, saveToRoot
104
- ? filePath.split(path_1.default.sep).pop() || ''
105
- : filePath
106
- .split(path_1.default.sep)
107
- .reduce(function (acc, item, i, arr) {
108
- return acc.length
109
- ? __spreadArray(__spreadArray([], __read(acc), false), [item], false) : arr[i - 1] === buildDestinationFolderName
110
- ? __spreadArray(__spreadArray([], __read(acc), false), [item], false) : acc;
111
- }, [])
112
- .join(path_1.default.sep));
134
+ path_1.default.join(buildDestinationTestFolder, (0, exports.getTestFileDestinationFragment)(filePath, buildDestinationFolderName, saveToRoot));
113
135
  return [4 /*yield*/, (0, utils_1.createFile)(destinationPath, dependencies)];
114
136
  case 4:
115
137
  _d.sent();
@@ -56,6 +56,31 @@ var test_1 = require("../../../../utils/test");
56
56
  var path_1 = __importDefault(require("path"));
57
57
  var compile_1 = require("../../compile");
58
58
  var chalk_1 = __importDefault(require("chalk"));
59
+ describe('getTestFileDestinationFragment', function () {
60
+ // sasjsconfig.json testConfig paths (testSetUp/testTearDown) are conventionally
61
+ // forward-slash, regardless of the OS the CLI is running on - these tests pin
62
+ // that a Windows-style host separator doesn't break the flattening/reduce logic
63
+ // when the config value itself uses forward slashes (see PLAN-windows-testsetup-path.md).
64
+ describe('saveToRoot = true (testSetUp/testTearDown)', function () {
65
+ it('flattens a forward-slash config path to its basename', function () {
66
+ expect((0, __1.getTestFileDestinationFragment)('sasjs/tests/testsetup.sas', 'tests', true)).toEqual('testsetup.sas');
67
+ });
68
+ it('flattens a backslash (Windows-native) path to its basename', function () {
69
+ expect((0, __1.getTestFileDestinationFragment)('sasjs\\tests\\testsetup.sas', 'tests', true)).toEqual('testsetup.sas');
70
+ });
71
+ it('returns an empty string for a path ending in a separator', function () {
72
+ expect((0, __1.getTestFileDestinationFragment)('sasjs/tests/', 'tests', true)).toEqual('');
73
+ });
74
+ });
75
+ describe('saveToRoot = false (service/job test files)', function () {
76
+ it('keeps the path fragment after the build destination folder name, for a forward-slash path', function () {
77
+ expect((0, __1.getTestFileDestinationFragment)('sasjsbuild/tests/services/admin/random.test.sas', 'tests', false)).toEqual(['services', 'admin', 'random.test.sas'].join(path_1.default.sep));
78
+ });
79
+ it('keeps the path fragment after the build destination folder name, for a backslash path', function () {
80
+ expect((0, __1.getTestFileDestinationFragment)('sasjsbuild\\tests\\services\\admin\\random.test.sas', 'tests', false)).toEqual(['services', 'admin', 'random.test.sas'].join(path_1.default.sep));
81
+ });
82
+ });
83
+ });
59
84
  describe('compileTestFile', function () {
60
85
  var appName = "cli-tests-compile-test-file-".concat((0, utils_1.generateTimestamp)());
61
86
  var temp = (0, test_1.generateTestTarget)(appName, '/Public/app', {
@@ -159,6 +184,57 @@ describe('compileTestFile', function () {
159
184
  }
160
185
  });
161
186
  }); });
187
+ it('should use default testVar/saveToRoot when omitted', function () { return __awaiter(void 0, void 0, void 0, function () {
188
+ var compileTree;
189
+ return __generator(this, function (_a) {
190
+ switch (_a.label) {
191
+ case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
192
+ case 1:
193
+ _a.sent();
194
+ compileTree = (0, __1.getCompileTree)(target);
195
+ return [4 /*yield*/, expect((0, __1.compileTestFile)(target, target.testConfig.testSetUp, undefined, undefined, false, compileTree)).toResolve()];
196
+ case 2:
197
+ _a.sent();
198
+ return [2 /*return*/];
199
+ }
200
+ });
201
+ }); });
202
+ it('should prepend a non-empty testVar to the compiled test file', function () { return __awaiter(void 0, void 0, void 0, function () {
203
+ var compileTree, testVar, compiledTestSetUpPath, compiledContent;
204
+ return __generator(this, function (_a) {
205
+ switch (_a.label) {
206
+ case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
207
+ case 1:
208
+ _a.sent();
209
+ compileTree = (0, __1.getCompileTree)(target);
210
+ testVar = '%let some_var=some_value;';
211
+ return [4 /*yield*/, (0, __1.compileTestFile)(target, target.testConfig.testSetUp, testVar, true, false, compileTree)];
212
+ case 2:
213
+ _a.sent();
214
+ compiledTestSetUpPath = path_1.default.join(buildPath, 'tests', 'testsetup.sas');
215
+ return [4 /*yield*/, (0, utils_1.readFile)(compiledTestSetUpPath)];
216
+ case 3:
217
+ compiledContent = _a.sent();
218
+ expect(compiledContent.indexOf(testVar)).toEqual(0);
219
+ return [2 /*return*/];
220
+ }
221
+ });
222
+ }); });
223
+ it('should skip copying a macro test file that is already present in the build folder', function () { return __awaiter(void 0, void 0, void 0, function () {
224
+ var macroFolderAbsolutePath;
225
+ return __generator(this, function (_a) {
226
+ switch (_a.label) {
227
+ case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
228
+ case 1:
229
+ _a.sent();
230
+ macroFolderAbsolutePath = path_1.default.join(__dirname, appName, 'sasjs', 'macros');
231
+ return [4 /*yield*/, expect((0, __1.copyTestMacroFiles)(macroFolderAbsolutePath)).toResolve()];
232
+ case 2:
233
+ _a.sent();
234
+ return [2 /*return*/];
235
+ }
236
+ });
237
+ }); });
162
238
  });
163
239
  describe('compileTestFlow', function () {
164
240
  it('should compile test flow', function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -198,6 +274,47 @@ describe('compileTestFile', function () {
198
274
  }
199
275
  });
200
276
  }); });
277
+ it('should return no tests when the tests folder is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
278
+ var testsFolderPath, testFlow;
279
+ return __generator(this, function (_a) {
280
+ switch (_a.label) {
281
+ case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
282
+ case 1:
283
+ _a.sent();
284
+ testsFolderPath = path_1.default.join(buildPath, 'tests');
285
+ return [4 /*yield*/, (0, utils_1.deleteFolder)(testsFolderPath)];
286
+ case 2:
287
+ _a.sent();
288
+ return [4 /*yield*/, (0, utils_1.createFolder)(testsFolderPath)];
289
+ case 3:
290
+ _a.sent();
291
+ return [4 /*yield*/, (0, __1.compileTestFlow)(target)];
292
+ case 4:
293
+ testFlow = _a.sent();
294
+ expect(testFlow.tests).toEqual([]);
295
+ expect(testFlow.testSetUp).toBeUndefined();
296
+ expect(testFlow.testTearDown).toBeUndefined();
297
+ return [2 /*return*/];
298
+ }
299
+ });
300
+ }); });
301
+ it('should not set testSetUp/testTearDown when the configured file is not among the compiled tests', function () { return __awaiter(void 0, void 0, void 0, function () {
302
+ var targetWithBogusSetup, testFlow;
303
+ return __generator(this, function (_a) {
304
+ switch (_a.label) {
305
+ case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
306
+ case 1:
307
+ _a.sent();
308
+ targetWithBogusSetup = new utils_1.Target(__assign(__assign({}, target.toJson()), { testConfig: __assign(__assign({}, target.testConfig), { testSetUp: 'does-not-exist-setup.sas', testTearDown: 'does-not-exist-teardown.sas' }) }));
309
+ return [4 /*yield*/, (0, __1.compileTestFlow)(targetWithBogusSetup)];
310
+ case 2:
311
+ testFlow = _a.sent();
312
+ expect(testFlow.testSetUp).toBeUndefined();
313
+ expect(testFlow.testTearDown).toBeUndefined();
314
+ return [2 /*return*/];
315
+ }
316
+ });
317
+ }); });
201
318
  it('should log coverage', function () { return __awaiter(void 0, void 0, void 0, function () {
202
319
  var expectedHeader, expectedData;
203
320
  return __generator(this, function (_a) {
@@ -68,10 +68,13 @@ describe('sasjs create', function () {
68
68
  afterEach(function () { return __awaiter(void 0, void 0, void 0, function () {
69
69
  return __generator(this, function (_a) {
70
70
  switch (_a.label) {
71
- case 0: return [4 /*yield*/, (0, utils_1.deleteFolder)(path_1.default.join(__dirname, 'test-app-create-*'))];
71
+ case 0:
72
+ if (!process.projectDir) return [3 /*break*/, 2];
73
+ return [4 /*yield*/, (0, utils_1.deleteFolder)(process.projectDir)];
72
74
  case 1:
73
75
  _a.sent();
74
- return [2 /*return*/];
76
+ _a.label = 2;
77
+ case 2: return [2 /*return*/];
75
78
  }
76
79
  });
77
80
  }); });
@@ -95,7 +95,7 @@ var getDocConfig_1 = require("./internal/getDocConfig");
95
95
  function generateDocs(target, config, outDirectory) {
96
96
  var _a, _b;
97
97
  return __awaiter(this, void 0, void 0, function () {
98
- var _c, serverUrl, newOutDirectory, enableLineage, doxyContentFromConfig, _d, macroCoreFolders, macroFolders, programFolders, serviceFolders, jobFolders, combinedFolders, PROJECT_NAME, PROJECT_BRIEF, packageJsonContent, packageJson, e_1, doxyContent, layoutFilePath, LAYOUT_FILE, doxyParams, doxyConfigPath, spinner, pathExists, _e, stderr, code, foldersListForDot;
98
+ var _c, serverUrl, newOutDirectory, enableLineage, doxyContentFromConfig, _d, macroCoreFolders, macroFolders, programFolders, serviceFolders, jobFolders, combinedFolders, PROJECT_NAME, PROJECT_BRIEF, packageJsonContent, packageJson, e_1, doxyContent, layoutFilePath, LAYOUT_FILE, readMePath, doxyParams, doxyConfigPath, spinner, pathExists, _e, stderr, code, foldersListForDot;
99
99
  var _this = this;
100
100
  return __generator(this, function (_f) {
101
101
  switch (_f.label) {
@@ -153,12 +153,20 @@ function generateDocs(target, config, outDirectory) {
153
153
  return [2 /*return*/, tmpFilePath];
154
154
  }
155
155
  });
156
- }); })];
156
+ }); })
157
+ // Doxygen matches USE_MDFILE_AS_MAINPAGE against the file's path exactly as it
158
+ // appears in INPUT - a bare name like "README.md" no longer matches an absolute
159
+ // INPUT path on newer Doxygen versions (see doxygen/doxygen#10110), which silently
160
+ // drops the README content instead of using it as the main page. Passing the same
161
+ // absolute path to both keeps them in sync regardless of Doxygen version.
162
+ ];
157
163
  case 5:
158
164
  LAYOUT_FILE = _f.sent();
165
+ readMePath = path_1.default.join(doxyContent.path, doxyContent.readMe);
159
166
  doxyParams = setVariableCmd({
160
167
  DOXY_HTML_OUTPUT: newOutDirectory,
161
- DOXY_INPUT: "\"".concat(path_1.default.join(doxyContent.path, doxyContent.readMe), "\" ").concat(combinedFolders),
168
+ DOXY_INPUT: "\"".concat(readMePath, "\" ").concat(combinedFolders),
169
+ DOXY_MAINPAGE: readMePath,
162
170
  HTML_EXTRA_FILES: "\"".concat(path_1.default.join(doxyContent.path, doxyContent.favIcon), "\""),
163
171
  HTML_EXTRA_STYLESHEET: "\"".concat(path_1.default.join(doxyContent.path, doxyContent.stylesheet), "\""),
164
172
  HTML_FOOTER: path_1.default.join(doxyContent.path, doxyContent.footer),
@@ -51,6 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  };
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  var path_1 = __importDefault(require("path"));
54
+ var shelljs_1 = __importDefault(require("shelljs"));
54
55
  var test_1 = require("../../../utils/test");
55
56
  var utils_1 = require("@sasjs/utils");
56
57
  var generateDocs_1 = require("../generateDocs");
@@ -454,4 +455,99 @@ describe('sasjs doc', function () {
454
455
  }
455
456
  });
456
457
  }); });
458
+ it('should throw when no macro/program/service/job folders are configured', function () { return __awaiter(void 0, void 0, void 0, function () {
459
+ var emptyTarget;
460
+ return __generator(this, function (_a) {
461
+ switch (_a.label) {
462
+ case 0:
463
+ emptyTarget = new utils_1.Target({
464
+ name: 'no-folders',
465
+ appLoc: '/Public/test/',
466
+ serverType: utils_1.ServerType.SasViya,
467
+ docConfig: { displayMacroCore: false }
468
+ });
469
+ return [4 /*yield*/, expect((0, generateDocs_1.generateDocs)(emptyTarget, {
470
+ docConfig: { displayMacroCore: false }
471
+ })).rejects.toThrow('Unable to locate folders for generating docs.')];
472
+ case 1:
473
+ _a.sent();
474
+ return [2 /*return*/];
475
+ }
476
+ });
477
+ }); });
478
+ it('should not toggle the spinner when LOG_LEVEL is Debug', function () { return __awaiter(void 0, void 0, void 0, function () {
479
+ var originalLogLevel;
480
+ return __generator(this, function (_a) {
481
+ switch (_a.label) {
482
+ case 0:
483
+ originalLogLevel = process.env.LOG_LEVEL;
484
+ process.env.LOG_LEVEL = 'Debug';
485
+ return [4 /*yield*/, expect((0, generateDocs_1.generateDocs)(defaultTarget, defaultConfig)).resolves.toEqual({
486
+ outDirectory: docOutputDefault
487
+ })];
488
+ case 1:
489
+ _a.sent();
490
+ process.env.LOG_LEVEL = originalLogLevel;
491
+ return [2 /*return*/];
492
+ }
493
+ });
494
+ }); });
495
+ it('should fall back to the default project name when package.json has none', function () { return __awaiter(void 0, void 0, void 0, function () {
496
+ var packageJsonPath, originalPackageJson, packageJson;
497
+ return __generator(this, function (_a) {
498
+ switch (_a.label) {
499
+ case 0:
500
+ packageJsonPath = path_1.default.join(__dirname, appName, 'package.json');
501
+ return [4 /*yield*/, (0, utils_1.readFile)(packageJsonPath)];
502
+ case 1:
503
+ originalPackageJson = _a.sent();
504
+ packageJson = JSON.parse(originalPackageJson);
505
+ delete packageJson.name;
506
+ return [4 /*yield*/, (0, utils_1.createFile)(packageJsonPath, JSON.stringify(packageJson))];
507
+ case 2:
508
+ _a.sent();
509
+ return [4 /*yield*/, expect((0, generateDocs_1.generateDocs)(defaultTarget, defaultConfig)).resolves.toEqual({
510
+ outDirectory: docOutputDefault
511
+ })];
512
+ case 3:
513
+ _a.sent();
514
+ return [4 /*yield*/, (0, utils_1.createFile)(packageJsonPath, originalPackageJson)];
515
+ case 4:
516
+ _a.sent();
517
+ return [2 /*return*/];
518
+ }
519
+ });
520
+ }); });
521
+ it('should throw a Doxygen-specific error when the shell command fails with an "error: " stderr', function () { return __awaiter(void 0, void 0, void 0, function () {
522
+ return __generator(this, function (_a) {
523
+ switch (_a.label) {
524
+ case 0:
525
+ jest.spyOn(shelljs_1.default, 'exec').mockReturnValueOnce({
526
+ code: 1,
527
+ stderr: 'error: bad config',
528
+ stdout: ''
529
+ });
530
+ return [4 /*yield*/, expect((0, generateDocs_1.generateDocs)(defaultTarget, defaultConfig)).rejects.toThrow('\nerror: bad config')];
531
+ case 1:
532
+ _a.sent();
533
+ return [2 /*return*/];
534
+ }
535
+ });
536
+ }); });
537
+ it('should throw a generic Doxygen-not-installed error for any other shell failure', function () { return __awaiter(void 0, void 0, void 0, function () {
538
+ return __generator(this, function (_a) {
539
+ switch (_a.label) {
540
+ case 0:
541
+ jest.spyOn(shelljs_1.default, 'exec').mockReturnValueOnce({
542
+ code: 1,
543
+ stderr: 'command not found',
544
+ stdout: ''
545
+ });
546
+ return [4 /*yield*/, expect((0, generateDocs_1.generateDocs)(defaultTarget, defaultConfig)).rejects.toThrow("The Doxygen application is not installed or configured.")];
547
+ case 1:
548
+ _a.sent();
549
+ return [2 /*return*/];
550
+ }
551
+ });
552
+ }); });
457
553
  });
@@ -54,10 +54,13 @@ describe('sasjs init', function () {
54
54
  afterEach(function () { return __awaiter(void 0, void 0, void 0, function () {
55
55
  return __generator(this, function (_a) {
56
56
  switch (_a.label) {
57
- case 0: return [4 /*yield*/, (0, utils_1.deleteFolder)(path_1.default.join(__dirname, 'test-app-init-*'))];
57
+ case 0:
58
+ if (!process.projectDir) return [3 /*break*/, 2];
59
+ return [4 /*yield*/, (0, utils_1.deleteFolder)(process.projectDir)];
58
60
  case 1:
59
61
  _a.sent();
60
- return [2 /*return*/];
62
+ _a.label = 2;
63
+ case 2: return [2 /*return*/];
61
64
  }
62
65
  });
63
66
  }); });
@@ -392,6 +392,19 @@ describe('JobCommand', function () {
392
392
  }
393
393
  });
394
394
  }); });
395
+ it('should log deprecation warning if returnStatusOnly flag is present', function () { return __awaiter(void 0, void 0, void 0, function () {
396
+ return __generator(this, function (_a) {
397
+ switch (_a.label) {
398
+ case 0:
399
+ jest.spyOn(process.logger, 'warn');
400
+ return [4 /*yield*/, executeCommandWrapper([jobPath, '--returnStatusOnly'])];
401
+ case 1:
402
+ _a.sent();
403
+ expect(process.logger.warn).toHaveBeenCalledWith('--returnStatusOnly (-r) flag is deprecated.');
404
+ return [2 /*return*/];
405
+ }
406
+ });
407
+ }); });
395
408
  });
396
409
  describe('for server type sas9', function () {
397
410
  beforeEach(function () {
@@ -549,19 +562,6 @@ describe('JobCommand', function () {
549
562
  });
550
563
  }); });
551
564
  });
552
- it('should log deprecation warning if returnStatusOnly flag is present', function () { return __awaiter(void 0, void 0, void 0, function () {
553
- return __generator(this, function (_a) {
554
- switch (_a.label) {
555
- case 0:
556
- jest.spyOn(process.logger, 'warn');
557
- return [4 /*yield*/, executeCommandWrapper([jobPath, '--returnStatusOnly'])];
558
- case 1:
559
- _a.sent();
560
- expect(process.logger.warn).toHaveBeenCalledWith('--returnStatusOnly (-r) flag is deprecated.');
561
- return [2 /*return*/];
562
- }
563
- });
564
- }); });
565
565
  });
566
566
  var setupMocksForViya = function () {
567
567
  jest.resetAllMocks();
@@ -22,7 +22,7 @@ INHERIT_DOCS = NO
22
22
  INLINE_INFO = NO
23
23
  INPUT = $(DOXY_INPUT)
24
24
  LAYOUT_FILE = $(LAYOUT_FILE)
25
- USE_MDFILE_AS_MAINPAGE = README.md
25
+ USE_MDFILE_AS_MAINPAGE = $(DOXY_MAINPAGE)
26
26
  MAX_INITIALIZER_LINES = 0
27
27
  PROJECT_NAME = $(PROJECT_NAME)
28
28
  PROJECT_LOGO = $(PROJECT_LOGO)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ var __generator = (this && this.__generator) || function (thisArg, body) {
35
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
36
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
37
+ function verb(n) { return function (v) { return step([n, v]); }; }
38
+ function step(op) {
39
+ if (f) throw new TypeError("Generator is already executing.");
40
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
41
+ 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;
42
+ if (y = 0, t) op = [op[0] & 2, t.value];
43
+ switch (op[0]) {
44
+ case 0: case 1: t = op; break;
45
+ case 4: _.label++; return { value: op[1], done: false };
46
+ case 5: _.label++; y = op[1]; op = [0]; continue;
47
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
48
+ default:
49
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
50
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
51
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
52
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
53
+ if (t[2]) _.ops.pop();
54
+ _.trys.pop(); continue;
55
+ }
56
+ op = body.call(thisArg, _);
57
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
58
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
59
+ }
60
+ };
61
+ Object.defineProperty(exports, "__esModule", { value: true });
62
+ var utils_1 = require("@sasjs/utils");
63
+ var configUtils = __importStar(require("../../../utils/config"));
64
+ var loadEnvVariablesModule = __importStar(require("../../../utils/loadEnvVariables"));
65
+ var test_1 = require("../../../utils/test");
66
+ var targetCommand_1 = require("../targetCommand");
67
+ var returnCode_1 = require("../returnCode");
68
+ describe('TargetCommand.getTargetInfo', function () {
69
+ beforeEach(function () {
70
+ process.logger = new utils_1.Logger(utils_1.LogLevel.Off);
71
+ jest.spyOn(process.logger, 'error');
72
+ });
73
+ it('exits with an internal error when target env variables fail to load', function () { return __awaiter(void 0, void 0, void 0, function () {
74
+ var processExitSpy, command;
75
+ return __generator(this, function (_a) {
76
+ switch (_a.label) {
77
+ case 0:
78
+ processExitSpy = (0, test_1.mockProcessExit)();
79
+ jest
80
+ .spyOn(loadEnvVariablesModule, 'loadTargetEnvVariables')
81
+ .mockImplementation(function () { return Promise.reject(new Error('env file missing')); });
82
+ command = new targetCommand_1.TargetCommand(['node', 'sasjs', 'job'], {
83
+ strict: false
84
+ });
85
+ return [4 /*yield*/, command.getTargetInfo()];
86
+ case 1:
87
+ _a.sent();
88
+ expect(process.logger.error).toHaveBeenCalledWith(expect.stringContaining('Error loading environment variables'), expect.any(Error));
89
+ expect(processExitSpy).toHaveBeenCalledWith(returnCode_1.ReturnCode.InternalError);
90
+ return [2 /*return*/];
91
+ }
92
+ });
93
+ }); });
94
+ it('exits with an internal error when the target cannot be found in configuration', function () { return __awaiter(void 0, void 0, void 0, function () {
95
+ var processExitSpy, command;
96
+ return __generator(this, function (_a) {
97
+ switch (_a.label) {
98
+ case 0:
99
+ processExitSpy = (0, test_1.mockProcessExit)();
100
+ jest
101
+ .spyOn(loadEnvVariablesModule, 'loadTargetEnvVariables')
102
+ .mockImplementation(function () { return Promise.resolve(); });
103
+ jest
104
+ .spyOn(configUtils, 'findTargetInConfiguration')
105
+ .mockImplementation(function () { return Promise.reject(new Error('target not found')); });
106
+ command = new targetCommand_1.TargetCommand(['node', 'sasjs', 'job'], {
107
+ strict: false
108
+ });
109
+ return [4 /*yield*/, command.getTargetInfo()];
110
+ case 1:
111
+ _a.sent();
112
+ expect(process.logger.error).toHaveBeenCalledWith('Error reading target from configuration: ', expect.any(Error));
113
+ expect(processExitSpy).toHaveBeenCalledWith(returnCode_1.ReturnCode.InternalError);
114
+ return [2 /*return*/];
115
+ }
116
+ });
117
+ }); });
118
+ });
@@ -52,6 +52,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  var utils_1 = require("../utils");
54
54
  var test_1 = require("../test");
55
+ var axios_1 = __importDefault(require("axios"));
55
56
  var utils_2 = require("@sasjs/utils");
56
57
  var node_1 = __importDefault(require("@sasjs/adapter/node"));
57
58
  var path_1 = __importDefault(require("path"));
@@ -230,6 +231,27 @@ describe('utils', function () {
230
231
  }
231
232
  });
232
233
  }); });
234
+ it('should populate all default rules when an existing .gitignore file is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
235
+ var gitIgnoreContent;
236
+ return __generator(this, function (_a) {
237
+ switch (_a.label) {
238
+ case 0: return [4 /*yield*/, (0, utils_2.createFile)(gitFilePath, '')];
239
+ case 1:
240
+ _a.sent();
241
+ return [4 /*yield*/, expect((0, utils_1.setupGitIgnore)(folderPath)).toResolve()];
242
+ case 2:
243
+ _a.sent();
244
+ return [4 /*yield*/, (0, utils_2.readFile)(gitFilePath)];
245
+ case 3:
246
+ gitIgnoreContent = _a.sent();
247
+ expect(gitIgnoreContent).toEqual('node_modules\nsasjsbuild\nsasjsresults\n.env*\n');
248
+ return [4 /*yield*/, (0, utils_2.deleteFile)(gitFilePath)];
249
+ case 4:
250
+ _a.sent();
251
+ return [2 /*return*/];
252
+ }
253
+ });
254
+ }); });
233
255
  });
234
256
  describe('chunk', function () {
235
257
  it('should return an array with only one element that is equal to text itself if text size is less than max length', function () {
@@ -384,4 +406,173 @@ describe('utils', function () {
384
406
  });
385
407
  }); }, 600000);
386
408
  });
409
+ describe('prefixAppLoc', function () {
410
+ it('should return null when path is falsy', function () {
411
+ expect((0, utils_1.prefixAppLoc)('/my/app', '')).toEqual(null);
412
+ });
413
+ it('should prepend a leading slash to appLoc if missing', function () {
414
+ expect((0, utils_1.prefixAppLoc)('my/app', 'program1')).toEqual('/my/app/program1');
415
+ });
416
+ it('should keep an already-absolute path unchanged', function () {
417
+ expect((0, utils_1.prefixAppLoc)('/my/app', '/absolute/path')).toEqual('/absolute/path');
418
+ });
419
+ it('should join an array path before prefixing each segment', function () {
420
+ expect((0, utils_1.prefixAppLoc)('/my/app', ['program1', 'program2'])).toEqual('/my/app/program1 /my/app/program2');
421
+ });
422
+ });
423
+ describe('isSASjsProject', function () {
424
+ it('should return true when sasjs/sasjsconfig.json exists under projectDir', function () { return __awaiter(void 0, void 0, void 0, function () {
425
+ var projectDir, originalProjectDir;
426
+ return __generator(this, function (_a) {
427
+ switch (_a.label) {
428
+ case 0:
429
+ projectDir = path_1.default.join(__dirname, "is-sasjs-project-".concat((0, utils_2.generateTimestamp)()));
430
+ originalProjectDir = process.projectDir;
431
+ return [4 /*yield*/, (0, utils_2.createFolder)(path_1.default.join(projectDir, 'sasjs'))];
432
+ case 1:
433
+ _a.sent();
434
+ return [4 /*yield*/, (0, utils_2.createFile)(path_1.default.join(projectDir, 'sasjs', 'sasjsconfig.json'), '{}')];
435
+ case 2:
436
+ _a.sent();
437
+ process.projectDir = projectDir;
438
+ return [4 /*yield*/, expect((0, utils_1.isSASjsProject)()).resolves.toEqual(true)];
439
+ case 3:
440
+ _a.sent();
441
+ process.projectDir = originalProjectDir;
442
+ return [4 /*yield*/, (0, utils_2.deleteFolder)(projectDir)];
443
+ case 4:
444
+ _a.sent();
445
+ return [2 /*return*/];
446
+ }
447
+ });
448
+ }); });
449
+ it('should return false when no sasjsconfig.json is found in any parent folder', function () { return __awaiter(void 0, void 0, void 0, function () {
450
+ var projectDir, originalProjectDir;
451
+ return __generator(this, function (_a) {
452
+ switch (_a.label) {
453
+ case 0:
454
+ projectDir = path_1.default.join(require('os').tmpdir(), "not-a-sasjs-project-".concat((0, utils_2.generateTimestamp)()));
455
+ originalProjectDir = process.projectDir;
456
+ return [4 /*yield*/, (0, utils_2.createFolder)(projectDir)];
457
+ case 1:
458
+ _a.sent();
459
+ process.projectDir = projectDir;
460
+ return [4 /*yield*/, expect((0, utils_1.isSASjsProject)()).resolves.toEqual(false)];
461
+ case 2:
462
+ _a.sent();
463
+ process.projectDir = originalProjectDir;
464
+ return [4 /*yield*/, (0, utils_2.deleteFolder)(projectDir)];
465
+ case 3:
466
+ _a.sent();
467
+ return [2 /*return*/];
468
+ }
469
+ });
470
+ }); });
471
+ });
472
+ describe('getNodeModulePath', function () {
473
+ it('should return the folder path of an installed module', function () { return __awaiter(void 0, void 0, void 0, function () {
474
+ var modulePath;
475
+ return __generator(this, function (_a) {
476
+ switch (_a.label) {
477
+ case 0: return [4 /*yield*/, (0, utils_1.getNodeModulePath)('@sasjs/utils')];
478
+ case 1:
479
+ modulePath = _a.sent();
480
+ expect(modulePath).not.toEqual('');
481
+ expect(modulePath.endsWith(path_1.default.join('@sasjs', 'utils'))).toEqual(true);
482
+ return [2 /*return*/];
483
+ }
484
+ });
485
+ }); });
486
+ it('should return an empty string when the module cannot be found', function () { return __awaiter(void 0, void 0, void 0, function () {
487
+ return __generator(this, function (_a) {
488
+ switch (_a.label) {
489
+ case 0: return [4 /*yield*/, expect((0, utils_1.getNodeModulePath)('sasjs-nonexistent-module-xyz')).resolves.toEqual('')];
490
+ case 1:
491
+ _a.sent();
492
+ return [2 /*return*/];
493
+ }
494
+ });
495
+ }); });
496
+ });
497
+ describe('getUniqServicesObj', function () {
498
+ it('should return an empty object when services is falsy', function () {
499
+ expect((0, utils_1.getUniqServicesObj)(undefined)).toEqual({});
500
+ });
501
+ it('should key services by their basename, keeping the first of any duplicates', function () {
502
+ expect((0, utils_1.getUniqServicesObj)(['a/b/service1', 'x/service2', 'y/service1'])).toEqual({
503
+ service1: 'a/b/service1',
504
+ service2: 'x/service2'
505
+ });
506
+ });
507
+ });
508
+ describe('convertToSASStatements', function () {
509
+ it('should convert macroVars to %let statements', function () {
510
+ expect((0, utils_1.convertToSASStatements)({
511
+ macroVars: { var1: 'val1', var2: 'val2' }
512
+ })).toEqual('%let var1=val1;\n%let var2=val2;\n');
513
+ });
514
+ it('should return an empty string when there are no macroVars', function () {
515
+ expect((0, utils_1.convertToSASStatements)({ macroVars: {} })).toEqual('');
516
+ });
517
+ });
518
+ describe('terminateProcess', function () {
519
+ it('should exit the process with the given status code', function () {
520
+ var mockExit = (0, test_1.mockProcessExit)();
521
+ (0, utils_1.terminateProcess)(2);
522
+ expect(mockExit).toHaveBeenCalledWith(2);
523
+ });
524
+ });
525
+ describe('isSasJsServerInServerMode', function () {
526
+ var target = new utils_2.Target({
527
+ name: 'test',
528
+ appLoc: '/Public/test/',
529
+ serverType: utils_2.ServerType.Sasjs,
530
+ serverUrl: 'https://sasjsserver.com'
531
+ });
532
+ afterEach(function () {
533
+ jest.restoreAllMocks();
534
+ });
535
+ it('should return true when the server reports server mode', function () { return __awaiter(void 0, void 0, void 0, function () {
536
+ return __generator(this, function (_a) {
537
+ switch (_a.label) {
538
+ case 0:
539
+ jest
540
+ .spyOn(axios_1.default, 'get')
541
+ .mockImplementation(function () { return Promise.resolve({ data: { mode: 'server' } }); });
542
+ return [4 /*yield*/, expect((0, utils_1.isSasJsServerInServerMode)(target)).resolves.toEqual(true)];
543
+ case 1:
544
+ _a.sent();
545
+ return [2 /*return*/];
546
+ }
547
+ });
548
+ }); });
549
+ it('should return false when the server does not report server mode', function () { return __awaiter(void 0, void 0, void 0, function () {
550
+ return __generator(this, function (_a) {
551
+ switch (_a.label) {
552
+ case 0:
553
+ jest
554
+ .spyOn(axios_1.default, 'get')
555
+ .mockImplementation(function () {
556
+ return Promise.resolve({ data: { mode: 'desktop' } });
557
+ });
558
+ return [4 /*yield*/, expect((0, utils_1.isSasJsServerInServerMode)(target)).resolves.toEqual(false)];
559
+ case 1:
560
+ _a.sent();
561
+ return [2 /*return*/];
562
+ }
563
+ });
564
+ }); });
565
+ it('should throw when the server info request fails', function () { return __awaiter(void 0, void 0, void 0, function () {
566
+ return __generator(this, function (_a) {
567
+ switch (_a.label) {
568
+ case 0:
569
+ jest.spyOn(axios_1.default, 'get').mockImplementation(function () { return Promise.reject(); });
570
+ return [4 /*yield*/, expect((0, utils_1.isSasJsServerInServerMode)(target)).rejects.toThrow("An error occurred while fetching server info from ".concat(target.serverUrl, "/SASjsApi/info"))];
571
+ case 1:
572
+ _a.sent();
573
+ return [2 /*return*/];
574
+ }
575
+ });
576
+ }); });
577
+ });
387
578
  });
@@ -182,16 +182,23 @@ function createTemplateApp(folderPath, template) {
182
182
  var _this = this;
183
183
  return __generator(this, function (_a) {
184
184
  return [2 /*return*/, new Promise(function (resolve, reject) { return __awaiter(_this, void 0, void 0, function () {
185
- var _a, stdout, stderr, code;
185
+ var probeFile, _a, stderr, code;
186
186
  return __generator(this, function (_b) {
187
- _a = downloadFile("https://username:password@github.com/sasjs/template_".concat(template), 'response.txt'), stdout = _a.stdout, stderr = _a.stderr, code = _a.code;
188
- if (stderr.includes('404: Not Found') || code) {
189
- return [2 /*return*/, reject(new Error("Template \"".concat(template, "\" is not a SASjs template")))];
187
+ switch (_b.label) {
188
+ case 0:
189
+ probeFile = 'response.txt';
190
+ _a = downloadFile("https://username:password@github.com/sasjs/template_".concat(template), probeFile), stderr = _a.stderr, code = _a.code;
191
+ return [4 /*yield*/, (0, utils_1.deleteFile)(probeFile).catch(function () { })];
192
+ case 1:
193
+ _b.sent();
194
+ if (stderr.includes('404: Not Found') || code) {
195
+ return [2 /*return*/, reject(new Error("Template \"".concat(template, "\" is not a SASjs template")))];
196
+ }
197
+ createApp(folderPath, "https://github.com/sasjs/template_".concat(template), 'https://github.com/sasjs/docs', function (err) {
198
+ return reject(new Error(err));
199
+ });
200
+ return [2 /*return*/, resolve()];
190
201
  }
191
- createApp(folderPath, "https://github.com/sasjs/template_".concat(template), 'https://github.com/sasjs/docs', function (err) {
192
- return reject(new Error(err));
193
- });
194
- return [2 /*return*/, resolve()];
195
202
  });
196
203
  }); })];
197
204
  });
@@ -274,14 +281,23 @@ var loadDocsSubmodule = function (docsUrl, folderPath, zipPath) { return __await
274
281
  }); };
275
282
  function downloadFile(url, filename) {
276
283
  if ((0, utils_1.isLinux)()) {
277
- return shelljs_1.default.exec("wget ".concat(url), { silent: true });
284
+ // -O <filename> writes to the given name; without it, wget defaults to the
285
+ // remote URL's basename, which silently ignores the caller's `filename` and
286
+ // can leave an unexpectedly-named file behind (e.g. a repo-existence probe
287
+ // meant to be discarded as `response.txt` instead landing as the repo name).
288
+ return shelljs_1.default.exec("wget ".concat(url).concat(filename ? ' -O ' + filename : ''), {
289
+ silent: true
290
+ });
278
291
  }
279
292
  else if ((0, utils_1.isWindows)()) {
280
293
  // First We set TLS12 & then we invoke request to download file.
281
294
  return shelljs_1.default.exec("powershell.exe \"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest ".concat(url, " ").concat(filename ? '-O ' + filename : '', "\""), { silent: true });
282
295
  }
283
296
  else {
284
- return shelljs_1.default.exec("curl ".concat(url, " -LO -f"), { silent: true });
297
+ // -o <filename> writes to the given name; -O (used previously) instead saves
298
+ // under the remote URL's basename, ignoring `filename` - see the wget comment
299
+ // above for the same issue.
300
+ return shelljs_1.default.exec("curl ".concat(url, " -L -f").concat(filename ? ' -o ' + filename : ' -O'), { silent: true });
285
301
  }
286
302
  }
287
303
  function setupNpmProject(folderName) {
package/package.json CHANGED
@@ -122,5 +122,5 @@
122
122
  "overrides": {
123
123
  "ws": "8.21.0"
124
124
  },
125
- "version": "4.18.1"
125
+ "version": "4.18.2"
126
126
  }