rspress-plugin-api-extractor 0.9.2 → 0.11.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.
Files changed (52) hide show
  1. package/BuildEnv.js +58 -0
  2. package/README.md +2 -1
  3. package/build-program.js +33 -30
  4. package/build-stages.js +47 -39
  5. package/errors.js +0 -1
  6. package/index.d.ts +22 -14
  7. package/layers/ConfigServiceLive.js +349 -400
  8. package/layers/HighlighterServiceLive.js +52 -0
  9. package/layers/ObservabilityLive.js +26 -7
  10. package/layers/OgServiceLive.js +134 -0
  11. package/layers/TwoslashCacheServiceLive.js +108 -0
  12. package/layers/TwoslashEnvironmentsLive.js +33 -0
  13. package/layers/TypeRegistryServiceLive.js +54 -47
  14. package/layers/build-metrics.js +32 -5
  15. package/layers/xdg.js +44 -0
  16. package/markdown/helpers.js +9 -55
  17. package/markdown/page-generators/class-page.js +8 -31
  18. package/markdown/page-generators/index-pages.js +6 -8
  19. package/markdown/page-generators/interface-page.js +7 -7
  20. package/markdown/shiki-utils.js +65 -10
  21. package/observability/EventBus.js +29 -9
  22. package/observability/heartbeat.js +1 -1
  23. package/observability/metric-report.js +124 -0
  24. package/observability/sinks/console-sink.js +6 -0
  25. package/observability/sinks/metrics-sink.js +64 -21
  26. package/observability/sinks/render-sink.js +86 -0
  27. package/observability/sinks/trace-sink.js +10 -17
  28. package/observability/spans.js +4 -2
  29. package/observability/sync-emitter.js +78 -0
  30. package/og-resolver.js +46 -287
  31. package/package.json +4 -5
  32. package/path-derivation.js +19 -1
  33. package/plugin.js +64 -52
  34. package/prettier-formatter.js +4 -10
  35. package/remark-api-codeblocks.js +33 -15
  36. package/remark-with-api.js +24 -27
  37. package/schemas/config.js +11 -7
  38. package/services/HighlighterService.js +30 -0
  39. package/services/OgService.js +23 -0
  40. package/services/PluginConfig.js +26 -0
  41. package/services/TwoslashCacheService.js +15 -0
  42. package/services/TwoslashEnvironments.js +7 -0
  43. package/shiki-transformer.js +55 -256
  44. package/twoslash-access.js +48 -0
  45. package/twoslash-cache.js +174 -0
  46. package/twoslash-patterns.js +1 -1
  47. package/twoslash-timing-wrapper.js +23 -0
  48. package/twoslash-transformer.js +153 -89
  49. package/vfs-registry.js +1 -31
  50. package/layers/PathDerivationServiceLive.js +0 -16
  51. package/runtime/components/MarkdownText/index.js +0 -34
  52. package/services/PathDerivationService.js +0 -7
@@ -1,28 +1,15 @@
1
1
  import { PluginEvent } from "./observability/events.js";
2
+ import { emitSync, syncBuildId } from "./observability/sync-emitter.js";
2
3
  import { DEFAULT_COMPILER_OPTIONS } from "./typescript-config.js";
4
+ import { Result } from "effect";
5
+ import { Markdown, Mdast } from "@effected/markdown";
6
+ import { TsEnumCodec } from "@effected/tsconfig-json";
3
7
  import { rendererRich, transformerTwoslash } from "@shikijs/twoslash";
4
- import { fromMarkdown } from "mdast-util-from-markdown";
5
8
  import { toHast } from "mdast-util-to-hast";
6
9
 
7
10
  //#region src/twoslash-transformer.ts
8
11
  /* v8 ignore start -- Shiki/Twoslash integration, requires full highlighter setup for testing */
9
12
  /**
10
- * Module-level emitter seam. Default is a no-op; wire in `setEventEmitter(emitSync)`
11
- * from plugin.ts right after the runtime emitter is created so that Twoslash error
12
- * events flow through the EventBus even though they fire in a sync Shiki callback
13
- * outside any Effect fiber.
14
- */
15
- let emitEvent = () => {};
16
- let currentBuildId = "";
17
- /**
18
- * Inject the runtime-bound emitter into the Twoslash module.
19
- * Call this right after `makeRuntimeEmitter` in plugin.ts.
20
- */
21
- function setEventEmitter(fn, buildId = "") {
22
- emitEvent = fn;
23
- currentBuildId = buildId;
24
- }
25
- /**
26
13
  * Module-level type routes map for resolving link references.
27
14
  * This is set by TwoslashManager.setTypeRoutes() before initialization.
28
15
  */
