@sasjs/cli 6.19.1 → 6.19.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.
@@ -114,13 +114,41 @@
114
114
  stamp = ''
115
115
  }
116
116
 
117
- // Registered relative to the page, and scoped to the directory it sits in:
118
- // the site is served from the origin root on one host and from a folder on
119
- // another (a demonstration site beside its own docs), and the worker's scope
120
- // has to follow the page rather than assume the root.
117
+ // Registered against the loader's own URL, which sits in <base>/host/, and
118
+ // scoped to the folder above it: the site is served from the origin root on
119
+ // one host and from a folder on another (a demonstration site beside its own
120
+ // docs). The page cannot be the reference - at `<base>` with no trailing
121
+ // slash a page-relative URL names the origin root, and './' resolves to '/',
122
+ // which is the mis-registration this is here to avoid.
123
+ //
124
+ // The tag the build emits is absolute (`<base>/host/boot.js`) when the target
125
+ // names a folder, and relative (`host/boot.js`) when it does not - so a site
126
+ // published in a folder without naming it depends on its own URL ending in a
127
+ // slash - and in both cases the loader's own URL then carries `<base>`. The
128
+ // fallback below is for the case where there is no currentScript to read.
129
+ var loaderSrc = (script && script.src) || loaderUrl()
130
+
131
+ // The loader always sits in <base>/host/, so without a currentScript the page
132
+ // still names the base. A path that does not end in a slash is ambiguous: it
133
+ // is the folder when its last segment has no dot (`/demo`), and a file when it
134
+ // does (`/demo/index.html`) - only the second has a segment to drop.
135
+ // `location.href` cannot do this: one level above `/demo` is the origin root.
136
+ function loaderUrl() {
137
+ var page = location.pathname
138
+ var lastSegment = page.slice(page.lastIndexOf('/') + 1)
139
+ var base
140
+
141
+ if (lastSegment.indexOf('.') === -1) {
142
+ base = page.charAt(page.length - 1) === '/' ? page : page + '/'
143
+ } else {
144
+ base = page.slice(0, page.lastIndexOf('/') + 1)
145
+ }
146
+
147
+ return location.origin + base + 'host/boot.js'
148
+ }
121
149
  navigator.serviceWorker
