@dwp/govuk-casa 8.0.0-alpha2 → 8.0.1

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/assets/css/casa-ie8.css +1 -1
  4. package/dist/assets/css/casa.css +1 -1
  5. package/dist/casa.d.ts +2 -1
  6. package/dist/casa.js +3 -1
  7. package/dist/lib/CasaTemplateLoader.d.ts +4 -2
  8. package/dist/lib/CasaTemplateLoader.js +26 -4
  9. package/dist/lib/JourneyContext.d.ts +38 -6
  10. package/dist/lib/JourneyContext.js +58 -17
  11. package/dist/lib/MutableRouter.js +6 -2
  12. package/dist/lib/Plan.d.ts +37 -4
  13. package/dist/lib/Plan.js +75 -11
  14. package/dist/lib/ValidationError.d.ts +6 -2
  15. package/dist/lib/ValidationError.js +7 -0
  16. package/dist/lib/ValidatorFactory.d.ts +72 -19
  17. package/dist/lib/ValidatorFactory.js +33 -20
  18. package/dist/lib/configuration-ingestor.d.ts +262 -0
  19. package/dist/lib/configuration-ingestor.js +464 -0
  20. package/dist/lib/configure.d.ts +26 -140
  21. package/dist/lib/configure.js +17 -45
  22. package/dist/lib/dirname.cjs +1 -1
  23. package/dist/lib/dirname.d.cts +2 -0
  24. package/dist/lib/end-session.d.ts +2 -1
  25. package/dist/lib/end-session.js +27 -7
  26. package/dist/lib/field.d.ts +39 -46
  27. package/dist/lib/field.js +75 -36
  28. package/dist/lib/index.d.ts +14 -0
  29. package/dist/lib/index.js +54 -0
  30. package/dist/lib/logger.d.ts +2 -1
  31. package/dist/lib/logger.js +3 -4
  32. package/dist/lib/nunjucks-filters.js +8 -0
  33. package/dist/lib/utils.d.ts +18 -2
  34. package/dist/lib/utils.js +56 -2
  35. package/dist/lib/validators/inArray.js +1 -1
  36. package/dist/lib/validators/index.js +0 -22
  37. package/dist/lib/validators/postalAddressObject.js +6 -2
  38. package/dist/lib/waypoint-url.d.ts +2 -1
  39. package/dist/lib/waypoint-url.js +3 -0
  40. package/dist/middleware/body-parser.d.ts +1 -0
  41. package/dist/middleware/body-parser.js +18 -9
  42. package/dist/middleware/data.d.ts +1 -2
  43. package/dist/middleware/data.js +9 -9
  44. package/dist/middleware/dirname.cjs +1 -1
  45. package/dist/middleware/dirname.d.cts +2 -0
  46. package/dist/middleware/gather-fields.d.ts +2 -1
  47. package/dist/middleware/gather-fields.js +6 -5
  48. package/dist/middleware/i18n.js +5 -1
  49. package/dist/middleware/post.js +6 -6
  50. package/dist/middleware/progress-journey.js +1 -1
  51. package/dist/middleware/sanitise-fields.js +9 -9
  52. package/dist/middleware/session.d.ts +2 -1
  53. package/dist/middleware/session.js +62 -55
  54. package/dist/middleware/skip-waypoint.js +2 -2
  55. package/dist/middleware/steer-journey.d.ts +2 -1
  56. package/dist/middleware/steer-journey.js +3 -0
  57. package/dist/middleware/validate-fields.js +7 -6
  58. package/dist/mjs/esm-wrapper.js +10 -0
  59. package/dist/routes/ancillary.d.ts +8 -1
  60. package/dist/routes/ancillary.js +7 -2
  61. package/dist/routes/dirname.cjs +1 -1
  62. package/dist/routes/dirname.d.cts +2 -0
  63. package/dist/routes/journey.js +14 -8
  64. package/dist/routes/static.js +4 -3
  65. package/package.json +42 -25
  66. package/views/casa/layouts/main.njk +2 -2
@@ -13,6 +13,24 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
13
13
  var _CasaTemplateLoader_instances, _CasaTemplateLoader_blockModifiers, _CasaTemplateLoader_applyBlockModifiers;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const nunjucks_1 = require("nunjucks");
16
+ /**
17
+ * @typedef {import('nunjucks').FileSystemLoaderOptions} FileSystemLoaderOptions
18
+ */
19
+ const VALID_BLOCKS = [
20
+ 'beforeContent',
21
+ 'bodyEnd',
22
+ 'bodyStart',
23
+ 'casaPageTitle',
24
+ 'content',
25
+ 'footer',
26
+ 'head',
27
+ 'header',
28
+ 'headIcons',
29
+ 'journey_form',
30
+ 'main',
31
+ 'pageTitle',
32
+ 'skipLink',
33
+ ];
16
34
  /**
17
35
  * @callback BlockModifier
18
36
  * @param {string} templateName Path to the template being modified
@@ -23,7 +41,7 @@ class CasaTemplateLoader extends nunjucks_1.FileSystemLoader {
23
41
  * Constructor.
24
42
  *
25
43
  * @param {string[]} searchPaths Template directories
26
- * @param {import('nunjucks').FileSystemLoaderOptions} opts Loader options
44
+ * @param {FileSystemLoaderOptions} opts Loader options
27
45
  */
