rspress-plugin-api-extractor 0.10.0 → 0.12.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 (54) hide show
  1. package/BuildEnv.js +58 -0
  2. package/build-program.js +34 -33
  3. package/build-stages.js +48 -42
  4. package/config-helpers.js +7 -7
  5. package/errors.js +1 -6
  6. package/index.d.ts +84 -86
  7. package/layers/AppLayer.js +67 -0
  8. package/layers/api-results.js +83 -0
  9. package/layers/build-metrics.js +1 -1
  10. package/layers/config-resolution.js +407 -0
  11. package/layers/external-types.js +74 -0
  12. package/layers/{ObservabilityLive.js → observability.js} +3 -3
  13. package/layers/type-environment.js +109 -0
  14. package/layers/xdg.js +44 -0
  15. package/markdown/helpers.js +9 -55
  16. package/markdown/page-generators/class-page.js +8 -31
  17. package/markdown/page-generators/index-pages.js +6 -8
  18. package/markdown/page-generators/interface-page.js +7 -7
  19. package/markdown/shiki-utils.js +65 -10
  20. package/model-loader.js +3 -3
  21. package/observability/EventBus.js +29 -7
  22. package/observability/heartbeat.js +1 -1
  23. package/observability/sinks/metrics-sink.js +1 -1
  24. package/observability/sinks/trace-sink.js +4 -4
  25. package/observability/spans.js +3 -1
  26. package/observability/sync-emitter.js +78 -0
  27. package/og-resolver.js +74 -284
  28. package/package.json +3 -4
  29. package/path-derivation.js +19 -1
  30. package/plugin.js +63 -91
  31. package/prettier-formatter.js +5 -11
  32. package/remark-api-codeblocks.js +11 -19
  33. package/remark-with-api.js +11 -21
  34. package/schemas/config.js +0 -2
  35. package/services/ConfigService.js +37 -2
  36. package/services/HighlighterService.js +75 -0
  37. package/services/OgService.js +190 -0
  38. package/services/PluginConfig.js +26 -0
  39. package/services/TwoslashCacheService.js +128 -2
  40. package/services/TwoslashEnvironments.js +35 -0
  41. package/services/TypeRegistryService.js +178 -2
  42. package/shiki-transformer.js +53 -234
  43. package/sync-node-fs.js +6 -6
  44. package/tsconfig-parser.js +77 -95
  45. package/twoslash-access.js +48 -0
  46. package/twoslash-transformer.js +106 -83
  47. package/vfs-registry.js +1 -31
  48. package/layers/ConfigServiceLive.js +0 -600
  49. package/layers/PathDerivationServiceLive.js +0 -16
  50. package/layers/TwoslashCacheServiceLive.js +0 -53
  51. package/layers/TypeRegistryServiceLive.js +0 -155
  52. package/markdown/index.js +0 -11
  53. package/schemas/index.js +0 -6
  54. package/services/PathDerivationService.js +0 -7
