@sasjs/cli 6.15.0 → 6.17.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.
@@ -1,4 +1,8 @@
1
1
  "use strict";
2
+ var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
3
+ if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
4
+ return cooked;
5
+ };
2
6
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
7
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
8
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -75,11 +79,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
75
79
  return (mod && mod.__esModule) ? mod : { "default": mod };
76
80
  };
77
81
  Object.defineProperty(exports, "__esModule", { value: true });
78
- exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.WEBOUT_SENTINELS = void 0;
82
+ exports.DRIVE_FORMAT = exports.MAX_SEED_BYTES = exports.MAX_BINARY_SEED_BYTES = exports.buildLintBundle = exports.WEBOUT_SENTINELS = void 0;
79
83
  exports.buildMockSite = buildMockSite;
80
84
  var crypto_1 = require("crypto");
81
85
  var fs_1 = __importDefault(require("fs"));
86
+ var os_1 = __importDefault(require("os"));
82
87
  var path_1 = __importDefault(require("path"));
88
+ var esbuild_1 = require("esbuild");
83
89
  var utils_1 = require("@sasjs/utils");
84
90
  /**
85
91
  * The webout() block is the one @sasjs/utils generates for the server, emitted
@@ -116,8 +122,141 @@ var ASSETS_FOLDER = path_1.default.join(__dirname, 'assets');
116
122
  */
117
123
  var API_BROWSER_BUILD = path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
118
124
  var toPosix = function (value) { return value.split(path_1.default.sep).join('/'); };
