counterfact 2.7.0 → 2.8.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 (44) hide show
  1. package/README.md +4 -159
  2. package/bin/counterfact.js +10 -2
  3. package/dist/app.js +74 -20
  4. package/dist/migrate/update-route-types.js +2 -3
  5. package/dist/repl/raw-http-client.js +19 -0
  6. package/dist/repl/repl.js +26 -7
  7. package/dist/repl/route-builder.js +68 -0
  8. package/dist/server/constants.js +8 -0
  9. package/dist/server/context-registry.js +54 -1
  10. package/dist/server/create-koa-app.js +27 -4
  11. package/dist/server/determine-module-kind.js +13 -0
  12. package/dist/server/dispatcher.js +46 -0
  13. package/dist/server/file-discovery.js +20 -9
  14. package/dist/server/is-proxy-enabled-for-path.js +12 -0
  15. package/dist/server/json-to-xml.js +10 -0
  16. package/dist/server/koa-middleware.js +18 -1
  17. package/dist/server/load-openapi-document.js +4 -11
  18. package/dist/server/module-dependency-graph.js +25 -0
  19. package/dist/server/module-loader.js +44 -21
  20. package/dist/server/module-tree.js +36 -0
  21. package/dist/server/openapi-document.js +69 -0
  22. package/dist/server/openapi-middleware.js +34 -5
  23. package/dist/server/registry.js +89 -0
  24. package/dist/server/response-builder.js +15 -0
  25. package/dist/server/scenario-registry.js +26 -0
  26. package/dist/server/tools.js +27 -0
  27. package/dist/server/transpiler.js +23 -9
  28. package/dist/typescript-generator/code-generator.js +117 -4
  29. package/dist/typescript-generator/coder.js +76 -0
  30. package/dist/typescript-generator/operation-coder.js +12 -4
  31. package/dist/typescript-generator/operation-type-coder.js +39 -4
  32. package/dist/typescript-generator/parameters-type-coder.js +2 -4
  33. package/dist/typescript-generator/prune.js +2 -1
  34. package/dist/typescript-generator/repository.js +76 -20
  35. package/dist/typescript-generator/requirement.js +69 -0
  36. package/dist/typescript-generator/{generate.js → scenario-file-generator.js} +98 -81
  37. package/dist/typescript-generator/script.js +70 -7
  38. package/dist/typescript-generator/specification.js +27 -0
  39. package/dist/util/ensure-directory-exists.js +7 -0
  40. package/dist/util/forward-slash-path.js +63 -0
  41. package/dist/util/read-file.js +11 -0
  42. package/dist/util/runtime-can-execute-erasable-ts.js +11 -0
  43. package/dist/util/windows-escape.js +18 -0
  44. package/package.json +4 -4
@@ -1,30 +1,84 @@
1
1
  import { RESERVED_WORDS } from "./reserved-words.js";