@@ -0,0 +1,78 @@
1
+ import { BuildId, Thresholds } from "../BuildEnv.js";
2
+ import { emit } from "./EventBus.js";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/observability/sync-emitter.ts
6
+ /**
7
+ * The one bridge from synchronous, fiber-less code to the EventBus.
8
+ *
9
+ * @remarks
10
+ * Seven modules run outside any Effect fiber — remark visitors, Shiki's
11
+ * `preprocess` hook, Prettier callbacks, the page-generation stages — and each
12
+ * carried its own byte-identical copy of this seam: a module-level
13
+ * `emitEvent`, a module-level `currentBuildId`, and a `setXEventEmitter(fn,
14
+ * buildId)` for `plugin.ts` to call. Two of them had already grown a third
15
+ * parameter for `slowCodeBlockMs`, which is how a duplicated seam decays: the
16
+ * copies stop being identical one caller at a time.
17
+ *
18
+ * The seam itself is forced. The duplication was not, and neither was the
19
+ * threading: every value those setters carried is now a `Context.Reference`
20
+ * read from the runtime, so the signature is one runtime and nothing else.
21
+ *
22
+ * **The runtime handed here must be synchronously buildable.** `runSync`
23
+ * builds the runtime's layer before running anything, so a runtime whose layer
24
+ * opens a database fails with `AsyncFiberError` at the first emit — from a
25
+ * remark plugin, during RSPress's render pass, invisible to every unit test.
26
+ * `plugin.ts` builds a small observability-only runtime for exactly this
27
+ * reason.
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+ const NOOP = {
32
+ emit: () => {},
33
+ buildId: "",
34
+ slowCodeBlockMs: Number.POSITIVE_INFINITY
35
+ };
36
+ let current = NOOP;
37
+ /**
38
+ * Bind the sync islands to a runtime.
39
+ *
40
+ * @remarks
41
+ * Call once, immediately after constructing the observability runtime. The
42
+ * References are read here rather than per emit: an emit happens per code
43
+ * block on a large site, and these values are fixed for the build.
44
+ */
45
+ function installSyncEmitter(runtime) {
46
+ const env = runtime.runSync(Effect.gen(function* () {
47
+ return {
48
+ buildId: yield* BuildId,
49
+ slowCodeBlockMs: (yield* Thresholds).slowCodeBlock
50
+ };
51
+ }));
52
+ current = {
53
+ emit: (event) => runtime.runSync(emit(event)),
54
+ buildId: env.buildId,
55
+ slowCodeBlockMs: env.slowCodeBlockMs
56
+ };
57
+ }
58
+ /** Emit an event from synchronous code. A no-op when nothing is installed. */
59
+ function emitSync(event) {
60
+ current.emit(event);
61
+ }
62
+ /** The current build's id, for a sync site assembling an `EventContext`. */
63
+ function syncBuildId() {
64
+ return current.buildId;
65
+ }
66
+ /**
67
+ * The slow-code-block threshold, for the two remark plugins that time blocks.
68
+ *
69
+ * @remarks
70
+ * The only piece of configuration a sync island needs beyond the build id, and
71
+ * the reason the old seams had begun growing divergent signatures.
72
+ */
73
+ function syncSlowCodeBlockMs() {
74
+ return current.slowCodeBlockMs;
75
+ }
76
+
77
+ //#endregion
78
+ export { emitSync, installSyncEmitter, syncBuildId, syncSlowCodeBlockMs };
package/og-resolver.js CHANGED
@@ -1,19 +1,6 @@
1
- import { PluginEvent } from "./observability/events.js";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { imageSizeFromFile } from "image-size/fromFile";
5
-
6
1
  //#region src/og-resolver.ts
7
- /** Module-level emitter injected by plugin.ts at startup. */
8
- let emitEvent = () => {};
9
- let currentBuildId = "";
10
- function setOgResolverEventEmitter(fn, buildId = "") {
11
- emitEvent = fn;
12
- currentBuildId = buildId;
13
- }
14
2
  /**
15
- * MIME type mappings for common image formats.
16
- * Used to determine the `og:image:type` meta tag value.
3
+ * MIME type mappings for common image formats, used for `og:image:type`.
17
4
  */