28
46
  constructor(searchPaths, opts) {
29
47
  super(searchPaths, opts);
@@ -47,9 +65,13 @@ class CasaTemplateLoader extends nunjucks_1.FileSystemLoader {
47
65
  * @param {string} block Block name, e.g. `bodyStart`
48
66
  * @param {BlockModifier} modifier Modifier function
49
67
  * @returns {void}
68
+ * @throws {Error} If provided with an unrecognised block
50
69
  */
51
70
  modifyBlock(block, modifier) {
52
- // TODO: Limit to only known block so the user can't do general string replacements
71
+ // Limit to only known block so the user can't do general string replacements
72
+ if (!VALID_BLOCKS.includes(block)) {
73
+ throw new Error(`Block "${String(block)}" is not a recognised template block.`);
74
+ }
53
75
  __classPrivateFieldGet(this, _CasaTemplateLoader_blockModifiers, "f").push({
54
76
  block,
55
77
  modifier,
@@ -58,9 +80,9 @@ class CasaTemplateLoader extends nunjucks_1.FileSystemLoader {
58
80
  }
59
81
  exports.default = CasaTemplateLoader;
60
82
  _CasaTemplateLoader_blockModifiers = new WeakMap(), _CasaTemplateLoader_instances = new WeakSet(), _CasaTemplateLoader_applyBlockModifiers = function _CasaTemplateLoader_applyBlockModifiers(name, source) {
61
- // TODO: This is open to abuse by plugin authors, e,g
62
- // `{% block bodyStart %}{% endblock %} <script src="evil.js"></script>`. Problem, or no?
63
83
  for (let i = 0, l = __classPrivateFieldGet(this, _CasaTemplateLoader_blockModifiers, "f").length; i < l; i++) {
84
+ // ESLint disabled as `i` is an integer
85
+ /* eslint-disable-next-line security/detect-object-injection */
64
86
  const { block, modifier } = __classPrivateFieldGet(this, _CasaTemplateLoader_blockModifiers, "f")[i];
65
87
  if (source.src.indexOf(`block ${block}`) > -1) {
66
88
  /* eslint-disable-next-line no-param-reassign */
@@ -1,3 +1,19 @@
1
+ /**
2
+ * @typedef {import('./configuration-ingestor').Page} Page
3
+ */
4
+ /**
5
+ * @callback ContextEventHandler
6
+ * @param {JourneyContext} journeyContext Context including changes
7
+ * @param {JourneyContext} previousContext Context prior to changes
8
+ * @returns {void}
9
+ */
10
+ /**
11
+ * @typedef {object} ContextEvent
12
+ * @property {string} waypoint Waypoint to watch for changes
13
+ * @property {string} [field] Field to watch for changes
14
+ * @property {ContextEventHandler} handler Handler to invoke when change happens
15
+ */
16
+ export function validateObjectKey(key?: string): string;
1
17
  export default class JourneyContext {
2
18
  static DEFAULT_CONTEXT_ID: string;
3
19
  /**
@@ -133,11 +149,11 @@ export default class JourneyContext {
133
149
  /**
134
150
  * Get data context for a specific a specific page.
135
151
  *
136
- * @param {string | PageMeta} page Page waypoint ID, or Page object.
152
+ * @param {string | Page} page Page waypoint ID, or Page object.
137
153
  * @returns {object} Page data.
138
154
  * @throws {TypeError} When page is invalid.
139
155
  */
140
- getDataForPage(page: string | any): object;
156
+ getDataForPage(page: string | Page): object;
141
157
  getData(): object;
142
158
  /**
143
159
  * Overwrite the data context with a new object.
@@ -150,16 +166,16 @@ export default class JourneyContext {
150
166
  * Write field form data from a page HTML form, into the `data` model.
151
167
  *
152
168
  * By default this will store the data as-is, keyed against the page's
153
- * waypoint ID. However, when passing a `PageMeta` instance, its
169
+ * waypoint ID. However, when passing a `Page` instance, its
154
170
  * `fieldWriter()` method will be called to transform the provided formData
155
171
  * before storing in `data`
156
172
  *
157
- * @param {string | PageMeta} page Page waypoint ID, or PageMeta object
173
+ * @param {string | Page} page Page waypoint ID, or Page object
158
174
  * @param {object} webFormData Data to overwrite with
159
175
  * @returns {JourneyContext} Chain
160
176
  * @throws {TypeError} When page is invalid.
161
177
  */
162
- setDataForPage(page: string | any, webFormData: object): JourneyContext;
178
+ setDataForPage(page: string | Page, webFormData: object): JourneyContext;
163
179
  /**
164
180
  * Return validation errors for all pages.
165
181
  *
@@ -251,7 +267,7 @@ export default class JourneyContext {
251
267
  * @param {ContextEvent[]} events Event listeners
252
268
  * @returns {JourneyContext} Chain
253
269
  */
254
- addEventListeners(events: any[]): JourneyContext;
270
+ addEventListeners(events: ContextEvent[]): JourneyContext;
255
271
  applyEventListeners({ event }: {
256
272
  event: any;
257
273
  }): JourneyContext;
@@ -263,4 +279,20 @@ export default class JourneyContext {
263
279
  isDefault(): boolean;
264
280
  #private;
265
281
  }
282
+ export type Page = import('./configuration-ingestor').Page;
283
+ export type ContextEventHandler = (journeyContext: JourneyContext, previousContext: JourneyContext) => void;
284
+ export type ContextEvent = {
285
+ /**
286
+ * Waypoint to watch for changes
287
+ */
288
+ waypoint: string;
289
+ /**
290
+ * Field to watch for changes
291
+ */
292
+ field?: string | undefined;
293
+ /**
294
+ * Handler to invoke when change happens
295
+ */
296
+ handler: ContextEventHandler;
297
+ };
266
298
  import ValidationError from "./ValidationError.js";
@@ -26,6 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  };
27
27
  var _JourneyContext_data, _JourneyContext_validation, _JourneyContext_nav, _JourneyContext_identity, _JourneyContext_eventListeners, _JourneyContext_eventListenerPreState;
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.validateObjectKey = void 0;
29
30
  /**
30
31
  * Represents the state of a user's journey through the Plan. It contains
31
32
  * information about:
@@ -39,7 +40,30 @@ const lodash_1 = __importDefault(require("lodash"));
39
40
  const ValidationError_js_1 = __importDefault(require("./ValidationError.js"));
40
41
  const logger_js_1 = __importDefault(require("./logger.js"));
41
42
  const { cloneDeep, isPlainObject, isObject, has, isEqual, } = lodash_1.default; // CommonJS
42
- const log = (0, logger_js_1.default)('class:journey-context');
43
+ const log = (0, logger_js_1.default)('lib:journey-context');
44
+ /**
45
+ * @typedef {import('./configuration-ingestor').Page} Page
46
+ */
47
+ /**
48
+ * @callback ContextEventHandler
49
+ * @param {JourneyContext} journeyContext Context including changes
50
+ * @param {JourneyContext} previousContext Context prior to changes
51
+ * @returns {void}
52
+ */
53
+ /**
54
+ * @typedef {object} ContextEvent
55
+ * @property {string} waypoint Waypoint to watch for changes
56
+ * @property {string} [field] Field to watch for changes
57
+ * @property {ContextEventHandler} handler Handler to invoke when change happens
58
+ */
59
+ function validateObjectKey(key = '') {
60
+ const keyLower = String.prototype.toLowerCase.call(key);
61
+ if (keyLower === 'prototype' || keyLower === '__proto__' || keyLower === 'constructor') {
62
+ throw new SyntaxError(`Invalid object key used, ${key}`);
63
+ }
64
+ return String(key);
65
+ }
66
+ exports.validateObjectKey = validateObjectKey;
43
67
  class JourneyContext {
44
68
  /**
45
69
  * Constructor.
@@ -123,16 +147,16 @@ class JourneyContext {
123
147
  /**
124
148
  * Get data context for a specific a specific page.
125
149
  *
126
- * @param {string | PageMeta} page Page waypoint ID, or Page object.
150
+ * @param {string | Page} page Page waypoint ID, or Page object.
127
151
  * @returns {object} Page data.
128
152
  * @throws {TypeError} When page is invalid.
129
153
  */
130
154
  getDataForPage(page) {
131
155
  if (typeof page === 'string') {
132
- return __classPrivateFieldGet(this, _JourneyContext_data, "f")[page];
156
+ return __classPrivateFieldGet(this, _JourneyContext_data, "f")[validateObjectKey(page)];
133
157
  }
134
158
  if (isPlainObject(page)) {
135
- return __classPrivateFieldGet(this, _JourneyContext_data, "f")[page.waypoint];
159
+ return __classPrivateFieldGet(this, _JourneyContext_data, "f")[validateObjectKey(page.waypoint)];
136
160
  }
137
161
  throw new TypeError(`Page must be a string or Page object. Got ${typeof page}`);
138
162
  }
@@ -153,21 +177,21 @@ class JourneyContext {
153
177
  * Write field form data from a page HTML form, into the `data` model.
154
178
  *
155
179
  * By default this will store the data as-is, keyed against the page's
156
- * waypoint ID. However, when passing a `PageMeta` instance, its
180
+ * waypoint ID. However, when passing a `Page` instance, its
157
181
  * `fieldWriter()` method will be called to transform the provided formData
158
182
  * before storing in `data`
159
183
  *
160
- * @param {string | PageMeta} page Page waypoint ID, or PageMeta object
184
+ * @param {string | Page} page Page waypoint ID, or Page object
161
185
  * @param {object} webFormData Data to overwrite with
162
186
  * @returns {JourneyContext} Chain
163
187
  * @throws {TypeError} When page is invalid.
164
188
  */
165
189
  setDataForPage(page, webFormData) {
166
190
  if (typeof page === 'string') {
167
- __classPrivateFieldGet(this, _JourneyContext_data, "f")[page] = webFormData;
191
+ __classPrivateFieldGet(this, _JourneyContext_data, "f")[validateObjectKey(page)] = webFormData;
168
192
  }
169
193
  else if (isPlainObject(page)) {
170
- __classPrivateFieldGet(this, _JourneyContext_data, "f")[page.waypoint] = webFormData;
194
+ __classPrivateFieldGet(this, _JourneyContext_data, "f")[validateObjectKey(page.waypoint)] = webFormData;
171
195
  }
172
196
  else {
173
197
  throw new TypeError(`Page must be a string or Page object. Got ${typeof page}`);
@@ -205,7 +229,7 @@ class JourneyContext {
205
229
  * @returns {JourneyContext} Chain.
206
230
  */
207
231
  clearValidationErrorsForPage(pageId) {
208
- __classPrivateFieldGet(this, _JourneyContext_validation, "f")[pageId] = null;
232
+ __classPrivateFieldGet(this, _JourneyContext_validation, "f")[validateObjectKey(pageId)] = null;
209
233
  return this;
210
234
  }
211
235
  /**
@@ -225,7 +249,7 @@ class JourneyContext {
225
249
  throw new SyntaxError('Field errors must be a ValidationError');
226
250
  }
227
251
  });
228
- __classPrivateFieldGet(this, _JourneyContext_validation, "f")[pageId] = errors;
252
+ __classPrivateFieldGet(this, _JourneyContext_validation, "f")[validateObjectKey(pageId)] = errors;
229
253
  return this;
230
254
  }
231
255
  /**
@@ -236,17 +260,21 @@ class JourneyContext {
236
260
  * @returns {ValidationError[]} An array of errors
237
261
  */
238
262
  getValidationErrorsForPage(pageId) {
239
- return __classPrivateFieldGet(this, _JourneyContext_validation, "f")[pageId] || [];
263
+ var _a;
264
+ return (_a = __classPrivateFieldGet(this, _JourneyContext_validation, "f")[validateObjectKey(pageId)]) !== null && _a !== void 0 ? _a : [];
240
265
  }
241
266
  getValidationErrorsForPageByField(pageId) {
242
267
  const errors = this.getValidationErrorsForPage(pageId);
243
268
  const obj = Object.create(null);
269
+ // ESLint disabled as `i` is an integer
270
+ /* eslint-disable security/detect-object-injection */
244
271
  for (let i = 0, l = errors.length; i < l; i++) {
245
272
  if (!obj[errors[i].field]) {
246
273
  obj[errors[i].field] = [];
247
274
  }
248
275
  obj[errors[i].field].push(errors[i]);
249
276
  }
277
+ /* eslint-enable security/detect-object-injection */
250
278
  return obj;
251
279
  }
252
280
  /**
@@ -258,7 +286,7 @@ class JourneyContext {
258
286
  */
259
287
  hasValidationErrorsForPage(pageId) {
260
288
  var _a, _b;
261
- return ((_b = (_a = __classPrivateFieldGet(this, _JourneyContext_validation, "f")) === null || _a === void 0 ? void 0 : _a[pageId]) === null || _b === void 0 ? void 0 : _b.length) > 0;
289
+ return ((_b = (_a = __classPrivateFieldGet(this, _JourneyContext_validation, "f")) === null || _a === void 0 ? void 0 : _a[validateObjectKey(pageId)]) === null || _b === void 0 ? void 0 : _b.length) > 0;
262
290
  }
263
291
  /**
264
292
  * Set language of the context.
@@ -277,7 +305,7 @@ class JourneyContext {
277
305
  * @returns {boolean} True if the page is valid.
278
306
  */
279
307
  isPageValid(pageId) {
280
- return __classPrivateFieldGet(this, _JourneyContext_validation, "f")[pageId] === null;
308
+ return __classPrivateFieldGet(this, _JourneyContext_validation, "f")[validateObjectKey(pageId)] === null;
281
309
  }
282
310
  /**
283
311
  * Remove information about these waypoints.
@@ -288,10 +316,13 @@ class JourneyContext {
288
316
  const newData = Object.create(null);
289
317
  const newValidation = Object.create(null);
290
318
  const toKeep = Object.keys(this.data).filter((w) => !waypoints.includes(w));
319
+ // ESLint disabled as `i` is an integer
320
+ /* eslint-disable security/detect-object-injection */
291
321
  for (let i = 0, l = toKeep.length; i < l; i++) {
292
322
  newData[toKeep[i]] = __classPrivateFieldGet(this, _JourneyContext_data, "f")[toKeep[i]];
293
323
  newValidation[toKeep[i]] = __classPrivateFieldGet(this, _JourneyContext_validation, "f")[toKeep[i]];
294
324
  }
325
+ /* eslint-enable security/detect-object-injection */
295
326
  __classPrivateFieldSet(this, _JourneyContext_data, Object.assign({}, newData), "f");
296
327
  __classPrivateFieldSet(this, _JourneyContext_validation, Object.assign({}, newValidation), "f");
297
328
  }
@@ -304,6 +335,8 @@ class JourneyContext {
304
335
  */
305
336
  invalidate(waypoints = []) {
306
337
  for (let i = 0, l = waypoints.length; i < l; i++) {
338
+ // ESLint disabled as `i` is an integer
339
+ /* eslint-disable-next-line security/detect-object-injection */
307
340
  this.removeValidationStateForPage(waypoints[i]);
308
341
  }
309
342
  }
@@ -325,12 +358,16 @@ class JourneyContext {
325
358
  return this;
326
359
  }
327
360
  applyEventListeners({ event }) {
328
- var _a, _b, _c, _d, _e, _f;
361
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
329
362
  if (!__classPrivateFieldGet(this, _JourneyContext_eventListeners, "f").length) {
330
363
  return this;
331
364
  }
332
365
  const previousContext = JourneyContext.fromObject(__classPrivateFieldGet(this, _JourneyContext_eventListenerPreState, "f"));
333
366
  const listeners = __classPrivateFieldGet(this, _JourneyContext_eventListeners, "f").filter((l) => l.event === event);
367
+ // ESLint disabled as `listeners[i]` uses an integer key, and the other keys
368
+ // are derived from the list of `listeners`, which are not manipulated at
369
+ // runtime (only set by dev in code).
370
+ /* eslint-disable security/detect-object-injection */
334
371
  for (let i = 0, l = listeners.length; i < l; i++) {
335
372
  const { waypoint, field, handler } = listeners[i];
336
373
  let logMessage;
@@ -341,17 +378,18 @@ class JourneyContext {
341
378
  }
342
379
  else if (waypoint && !field) {
343
380
  logMessage = `Calling waypoint-specific event handler on "${waypoint}"`;
344
- runHandler = !isEqual((_a = this.data) === null || _a === void 0 ? void 0 : _a[waypoint], (_b = previousContext.data) === null || _b === void 0 ? void 0 : _b[waypoint]);
381
+ runHandler = ((_a = previousContext.data) === null || _a === void 0 ? void 0 : _a[waypoint]) !== undefined && !isEqual((_b = this.data) === null || _b === void 0 ? void 0 : _b[waypoint], (_c = previousContext.data) === null || _c === void 0 ? void 0 : _c[waypoint]);
345
382
  }
346
383
  else if (waypoint && field) {
347
384
  logMessage = `Calling field-specific event handler on "${waypoint} : ${field}"`;
348
- runHandler = !isEqual((_d = (_c = this.data) === null || _c === void 0 ? void 0 : _c[waypoint]) === null || _d === void 0 ? void 0 : _d[field], (_f = (_e = previousContext.data) === null || _e === void 0 ? void 0 : _e[waypoint]) === null || _f === void 0 ? void 0 : _f[field]);
385
+ runHandler = ((_e = (_d = previousContext.data) === null || _d === void 0 ? void 0 : _d[waypoint]) === null || _e === void 0 ? void 0 : _e[field]) !== undefined && !isEqual((_g = (_f = this.data) === null || _f === void 0 ? void 0 : _f[waypoint]) === null || _g === void 0 ? void 0 : _g[field], (_j = (_h = previousContext.data) === null || _h === void 0 ? void 0 : _h[waypoint]) === null || _j === void 0 ? void 0 : _j[field]);
349
386
  }
350
387
  if (runHandler) {
351
388
  log.trace(logMessage);
352
389
  handler({ journeyContext: this, previousContext });
353
390
  }
354
391
  }
392
+ /* eslint-enable security/detect-object-injection */
355
393
  return this;
356
394
  }
357
395
  /* ----------------------------------------------- session context handling */
@@ -425,6 +463,8 @@ class JourneyContext {
425
463
  */
426
464
  static getContextById(session, id) {
427
465
  if (has(session === null || session === void 0 ? void 0 : session.journeyContextList, id)) {
466
+ // ESLint disabled as `id` has been verified as an "own" property
467
+ /* eslint-disable-next-line security/detect-object-injection */
428
468
  return JourneyContext.fromObject(session.journeyContextList[id]);
429
469
  }
430
470
  return undefined;
@@ -518,7 +558,8 @@ class JourneyContext {
518
558
  }
519
559
  static removeContextById(session, id) {
520
560
  if (session && has(session.journeyContextList, id)) {
521
- /* eslint-disable-next-line no-param-reassign */
561
+ // ESLint disabled as `id` has been verified as an "own" property
562
+ /* eslint-disable-next-line security/detect-object-injection, no-param-reassign */
522
563
  delete session.journeyContextList[id];
523
564
  }
524
565
  }
@@ -48,6 +48,9 @@ class MutableRouter {
48
48
  return __classPrivateFieldGet(this, _MutableRouter_router, "f");
49
49
  }
50
50
  __classPrivateFieldGet(this, _MutableRouter_stack, "f").forEach(({ method, args }) => {
51
+ // ESLint disabled as `#router` is dev-controlled, and `seal()` is only
52
+ // run at boot-time before any user interaction
53
+ /* eslint-disable-next-line security/detect-object-injection */
51
54
  __classPrivateFieldGet(this, _MutableRouter_router, "f")[method].call(__classPrivateFieldGet(this, _MutableRouter_router, "f"), ...args);
52
55
  });
53
56
  __classPrivateFieldSet(this, _MutableRouter_sealed, true, "f");
@@ -115,7 +118,6 @@ class MutableRouter {
115
118
  __classPrivateFieldGet(this, _MutableRouter_instances, "m", _MutableRouter_prepend).call(this, 'use', path, ...callbacks);
116
119
  }
117
120
  /* -------------------------------------------------------------- replacers */
118
- // TODO: How do we handle multiple routes on the same path?
119
121
  /**
120
122
  * Replace middleware function(s) that were mounted using the `all()` method.
121
123
  *
@@ -264,12 +266,14 @@ _MutableRouter_stack = new WeakMap(), _MutableRouter_router = new WeakMap(), _Mu
264
266
  if (__classPrivateFieldGet(this, _MutableRouter_sealed, "f")) {
265
267
  throw new Error('Cannot alter middleware in a sealed mutable router');
266
268
  }
267
- const index = __classPrivateFieldGet(this, _MutableRouter_stack, "f").findIndex((command) => `${command.method}|${command.path}` === `${method}|${path}`);
269
+ const finder = (command) => `${command.method}|${command.path}` === `${method}|${path}`;
270
+ const index = __classPrivateFieldGet(this, _MutableRouter_stack, "f").findIndex(finder);
268
271
  if (index > -1) {
269
272
  __classPrivateFieldGet(this, _MutableRouter_stack, "f").splice(index, 1, {
270
273
  method,
271
274
  path,
272
275
  args: [path, ...callbacks],
273
276
  });
277
+ __classPrivateFieldSet(this, _MutableRouter_stack, __classPrivateFieldGet(this, _MutableRouter_stack, "f").filter((command, idx) => idx <= index || !finder(command)), "f");
274
278
  }
275
279
  };
@@ -1,12 +1,44 @@
1
1
  export default class Plan {
2
- static isExitNode(name: any): boolean;
2
+ /**
3
+ * Waypoints using the url:// protocol are known as "exit nodes" as they
4
+ * indicate an exit point to another Plan.
5
+ *
6
+ * @param {string} name Waypoint name
7
+ * @returns {boolean} True if the waypoint is a url:// type
8
+ */
9
+ static isExitNode(name: string): boolean;
3
10
  /**
4
11
  * Create a Plan.
5
12
  *
6
13
  * @param {object} opts Options
14
+ * @param {boolean} [opts.validateBeforeRouteCondition=true] Check page validity before conditions
15
+ * @param {Function|string} [opts.arbiter=undefined] Arbitration mechanism
7
16
  */
8
- constructor(opts?: object);
17
+ constructor(opts?: {
18
+ validateBeforeRouteCondition?: boolean | undefined;
19
+ arbiter?: string | Function | undefined;
20
+ });
9
21
  getOptions(): any;
22
+ /**
23
+ * Retrieve the list of skippable waypoints.
24
+ *
25
+ * @returns {string[]} List of skippable waypoints
26
+ */
27
+ getSkippables(): string[];
28
+ /**
29
+ * Add one or more skippable waypoints.
30
+ *
31
+ * @param {...string} waypoints Waypoints
32
+ * @returns {Plan}{ Chain}
33
+ */
34
+ addSkippables(...waypoints: string[]): Plan;
35
+ /**
36
+ * Check if the user can skip the named waypoint.
37
+ *
38
+ * @param {string} waypoint Waypoint
39
+ * @returns {boolean} True if waypoint can be skipped
40
+ */
41
+ isSkippable(waypoint: string): boolean;
10
42
  getWaypoints(): any;
11
43
  containsWaypoint(waypoint: any): any;
12
44
  getRoutes(): any;
@@ -114,8 +146,9 @@ export default class Plan {
114
146
  * Get raw graph data structure. This can be used with other libraries to
115
147
  * generate graph visualisations, for example.
116
148
  *
117
- * @returns {graphlib.Graph} Graph data structure.
149
+ * @returns {Graph} Graph data structure.
118
150
  */
119
- getGraphStructure(): any;
151
+ getGraphStructure(): Graph;
152
+ #private;
120
153
  }
121
154
  import JourneyContext from "./JourneyContext.js";
package/dist/lib/Plan.js CHANGED
@@ -1,12 +1,24 @@
1
1
  "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
2
13
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
14
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
15
  };
16
+ var _Plan_skippableWaypoints;
5
17
  Object.defineProperty(exports, "__esModule", { value: true });
6
18
  const graphlib_1 = require("graphlib");
7
19
  const JourneyContext_js_1 = __importDefault(require("./JourneyContext.js"));
8
20
  const logger_js_1 = __importDefault(require("./logger.js"));
9
- const log = (0, logger_js_1.default)('class:plan');
21
+ const log = (0, logger_js_1.default)('lib:plan');
10
22
  /**
11
23
  * Will check if the source waypoint has specifically passed validation, i.e
12
24
  * there is a "null" validation entry for the route source.
@@ -35,6 +47,9 @@ function validateWaypointId(val) {
35
47
  if (typeof val !== 'string') {
36
48
  throw new TypeError(`Expected waypoint id to be a string, got ${typeof val}`);
37
49
  }
50
+ if (val.substr(0, 6) === 'url://' && !val.endsWith('/')) {
51
+ throw new SyntaxError('url:// waypoints must include a trailing /');
52
+ }
38
53
  }
39
54
  function validateRouteName(val) {
40
55
  if (typeof val !== 'string') {
@@ -43,6 +58,7 @@ function validateRouteName(val) {
43
58
  else if (!['next', 'prev'].includes(val)) {
44
59
  throw new ReferenceError(`Expected route name to be one of next or prev. Got ${val}`);
45
60
  }
61
+ return val;
46
62
  }
47
63
  function validateRouteCondition(val) {
48
64
  if (!(val instanceof Function)) {
@@ -74,15 +90,18 @@ const makeRouteObject = (dgraph, edge) => {
74
90
  const reExitNodeProtocol = /^[a-z]+:\/\//i;
75
91
  const priv = new WeakMap();
76
92
  class Plan {
77
- static isExitNode(name) {
78
- return reExitNodeProtocol.test(name);
79
- }
80
93
  /**
81
94
  * Create a Plan.
82
95
  *
83
96
  * @param {object} opts Options
97
+ * @param {boolean} [opts.validateBeforeRouteCondition=true] Check page validity before conditions
98
+ * @param {Function|string} [opts.arbiter=undefined] Arbitration mechanism
84
99
  */
85
100
  constructor(opts = {}) {
101
+ /**
102
+ * @type {string[]} These waypoints can be skipped
103
+ */
104
+ _Plan_skippableWaypoints.set(this, void 0);
86
105
  // This is our directed, multigraph representation
87
106
  const dgraph = new graphlib_1.Graph({
88
107
  directed: true,
@@ -106,10 +125,48 @@ class Plan {
106
125
  },
107
126
  options,
108
127
  });
128
+ __classPrivateFieldSet(this, _Plan_skippableWaypoints, [], "f");
129
+ }
130
+ /**
131
+ * Waypoints using the url:// protocol are known as "exit nodes" as they
132
+ * indicate an exit point to another Plan.
133
+ *
134
+ * @param {string} name Waypoint name
135
+ * @returns {boolean} True if the waypoint is a url:// type
136
+ */
137
+ static isExitNode(name) {
138
+ return reExitNodeProtocol.test(name);
109
139
  }
110
140
  getOptions() {
111
141
  return priv.get(this).options;
112
142
  }
143
+ /**
144
+ * Retrieve the list of skippable waypoints.
145
+ *
146
+ * @returns {string[]} List of skippable waypoints
147
+ */
148
+ getSkippables() {
149
+ return __classPrivateFieldGet(this, _Plan_skippableWaypoints, "f");
150
+ }
151
+ /**
152
+ * Add one or more skippable waypoints.
153
+ *
154
+ * @param {...string} waypoints Waypoints
155
+ * @returns {Plan}{ Chain}
156
+ */
157
+ addSkippables(...waypoints) {
158
+ __classPrivateFieldSet(this, _Plan_skippableWaypoints, [...__classPrivateFieldGet(this, _Plan_skippableWaypoints, "f"), ...waypoints], "f");
159
+ return this;
160
+ }
161
+ /**
162
+ * Check if the user can skip the named waypoint.
163
+ *
164
+ * @param {string} waypoint Waypoint
165
+ * @returns {boolean} True if waypoint can be skipped
166
+ */
167
+ isSkippable(waypoint) {
168
+ return __classPrivateFieldGet(this, _Plan_skippableWaypoints, "f").indexOf(waypoint) > -1;
169
+ }
113
170
  getWaypoints() {
114
171
  return priv.get(this).dgraph.nodes();
115
172
  }
@@ -121,7 +178,7 @@ class Plan {
121
178
  return self.dgraph.edges().map((edge) => makeRouteObject(self.dgraph, edge));
122
179
  }
123
180
  getRouteCondition(src, tgt, name) {
124
- return priv.get(this).follows[name][`${src}/${tgt}`];
181
+ return priv.get(this).follows[validateRouteName(name)][`${src}/${tgt}`];
125
182
  }
126
183
  /**
127
184
  * Return all outward routes (out-edges) from the given waypoint, to the
@@ -149,6 +206,8 @@ class Plan {
149
206
  addSequence(...waypoints) {
150
207
  // Setup simple double routes (next/prev) between all waypoints in this list
151
208
  for (let i = 0, l = waypoints.length - 1; i < l; i += 1) {
209
+ // ESLint disabled as `i` is an integer
210
+ /* eslint-disable-next-line security/detect-object-injection */
152
211
  this.setRoute(waypoints[i], waypoints[i + 1]);
153
212
  }
154
213
  }
@@ -222,12 +281,12 @@ class Plan {
222
281
  validateRouteCondition(follow);
223
282
  }
224
283
  // Get routing function name to label edge
225
- const label = follow && follow.name;
284
+ const conditionName = follow && follow.name;
226
285
  // Warn if we're overwriting an existing edge on the same name
227
286
  if (self.dgraph.hasEdge(src, tgt, name)) {
228
287
  log.warn('Setting a route that already exists (%s, %s, %s). Will be overridden', src, tgt, name);
229
288
  }
230
- self.dgraph.setEdge(src, tgt, { label }, name);
289
+ self.dgraph.setEdge(src, tgt, { conditionName }, name);
231
290
  // Determine which follow function to use
232
291
  let followFunc;
233
292
  if (follow) {
@@ -253,6 +312,8 @@ class Plan {
253
312
  else {
254
313
  followFunc = defaultPrevFollow;
255
314
  }
315
+ // ESLint disabled as `name` has been validated further above
316
+ /* eslint-disable-next-line security/detect-object-injection */
256
317
  self.follows[name][`${src}/${tgt}`] = followFunc;
257
318
  return this;
258
319
  }
@@ -304,9 +365,7 @@ class Plan {
304
365
  if (!self.dgraph.hasNode(startWaypoint)) {
305
366
  throw new ReferenceError(`Plan does not contain waypoint '${startWaypoint}'`);
306
367
  }
307
- if (routeName === undefined) {
308
- throw new ReferenceError('Route name must be provided');
309
- }
368
+ validateRouteName(routeName);
310
369
  const history = new Map();
311
370
  const traverse = (startWP) => {
312
371
  let target = self.dgraph.outEdges(startWP).filter((e) => {
@@ -315,6 +374,8 @@ class Plan {
315
374
  }
316
375
  const route = makeRouteObject(self.dgraph, e);
317
376
  try {
377
+ // ESLint disabled as `routeName` has been validated further above
378
+ /* eslint-disable-next-line security/detect-object-injection */
318
379
  return self.follows[routeName][`${e.v}/${e.w}`](route, context);
319
380
  }
320
381
  catch (ex) {
@@ -357,6 +418,8 @@ class Plan {
357
418
  const results = new Array(totalTrav + 1);
358
419
  results[0] = route;
359
420
  for (let i = 0; i < totalTrav; i++) {
421
+ // ESLint disabled as `i` is an integer
422
+ /* eslint-disable-next-line security/detect-object-injection */
360
423
  results[i + 1] = traversed[i];
361
424
  }
362
425
  return results;
@@ -376,10 +439,11 @@ class Plan {
376
439
  * Get raw graph data structure. This can be used with other libraries to
377
440
  * generate graph visualisations, for example.
378
441
  *
379
- * @returns {graphlib.Graph} Graph data structure.
442
+ * @returns {Graph} Graph data structure.
380
443
  */
381
444
  getGraphStructure() {
382
445
  return priv.get(this).dgraph;
383
446
  }
384
447
  }
385
448
  exports.default = Plan;
449
+ _Plan_skippableWaypoints = new WeakMap();
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @typedef {import('./ValidatorFactory').ValidateContext} ValidateContext
3
+ */
1
4
  export default class ValidationError {
2
5
  /**
3
6
  * Make a ValidationError instance from a primitive object (or a function that
@@ -61,10 +64,11 @@ export default class ValidationError {
61
64
  * @param {ValidateContext} context See structure above
62
65
  * @returns {ValidationError} Chain
63
66
  */
64
- withContext(context: any): ValidationError;
67
+ withContext(context: ValidateContext): ValidationError;
65
68
  variables: any;
66
- field: any;
69
+ field: string | undefined;
67
70
  fieldHref: string | undefined;
68
71
  focusSuffix: any;
69
72
  validator: any;
70
73
  }
74
+ export type ValidateContext = import('./ValidatorFactory').ValidateContext;