119
- var listFiles = function (folder, base) {
125
+ /**
126
+ * The linter, as a service worker can run it.
127
+ *
128
+ * `@sasjs/api/code` is a node-only entry because `@sasjs/lint` resolves its
129
+ * configuration by walking a filesystem - but the linter's core (`lintText`,
130
+ * `formatText`) reads only the text it is given and the configuration it is
131
+ * passed, so the two can run anywhere the module graph can be built. This
132
+ * bundle is that graph: the package's own code service, the linter it wraps, and
133
+ * shims for the four node modules reached at import time (`path` and `os` for
134
+ * resolution, `fs` and `@sasjs/utils/file` for the config lookup that a mock
135
+ * never makes, because it hands the linter the default configuration).
136
+ *
137
+ * It is built from the installed @sasjs/api and @sasjs/lint on every mock
138
+ * build, so the rules a client sees are the version the CLI resolved - a
139
+ * committed bundle would be a second copy to drift.
140
+ */
141
+ var LINT_BUNDLE_FILES = {
142
+ 'entry.js': [
143
+ "import './process.js'",
144
+ "import { createCodeService } from '@sasjs/api/code'",
145
+ 'globalThis.SASjsCode = { createCodeService }'
146
+ ].join('\n'),
147
+ 'process.js': [
148
+ '// the linter reads process.env for a default and process.cwd() for a root;',
149
+ '// neither is a real lookup in a worker, and both are answered in one place',
150
+ 'if (typeof globalThis.process === "undefined")',
151
+ ' globalThis.process = {',
152
+ ' env: {},',
153
+ ' platform: "browser",',
154
+ ' cwd: () => "/",',
155
+ ' versions: {},',
156
+ ' argv: [],',
157
+ ' version: "v0"',
158
+ ' }'
159
+ ].join('\n'),
160
+ 'fs.js': [
161
+ 'const unavailable = (name) => () => {',
162
+ ' throw new Error(name + " is not available in the browser host")',
163
+ '}',
164
+ 'module.exports = {',
165
+ ' readFileSync: unavailable("readFileSync"),',
166
+ ' writeFileSync: unavailable("writeFileSync"),',
167
+ ' existsSync: () => false,',
168
+ ' promises: {}',
169
+ '}'
170
+ ].join('\n'),
171
+ 'os.js': [
172
+ 'module.exports = {',
173
+ ' homedir: () => "/",',
174
+ ' platform: () => "browser",',
175
+ ' tmpdir: () => "/tmp",',
176
+ ' EOL: "\\n"',
177
+ '}'
178
+ ].join('\n'),
179
+ 'utils-file.js': [
180
+ 'const unavailable = (name) => async () => {',
181
+ ' throw new Error(name + " is not available in the browser host")',
182
+ '}',
183
+ 'module.exports = {',
184
+ ' readFile: unavailable("readFile"),',
185
+ ' writeFile: unavailable("writeFile"),',
186
+ ' fileExists: async () => false,',
187
+ ' listFilesInFolder: async () => []',
188
+ '}'
189
+ ].join('\n'),
190
+ 'path.js': String.raw(templateObject_1 || (templateObject_1 = __makeTemplateObject(["// the posix half of path, which is all the linter uses: the rules name files,\n// and a mock hands the linter no folder to walk\nconst SEP = String.fromCharCode(92)\nconst clean = (value) =>\n String(value).split(SEP).join(\"/\").replace(//+/g, \"/\")\nconst strip = (value) => clean(value).replace(//+$/, \"\")\nconst basename = (value) => strip(value).split(\"/\").pop() || \"\"\nconst extname = (value) => {\n const base = basename(value)\n const dot = base.lastIndexOf(\".\")\n return dot > 0 ? base.slice(dot) : \"\"\n}\nconst dirname = (value) => {\n const parts = clean(value).split(\"/\")\n parts.pop()\n return parts.join(\"/\") || \".\"\n}\nconst join = (...parts) => clean(parts.filter(Boolean).join(\"/\"))\nmodule.exports = {\n sep: \"/\",\n delimiter: \":\",\n basename,\n extname,\n dirname,\n join,\n resolve: (...parts) => clean(parts.filter(Boolean).join(\"/\")),\n relative: (from, to) => clean(to).replace(clean(from) + \"/\", \"\"),\n isAbsolute: (value) => String(value).startsWith(\"/\"),\n normalize: clean,\n parse: (value) => ({\n root: \"\",\n dir: dirname(value),\n base: basename(value),\n ext: extname(value),\n name: basename(value).slice(0, basename(value).length - extname(value).length)\n })\n}"], ["// the posix half of path, which is all the linter uses: the rules name files,\n// and a mock hands the linter no folder to walk\nconst SEP = String.fromCharCode(92)\nconst clean = (value) =>\n String(value).split(SEP).join(\"/\").replace(/\\/+/g, \"/\")\nconst strip = (value) => clean(value).replace(/\\/+$/, \"\")\nconst basename = (value) => strip(value).split(\"/\").pop() || \"\"\nconst extname = (value) => {\n const base = basename(value)\n const dot = base.lastIndexOf(\".\")\n return dot > 0 ? base.slice(dot) : \"\"\n}\nconst dirname = (value) => {\n const parts = clean(value).split(\"/\")\n parts.pop()\n return parts.join(\"/\") || \".\"\n}\nconst join = (...parts) => clean(parts.filter(Boolean).join(\"/\"))\nmodule.exports = {\n sep: \"/\",\n delimiter: \":\",\n basename,\n extname,\n dirname,\n join,\n resolve: (...parts) => clean(parts.filter(Boolean).join(\"/\")),\n relative: (from, to) => clean(to).replace(clean(from) + \"/\", \"\"),\n isAbsolute: (value) => String(value).startsWith(\"/\"),\n normalize: clean,\n parse: (value) => ({\n root: \"\",\n dir: dirname(value),\n base: basename(value),\n ext: extname(value),\n name: basename(value).slice(0, basename(value).length - extname(value).length)\n })\n}"])))
191
+ };
192
+ /**
193
+ * The `node_modules` the CLI's own dependency lives in.
194
+ *
195
+ * esbuild resolves a bare import from the entry file upwards, which is wrong for
196
+ * a scratch entry outside the install: the walk would miss the installed
197
+ * packages entirely. `nodePaths` states where to look instead, and it is taken
198
+ * from `require.resolve` so it is the tree the CLI actually loaded from - a
199
+ * hoisted install, a nested one, and a pnpm store symlink all answer with their
200
+ * own location.
201
+ */
202
+ var NODE_MODULES = (function () {
203
+ var resolved = require.resolve('@sasjs/api');
204
+ var marker = "".concat(path_1.default.sep, "node_modules").concat(path_1.default.sep);
205
+ var index = resolved.lastIndexOf(marker);
206
+ return index < 0 ? undefined : resolved.slice(0, index + marker.length - 1);
207
+ })();
208
+ /**
209
+ * Builds `lint-browser.js` from the installed packages.
210
+ *
211
+ * The shim sources are written to the system temp folder, never beside this
212
+ * module: an installed package can be read-only (a global install, a pnpm store
213
+ * symlink, a container that builds as another user), and a build that writes
214
+ * inside it fails with EACCES for exactly the users least able to diagnose it.
215
+ * Resolution is kept by naming the CLI's `node_modules` through `nodePaths`.
216
+ */
217
+ var buildLintBundle = function (destination) {
120
218
  var e_1, _a;
219
+ var work = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'sasjs-lint-'));
220
+ try {
221
+ try {
222
+ for (var _b = __values(Object.entries(LINT_BUNDLE_FILES)), _c = _b.next(); !_c.done; _c = _b.next()) {
223
+ var _d = __read(_c.value, 2), name_1 = _d[0], contents = _d[1];
224
+ fs_1.default.writeFileSync(path_1.default.join(work, name_1), contents);
225
+ }
226
+ }
227
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
228
+ finally {
229
+ try {
230
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
231
+ }
232
+ finally { if (e_1) throw e_1.error; }
233
+ }
234
+ (0, esbuild_1.buildSync)({
235
+ entryPoints: [path_1.default.join(work, 'entry.js')],
236
+ bundle: true,
237
+ platform: 'browser',
238
+ format: 'iife',
239
+ outfile: destination,
240
+ nodePaths: NODE_MODULES ? [NODE_MODULES] : [],
241
+ alias: {
242
+ fs: path_1.default.join(work, 'fs.js'),
243
+ path: path_1.default.join(work, 'path.js'),
244
+ os: path_1.default.join(work, 'os.js'),
245
+ '@sasjs/utils/file': path_1.default.join(work, 'utils-file.js')
246
+ },
247
+ logLevel: 'silent'
248
+ });
249
+ }
250
+ catch (error) {
251
+ throw new Error("The browser lint bundle could not be built from @sasjs/api/code: ".concat((error && error.message) || error));
252
+ }
253
+ finally {
254
+ fs_1.default.rmSync(work, { recursive: true, force: true });
255
+ }
256
+ };
257
+ exports.buildLintBundle = buildLintBundle;
258
+ var listFiles = function (folder, base) {
259
+ var e_2, _a;
121
260
  if (base === void 0) { base = folder; }
122
261
  var entries = fs_1.default.readdirSync(folder, { withFileTypes: true });
123
262
  var files = [];
@@ -131,17 +270,17 @@ var listFiles = function (folder, base) {
131
270
  files.push(toPosix(path_1.default.relative(base, absolute)));
132
271
  }
133
272
  }
