@sasjs/cli 6.14.0 → 6.16.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.
@@ -28,6 +28,16 @@ export declare const WEBOUT_SENTINELS: {
28
28
  userId: string;
29
29
  apiServerUrl: string;
30
30
  };
31
+ /**
32
+ * Builds `lint-browser.js` from the installed packages.
33
+ *
34
+ * The shim sources are written to the system temp folder, never beside this
35
+ * module: an installed package can be read-only (a global install, a pnpm store
36
+ * symlink, a container that builds as another user), and a build that writes
37
+ * inside it fails with EACCES for exactly the users least able to diagnose it.
38
+ * Resolution is kept by naming the CLI's `node_modules` through `nodePaths`.
39
+ */
40
+ export declare const buildLintBundle: (destination: string) => void;
31
41
  /**
32
42
  * The bounds on what the browser drive seeds. A byte entry is carried as base64
33
43
  * in vfs-seed.json and held in the worker's memory, so a large artefact - an
@@ -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
  };
@@ -391,6 +530,37 @@ var EXECUTABLE_TYPES = [
391
530
  'text/javascript',
392
531
  'application/javascript'
393
532
  ];
533
+ /**
534
+ * Tells the interface that this page is answered by the browser host.
535
+ *
536
+ * A mock site is the server's own interface running against a service worker,
537
+ * and nothing in the document said so: the API answers the same routes, the same
538
+ * mode and the same run times a desktop server does, so an interface could not
539
+ * tell a visitor what they were looking at. sasjs/server reads the tag and, where
540
+ * it is absent, knows the server answered the page.
541
+ *
542
+ * The insertion is reported when the page has no `<head>`, the way the other two
543
+ * rewrites report markup they could not find - silently leaving the tag off a
544
+ * page the browser host cannot mark is what the reader of the absence rule would
545
+ * misread as "a server answered this". The check for an existing tag is scoped
546
+ * to the head, because a page may mention the tag in a script or a template
547
+ * without carrying it, and a document-wide test would then insert nothing.
548
+ */
549
+ var HOST_TAG = '<meta name="sasjs-host" content="browser" />';
550
+ var HOST_TAG_PATTERN = /<meta\s+name=["']sasjs-host["']/i;
551
+ var markBrowserHost = function (html, warnings) {
552
+ var head = /<head(\s[^>]*)?>/i.exec(html);
553
+ if (!head) {
554
+ warnings.push('no <head> element found - the sasjs-host tag was not added, so the page will read as server-answered');
555
+ return html;
556
+ }
557
+ var start = head.index + head[0].length;
558
+ var close = html.toLowerCase().indexOf('</head>', start);
559
+ var inHead = close < 0 ? html.slice(start) : html.slice(start, close);
560
+ if (HOST_TAG_PATTERN.test(inHead))
561
+ return html;
562
+ return html.slice(0, start) + HOST_TAG + html.slice(start);
563
+ };
394
564
  /**
395
565
  * Points the app at its own origin and replaces its script tags with the boot
396
566
  * loader. Both changes are reported when the expected markup is absent, rather
@@ -399,14 +569,7 @@ var EXECUTABLE_TYPES = [
399
569
  var patchIndexHtml = function (html, version) {
400
570
  var warnings = [];
401
571
  var patched = html;
402
- // States where this page's API comes from, for anything in the page that
403
- // wants to say so: the server's own interface reads it to describe the host
404
- // it is running on, and a browser host is a different thing to explain than a
405
- // server process. Absence means the server answered the page.
406
- if (!/<meta\s+name=["']sasjs-host["']/i.test(patched))
407
- patched = patched.replace(/<head(\s[^>]*)?>/i, function (head) {
408
- return "".concat(head, "\n <meta name=\"sasjs-host\" content=\"browser\" />");
409
- });
572
+ patched = markBrowserHost(patched, warnings);
410
573
  if (/<sasjs[^>]*>/i.test(patched)) {
411
574
  patched = patched.replace(/(<sasjs[^>]*?)\sserverurl="[^"]*"/i, '$1 serverurl=""');
412
575
  patched = patched.replace(/(<sasjs[^>]*?)\sloginmechanism="[^"]*"/i, '$1 loginmechanism="Default"');
@@ -417,7 +580,7 @@ var patchIndexHtml = function (html, version) {
417
580
  var scripts = [];
418
581
  var tagPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>[ \t]*\r?\n?/gi;
419
582
  patched = patched.replace(tagPattern, function (tag, source, body) {
420
- var e_6, _a;
583
+ var e_7, _a;
421
584
  var attributes = parseAttributes(source);
422
585
  var type = (attributes.type || '').trim().toLowerCase();
423
586
  var inlined = !attributes.src;
@@ -442,16 +605,16 @@ var patchIndexHtml = function (html, version) {
442
605
  continue;
443
606
  if (attribute === 'type' && !attributes.type)
444
607
  continue;
445
- var name_1 = REPLAYED_ATTRIBUTES[attribute];
446
- spec[name_1] = attribute === 'nomodule' ? 'true' : attributes[attribute];
608
+ var name_2 = REPLAYED_ATTRIBUTES[attribute];
609
+ spec[name_2] = attribute === 'nomodule' ? 'true' : attributes[attribute];
447
610
  }
448
611
  }
449
- catch (e_6_1) { e_6 = { error: e_6_1 }; }
612
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
450
613
  finally {
451
614
  try {
452
615
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
453
616
  }
454
- finally { if (e_6) throw e_6.error; }
617
+ finally { if (e_7) throw e_7.error; }
455
618
  }
456
619
  scripts.push(spec);
457
620
  return '';
@@ -484,7 +647,7 @@ var patchIndexHtml = function (html, version) {
484
647
  function buildMockSite(target) {
485
648
  return __awaiter(this, void 0, void 0, function () {
486
649
  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;
487
- var e_7, _e;
650
+ var e_8, _e;
488
651
  var _f, _g, _h;
489
652
  return __generator(this, function (_j) {
490
653
  switch (_j.label) {
@@ -573,14 +736,24 @@ function buildMockSite(target) {
573
736
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, asset), path_1.default.join(hostFolder, asset));
574
737
  }
575
738
  }
576
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
739
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
577
740
  finally {
578
741
  try {
579
742
  if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
580
743
  }
581
- finally { if (e_7) throw e_7.error; }
744
+ finally { if (e_8) throw e_8.error; }
582
745
  }
583
746
  fs_1.default.copyFileSync(API_BROWSER_BUILD, path_1.default.join(hostFolder, 'api-browser.js'));
747
+ // 2. the linter, bundled from the installed packages so the code routes answer
748
+ // with the rules this CLI resolved rather than a second implementation
749
+ return [4 /*yield*/, (0, exports.buildLintBundle)(path_1.default.join(hostFolder, 'lint-browser.js'))
750
+ // sw.js sits at the site root: a worker can only control the folder it is
751
+ // served from, and the app has to be controlled from /
752
+ ];
753
+ case 3:
754
+ // 2. the linter, bundled from the installed packages so the code routes answer
755
+ // with the rules this CLI resolved rather than a second implementation
756
+ _j.sent();
584
757
  // sw.js sits at the site root: a worker can only control the folder it is