122
- .register(new URL('sw.js' + stamp, location.href).href, {
123
- scope: './',
150
+ .register(new URL('../sw.js' + stamp, loaderSrc).href, {
151
+ scope: new URL('../', loaderSrc).pathname,
124
152
  updateViaCache: 'none'
125
153
  })
126
154
  .then(function () {
@@ -517,8 +517,9 @@ var escapeAttribute = function (value) {
517
517
  * order, plus the inline snippets some pages use to set up a global before the
518
518
  * bundle runs - so the host does not dictate how the app is built.
519
519
  */
520
- var bootTag = function (scripts, version) {
521
- return "<script src=\"host/boot.js?v=".concat(version, "\" data-scripts='").concat(escapeAttribute(JSON.stringify(scripts)), "'></script>");
520
+ var bootTag = function (scripts, version, basePath) {
521
+ if (basePath === void 0) { basePath = ''; }
522
+ return "<script src=\"".concat(basePath ? "".concat(basePath, "/host/boot.js") : 'host/boot.js', "?v=").concat(version, "\" data-scripts='").concat(escapeAttribute(JSON.stringify(scripts)), "'></script>");
522
523
  };
523
524
  /**
524
525
  * Script types the browser executes. Anything else (`application/ld+json`, a
@@ -531,28 +532,36 @@ var EXECUTABLE_TYPES = [
531
532
  'application/javascript'
532
533
  ];
533
534
  /**
534
- * The folder the site is published under, as a path with no trailing slash.
535
+ * The folder the site is published under, with a leading slash and no trailing
536
+ * one.
535
537
  *
536
538
  * Empty for the origin root, which is the ordinary case. A host that serves the
537
539
  * demonstration site beside its documentation (a GitHub Pages site whose root is
538
540
  * the docs) names the folder here, and every path the build emits follows it.
541
+ *
542
+ * The shapes the released build accepts keep working - a bare `demo`, a leading
543
+ * or trailing slash, surrounding space - and are normalised rather than
544
+ * refused, because a `fix(mock)` release must not fail a build that passes
545
+ * today. The sasjsconfig schema states the canonical form; this only rejects
546
+ * what could never have worked, a segment that is `.`, `..` or empty.
539
547
  */
540
548
  var normaliseBasePath = function (value) {
541
549
  if (!value)
542
550
  return '';
543
- var base = String(value).trim();
544
- if (!base.startsWith('/'))
545
- base = '/' + base;
546
- base = base.replace(/\/+$/, '');
547
- var segments = base.split('/').slice(1);
551
+ var segments = String(value)
552
+ .trim()
553
+ .replace(/^\/+/, '')
554
+ .replace(/\/+$/, '')
555
+ .split('/');
548
556
  if (!segments.length ||
549
557
  segments.some(function (segment) {
550
- return !/^[A-Za-z0-9._~-]+$/.test(segment) ||
558
+ return !segment ||
551
559
  segment === '.' ||
552
- segment === '..';
560
+ segment === '..' ||
561
+ !/^[A-Za-z0-9._~-]+$/.test(segment);
553
562
  }))
554
563
  throw new Error("mockWebBasePath is not a folder path: ".concat(JSON.stringify(value), ". Use a path such as \"/demo\", or leave it unset for the origin root."));
555
- return base;
564
+ return '/' + segments.join('/');
556
565
  };
557
566
  /**
558
567
  * Tells the interface that this page is answered by the browser host.
@@ -648,7 +657,7 @@ var patchIndexHtml = function (html, version, basePath) {
648
657
  return '';
649
658
  });
650
659
  if (scripts.length) {
651
- var tag = bootTag(scripts, version);
660
+ var tag = bootTag(scripts, version, basePath);
652
661
  patched = /<\/body>/i.test(patched)
653
662
  ? patched.replace(/<\/body>/i, "".concat(tag, "\n</body>"))
654
663
  : patched + tag;
@@ -56,15 +56,24 @@ var loadBoot = function (specs, options) {
56
56
  };
57
57
  return element;
58
58
  };
59
+ // The page carries an absolute boot tag, `<base>/host/boot.js`, so the
60
+ // loader's own URL names the folder whether or not the page URL ends in a
61
+ // slash - a relative tag would resolve against the page and, at `/demo`, name
62
+ // the origin root.
63
+ var pageHref = options.href || 'http://localhost:8123/';
64
+ var stamp = options.noStamp ? '' : '?v=abc123';
65
+ // the tag the build emits: absolute when the target names a folder, relative
66
+ // to the page when it does not
67
+ var loaderSrc = options.basePath
68
+ ? "".concat(new URL(pageHref).origin).concat(options.basePath, "/host/boot.js").concat(stamp)
69
+ : new URL('host/boot.js' + stamp, pageHref).href;
59
70
  var document = {
60
71
  currentScript: options.noScript
61
72
  ? null
62
73
  : {
63
74
  // the builder stamps the boot tag with the host version, and the
64
75
  // loader reuses that query for the worker's own URL
65
- src: options.noStamp
66
- ? 'http://localhost:8123/host/boot.js'
67
- : 'http://localhost:8123/host/boot.js?v=abc123',
76
+ src: loaderSrc,
68
77
  getAttribute: function (name) {
69
78
  if (name !== 'data-scripts')
70
79
  return null;
@@ -126,7 +135,9 @@ var loadBoot = function (specs, options) {
126
135
  }
127
136
  };
128
137
  global.location = {
129
- href: options.href || 'http://localhost:8123/',
138
+ href: pageHref,
139
+ pathname: new URL(pageHref).pathname,
140
+ origin: new URL(pageHref).origin,
130
141
  reload: function () { return timeline.push('reload'); }
131
142
  };
132
143
  jest.spyOn(console, 'error').mockImplementation(function () { });
@@ -185,7 +196,7 @@ describe('the boot loader', function () {
185
196
  // root on one host and from /demo beside its docs on another.
186
197
  expect(registered).toEqual([
187
198
  'http://localhost:8123/sw.js?v=abc123 ' +
188
- JSON.stringify({ scope: './', updateViaCache: 'none' })
199
+ JSON.stringify({ scope: '/', updateViaCache: 'none' })
189
200
  ]);
190
201
  return [2 /*return*/];
191
202
  }
@@ -196,13 +207,104 @@ describe('the boot loader', function () {
196
207
  return __generator(this, function (_a) {
197
208
  switch (_a.label) {
198
209
  case 0:
199
- registered = loadBoot([], { href: 'http://localhost:8123/demo/' }).registered;
210
+ registered = loadBoot([], {
211
+ href: 'http://localhost:8123/demo/',
212
+ basePath: '/demo'
213
+ }).registered;
200
214
  return [4 /*yield*/, settle()];
201
215
  case 1:
202
216
  _a.sent();
203
217
  expect(registered).toEqual([
204
218
  'http://localhost:8123/demo/sw.js?v=abc123 ' +
205
- JSON.stringify({ scope: './', updateViaCache: 'none' })
219
+ JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
220
+ ]);
221
+ return [2 /*return*/];
222
+ }
223
+ });
224
+ }); });
225
+ it('registers under the folder when the page has no trailing slash', function () { return __awaiter(void 0, void 0, void 0, function () {
226
+ var registered;
227
+ return __generator(this, function (_a) {
228
+ switch (_a.label) {
229
+ case 0:
230
+ registered = loadBoot([], {
231
+ href: 'http://localhost:8123/demo',
232
+ basePath: '/demo'
233
+ }).registered;
234
+ return [4 /*yield*/, settle()];
235
+ case 1:
236
+ _a.sent();
237
+ expect(registered).toEqual([
238
+ 'http://localhost:8123/demo/sw.js?v=abc123 ' +
239
+ JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
240
+ ]);
241
+ return [2 /*return*/];
242
+ }
243
+ });
244
+ }); });
245
+ it('finds the folder from the page when the document has no currentScript', function () { return __awaiter(void 0, void 0, void 0, function () {
246
+ var registered;
247
+ return __generator(this, function (_a) {
248
+ switch (_a.label) {
249
+ case 0:
250
+ registered = loadBoot([], {
251
+ href: 'http://localhost:8123/demo/',
252
+ basePath: '/demo',
253
+ noScript: true
254
+ }).registered;
255
+ return [4 /*yield*/, settle()
256
+ // no tag, no version: the fallback cannot carry the build's stamp, so this
257
+ // path registers the folder's worker without a cache-busting query
258
+ ];
259
+ case 1:
260
+ _a.sent();
261
+ // no tag, no version: the fallback cannot carry the build's stamp, so this
262
+ // path registers the folder's worker without a cache-busting query
263
+ expect(registered).toEqual([
264
+ 'http://localhost:8123/demo/sw.js ' +
265
+ JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
266
+ ]);
267
+ return [2 /*return*/];
268
+ }
269
+ });
270
+ }); });
271
+ it('treats a path with no trailing slash as the folder, not a file', function () { return __awaiter(void 0, void 0, void 0, function () {
272
+ var registered;
273
+ return __generator(this, function (_a) {
274
+ switch (_a.label) {
275
+ case 0:
276
+ registered = loadBoot([], {
277
+ href: 'http://localhost:8123/demo',
278
+ basePath: '/demo',
279
+ noScript: true
280
+ }).registered;
281
+ return [4 /*yield*/, settle()];
282
+ case 1:
283
+ _a.sent();
284
+ expect(registered).toEqual([
285
+ 'http://localhost:8123/demo/sw.js ' +
286
+ JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
287
+ ]);
288
+ return [2 /*return*/];
289
+ }
290
+ });
291
+ }); });
292
+ it('drops a file name from the path, and keeps the folder above it', function () { return __awaiter(void 0, void 0, void 0, function () {
293
+ var registered;
294
+ return __generator(this, function (_a) {
295
+ switch (_a.label) {
296
+ case 0:
297
+ registered = loadBoot([], {
298
+ href: 'http://localhost:8123/demo/index.html',
299
+ basePath: '/demo',
300
+ noScript: true
301
+ }).registered;
302
+ return [4 /*yield*/, settle()];
303
+ case 1:
304
+ _a.sent();
305
+ expect(registered).toEqual([
306
+ 'http://localhost:8123/demo/sw.js ' +
307
+ JSON.stringify({ scope: '/demo/', updateViaCache: 'none' })
206
308
  ]);
207
309
  return [2 /*return*/];
208
310
  }
@@ -375,9 +375,9 @@ describe('browser-hosted build', function () {
375
375
  _a.sent();
376
376
  html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
377
377
  expect(html).toContain('serverurl="/demo"');
378
- // the loader and the host's files stay relative to the page, so the same
379
- // build works at the origin root and under the folder
380
- expect(html).toContain('<script src="host/boot.js?v=');
378
+ // the boot tag is absolute and names the folder, so a page at `/demo` with
379
+ // no trailing slash still loads the loader from `/demo/host/`
380
+ expect(html).toContain('<script src="/demo/host/boot.js?v=');
381
381
  return [2 /*return*/];
382
382
  }
383
383
  });
@@ -395,6 +395,50 @@ describe('browser-hosted build', function () {
395
395
  }
396
396
  });
397
397
  }); });
398
+ it('normalises the folder shapes the released build already accepts', function () { return __awaiter(void 0, void 0, void 0, function () {
399
+ var _a, _b, value, html_1, e_3_1, html;
400
+ var e_3, _c;
401
+ return __generator(this, function (_d) {
402
+ switch (_d.label) {
403
+ case 0:
404
+ _d.trys.push([0, 5, 6, 7]);
405
+ _a = __values(['/demo/', ' /demo ', 'demo']), _b = _a.next();
406
+ _d.label = 1;
407
+ case 1:
408
+ if (!!_b.done) return [3 /*break*/, 4];
409
+ value = _b.value;
410
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockWebBasePath: value }))];
411
+ case 2:
412
+ _d.sent();
413
+ html_1 = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
414
+ expect(html_1).toContain('serverurl="/demo"');
415
+ _d.label = 3;
416
+ case 3:
417
+ _b = _a.next();
418
+ return [3 /*break*/, 1];
419
+ case 4: return [3 /*break*/, 7];
420
+ case 5:
421
+ e_3_1 = _d.sent();
422
+ e_3 = { error: e_3_1 };
423
+ return [3 /*break*/, 7];
424
+ case 6:
425
+ try {
426
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
427
+ }
428
+ finally { if (e_3) throw e_3.error; }
429
+ return [7 /*endfinally*/];
430
+ case 7:
431
+ // explicit empty is the origin root, as it was before the option existed
432
+ return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target({ mockWebBasePath: '' }))];
433
+ case 8:
434
+ // explicit empty is the origin root, as it was before the option existed
435
+ _d.sent();
436
+ html = fs_1.default.readFileSync(path_1.default.join(out(), 'index.html'), 'utf8');
437
+ expect(html).toContain('serverurl=""');
438
+ return [2 /*return*/];
439
+ }
440
+ });
441
+ }); });
398
442
  it('points the app at itself and installs the boot loader', function () { return __awaiter(void 0, void 0, void 0, function () {
399
443
  var html;
400
444
  return __generator(this, function (_a) {
@@ -630,7 +674,7 @@ describe('browser-hosted build', function () {
630
674
  }); });
631
675
  it('stops seeding bytes once the drive budget is spent, and names the file', function () { return __awaiter(void 0, void 0, void 0, function () {
632
676
  var chunk, _a, _b, name_1, seed, seeded;
633
- var e_3, _c;
677
+ var e_4, _c;
634
678
  var _d;
635
679
  return __generator(this, function (_e) {
636
680
  switch (_e.label) {
@@ -643,12 +687,12 @@ describe('browser-hosted build', function () {
643
687
  fs_1.default.writeFileSync(path_1.default.join(root, 'sasjsbuild', 'mock-storage', name_1), chunk);
644
688
  }
645
689
  }
646
- catch (e_3_1) { e_3 = { error: e_3_1 }; }
690
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
647
691
  finally {
648
692
  try {
649
693
  if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
650
694
  }
651
- finally { if (e_3) throw e_3.error; }
695
+ finally { if (e_4) throw e_4.error; }
652
696
  }
653
697
  return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
654
698
  // the budget is spent in path order, so the drive is the same on every host:
@@ -734,7 +778,7 @@ describe('browser-hosted build', function () {
734
778
  */
735
779
  it('builds an interface that has no services, and says the drive is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
736
780
  var _a, _b, folder, warn, result;
737
- var e_4, _c;
781
+ var e_5, _c;
738
782
  var _d;
739
783
  return __generator(this, function (_e) {
740
784
  switch (_e.label) {
@@ -749,12 +793,12 @@ describe('browser-hosted build', function () {
749
793
  });
750
794
  }
751
795
  }
752
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
796
+ catch (e_5_1) { e_5 = { error: e_5_1 }; }
753
797
  finally {
754
798
  try {
755
799
  if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
756
800
  }
757
- finally { if (e_4) throw e_4.error; }
801
+ finally { if (e_5) throw e_5.error; }
758
802
  }
759
803
  warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
760
804
  return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
@@ -776,7 +820,7 @@ describe('browser-hosted build', function () {
776
820
  */
777
821
  it('warns when the build folder holds only folders that are not programs', function () { return __awaiter(void 0, void 0, void 0, function () {
778
822
  var _a, _b, folder, warn, result;
779
- var e_5, _c;
823
+ var e_6, _c;
780
824
  var _d;
781
825
  return __generator(this, function (_e) {
782
826
  switch (_e.label) {
@@ -790,12 +834,12 @@ describe('browser-hosted build', function () {
790
834
  });
791
835
  }
792
836
  }
793
- catch (e_5_1) { e_5 = { error: e_5_1 }; }
837
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
794
838
  finally {
795
839
  try {
796
840
  if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
797
841
  }
798
- finally { if (e_5) throw e_5.error; }
842
+ finally { if (e_6) throw e_6.error; }
799
843
  }
800
844
  write(path_1.default.join(root, 'sasjsbuild', 'docs', 'index.html'), '<html></html>');
801
845
  warn = jest.spyOn((_d = process.logger) !== null && _d !== void 0 ? _d : console, 'warn');
@@ -898,8 +942,8 @@ describe('browser-hosted build', function () {
898
942
  });
899
943
  }); });
900
944
  it('refuses a mockWebFolder that would delete the project or its inputs', function () { return __awaiter(void 0, void 0, void 0, function () {
901
- var _a, _b, mockWebFolder, e_6_1;
902
- var e_6, _c;
945
+ var _a, _b, mockWebFolder, e_7_1;
946
+ var e_7, _c;
903
947
  return __generator(this, function (_d) {
904
948
  switch (_d.label) {
905
949
  case 0:
@@ -930,14 +974,14 @@ describe('browser-hosted build', function () {
930
974
  return [3 /*break*/, 1];
931
975
  case 4: return [3 /*break*/, 7];
932
976
  case 5:
933
- e_6_1 = _d.sent();
934
- e_6 = { error: e_6_1 };
977
+ e_7_1 = _d.sent();
978
+ e_7 = { error: e_7_1 };
935
979
  return [3 /*break*/, 7];
936
980
  case 6:
937
981
  try {
938
982
  if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
939
983
  }
940
- finally { if (e_6) throw e_6.error; }
984
+ finally { if (e_7) throw e_7.error; }
941
985
  return [7 /*endfinally*/];
942
986
  case 7:
943
987
  expect(fs_1.default.existsSync(path_1.default.join(root, 'client', 'dist', 'index.html'))).toBe(true);
@@ -5,3 +5,4 @@ export { TargetScope } from './targetScope';
5
5
  export { TestFlow, Coverage, CoverageReport, CoverageSummary, CoverageType, CoverageState, TestResults, TestResultStatus, TestDescription, TestResult, TestResultDescription, TestResultCsv } from './testing';
6
6
  export { Flow, FlowWave, FlowWaveJob } from './flow';
7
7
  export * from './log';
8
+ export * from './mockWebBasePath';
@@ -24,3 +24,4 @@ Object.defineProperty(exports, "CoverageType", { enumerable: true, get: function
24
24
  Object.defineProperty(exports, "CoverageState", { enumerable: true, get: function () { return testing_1.CoverageState; } });
25
25
  Object.defineProperty(exports, "TestResultStatus", { enumerable: true, get: function () { return testing_1.TestResultStatus; } });
26
26
  __exportStar(require("./log"), exports);
27
+ __exportStar(require("./mockWebBasePath"), exports);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `mockWebBasePath` on a stream config.
3
+ *
4
+ * A `.ts` rather than a `.d.ts` so it compiles into `build/types/` with the
5
+ * rest of the package: tsc does not emit an ambient input file, and this one
6
+ * would otherwise type the CLI's own build and vanish from the published one.
7
+ *
8
+ * The option belongs to the mock target the CLI reads, but `StreamConfig` in
9
+ * @sasjs/utils 4.9.0 predates it, so without this the property is invisible to
10
+ * anyone typing a sasjsconfig and to anything that round-trips a target through
11
+ * the type. The field lands upstream in sasjs/utils#317; when the pin moves past
12
+ * that release, delete this file and the declaration merges from the package
13
+ * itself.
14
+ */
15
+ export {};
16
+ declare module '@sasjs/utils/types/config' {
17
+ interface StreamConfig {
18
+ /**
19
+ * The folder the mock site is published in, as a path with no trailing
20
+ * slash (`/demo`). Unset means the origin root.
21
+ */
22
+ mockWebBasePath?: string;
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -125,5 +125,5 @@
125
125
  "ws": "8.21.0",
126
126
  "nanoid": "3.3.18"
127
127
  },
128
- "version": "6.19.1"
128
+ "version": "6.19.2"
129
129
  }