2
+ /**
3
+ * Base class for all code-generation helpers in the TypeScript generator.
4
+ *
5
+ * A `Coder` wraps a single {@link Requirement} node from the OpenAPI spec and
6
+ * knows how to emit TypeScript code for it. Subclasses override
7
+ * {@link writeCode} to produce the actual source text.
8
+ *
9
+ * Coders are used by {@link Script} and {@link Repository} to lazily generate
10
+ * exports and imports, resolving `$ref` references through the
11
+ * {@link Specification} before writing.
12
+ */
2
13
  export class Coder {
3
14
  requirement;
4
15
  constructor(requirement) {
5
16
  this.requirement = requirement;
6
17
  }
18
+ /**
19
+ * A stable cache key for this coder, composed of the constructor name and
20
+ * either the `$ref` value (for references) or the requirement URL.
21
+ */
7
22
  get id() {
8
23
  if (this.requirement.isReference) {
9
24
  return `${this.constructor.name}@${this.requirement.data["$ref"]}`;
10
25
  }
11
26
  return `${this.constructor.name}@${this.requirement.url}`;
12
27
  }
28
+ /**
29
+ * Optional preamble emitted before the `export` keyword.
30
+ *
31
+ * Subclasses can return a string (e.g. a type alias) that must appear in the
32
+ * output before this coder's export statement.
33
+ *
34
+ * @param _path - The path of the script being written (unused in base class).
35
+ */
13
36
  beforeExport(_path) {
14
37
  return "";
15
38
  }
39
+ /**
40
+ * Returns a JSDoc comment block to be placed immediately before the export.
41
+ *
42
+ * Returns `""` by default; subclasses override this to surface OpenAPI
43
+ * metadata (description, summary, examples, etc.).
44
+ */
16
45
  jsdoc() {
17
46
  return "";
18
47
  }
48
+ /**
49
+ * Writes this coder's contribution to `script`.
50
+ *
51
+ * When the requirement is a `$ref`, delegates to {@link Script.import} so
52
+ * the reference target is exported from its own module. Otherwise calls
53
+ * {@link writeCode}.
54
+ *
55
+ * @param script - The script being assembled.
56
+ * @returns The TypeScript source text for this coder's export value.
57
+ */
19
58
  write(script) {
20
59
  if (this.requirement.isReference) {
21
60
  return script.import(this);
22
61
  }
23
62
  return this.writeCode(script);
24
63
  }
64
+ /**
65
+ * Generates the TypeScript source text for this coder's value.
66
+ *
67
+ * This method is abstract — subclasses **must** override it.
68
+ *
69
+ * @param _script - The script being assembled.
70
+ * @throws Always — callers should never reach the base implementation.
71
+ */
25
72
  writeCode(_script) {
26
73
  throw new Error("write() is abstract and should be overwritten by a subclass");
27
74
  }
75
+ /**
76
+ * Resolves `$ref` references by returning the target coder.
77
+ *
78
+ * When this coder's requirement is not a reference, returns `this`.
79
+ * Otherwise loads the referenced requirement and wraps it in an instance of
80
+ * the same concrete coder class.
81
+ */
28
82
  async delegate() {
29
83
  if (!this.requirement.isReference) {
30
84
  return this;
@@ -32,6 +86,15 @@ export class Coder {
32
86
  const requirement = await this.requirement.reference();
33
87
  return new this.constructor(requirement);
34
88
  }
89
+ /**
90
+ * Generator that yields candidate export names for this coder.
91
+ *
92
+ * The first name is derived from the last path segment of the requirement
93
+ * URL, sanitised to be a valid TypeScript identifier. Subsequent names have
94
+ * an incrementing numeric suffix to resolve collisions.
95
+ *
96
+ * @param rawName - Override the starting name (used by subclasses).
97
+ */
35
98
  *names(rawName = this.requirement.url.split("/").at(-1)) {
36
99
  const name = rawName
37
100
  .replace(/^\d/u, (digit) => `_${digit}`)
@@ -45,9 +108,22 @@ export class Coder {
45
108
  yield baseName + index;
46
109
  }
47
110
  }
111
+ /**
112
+ * Returns an optional TypeScript type annotation string to be placed between
113
+ * the export name and its value (`export const name: <type> = value`).
114
+ *
115
+ * Returns `""` by default.
116
+ */
48
117
  typeDeclaration(_namespace, _script) {
49
118
  return "";
50
119
  }
120
+ /**
121
+ * Returns the repository-relative path of the script where this coder's
122
+ * export should live when imported by another script.
123
+ *
124
+ * Subclasses override this to place type exports in `types/paths/…` and
125
+ * route exports in `routes/…`.
126
+ */
51
127
  modulePath() {
52
128
  return "did-not-override-coder-modulePath.ts";
53
129
  }
@@ -1,6 +1,16 @@
1
- import nodePath from "node:path";
1
+ import { pathJoin } from "../util/forward-slash-path.js";
2
2
  import { Coder } from "./coder.js";
3
3
  import { OperationTypeCoder, } from "./operation-type-coder.js";
4
+ /**
5
+ * Generates the default route handler stub for a single OpenAPI operation.
6
+ *
7
+ * The generated stub calls `$.response[statusCode].random()` (or `.empty()`)
8
+ * for the first response defined in the spec. It is only written when no
9
+ * handler file exists yet — users are expected to replace it with real logic.
10
+ *
11
+ * The corresponding TypeScript type is emitted by {@link OperationTypeCoder}
12
+ * into the `types/paths/…` tree.
13
+ */
4
14
  export class OperationCoder extends Coder {
5
15
  requestMethod;
6
16
  securitySchemes;
@@ -38,8 +48,6 @@ export class OperationCoder extends Coder {
38
48
  .split("/")
39
49
  .at(-2)
40
50
  .replaceAll("~1", "/");
41
- return `${nodePath
42
- .join("routes", pathString)
43
- .replaceAll("\\", "/")}.types.ts`;
51
+ return `${pathJoin("routes", pathString)}.types.ts`;
44
52
  }
45
53
  }
@@ -1,4 +1,4 @@
1
- import nodePath from "node:path";
1
+ import { pathJoin } from "../util/forward-slash-path.js";
2
2
  import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
3
3
  import { buildJsDoc } from "./jsdoc.js";
4
4
  import { ParameterExportTypeCoder } from "./parameter-export-type-coder.js";
@@ -24,6 +24,15 @@ function sanitizeIdentifier(value) {
24
24
  }
25
25
  return result || "_";
26
26
  }
27
+ /**
28
+ * Generates the TypeScript type for a single OpenAPI operation.
29
+ *
30
+ * The emitted type describes the function signature that a Counterfact route
31
+ * handler must satisfy, including strongly-typed `query`, `path`, `headers`,
32
+ * `cookie`, `body`, `context`, `response`, and `user` arguments.
33
+ *
34
+ * Output is written to `types/paths/<route>.types.ts`.
35
+ */
27
36
  export class OperationTypeCoder extends TypeCoder {
28
37
  requestMethod;
29
38
  securitySchemes;
@@ -35,6 +44,10 @@ export class OperationTypeCoder extends TypeCoder {
35
44
  this.requestMethod = requestMethod;
36
45
  this.securitySchemes = securitySchemes;
37
46
  }
47
+ /**
48
+ * Returns the base identifier for this operation, derived from its
49
+ * `operationId` (sanitised) or falling back to `HTTP_<METHOD>`.
50
+ */
38
51
  getOperationBaseName() {
39
52
  const operationId = this.requirement.get("operationId")?.data;
40
53
  return operationId
@@ -47,6 +60,19 @@ export class OperationTypeCoder extends TypeCoder {
47
60
  names() {
48
61
  return super.names(this.getOperationBaseName());
49
62
  }
63
+ /**
64
+ * Generates and exports a named parameter type (e.g. `ListPets_Query`) from
65
+ * `modulePath` and returns the exported type name.
66
+ *
67
+ * Returns `"never"` without creating an export when `inlineType` is
68
+ * `"never"`.
69
+ *
70
+ * @param script - The script being assembled.
71
+ * @param parameterKind - `"query"`, `"path"`, `"headers"`, or `"cookie"`.
72
+ * @param inlineType - The inline TypeScript type string to export.
73
+ * @param baseName - The base identifier prefix for the exported type name.
74
+ * @param modulePath - The repository-relative path of the type file.
75
+ */
50
76
  exportParameterType(script, parameterKind, inlineType, baseName, modulePath) {
51
77
  if (inlineType === "never") {
52
78
  return "never";
@@ -57,6 +83,11 @@ export class OperationTypeCoder extends TypeCoder {
57
83
  coder._modulePath = modulePath;
58
84
  return script.export(coder, true);
59
85
  }
86
+ /**
87
+ * Returns the union of all possible response type shapes for this operation.
88
+ *
89
+ * @param script - The script being assembled (used to resolve imports).
90
+ */
60
91
  responseTypes(script) {
61
92
  return this.requirement
62
93
  .get("responses")
@@ -96,10 +127,14 @@ export class OperationTypeCoder extends TypeCoder {
96
127
  .split("/")
97
128
  .at(-2)
98
129
  .replaceAll("~1", "/");
99
- return `${nodePath
100
- .join("types/paths", pathString === "/" ? "/index" : pathString)
101
- .replaceAll("\\", "/")}.types.ts`;
130
+ return `${pathJoin("types/paths", pathString === "/" ? "/index" : pathString)}.types.ts`;
102
131
  }
132
+ /**
133
+ * Returns the TypeScript type for the `user` argument.
134
+ *
135
+ * When the operation is protected by HTTP Basic auth, the type is
136
+ * `{username?: string, password?: string}`. Otherwise it is `"never"`.
137
+ */
103
138
  userType() {
104
139
  if (this.securitySchemes.some(({ scheme, type }) => type === "http" && scheme === "basic")) {
105
140
  return "{username?: string, password?: string}";
@@ -1,4 +1,4 @@
1
- import nodePath from "node:path";
1
+ import { pathJoin } from "../util/forward-slash-path.js";
2
2
  import { buildJsDoc } from "./jsdoc.js";
3
3
  import { SchemaTypeCoder } from "./schema-type-coder.js";
4
4
  import { TypeCoder } from "./type-coder.js";
@@ -39,8 +39,6 @@ export class ParametersTypeCoder extends TypeCoder {
39
39
  .split("/")
40
40
  .at(-2)
41
41
  .replaceAll("~1", "/");
42
- return `${nodePath
43
- .join("parameters", pathString)
44
- .replaceAll("\\", "/")}.types.ts`;
42
+ return `${pathJoin("parameters", pathString)}.types.ts`;
45
43
  }
46
44
  }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import nodePath from "node:path";
3
3
  import createDebug from "debug";
4
+ import { toForwardSlashPath } from "../util/forward-slash-path.js";
4
5
  const debug = createDebug("counterfact:typescript-generator:prune");
5
6
  /**
6
7
  * Collects all .ts route files in a directory recursively.
@@ -87,7 +88,7 @@ export async function pruneRoutes(destination, openApiPaths) {
87
88
  debug("actual route files: %o", actualFiles);
88
89
  let prunedCount = 0;
89
90
  for (const file of actualFiles) {
90
- const normalizedFile = file.replaceAll("\\", "/");
91
+ const normalizedFile = toForwardSlashPath(file);
91
92
  if (!expectedFiles.has(normalizedFile)) {
92
93
  const fullPath = nodePath.join(routesDir, file);
93
94
  debug("pruning %s", fullPath);
@@ -4,17 +4,33 @@ import nodePath, { dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import createDebug from "debug";
6
6
  import { ensureDirectoryExists } from "../util/ensure-directory-exists.js";
7
+ import { toForwardSlashPath, pathJoin, pathRelative, pathDirname, } from "../util/forward-slash-path.js";
7
8
  import { CONTEXT_FILE_TOKEN } from "./context-file-token.js";
8
9
  import { Script } from "./script.js";
9
10
  import { escapePathForWindows } from "../util/windows-escape.js";
10
11
  const debug = createDebug("counterfact:server:repository");
11
- const __dirname = dirname(fileURLToPath(import.meta.url)).replaceAll("\\", "/");
12
+ const __dirname = toForwardSlashPath(dirname(fileURLToPath(import.meta.url)));
12
13
  debug("dirname is %s", __dirname);
14
+ /**
15
+ * Collection of {@link Script} objects keyed by their repository-relative
16
+ * path.
17
+ *
18
+ * Coders call {@link get} to obtain (or create) the script where they should
19
+ * export their generated TypeScript. After all coders have been registered,
20
+ * {@link writeFiles} waits for every script to finish and writes the output to
21
+ * disk.
22
+ */
13
23
  export class Repository {
14
24
  scripts;
15
25
  constructor() {
16
26
  this.scripts = new Map();
17
27
  }
28
+ /**
29
+ * Returns the {@link Script} for `path`, creating it if it does not yet
30
+ * exist.
31
+ *
32
+ * @param path - Repository-relative path (e.g. `"routes/pets.ts"`).
33
+ */
18
34
  get(path) {
19
35
  debug("getting script at %s", path);
20
36
  if (this.scripts.has(path)) {
@@ -26,12 +42,22 @@ export class Repository {
26
42
  this.scripts.set(path, script);
27
43
  return script;
28
44
  }
45
+ /** Waits until all scripts have resolved all of their pending export promises. */
29
46
  async finished() {
30
47
  while (Array.from(this.scripts.values()).some((script) => script.isInProgress())) {
31
48
  debug("waiting for %i scripts to finish", this.scripts.size);
32
49
  await Promise.all(Array.from(this.scripts.values(), (script) => script.finished()));
33
50
  }
34
51
  }
52
+ /**
53
+ * Copies the compiled `counterfact-types` directory from the Counterfact
54
+ * distribution into the generated output tree.
55
+ *
56
+ * Returns `false` when the source directory does not exist (e.g. running
57
+ * from source without a prior build).
58
+ *
59
+ * @param destination - The root of the generated output tree.
60
+ */
35
61
  async copyCoreFiles(destination) {
36
62
  const sourcePath = nodePath.join(__dirname, "../../dist/server/counterfact-types");
37
63
  const destinationPath = nodePath.join(destination, "counterfact-types");
@@ -40,13 +66,23 @@ export class Repository {
40
66
  }
41
67
  return fs.cp(sourcePath, destinationPath, { recursive: true });
42
68
  }
69
+ /**
70
+ * Waits for all scripts to finish, then writes each one to disk.
71
+ *
72
+ * Route files (`routes/…`) are never overwritten if they already exist on
73
+ * disk, preserving user edits. Type files (`types/…`) are always
74
+ * overwritten.
75
+ *
76
+ * @param destination - Absolute path to the output root directory.
77
+ * @param options - Controls which artefacts are written.
78
+ */
43
79
  async writeFiles(destination, { routes, types }) {
44
80
  debug("waiting for %i or more scripts to finish before writing files", this.scripts.size);
45
81
  await this.finished();
46
82
  debug("all %i scripts are finished", this.scripts.size);
47
83
  const writeFiles = Array.from(this.scripts.entries(), async ([path, script]) => {
48
84
  const contents = await script.contents();
49
- const fullPath = escapePathForWindows(nodePath.join(destination, path).replaceAll("\\", "/"));
85
+ const fullPath = escapePathForWindows(pathJoin(destination, path));
50
86
  await ensureDirectoryExists(fullPath);
51
87
  const shouldWriteRoutes = routes && path.startsWith("routes");
52
88
  const shouldWriteTypes = types && !path.startsWith("routes");
@@ -70,37 +106,57 @@ export class Repository {
70
106
  await this.createDefaultContextFile(destination);
71
107
  }
72
108
  }
109
+ /**
110
+ * Creates the default `routes/_.context.ts` file if it does not already
111
+ * exist.
112
+ *
113
+ * @param destination - Absolute path to the output root directory.
114
+ */
73
115
  async createDefaultContextFile(destination) {
74
116
  const contextFilePath = nodePath.join(destination, "routes", "_.context.ts");
75
117
  if (existsSync(contextFilePath)) {
76
118
  return;
77
119
  }
78
120
  await ensureDirectoryExists(contextFilePath);
79
- await fs.writeFile(contextFilePath, `/**
80
- * This is the default context for Counterfact.
81
- *
82
- * It defines the context object in the REPL
83
- * and the $.context object in the code.
84
- *
85
- * Add properties and methods to suit your needs.
86
- *
87
- * See https://counterfact.dev/docs/usage.html#working-with-state-the-codecontextcode-object-and-codecontexttscode
88
- */
89
- export class Context {
121
+ await fs.writeFile(contextFilePath, `import type { Context$ } from "../types/_.context.js";
122
+
123
+ /**
124
+ * This is the default context for Counterfact.
125
+ *
126
+ * It defines the context object in the REPL
127
+ * and the $.context object in the code.
128
+ *
129
+ * Add properties and methods to suit your needs.
130
+ *
131
+ * See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md
132
+ */
90
133
 
134
+ export class Context {
135
+ constructor($: Context$) {
136
+ void $;
137
+ }
91
138
  }
92
139
  `);
93
140
  }
141
+ /**
142
+ * Returns the path of the `_.context.ts` file that is nearest to `path` in
143
+ * the directory hierarchy, relative to the script's output directory.
144
+ *
145
+ * @param destination - Output root directory.
146
+ * @param path - Repository-relative path of the script being generated.
147
+ */
94
148
  findContextPath(destination, path) {
95
- return nodePath
96
- .relative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path))
97
- .replaceAll("\\", "/");
149
+ return pathRelative(nodePath.join(destination, nodePath.dirname(path)), this.nearestContextFile(destination, path));
98
150
  }
151
+ /**
152
+ * Walks up the directory tree from `path` to find the nearest
153
+ * `_.context.ts` file, falling back to `routes/_.context.ts` at the root.
154
+ *
155
+ * @param destination - Output root directory.
156
+ * @param path - Repository-relative path to start from.
157
+ */
99
158
  nearestContextFile(destination, path) {
100
- const directory = nodePath
101
- .dirname(path)
102
- .replaceAll("\\", "/")
103
- .replace("types/paths", "routes");
159
+ const directory = pathDirname(path).replace("types/paths", "routes");
104
160
  const candidate = nodePath.join(destination, directory, "_.context.ts");
105
161
  if (directory.length <= 1) {
106
162
  // No _context.ts was found so import the one that should be in the root
@@ -1,3 +1,11 @@
1
+ /**
2
+ * A node in the dereferenced OpenAPI spec tree.
3
+ *
4
+ * A `Requirement` wraps a raw JSON object (`data`) together with its location
5
+ * (`url`, a JSON Pointer) and a back-reference to the owning
6
+ * {@link Specification}. Navigation methods (`get`, `select`, `find`, …)
7
+ * transparently follow `$ref` pointers.
8
+ */
1
9
  export class Requirement {
2
10
  data;
3
11
  url;
@@ -7,18 +15,36 @@ export class Requirement {
7
15
  this.url = url;
8
16
  this.specification = specification;
9
17
  }
18
+ /** `true` when this node is a JSON Reference (`$ref`) rather than inline data. */
10
19
  get isReference() {
11
20
  return this.data["$ref"] !== undefined;
12
21
  }
22
+ /**
23
+ * Resolves the `$ref` and returns the target {@link Requirement}.
24
+ *
25
+ * @throws When `isReference` is `false` or the specification is not set.
26
+ */
13
27
  reference() {
14
28
  return this.specification.getRequirement(this.data["$ref"]);
15
29
  }
30
+ /**
31
+ * Returns `true` when this node has a child property named `item`.
32
+ *
33
+ * Transparently follows `$ref` references.
34
+ *
35
+ * @param item - The property key to check.
36
+ */
16
37
  has(item) {
17
38
  if (this.isReference) {
18
39
  return this.reference().has(item);
19
40
  }
20
41
  return item in this.data;
21
42
  }
43
+ /**
44
+ * Returns the child {@link Requirement} for `item`, or `undefined`.
45
+ *
46
+ * @param item - The property key (string) or array index (number).
47
+ */
22
48
  get(item) {
23
49
  if (this.isReference) {
24
50
  return this.reference().get(item);
@@ -29,6 +55,16 @@ export class Requirement {
29
55
  }
30
56
  return new Requirement(this.data[key], `${this.url}/${this.escapeJsonPointer(key)}`, this.specification);
31
57
  }
58
+ /**
59
+ * Navigates to a descendant node using a slash-delimited JSON Pointer path.
60
+ *
61
+ * Tilde-escaped characters (`~0` → `~`, `~1` → `/`) and percent-encoded
62
+ * characters are unescaped during traversal.
63
+ *
64
+ * @param path - A slash-delimited path (e.g. `"responses/200/content"`).
65
+ * @returns The target {@link Requirement}, or `undefined` if the path does
66
+ * not exist.
67
+ */
32
68
  select(path) {
33
69
  const parts = path
34
70
  .split("/")
@@ -54,19 +90,42 @@ export class Requirement {
54
90
  }
55
91
  return result;
56
92
  }
93
+ /**
94
+ * Iterates over all child properties and calls `callback` with each child
95
+ * requirement and its key.
96
+ *
97
+ * @param callback - Called for each child with `(child, key)`.
98
+ */
57
99
  forEach(callback) {
58
100
  Object.keys(this.data).forEach((key) => {
59
101
  callback(this.select(this.escapeJsonPointer(key)), key);
60
102
  });
61
103
  }
104
+ /**
105
+ * Maps over all child properties and returns the collected results.
106
+ *
107
+ * @param callback - Transformation function called with `(child, key)`.
108
+ */
62
109
  map(callback) {
63
110
  const result = [];
64
111
  this.forEach((value, key) => result.push(callback(value, key)));
65
112
  return result;
66
113
  }
114
+ /**
115
+ * Maps and flattens over all child properties.
116
+ *
117
+ * @param callback - Transformation function that may return a value or an
118
+ * array of values.
119
+ */
67
120
  flatMap(callback) {
68
121
  return this.map(callback).flat();
69
122
  }
123
+ /**
124
+ * Returns the first child for which `callback` returns `true`, or
125
+ * `undefined` when nothing matches.
126
+ *
127
+ * @param callback - Predicate called with `(child, key)`.
128
+ */
70
129
  find(callback) {
71
130
  let result;
72
131
  this.forEach((value, key) => {
@@ -76,11 +135,21 @@ export class Requirement {
76
135
  });
77
136
  return result;
78
137
  }
138
+ /**
139
+ * Escapes a JSON Pointer token: `~` → `~0`, `/` → `~1`.
140
+ *
141
+ * @param value - The token to escape.
142
+ */
79
143
  escapeJsonPointer(value) {
80
144
  if (typeof value !== "string")
81
145
  return value;
82
146
  return value.replaceAll("~", "~0").replaceAll("/", "~1");
83
147
  }
148
+ /**
149
+ * Unescapes a JSON Pointer token: `~1` → `/`, `~0` → `~`.
150
+ *
151
+ * @param pointer - The token to unescape.
152
+ */
84
153
  unescapeJsonPointer(pointer) {
85
154
  if (typeof pointer !== "string")
86
155
  return pointer;