585
758
  // served from, and the app has to be controlled from /
586
759
  fs_1.default.copyFileSync(path_1.default.join(ASSETS_FOLDER, 'sw.js'), path_1.default.join(outputFolder, 'sw.js'));
@@ -606,6 +779,7 @@ function buildMockSite(target) {
606
779
  'intercept.js',
607
780
  'boot.js',
608
781
  'api-browser.js',
782
+ 'lint-browser.js',
609
783
  'webout-prelude.json'
610
784
  ]
611
785
  .map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
@@ -616,7 +790,8 @@ function buildMockSite(target) {
616
790
  fs_1.default.writeFileSync(swPath, fs_1.default
617
791
  .readFileSync(swPath, 'utf8')
618
792
  .replace("importScripts('/host/runtime-core.js')", "importScripts('/host/runtime-core.js?v=".concat(hostVersion, "')"))
619
- .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')")));
793
+ .replace("importScripts('/host/api-browser.js')", "importScripts('/host/api-browser.js?v=".concat(hostVersion, "')"))
794
+ .replace("importScripts('/host/lint-browser.js')", "importScripts('/host/lint-browser.js?v=".concat(hostVersion, "')")));
620
795
  snapshotSource = path_1.default.join(buildFolder, 'mock-snapshot.json');
621
796
  hasSnapshot = fs_1.default.existsSync(snapshotSource);
622
797
  if (hasSnapshot) {
@@ -649,3 +824,4 @@ function buildMockSite(target) {
649
824
  });
650
825
  });
651
826
  }