18
5
  const IMAGE_MIME_TYPES = {
19
6
  jpg: "image/jpeg",
@@ -24,282 +11,85 @@ const IMAGE_MIME_TYPES = {
24
11
  svg: "image/svg+xml"
25
12
  };
26
13
  /**
27
- * Resolves Open Graph image configurations into fully-qualified metadata.
14
+ * The `og:image:type` value for a detected image format, or `undefined` for a
15
+ * format with no mapping.
16
+ */
17
+ function imageMimeType(type) {
18
+ if (type == null) return void 0;
19
+ return IMAGE_MIME_TYPES[type.toLowerCase()];
20
+ }
21
+ /**
22
+ * Turn a configured image URL into an absolute one.
28
23
  *
29
- * This class handles the conversion of flexible OG image configuration formats
30
- * (strings or metadata objects) into complete `OpenGraphImageMetadata` objects
31
- * with resolved URLs and auto-detected dimensions for local images.
24
+ * @returns The absolute URL, or `undefined` when the input is neither an
25
+ * absolute `http(s)` URL nor a site-root-relative path. A bare relative path
26
+ * is deliberately rejected rather than guessed at there is no base to
27
+ * resolve it against that would not silently produce a broken link.
28
+ */
29
+ function resolveOgUrl(siteUrl, url) {
30
+ if (url.startsWith("http://") || url.startsWith("https://")) return url;
31
+ if (url.startsWith("/")) return `${siteUrl}${url}`;
32
+ }
33
+ /**
34
+ * Derive the site URL prefix from RSPress's own config.
32
35
  *
33
36
  * @remarks
34
- * The resolver supports two input formats:
35
- * - **String format**: A URL or path that will be resolved and optionally enhanced
36
- * with auto-detected dimensions if pointing to a local file.
37
- * - **Object format**: Detailed metadata with explicit properties that will be
38
- * validated and URL-resolved.
37
+ * Replaces the plugin's former `siteUrl` option. RSPress already knows where a
38
+ * site is deployed {@link https://rspress.rs/api/config/config-basic#siteorigin | `siteOrigin`}
39
+ * plus `base` so asking for it a second time invited the two to disagree, and
40
+ * a plugin-level answer that contradicted the site's own would silently emit
41
+ * canonical and `og:url` tags pointing at a host the site is not served from.
39
42
  *
40
- * @example Basic usage with a relative path
41
- * ```typescript
42
- * const resolver = new OpenGraphResolver({
43
- * siteUrl: "https://example.com",
44
- * docsRoot: "/path/to/docs"
45
- * });
43
+ * RSPress concatenates as `siteOrigin + base + routePath`, and **this follows
44
+ * its documented fallback exactly**: with no `siteOrigin`, RSPress uses
45
+ * `base + routePath`. So an unset origin yields a ROOT-RELATIVE prefix rather
46
+ * than nothing.
46
47
  *
47
- * const metadata = await resolver.resolve(
48
- * "/images/og-api.png",
49
- * "my-package",
50
- * "MyClass"
51
- * );
52
- * // Result: { url: "https://example.com/images/og-api.png", width: 1200, height: 630, ... }
53
- * ```
48
+ * That fallback is what makes the tags inspectable in `rspress dev`, where the
49
+ * site is served from `localhost` and no configured origin could be correct
50
+ * anyway. A root-relative `/images/og.png` resolves against the page's own
51
+ * origin in the browser; it is a *relative* path (`images/og.png`, no leading
52
+ * slash) that has no base to resolve against, and this never emits one.
54
53
  *
55
- * @example Using detailed configuration
56
- * ```typescript
57
- * const metadata = await resolver.resolve(
58
- * {
59
- * url: "/images/og.png",
60
- * alt: "Custom alt text",
61
- * width: 1200,
62
- * height: 630
63
- * },
64
- * "my-package"
65
- * );
66
- * ```
54
+ * @returns The prefix to put in front of a route that already begins with `/`.
55
+ * `""` when the site declares neither `siteOrigin` nor a non-root `base`, which
56
+ * leaves every URL root-relative. Never has a trailing slash, since every
57
+ * caller appends a route starting with `/`.
67
58
  */
68
- var OpenGraphResolver = class {
69
- siteUrl;
70
- docsRoot;
71
- /**
72
- * Creates a new OpenGraphResolver instance.
73
- *
74
- * @param options - Configuration options for the resolver.
75
- * `siteUrl`: Base URL for the website (e.g., "https://example.com"),
76
- * used to construct absolute URLs from relative paths.
77
- * `docsRoot`: Optional root directory for documentation files;
78
- * when provided, enables auto-detection of image dimensions for local files
79
- * by looking in the `public` subdirectory.
80
- */
81
- constructor(options) {
82
- this.siteUrl = options.siteUrl;
83
- if (options.docsRoot != null) this.docsRoot = options.docsRoot;
84
- }
85
- /**
86
- * Resolves an Open Graph image configuration into complete metadata.
87
- *
88
- * Handles both string URLs/paths and detailed metadata objects, converting them
89
- * into fully-qualified `OpenGraphImageMetadata` with absolute URLs.
90
- *
91
- * @param config - The OG image configuration to resolve. Can be:
92
- * - A string URL (absolute or relative path starting with `/`)
93
- * - An `OpenGraphImageMetadata` object with explicit properties
94
- * - `undefined` to indicate no OG image
95
- * @param packageName - The package name for generating default alt text
96
- * @param apiName - Optional API name for more descriptive alt text
97
- * @returns Resolved metadata with absolute URLs, or `undefined` if:
98
- * - `config` is `undefined`
99
- * - The URL format is invalid
100
- *
101
- * @example Resolve a relative path
102
- * ```typescript
103
- * const metadata = await resolver.resolve("/images/og.png", "my-lib");
104
- * // Returns: { url: "https://example.com/images/og.png", alt: "my-lib API Documentation", ... }
105
- * ```
106
- *
107
- * @example Resolve an absolute URL
108
- * ```typescript
109
- * const metadata = await resolver.resolve("https://cdn.example.com/og.png", "my-lib");
110
- * // Returns: { url: "https://cdn.example.com/og.png", alt: "my-lib API Documentation" }
111
- * ```
112
- */
113
- async resolve(config, packageName, apiName) {
114
- if (!config) return;
115
- if (typeof config === "object") return this.resolveFromMetadata(config, packageName, apiName);
116
- return this.resolveFromString(config, packageName, apiName);
117
- }
118
- /**
119
- * Resolves a metadata object configuration into complete OG image metadata.
120
- *
121
- * Validates and resolves URLs in the provided metadata object, ensuring all
122
- * URLs are absolute and properly formatted.
123
- *
124
- * @param metadata - The metadata object containing OG image properties
125
- * @param packageName - Package name for default alt text generation
126
- * @param apiName - Optional API name for more descriptive alt text
127
- * @returns Resolved metadata with absolute URLs, or `undefined` if URL is invalid
128
- */
129
- async resolveFromMetadata(metadata, packageName, apiName) {
130
- const { url, secureUrl, type, width, height, alt } = metadata;
131
- const resolvedUrl = this.resolveUrl(url);
132
- if (!resolvedUrl) {
133
- emitEvent(PluginEvent.ConfigValidationWarning({
134
- ctx: { buildId: currentBuildId },
135
- field: "ogImage.url",
136
- value: url,
137
- reason: "invalid URL format",
138
- level: "warn"
139
- }));
140
- return;
141
- }
142
- let resolvedSecureUrl;
143
- if (secureUrl) {
144
- if (secureUrl.startsWith("https://")) resolvedSecureUrl = secureUrl;
145
- else emitEvent(PluginEvent.ConfigValidationWarning({
146
- ctx: { buildId: currentBuildId },
147
- field: "ogImage.secureUrl",
148
- value: secureUrl,
149
- reason: "secureUrl must be absolute HTTPS",
150
- level: "warn"
151
- }));
152
- }
153
- return {
154
- url: resolvedUrl,
155
- secureUrl: resolvedSecureUrl,
156
- type,
157
- width,
158
- height,
159
- alt: alt ?? this.generateAltText(packageName, apiName)
160
- };
161
- }
162
- /**
163
- * Resolves a string URL/path into complete OG image metadata.
164
- *
165
- * For relative paths pointing to local files, this method will attempt to:
166
- * 1. Locate the file in the docs `public` directory
167
- * 2. Read the image dimensions using `image-size`
168
- * 3. Determine the MIME type from the file extension
169
- *
170
- * @param imageUrl - The image URL or path to resolve
171
- * @param packageName - Package name for alt text generation
172
- * @param apiName - Optional API name for more descriptive alt text
173
- * @returns Resolved metadata with auto-detected dimensions for local files,
174
- * or `undefined` if the URL format is invalid
175
- */
176
- async resolveFromString(imageUrl, packageName, apiName) {
177
- const resolvedUrl = this.resolveUrl(imageUrl);
178
- if (!resolvedUrl) {
179
- emitEvent(PluginEvent.ConfigValidationWarning({
180
- ctx: { buildId: currentBuildId },
181
- field: "ogImage",
182
- value: imageUrl,
183
- reason: "invalid URL format",
184
- level: "warn"
185
- }));
186
- return;
187
- }
188
- const localPath = this.findLocalImage(imageUrl);
189
- const dimensions = localPath ? await this.readImageDimensions(localPath) : void 0;
190
- return {
191
- url: resolvedUrl,
192
- type: dimensions?.type,
193
- width: dimensions?.width,
194
- height: dimensions?.height,
195
- alt: this.generateAltText(packageName, apiName)
196
- };
197
- }
198
- /**
199
- * Resolves a URL string to an absolute URL.
200
- *
201
- * @param url - The URL to resolve (absolute URL or relative path)
202
- * @returns The absolute URL, or `undefined` if the format is invalid
203
- */
204
- resolveUrl(url) {
205
- if (url.startsWith("http://") || url.startsWith("https://")) return url;
206
- if (url.startsWith("/")) return `${this.siteUrl}${url}`;
207
- }
208
- /**
209
- * Attempts to find a local image file in the docs public directory.
210
- *
211
- * @param imagePath - The relative image path (starting with `/`)
212
- * @returns The absolute file path if found, or `undefined` if not found
213
- * or if `docsRoot` is not configured
214
- */
215
- findLocalImage(imagePath) {
216
- if (!this.docsRoot || !imagePath.startsWith("/")) return;
217
- const publicPath = path.join(this.docsRoot, "public", imagePath);
218
- if (fs.existsSync(publicPath)) return publicPath;
219
- }
220
- /**
221
- * Reads image dimensions and type from a local file.
222
- *
223
- * @param filePath - Absolute path to the image file
224
- * @returns Object containing width, height, and MIME type if successful,
225
- * or `undefined` if the file cannot be read or analyzed
226
- */
227
- async readImageDimensions(filePath) {
228
- try {
229
- const dimensions = await imageSizeFromFile(filePath);
230
- let mimeType;
231
- if (dimensions.type) mimeType = IMAGE_MIME_TYPES[dimensions.type.toLowerCase()];
232
- return {
233
- width: dimensions.width,
234
- height: dimensions.height,
235
- ...mimeType != null ? { type: mimeType } : {}
236
- };
237
- } catch (error) {
238
- emitEvent(PluginEvent.ConfigValidationWarning({
239
- ctx: { buildId: currentBuildId },
240
- field: "ogImage",
241
- value: filePath,
242
- reason: error.message ?? String(error),
243
- level: "warn"
244
- }));
245
- return;
246
- }
247
- }
248
- /**
249
- * Generates descriptive alt text for the OG image.
250
- *
251
- * @param packageName - The package name
252
- * @param apiName - Optional API name for more specific text
253
- * @returns Generated alt text string
254
- */
255
- generateAltText(packageName, apiName) {
256
- if (apiName) return `${apiName} - ${packageName} API Documentation`;
257
- return `${packageName} API Documentation`;
258
- }
259
- /**
260
- * Creates complete Open Graph metadata for an API documentation page.
261
- *
262
- * This static factory method builds a complete `OpenGraphMetadata` object
263
- * suitable for inclusion in page frontmatter, combining resolved image
264
- * metadata with article-specific information.
265
- *
266
- * @param options - Configuration for the OG metadata.
267
- * Includes `siteUrl`, `pageRoute`, `description`, `publishedTime`,
268
- * `modifiedTime`, `section`, `packageName`, and optional `ogImage`.
269
- * @returns Complete Open Graph metadata object
270
- *
271
- * @example
272
- * ```typescript
273
- * const ogMetadata = OpenGraphResolver.createPageMetadata({
274
- * siteUrl: "https://example.com",
275
- * pageRoute: "/api/classes/MyClass",
276
- * description: "MyClass provides...",
277
- * publishedTime: "2024-01-15T10:00:00Z",
278
- * modifiedTime: "2024-01-20T15:30:00Z",
279
- * section: "Classes",
280
- * packageName: "my-library",
281
- * ogImage: resolvedImageMetadata
282
- * });
283
- * ```
284
- */
285
- static createPageMetadata(options) {
286
- return {
287
- siteUrl: options.siteUrl,
288
- pageRoute: options.pageRoute,
289
- description: options.description,
290
- publishedTime: options.publishedTime,
291
- modifiedTime: options.modifiedTime,
292
- section: options.section,
293
- tags: [
294
- "TypeScript",
295
- "API",
296
- options.packageName
297
- ],
298
- ...options.ogImage != null ? { ogImage: options.ogImage } : {},
299
- ogType: "article"
300
- };
301
- }
302
- };
59
+ function deriveSiteUrl(siteOrigin, base) {
60
+ const origin = (siteOrigin ?? "").trim().replace(/\/+$/, "");
61
+ const path = (base ?? "/").trim();
62
+ return `${origin}${path === "" || path === "/" ? "" : `/${path.replace(/^\/+/, "").replace(/\/+$/, "")}`}`;
63
+ }
64
+ /** Descriptive alt text for a package's (or one API's) OG image. */
65
+ function ogAltText(packageName, apiName) {
66
+ return apiName ? `${apiName} - ${packageName} API Documentation` : `${packageName} API Documentation`;
67
+ }
68
+ /**
69
+ * Assemble the complete Open Graph metadata for one documentation page.
70
+ *
71
+ * @remarks
72
+ * Was `OpenGraphResolver.createPageMetadata`. It never touched the resolver's
73
+ * instance state, so it is a free function now rather than a static on a class
74
+ * that no longer exists.
75
+ */
76
+ function createPageMetadata(options) {
77
+ return {
78
+ siteUrl: options.siteUrl,
79
+ pageRoute: options.pageRoute,
80
+ description: options.description,
81
+ publishedTime: options.publishedTime,
82
+ modifiedTime: options.modifiedTime,
83
+ section: options.section,
84
+ tags: [
85
+ "TypeScript",
86
+ "API",
87
+ options.packageName
88
+ ],
89
+ ...options.ogImage != null ? { ogImage: options.ogImage } : {},
90
+ ogType: "article"
91
+ };
92
+ }
303
93
 
304
94
  //#endregion
305
- export { OpenGraphResolver, setOgResolverEventEmitter };
95
+ export { createPageMetadata, deriveSiteUrl, imageMimeType, ogAltText, resolveOgUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rspress-plugin-api-extractor",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "RSPress plugin for generating API documentation from TypeScript API Extractor models",
6
6
  "keywords": [
@@ -53,16 +53,15 @@
53
53
  "@microsoft/api-extractor-model": "^7.33.11",
54
54
  "@shikijs/twoslash": "^4.4.3",
55
55
  "@tsdoctor/bundle": "0.2.0",
56
- "@tsdoctor/model": "0.2.2",
56
+ "@tsdoctor/model": "0.3.0",
57
57
  "@tsdoctor/registry": "0.2.1",
58
- "@tsdoctor/snapshot": "0.1.1",
58
+ "@tsdoctor/snapshot": "0.2.0",
59
59
  "@typescript/vfs": "^1.6.4",
60
60
  "clsx": "^2.1.1",
61
61
  "effect": "4.0.0-rc.109",
62
62
  "hast-util-to-jsx-runtime": "^2.3.6",
63
63
  "image-size": "^2.0.2",
64
64
  "ioredis": "^5.7.0",
65
- "mdast-util-from-markdown": "^2.0.3",
66
65
  "mdast-util-to-hast": "^13.2.1",
67
66
  "open": "^11.0.0",
68
67
  "prettier": "^3.9.6",
@@ -43,6 +43,24 @@ function deriveOutputPaths(input) {
43
43
  }
44
44
  return results;
45
45
  }
46
+ /**
47
+ * The API scope key derived from a base route.
48
+ *
49
+ * @remarks
50
+ * Load-bearing and previously duplicated. Config resolution registers each
51
+ * API's Twoslash environment under this key and the build program looks it up
52
+ * by the same key; if the two derivations disagree, every lookup misses and
53
+ * `getTransformer` falls back to the build-wide environment. Per-scope
54
+ * type-checking degrades to build-wide with no error and nothing visibly
55
+ * wrong in the output — the failure mode is silent, which is why one
56
+ * definition matters more here than the duplication was costing.
57
+ *
58
+ * Falls back to the package name so a single-API site mounted at `/` still
59
+ * gets a non-empty scope.
60
+ */
61
+ function apiScopeOf(baseRoute, packageName) {
62
+ return baseRoute.replace(/^\//, "").split("/")[0] || packageName;
63
+ }
46
64
 
47
65
  //#endregion
48
- export { deriveOutputPaths, normalizeBaseRoute, unscopedName };
66
+ export { apiScopeOf, deriveOutputPaths, normalizeBaseRoute, unscopedName };