@@ -116,8 +103,10 @@ function addLinkClasses(node) {
116
103
  * Render markdown content to HAST (Hypertext Abstract Syntax Tree) elements.
117
104
  *
118
105
  * This function converts markdown strings (from TSDoc comments) into HAST nodes
119
- * that can be rendered in Twoslash hover popups. It uses mdast-util-from-markdown
120
- * to parse the markdown and mdast-util-to-hast to convert to HAST.
106
+ * that can be rendered in Twoslash hover popups. It parses with
107
+ * `@effected/markdown` (CommonMark dialect) and converts to HAST with
108
+ * `mdast-util-to-hast`, which stays because markdown-to-HTML is permanently
109
+ * out of scope for the kit.
121
110
  *
122
111
  * TSDoc link references are transformed to markdown links before parsing.
123
112
  * Whitespace is normalized for proper inline display.
@@ -131,8 +120,12 @@ function renderMarkdown(markdown) {
131
120
  try {
132
121
  let transformed = transformTsDocLinks(markdown);
133
122
  transformed = transformed.split(/\n\n+/).map((para) => para.replace(/\s+/g, " ").trim()).filter((para) => para.length > 0).join("\n\n");
134
- const mdast = fromMarkdown(transformed);
135
- const hast = toHast(mdast);
123
+ const parsed = Markdown.parseResult(transformed, { dialect: "commonmark" });
124
+ if (Result.isFailure(parsed)) return [{
125
+ type: "text",
126
+ value: markdown
127
+ }];
128
+ const hast = toHast(Mdast.toMdast(parsed.success));
136
129
  if (hast && "children" in hast) {
137
130
  const children = hast.children;
138
131
  for (const child of children) addLinkClasses(child);
@@ -228,12 +221,86 @@ function renderMarkdownInline(markdown, context) {
228
221
  *
229
222
  * @see {@link TypeRegistryService} for VFS generation
230
223
  */
231
- var TwoslashManager = class TwoslashManager {
232
- static instance = null;
224
+ /**
225
+ * Fingerprint a compiler configuration so environments can be deduped and code
226
+ * blocks routed to the right one. Keys are sorted, so two configurations that
227
+ * differ only in property order share an environment.
228
+ */
229
+ /**
230
+ * Convert resolved compiler options from the tsconfig JSON spelling to the
231
+ * programmatic one a real compiler expects.
232
+ *
233
+ * @remarks
234
+ * Two spellings meet here, and only here. `tsconfig.json` writes
235
+ * `lib: ["ESNext", "DOM"]` and `target: "esnext"`; `ts.CompilerOptions` wants
236
+ * lib FILE NAMES (`lib.esnext.d.ts`) and numeric enums. `DEFAULT_COMPILER_OPTIONS`
237
+ * is authored in the tsconfig spelling, and a tsconfig discovered from disk
238
+ * arrives already converted by `ts.parseJsonConfigFileContent`, so both forms
239
+ * reach this function — which is why the conversion must be idempotent rather
240
+ * than one-directional.
241
+ *
242
+ * Exported for the four-path regression test: no runtime path in this repo
243
+ * reaches the broken spelling (an unscoped block inherits the first registered
244
+ * environment, not the raw default), so a synthetic test compiling each
245
+ * resolution path through the real compiler is the only verification there is.
246
+ */
247
+ function toProgrammaticCompilerOptions(options) {
248
+ return TsEnumCodec.encodeCompilerOptions(options);
249
+ }
250
+ /**
251
+ * Fingerprint a compiler configuration, for keying the environment map.
252
+ *
253
+ * @remarks
254
+ * INVARIANT: every call site must pass options that have already been through
255
+ * {@link toProgrammaticCompilerOptions}. There are two — `initialize`, which
256
+ * stores an environment under this key, and `registerScope`, which looks one
257
+ * up by it. Both must encode, and encode the same way.
258
+ *
259
+ * The failure mode is SILENT. Encode at one site and not the other and the
260
+ * keys stop matching, so `getTransformer(scope)` finds nothing and falls back
261
+ * to the default environment: per-scope type-checking quietly degrades to
262
+ * build-wide, no error is raised, and nothing in the output looks wrong.
263
+ *
264
+ * This is not hypothetical. Task 1.2 moved the `initialize` fingerprint behind
265
+ * the encoder as a step specified — and reviewed — as a no-op, left
266
+ * `registerScope` on the raw options, and the full 994-test suite stayed green
267
+ * over the defect. The mutation that should have caught it (fingerprinting the
268
+ * pre-encoded value) also survived that suite. What caught it was a test
269
+ * written specifically for the hazard, which then failed for this second,
270
+ * unanticipated reason. `__test__/twoslash-transformer.test.ts` now pins both
271
+ * halves; keep that test whenever this code moves.
272
+ */
273
+ function twoslashConfigKey(options) {
274
+ const entries = Object.entries(options).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
275
+ return JSON.stringify(entries);
276
+ }
277
+ /**
278
+ * The mutable registry behind {@link TwoslashEnvironments}.
279
+ *
280
+ * @remarks
281
+ * A plain class the Layer constructs, not a singleton. `getInstance()` and the
282
+ * static `reset()` that stood in for layer substitution are gone: a test that
283
+ * wants a different environment set provides a different layer.
284
+ */
285
+ var TwoslashEnvironmentRegistry = class {
233
286
  /**
234
- * Twoslash transformer instance
287
+ * Transformers keyed by compiler-config fingerprint.
288
+ *
289
+ * One environment per DISTINCT configuration, not per API: two packages
290
+ * documented under the same compiler options share an environment, and with
291
+ * it the TypeScript language services Twoslash builds per block. A build
292
+ * where every API agrees on its config therefore costs exactly what the
293
+ * single shared environment used to.
235
294
  */
236
- transformer = null;
295
+ environments = /* @__PURE__ */ new Map();
296
+ /** API scope to config fingerprint, for `getTransformer(scope)`. */
297
+ scopeConfigs = /* @__PURE__ */ new Map();
298
+ /**
299
+ * Fingerprint of the first environment initialized, used for code blocks
300
+ * that carry no scope — a `with-api` fence in a page outside any documented
301
+ * package's route.
302
+ */
303
+ defaultConfigKey = null;
237
304
  /**
238
305
  * VFS keys snapshot captured at initialize() time.
239
306
  * Returned by vfsKeysSnapshot() for TwoslashCheckFailed events.
@@ -243,7 +310,7 @@ var TwoslashManager = class TwoslashManager {
243
310
  * Resolved compiler options captured at initialize() time.
244
311
  * Returned by compilerOptionsSnapshot() for TwoslashCheckFailed events.
245
312
  */
246
- _resolvedCompilerOptions = DEFAULT_COMPILER_OPTIONS;
313
+ _resolvedCompilerOptions = toProgrammaticCompilerOptions(DEFAULT_COMPILER_OPTIONS);
247
314
  /**
248
315
  * Path of the file whose code block is currently being processed.
249
316
  * Used to attribute Twoslash error events to a source file. Defaults to
@@ -252,33 +319,25 @@ var TwoslashManager = class TwoslashManager {
252
319
  */
253
320
  currentFilePath = "unknown";
254
321
  /**
255
- * Private constructor to enforce singleton pattern
256
- */
257
- constructor() {}
258
- /**
259
- * Get the singleton instance of TwoslashManager
260
- */
261
- static getInstance() {
262
- if (!TwoslashManager.instance) TwoslashManager.instance = new TwoslashManager();
263
- return TwoslashManager.instance;
264
- }
265
- /**
266
- * Initialize the Twoslash transformer with a TypeScript environment cache.
267
- * This enables type-aware documentation with hover information and IntelliSense.
322
+ * Build an environment for a configuration, or return if one exists.
268
323
  *
269
- * @param vfs - Virtual file system mapping file paths to .d.ts content
270
- * @param _reserved - Reserved parameter (previously errorStatsCollector, now tracked via Effect Metrics)
271
- * @param _reserved2 - Reserved parameter (previously logger, now uses console)
272
- * @param tsEnvCache - TypeScript virtual environment cache for reusing language services
273
- * @param compilerOptions - TypeScript compiler options for Twoslash (defaults to DEFAULT_COMPILER_OPTIONS)
324
+ * @remarks
325
+ * The signature this replaces took six positional parameters, two named
326
+ * `_reserved`/`_reserved2` and one (`tsEnvCache`) that every call site
327
+ * passed as `undefined`. Dropping `tsEnvCache` changes nothing at runtime
328
+ * for exactly that reason Twoslash was never handed a shared environment
329
+ * cache.
274
330
  */
275
- initialize(vfs, _reserved, _reserved2, tsEnvCache, compilerOptions) {
331
+ registerEnvironment({ vfs, compilerOptions, typesCache }) {
276
332
  const extraFiles = {};
277
333
  for (const [path, content] of vfs.entries()) extraFiles[path] = content;
278
334
  this._vfsKeys = Array.from(vfs.keys());
279
- const resolvedOptions = compilerOptions ?? DEFAULT_COMPILER_OPTIONS;
335
+ const resolvedOptions = toProgrammaticCompilerOptions(compilerOptions ?? DEFAULT_COMPILER_OPTIONS);
280
336
  this._resolvedCompilerOptions = resolvedOptions;
281
- this.transformer = transformerTwoslash({
337
+ const configKey = twoslashConfigKey(resolvedOptions);
338
+ if (this.defaultConfigKey === null) this.defaultConfigKey = configKey;
339
+ if (this.environments.has(configKey)) return;
340
+ const transformer = transformerTwoslash({
282
341
  renderer: rendererRich({
283
342
  processHoverInfo: (info) => {
284
343
  return info.replace(/^\(([\w-]+)\)\s+/gm, "").replace(/\nimport .*$/gm, "").trim();
@@ -287,7 +346,7 @@ var TwoslashManager = class TwoslashManager {
287
346
  renderMarkdown,
288
347
  renderMarkdownInline
289
348
  }),
290
- ...tsEnvCache != null ? { cache: tsEnvCache } : {},
349
+ ...typesCache != null ? { typesCache } : {},
291
350
  twoslashOptions: {
292
351
  extraFiles,
293
352
  compilerOptions: resolvedOptions,
@@ -299,13 +358,26 @@ var TwoslashManager = class TwoslashManager {
299
358
  this.handleTwoslashError(error, code, this.currentFilePath);
300
359
  }
301
360
  });
361
+ this.environments.set(configKey, transformer);
362
+ }
363
+ /**
364
+ * Associate an API scope with the compiler configuration it is documented
365
+ * under, so its code blocks are type-checked with that configuration.
366
+ */
367
+ registerScope(apiScope, compilerOptions) {
368
+ this.scopeConfigs.set(apiScope, twoslashConfigKey(toProgrammaticCompilerOptions(compilerOptions)));
302
369
  }
303
370
  /**
304
- * Get the initialized Twoslash transformer.
305
- * Returns null if not initialized.
371
+ * Get the Twoslash transformer for an API scope.
372
+ *
373
+ * An unknown or absent scope falls back to the first environment built: a
374
+ * `with-api` fence can appear on a page outside any documented package's
375
+ * route, and type-checking it under some configuration beats not checking it.
376
+ * Returns null before any environment is initialized.
306
377
  */
307
- getTransformer() {
308
- return this.transformer;
378
+ transformerFor(apiScope) {
379
+ const key = (apiScope != null ? this.scopeConfigs.get(apiScope) : void 0) ?? this.defaultConfigKey;
380
+ return key != null ? this.environments.get(key) ?? null : null;
309
381
  }
310
382
  /**
311
383
  * Set the source file path used to attribute subsequent Twoslash error events.
@@ -320,13 +392,9 @@ var TwoslashManager = class TwoslashManager {
320
392
  * Clear the Twoslash transformer (useful for testing or reinitializing)
321
393
  */
322
394
  clear() {
323
- this.transformer = null;
324
- }
325
- /**
326
- * Reset the singleton instance (useful for testing)
327
- */
328
- static reset() {
329
- TwoslashManager.instance = null;
395
+ this.environments.clear();
396
+ this.scopeConfigs.clear();
397
+ this.defaultConfigKey = null;
330
398
  }
331
399
  /** Returns VFS keys snapshotted at initialize() time. Empty array before initialize(). */
332
400
  vfsKeysSnapshot() {
@@ -340,9 +408,9 @@ var TwoslashManager = class TwoslashManager {
340
408
  const message = error instanceof Error ? error.message : String(error);
341
409
  const match = /TS(\d+)/.exec(message);
342
410
  const tsCode = match ? Number(match[1]) : 0;
343
- emitEvent(PluginEvent.TwoslashDiagnostic({
411
+ emitSync(PluginEvent.TwoslashDiagnostic({
344
412
  ctx: {
345
- buildId: currentBuildId,
413
+ buildId: syncBuildId(),
346
414
  file
347
415
  },
348
416
  level: "warn",
@@ -353,9 +421,9 @@ var TwoslashManager = class TwoslashManager {
353
421
  message,
354
422
  snippet: ""
355
423
  }));
356
- emitEvent(PluginEvent.TwoslashCheckFailed({
424
+ emitSync(PluginEvent.TwoslashCheckFailed({
357
425
  ctx: {
358
- buildId: currentBuildId,
426
+ buildId: syncBuildId(),
359
427
  file
360
428
  },
361
429
  level: "trace",
@@ -369,34 +437,30 @@ var TwoslashManager = class TwoslashManager {
369
437
  * Test seam: drive `handleTwoslashError` directly without going through the Shiki transformer.
370
438
  * @internal
371
439
  */
372
- handleTwoslashErrorForTest(error, code, file) {
440
+ reportErrorForTest(error, code, file) {
373
441
  this.handleTwoslashError(error, code, file);
374
442
  }
375
- /**
376
- * Set the type routes map for resolving link references in hover docs.
377
- * This should be called before initialize() to enable type linking.
378
- *
379
- * @param routes - Map of type names to their documentation URLs
380
- */
381
- static setTypeRoutes(routes) {
382
- typeRoutes = routes;
383
- }
384
- /**
385
- * Add routes to the existing type routes map.
386
- * Useful for adding routes from multiple packages.
387
- *
388
- * @param routes - Map of type names to their documentation URLs
389
- */
390
- static addTypeRoutes(routes) {
391
- for (const [name, route] of routes) typeRoutes.set(name, route);
392
- }
393
- /**
394
- * Clear the type routes map (useful for testing)
395
- */
396
- static clearTypeRoutes() {
397
- typeRoutes.clear();
398
- }
399
443
  };
444
+ /** Merge routes in, so a multi-API build accumulates every scope's names. */
445
+ function addTypeRoutes(routes) {
446
+ for (const [name, route] of routes) typeRoutes.set(name, route);
447
+ }
448
+ /**
449
+ * Clear the accumulated type routes.
450
+ *
451
+ * @remarks
452
+ * Called from `config()` at the start of every build. `addTypeRoutes` only
453
+ * ever adds, so without this a dev session keeps routes for items that have
454
+ * since been renamed or removed, and a multi-API build leaks every scope's
455
+ * names into one map.
456
+ *
457
+ * Clears ONLY the routes. The environments are per-build too, but they are
458
+ * owned by the layer now, so nothing here can discard the per-scope
459
+ * transformers `ConfigServiceLive` just built.
460
+ */
461
+ function clearTypeRoutes() {
462
+ typeRoutes.clear();
463
+ }
400
464
 
401
465
  //#endregion
402
- export { TwoslashManager, setEventEmitter };
466
+ export { TwoslashEnvironmentRegistry, addTypeRoutes, clearTypeRoutes, toProgrammaticCompilerOptions };
package/vfs-registry.js CHANGED
@@ -34,35 +34,6 @@ var VfsRegistryImpl = class {
34
34
  return this.configs.get(apiScope);
35
35
  }
36
36
  /**
37
- * Get the VFS configuration by matching a file path to an API scope.
38
- *
39
- * This method extracts the API scope from a file path and returns
40
- * the corresponding VFS configuration.
41
- *
42
- * @param filePath - The absolute file path being processed
43
- * @returns The VFS configuration, or undefined if not found
44
- */
45
- getByFilePath(filePath) {
46
- const apiScope = this.extractApiScope(filePath);
47
- if (!apiScope) return;
48
- return this.get(apiScope);
49
- }
50
- /**
51
- * Extract the API scope from a file path.
52
- *
53
- * Path patterns:
54
- * - `docs/en/api-scope/rest.mdx`
55
- * - `website/docs/en/api-scope/rest.mdx`
56
- *
57
- * @param filePath - The file path to extract from
58
- * @returns The API scope, or undefined if not matched
59
- */
60
- extractApiScope(filePath) {
61
- const match = filePath.replace(/\\/g, "/").match(/(?:^|\/)(docs\/en|website\/docs\/en)\/([^/]+)(?:\/|$)/);
62
- if (!match) return;
63
- return match[2];
64
- }
65
- /**
66
37
  * Check if any VFS configurations are registered.
67
38
  *
68
39
  * @returns True if at least one configuration is registered
@@ -96,7 +67,6 @@ var VfsRegistryImpl = class {
96
67
  * ```ts
97
68
  * // In beforeBuild hook:
98
69
  * VfsRegistry.register("claude-binary-plugin", {
99
- * vfs: combinedVfs,
100
70
  * highlighter,
101
71
  * twoslashTransformer,
102
72
  * crossLinker: shikiCrossLinker,
@@ -105,7 +75,7 @@ var VfsRegistryImpl = class {
105
75
  * });
106
76
  *
107
77
  * // In remark plugin:
108
- * const config = VfsRegistry.getByFilePath(file.path);
78
+ * const config = VfsRegistry.get(apiScope);
109
79
  * if (config) {
110
80
  * // Generate HAST with Shiki, then post-process with cross-linker
111
81
  * let hast = await generateShikiHast(code, config.highlighter, transformers);
@@ -1,16 +0,0 @@
1
- import { deriveOutputPaths, normalizeBaseRoute } from "../path-derivation.js";
2
- import { PathDerivationService } from "../services/PathDerivationService.js";
3
- import { Effect, Layer } from "effect";
4
-
5
- //#region src/layers/PathDerivationServiceLive.ts
6
- const PathDerivationServiceLive = Layer.succeed(PathDerivationService, {
7
- derivePaths: (input) => Effect.succeed(deriveOutputPaths({
8
- ...input,
9
- locales: [...input.locales],
10
- versions: [...input.versions]
11
- })),
12
- normalizeBaseRoute: (route) => Effect.succeed(normalizeBaseRoute(route))
13
- });
14
-
15
- //#endregion
16
- export { PathDerivationServiceLive };
@@ -1,34 +0,0 @@
1
- import { Fragment, jsx } from "react/jsx-runtime";
2
-
3
- //#region src/runtime/components/MarkdownText/index.tsx
4
- /**
5
- * Renders plain text with markdown links as React elements.
6
- * Only supports basic markdown links: [text](url)
7
- */
8
- function MarkdownText({ children }) {
9
- const parts = parseMarkdownLinks(children);
10
- return /* @__PURE__ */ jsx(Fragment, { children: parts });
11
- }
12
- /**
13
- * Parse markdown links and return array of React nodes
14
- */
15
- function parseMarkdownLinks(text) {
16
- const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
17
- const parts = [];
18
- let lastIndex = 0;
19
- let key = 0;
20
- for (const match of text.matchAll(linkRegex)) {
21
- if (match.index !== void 0 && match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
22
- const [fullMatch, linkText, url] = match;
23
- parts.push(/* @__PURE__ */ jsx("a", {
24
- href: url,
25
- children: linkText
26
- }, key++));
27
- lastIndex = (match.index ?? 0) + fullMatch.length;
28
- }
29
- if (lastIndex < text.length) parts.push(text.slice(lastIndex));
30
- return parts;
31
- }
32
-
33
- //#endregion
34
- export { MarkdownText, MarkdownText as default };
@@ -1,7 +0,0 @@
1
- import { Context } from "effect";
2
-
3
- //#region src/services/PathDerivationService.ts
4
- var PathDerivationService = class extends Context.Service()("rspress-plugin-api-extractor/PathDerivationService") {};
5
-
6
- //#endregion
7
- export { PathDerivationService };