827
+ 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) {
@@ -368,18 +391,91 @@ describe('browser-hosted build', function () {
368
391
  });
369
392
  }); });
370
393
  it('states in the page that the API is answered here', function () { return __awaiter(void 0, void 0, void 0, function () {
371
- var html;
394
+ var html, tag;
372
395
  return __generator(this, function (_a) {
373
396
  switch (_a.label) {
374
397
  case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
375
398
  case 1:
376
399
  _a.sent();
377
400
  html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
378
- // The interface reads this to tell a browser host from a server: the API
379
- // cannot, because a browser host answers the same routes and the same
380
- // /SASjsApi/info shape. Only the host knows, so the host says.
401
+ tag = '<meta name="sasjs-host" content="browser" />';
402
+ expect(html).toContain(tag);
403
+ expect(html.indexOf(tag)).toBeLessThan(html.indexOf('</head>'));
404
+ // once: the tag it inserts is the one the guard then finds
405
+ expect(html.split('sasjs-host').length - 1).toBe(1);
406
+ return [2 /*return*/];
407
+ }
408
+ });
409
+ }); });
410
+ it('leaves a page that already carries the host tag alone', function () { return __awaiter(void 0, void 0, void 0, function () {
411
+ var html;
412
+ return __generator(this, function (_a) {
413
+ switch (_a.label) {
414
+ case 0:
415
+ write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
416
+ '<!doctype html><html><head>',
417
+ '<meta name="sasjs-host" content="browser">',
418
+ '</head><body><script src="main.abc123.js"></script></body></html>'
419
+ ].join('\n'));
420
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
421
+ case 1:
422
+ _a.sent();
423
+ html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
424
+ expect(html.split('sasjs-host').length - 1).toBe(1);
425
+ return [2 /*return*/];
426
+ }
427
+ });
428
+ }); });
429
+ it('inserts the tag when the page only mentions it outside the head', function () { return __awaiter(void 0, void 0, void 0, function () {
430
+ var html;
431
+ return __generator(this, function (_a) {
432
+ switch (_a.label) {
433
+ case 0:
434
+ write(path_1.default.join(root, 'client', 'dist', 'index.html'), [
435
+ '<!doctype html><html><head><title>App</title></head><body>',
436
+ '<script src="main.abc123.js"></script>',
437
+ '<template><meta name="sasjs-host" content="browser"></template>',
438
+ '</body></html>'
439
+ ].join('\n'));
440
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
441
+ case 1:
442
+ _a.sent();
443
+ html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
444
+ // the template's copy is not the page's tag: the head gets its own
445
+ expect(html.split('sasjs-host').length - 1).toBe(2);
446
+ expect(html.indexOf('content="browser" />')).toBeLessThan(html.indexOf('</head>'));
447
+ return [2 /*return*/];
448
+ }
449
+ });
450
+ }); });
451
+ it('marks a page whose head is never closed', function () { return __awaiter(void 0, void 0, void 0, function () {
452
+ var html;
453
+ return __generator(this, function (_a) {
454
+ switch (_a.label) {
455
+ case 0:
456
+ write(path_1.default.join(root, 'client', 'dist', 'index.html'), '<!doctype html><html><head><title>App</title><body><script src="main.abc123.js"></script></body></html>');
457
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
458
+ case 1:
459
+ _a.sent();
460
+ html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
381
461
  expect(html).toContain('<meta name="sasjs-host" content="browser" />');
382
- expect(html.indexOf('sasjs-host')).toBeLessThan(html.indexOf('</head>'));
462
+ return [2 /*return*/];
463
+ }
464
+ });
465
+ }); });
466
+ it('warns when a page has no head to mark', function () { return __awaiter(void 0, void 0, void 0, function () {
467
+ var html;
468
+ var _a;
469
+ return __generator(this, function (_b) {
470
+ switch (_b.label) {
471
+ case 0:
472
+ write(path_1.default.join(root, 'client', 'dist', 'index.html'), '<!doctype html><html><body><script src="main.abc123.js"></script></body></html>');
473
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
474
+ case 1:
475
+ _b.sent();
476
+ expect((_a = process.logger) === null || _a === void 0 ? void 0 : _a.warn).toHaveBeenCalledWith(expect.stringContaining('no <head> element found'));
477
+ html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
478
+ expect(html).not.toContain('sasjs-host');
383
479
  return [2 /*return*/];
384
480
  }
385
481
  });