134
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
273
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
135
274
  finally {
136
275
  try {
137
276
  if (entries_1_1 && !entries_1_1.done && (_a = entries_1.return)) _a.call(entries_1);
138
277
  }
139
- finally { if (e_1) throw e_1.error; }
278
+ finally { if (e_2) throw e_2.error; }
140
279
  }
141
280
  return files;
142
281
  };
143
282
  var copyFolder = function (from, to) {
144
- var e_2, _a;
283
+ var e_3, _a;
145
284
  fs_1.default.mkdirSync(to, { recursive: true });
146
285
  try {
147
286
  for (var _b = __values(fs_1.default.readdirSync(from, { withFileTypes: true })), _c = _b.next(); !_c.done; _c = _b.next()) {
@@ -154,12 +293,12 @@ var copyFolder = function (from, to) {
154
293
  fs_1.default.copyFileSync(source, target);
155
294
  }
156
295
  }
157
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
296
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
158
297
  finally {
159
298
  try {
160
299
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
161
300
  }
162
- finally { if (e_2) throw e_2.error; }
301
+ finally { if (e_3) throw e_3.error; }
163
302
  }
164
303
  };
165
304
  /**
@@ -221,7 +360,7 @@ var readDriveFile = function (absolute) {
221
360
  };
222
361
  };
223
362
  var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
224
- var e_3, _a, e_4, _b, e_5, _c;
363
+ var e_4, _a, e_5, _b, e_6, _c;
225
364
  if (exclude === void 0) { exclude = []; }
226
365
  if (excludeFolders === void 0) { excludeFolders = []; }
227
366
  var files = {};
@@ -247,26 +386,26 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
247
386
  candidates.push({ key: key, absolute: path_1.default.join(folder, relative) });
248
387
  };
249
388
  try {
250
- for (var _d = (e_4 = void 0, __values(listFiles(folder))), _e = _d.next(); !_e.done; _e = _d.next()) {
389
+ for (var _d = (e_5 = void 0, __values(listFiles(folder))), _e = _d.next(); !_e.done; _e = _d.next()) {
251
390
  var relative = _e.value;
252
391
  _loop_1(relative);
253
392
  }
254
393
  }
255
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
394
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
256
395
  finally {
257
396
  try {
258
397
  if (_e && !_e.done && (_b = _d.return)) _b.call(_d);
259
398
  }
260
- finally { if (e_4) throw e_4.error; }
399
+ finally { if (e_5) throw e_5.error; }
261
400
  }
262
401
  }
263
402
  }
264
- catch (e_3_1) { e_3 = { error: e_3_1 }; }
403
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
265
404
  finally {
266
405
  try {
267
406
  if (directories_1_1 && !directories_1_1.done && (_a = directories_1.return)) _a.call(directories_1);
268
407
  }
269
- finally { if (e_3) throw e_3.error; }
408
+ finally { if (e_4) throw e_4.error; }
270
409
  }
271
410
  // The budget is spent in path order, not in whatever order the folder listing
272
411
  // returns, so the same project yields the same drive - and the same warning -
@@ -294,12 +433,12 @@ var seedFromBuild = function (buildFolder, appLoc, exclude, excludeFolders) {
294
433
  files[candidate.key] = file.contents;
295
434
  }
296
435
  }
297
- catch (e_5_1) { e_5 = { error: e_5_1 }; }
436
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
298
437
  finally {
299
438
  try {
300
439
  if (candidates_1_1 && !candidates_1_1.done && (_c = candidates_1.return)) _c.call(candidates_1);
301
440
  }
302
- finally { if (e_5) throw e_5.error; }
441
+ finally { if (e_6) throw e_6.error; }
303
442
  }
304
443
  return { files: files, skipped: skipped };
305
444
  };
@@ -333,12 +472,12 @@ var assertSafeOutputFolder = function (outputFolder, projectDir, buildFolder, we
333
472
  !isSameOrChild(buildFolder, outputFolder))
334
473
  throw refusal('that folder is outside the project');
335
474
  };
336
- var countJsPrograms = function (files) {
475
+ var countPrograms = function (files, extension) {
337
476
  return Object.keys(files).filter(function (key) {
338
477
  var inProgramFolder = PROGRAM_FOLDERS.some(function (folder) {
339
478
  return key.includes("/".concat(folder, "/"));
340
479
  });
341
- return inProgramFolder && key.toLowerCase().endsWith('.js');
480
+ return inProgramFolder && key.toLowerCase().endsWith(extension);
342
481
  }).length;
343
482
  };
344
483
  /** The script-tag attributes that change how an app's script runs. */
@@ -441,7 +580,7 @@ var patchIndexHtml = function (html, version) {
441
580
  var scripts = [];
442
581
  var tagPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>[ \t]*\r?\n?/gi;
443
582
  patched = patched.replace(tagPattern, function (tag, source, body) {
444
- var e_6, _a;
583
+ var e_7, _a;
445
584
  var attributes = parseAttributes(source);
446
585
  var type = (attributes.type || '').trim().toLowerCase();
447
586
  var inlined = !attributes.src;
@@ -466,16 +605,16 @@ var patchIndexHtml = function (html, version) {
466
605
  continue;
467
606
  if (attribute === 'type' && !attributes.type)
468
607
  continue;
469
- var name_1 = REPLAYED_ATTRIBUTES[attribute];
470
- spec[name_1] = attribute === 'nomodule' ? 'true' : attributes[attribute];
608
+ var name_2 = REPLAYED_ATTRIBUTES[attribute];
609
+ spec[name_2] = attribute === 'nomodule' ? 'true' : attributes[attribute];
471
610
  }
472
611
  }
473
- catch (e_6_1) { e_6 = { error: e_6_1 }; }
612
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
474
613
  finally {
475
614
  try {
476
615
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
477
616
  }
478
- finally { if (e_6) throw e_6.error; }
617
+ finally { if (e_7) throw e_7.error; }
479
618
  }
480
619
  scripts.push(spec);
481
620
  return '';
@@ -507,11 +646,11 @@ var patchIndexHtml = function (html, version) {
507
646
  */
508
647
  function buildMockSite(target) {
509
648
  return __awaiter(this, void 0, void 0, function () {
510
- var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
511
- var e_7, _e;
512
- var _f, _g, _h;
513
- return __generator(this, function (_j) {
514
- switch (_j.label) {
649
+ var projectDir, appLocRaw, appLoc, buildFolder, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, swPath, snapshotSource, hasSnapshot, indexPath, _d, patched, warnings;
650
+ var e_8, _e;
651
+ var _f, _g, _h, _j;
652
+ return __generator(this, function (_k) {
653
+ switch (_k.label) {
515
654
  case 0:
516
655
  projectDir = process.projectDir || process.cwd();
517
656
  appLocRaw = target.appLoc;
@@ -523,7 +662,7 @@ function buildMockSite(target) {
523
662
  path_1.default.join(projectDir, 'sasjsbuild');
524
663
  return [4 /*yield*/, (0, utils_1.folderExists)(buildFolder)];
525
664
  case 1:
526
- if (!(_j.sent())) {
665
+ if (!(_k.sent())) {
527
666
  throw new Error("Build folder not found: ".concat(buildFolder, ". Run `sasjs compile` first."));
528
667
  }
529
668
  webSourceRaw = (_f = target.streamConfig) === null || _f === void 0 ? void 0 : _f.webSourcePath;
@@ -533,7 +672,7 @@ function buildMockSite(target) {
533
672
  webFolder = path_1.default.resolve(projectDir, webSourceRaw);
534
673
  return [4 /*yield*/, (0, utils_1.folderExists)(webFolder)];
535
674
  case 2:
536
- if (!(_j.sent())) {
675
+ if (!(_k.sent())) {
537
676
  throw new Error("Web source folder not found: ".concat(webFolder, ". Check streamConfig.webSourcePath - build the frontend first."));
538
677
  }
539
678
  if (!fs_1.default.existsSync(path_1.default.join(webFolder, 'index.html'))) {
@@ -551,12 +690,26 @@ function buildMockSite(target) {
551
690
  excluded.push("/sasjs/drive/files".concat(appLoc, "/services/").concat(streamConfig.streamWebFolder, "/"));
552
691
  }
553
692
  _a = seedFromBuild(buildFolder, appLoc, excluded, [outputFolder]), seed = _a.files, skipped = _a.skipped;
554
- jsPrograms = countJsPrograms(seed);
693
+ jsPrograms = countPrograms(seed, '.js');
694
+ sasPrograms = countPrograms(seed, '.sas');
555
695
  driveEntries = Object.keys(seed).length;
556
- if (jsPrograms === 0) {
557
- throw new Error("No .js programs in ".concat(buildFolder, "/services, /jobs or /tests. This target builds SAS code, which cannot run in a browser. ") +
696
+ // A build holding SAS and no JavaScript is a SAS target: its services cannot
697
+ // run here, and seeding them would produce a site whose buttons answer 400.
698
+ // A build holding NEITHER is a frontend host - the app is the whole thing and
699
+ // the worker answers its API - which is what an interface with no services
700
+ // compiles to, and the case this site is built for.
701
+ if (jsPrograms === 0 && sasPrograms > 0) {
702
+ throw new Error("No .js programs in ".concat(buildFolder, "/services, /jobs or /tests, but ").concat(sasPrograms, " SAS program(s). This target builds SAS code, which cannot run in a browser. ") +
558
703
  'Build a target whose services are JavaScript (typically a mocks project).');
559
704
  }
705
+ // No PROGRAMS is a different thing again: a compile that produced nothing, or
706
+ // a stale sasjsbuild from an unrelated command - `sasjs doc` leaves a folder
707
+ // behind that is not a program folder but does reach the drive, so the count
708
+ // that decides this is of programs, not of files.
709
+ if (jsPrograms + sasPrograms === 0) {
710
+ (_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("No compiled programs in ".concat(buildFolder, ": this site ships with an empty drive. ") +
711
+ 'That is expected for an interface with no services; for anything else, check that compile wrote the target.');
712
+ }
560
713
  // The package supplies the drive rules the worker answers with, so an install
561
714
  // without its browser build is a build that cannot run. Checked here, with the
562
715
  // other refusals, so a missing dependency leaves the previous site in place -
@@ -567,7 +720,7 @@ function buildMockSite(target) {
567
720
  'or check that @sasjs/api still publishes build/browser.js.');
568
721
  }
569
722
  if (skipped.length) {
570
- (_h = process.logger) === null || _h === void 0 ? void 0 : _h.warn("Left ".concat(skipped.length, " file(s) off the mock drive, over the ").concat(megabytes(exports.MAX_BINARY_SEED_BYTES), " per-file or ").concat(megabytes(exports.MAX_SEED_BYTES), " total the browser build seeds: ") +
723
+ (_j = process.logger) === null || _j === void 0 ? void 0 : _j.warn("Left ".concat(skipped.length, " file(s) off the mock drive, over the ").concat(megabytes(exports.MAX_BINARY_SEED_BYTES), " per-file or ").concat(megabytes(exports.MAX_SEED_BYTES), " total the browser build seeds: ") +
571
724
  "".concat(skipped
572
725
  .slice(0, 3)
573
726
  .map(function (file) {
@@ -597,14 +750,24 @@ function buildMockSite(target) {
597
750
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
598
751
  }
599
752
  }
600
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
753
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
601
754
  finally {
602
755
  try {
603
756
  if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
604
757
  }
605
- finally { if (e_7) throw e_7.error; }
758
+ finally { if (e_8) throw e_8.error; }
606
759
  }
607
760
  fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
761
+ // 2. the linter, bundled from the installed packages so the code routes answer
762
+ // with the rules this CLI resolved rather than a second implementation
763
+ return [4 /*yield*/, (0, exports.buildLintBundle)(path_1.default.join(hostFolder, 'lint-browser.js'))
764
+ // sw.js sits at the site root: a worker can only control the folder it is
765
+ // served from, and the app has to be controlled from /
766
+ ];
767
+ case 3:
768
+ // 2. the linter, bundled from the installed packages so the code routes answer
769
+ // with the rules this CLI resolved rather than a second implementation
770
+ _k.sent();
608
771
  // sw.js sits at the site root: a worker can only control the folder it is
609
772
  // served from, and the app has to be controlled from /
610
773
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -630,6 +793,7 @@ function buildMockSite(target) {
630
793
  'intercept.js',
631
794
  'boot.js',
632
795
  'api-browser.js',
796
+ 'lint-browser.js',
633
797
  'webout-prelude.json'
634
798
  ]
635
799
  .map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
@@ -640,7 +804,8 @@ function buildMockSite(target) {
640
804
  fs_1.default.writeFileSync(swPath, fs_1.default
641
805
  .readFileSync(swPath, 'utf8')
642
806
  .replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
643
- .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')")));
807
+ .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')"))
808
+ .replace("importScripts('/host/lint-browser.js')", "importScripts('/host/lint-browser.js?v=".concat(hostVersion, "')")));
644
809
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
645
810
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
646
811
  if (hasSnapshot) {
@@ -673,3 +838,4 @@ function buildMockSite(target) {
673
838
  });
674
839
  });
675
840
  }
841
+ var templateObject_1;
@@ -169,7 +169,8 @@ describe('browser-hosted build', function () {
169
169
  'persist.js',
170
170
  'intercept.js',
171
171
  'boot.js',
172
- 'api-browser.js'
172
+ 'api-browser.js',
173
+ 'lint-browser.js'
173
174
  ]), _b = _a.next(); !_b.done; _b = _a.next()) {
174
175
  asset = _b.value;
175
176
  expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', asset))).toBe(true);
@@ -209,11 +210,33 @@ describe('browser-hosted build', function () {
209
210
  // a rebuild that changes either file has to change the URL
210
211
  expect(sw).toMatch(/importScripts\('\/host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
211
212
  expect(sw).toMatch(/importScripts\('\/host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
213
+ expect(sw).toMatch(/importScripts\('\/host\/lint-browser\.js\?v=[0-9a-f]{10}'\)/);
212
214
  expect(sw).not.toContain("importScripts('/host/runtime-core.js')");
213
215
  return [2 /*return*/];
214
216
  }
215
217
  });
216
218
  }); });
219
+ it('bundles the linter from the installed packages, ready for a worker', function () { return __awaiter(void 0, void 0, void 0, function () {
220
+ var bundle, vm, realm;
221
+ return __generator(this, function (_a) {
222
+ switch (_a.label) {
223
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
224
+ case 1:
225
+ _a.sent();
226
+ bundle = fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'lint-browser.js'), 'utf8');
227
+ // the bundle carries the package's code service, not a second linter
228
+ expect(bundle).toContain('SASjsCode');
229
+ expect(bundle).toContain('createCodeService');
230
+ vm = require('vm');
231
+ realm = { globalThis: null, console: console };
232
+ realm.globalThis = realm;
233
+ vm.createContext(realm);
234
+ vm.runInContext(bundle, realm);
235
+ expect(typeof realm.SASjsCode.createCodeService).toBe('function');
236
+ return [2 /*return*/];
237
+ }
238
+ });
239
+ }); });
217
240
  it('names the package when its browser build is missing, and keeps the previous site', function () { return __awaiter(void 0, void 0, void 0, function () {
218
241
  var browserBuild, exists;
219
242
  return __generator(this, function (_a) {
@@ -667,6 +690,95 @@ describe('browser-hosted build', function () {
667
690
  }
668
691
  });
669
692
  }); });
693
+ /**
694
+ * The case this build exists for: an interface with no services at all.
695
+ * sasjs/server's own frontend compiles to nothing, and the worker answers its
696
+ * API - so the target must build, ship the web app, and say out loud that the
697
+ * drive it ships is empty.
698
+ */
699
+ it('builds an interface that has no services, and says the drive is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
700
+ var _a, _b, folder, warn, result;
701
+ var e_4, _c;
702
+ var _d;
703
+ return __generator(this, function (_e) {
704
+ switch (_e.label) {
705
+ case 0:
706
+ try {
707
+ // nothing a drive could be seeded from: no services, no sync folder
708
+ for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
709
+ folder = _b.value;
710
+ fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
711
+ recursive: true,
712
+ force: true
713
+ });
714
+ }
715
+ }
716
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
717
+ finally {
718
+ try {
719
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
720
+ }
721
+ finally { if (e_4) throw e_4.error; }
722
+ }
723
+ warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
724
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
725
+ case 1:
726
+ result = _e.sent();
727
+ expect(result.jsPrograms).toBe(0);
728
+ expect(result.driveEntries).toBe(0);
729
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'index.html'))).toBe(true);
730
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('ships with an empty drive'));
731
+ warn.mockRestore();
732
+ return [2 /*return*/];
733
+ }
734
+ });
735
+ }); });
736
+ /**
737
+ * A stale build folder is the case the warning exists for: `sasjs doc` and
738
+ * other commands leave folders behind that are not program folders, so a count
739
+ * of files on the drive says nothing about whether anything was compiled.
740
+ */
741
+ it('warns when the build folder holds only folders that are not programs', function () { return __awaiter(void 0, void 0, void 0, function () {
742
+ var _a, _b, folder, warn, result;
743
+ var e_5, _c;
744
+ var _d;
745
+ return __generator(this, function (_e) {
746
+ switch (_e.label) {
747
+ case 0:
748
+ try {
749
+ for (_a = __values(['services', 'mock-storage']), _b = _a.next(); !_b.done; _b = _a.next()) {
750
+ folder = _b.value;
751
+ fs_1.default.rmSync(path_1.default.join(root, 'sasjsbuild', folder), {
752
+ recursive: true,
753
+ force: true
754
+ });
755
+ }
756
+ }
757
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
758
+ finally {
759
+ try {
760
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
761
+ }
762
+ finally { if (e_5) throw e_5.error; }
763
+ }
764
+ write(path_1.default.join(root, 'sasjsbuild', 'docs', 'index.html'), '<html></html>');
765
+ warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
766
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
767
+ // the drive is NOT empty - the docs folder reaches it - so the file count
768
+ // alone would have kept quiet
769
+ ];
770
+ case 1:
771
+ result = _e.sent();
772
+ // the drive is NOT empty - the docs folder reaches it - so the file count
773
+ // alone would have kept quiet
774
+ expect(result.driveEntries).toBeGreaterThan(0);
775
+ expect(result.jsPrograms).toBe(0);
776
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('No compiled programs'));
777
+ warn.mockRestore();
778
+ return [2 /*return*/];
779
+ }
780
+ });
781
+ }); });
670
782
  it('refuses to build without a web app', function () { return __awaiter(void 0, void 0, void 0, function () {
671
783
  return __generator(this, function (_a) {
672
784
  switch (_a.label) {
@@ -750,8 +862,8 @@ describe('browser-hosted build', function () {
750
862
  });
751
863
  }); });
752
864
  it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
753
- var _a, _b, mockWebFolder, e_4_1;
754
- var e_4, _c;
865
+ var _a, _b, mockWebFolder, e_6_1;
866
+ var e_6, _c;
755
867
  return __generator(this, function (_d) {
756
868
  switch (_d.label) {
757
869
  case 0:
@@ -782,14 +894,14 @@ describe('browser-hosted build', function () {
782
894
  return [3 /*break*/, 1];
783
895
  case 4: return [3 /*break*/, 7];
784
896
  case 5:
785
- e_4_1 = _d.sent();
786
- e_4 = { error: e_4_1 };
897
+ e_6_1 = _d.sent();
898
+ e_6 = { error: e_6_1 };
787
899
  return [3 /*break*/, 7];
788
900
  case 6:
789
901
  try {
790
902
  if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
791
903
  }
792
- finally { if (e_4) throw e_4.error; }
904
+ finally { if (e_6) throw e_6.error; }
793
905
  return [7 /*endfinally*/];
794
906
  case 7:
795
907
  expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);