rspress-plugin-api-extractor 0.10.0 → 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.
@@ -1,26 +1,14 @@
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
- /**
9
- * Module-level emitter seam. Default is a no-op; wire in `setEventEmitter(emitSync)`
10
- * from plugin.ts right after the runtime emitter is created so that Twoslash error
11
- * events flow through the EventBus even though they fire in a sync Shiki callback
12
- * outside any Effect fiber.
13
- */
14
- let emitEvent = () => {};
15
- let currentBuildId = "";
16
- /**
17
- * Inject the runtime-bound emitter into the Twoslash module.
18
- * Call this right after `makeRuntimeEmitter` in plugin.ts.
19
- */
20
- function setEventEmitter(fn, buildId = "") {
21
- emitEvent = fn;
22
- currentBuildId = buildId;
23
- }
11
+ /* v8 ignore start -- Shiki/Twoslash integration, requires full highlighter setup for testing */
24
12
  /**
25
13
  * Module-level type routes map for resolving link references.
26
14
  * This is set by TwoslashManager.setTypeRoutes() before initialization.
@@ -115,8 +103,10 @@ function addLinkClasses(node) {
115
103
  * Render markdown content to HAST (Hypertext Abstract Syntax Tree) elements.
116
104
  *
117
105
  * This function converts markdown strings (from TSDoc comments) into HAST nodes
118
- * that can be rendered in Twoslash hover popups. It uses mdast-util-from-markdown
119
- * 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.
120
110
  *
121
111
  * TSDoc link references are transformed to markdown links before parsing.
122
112
  * Whitespace is normalized for proper inline display.
@@ -130,8 +120,12 @@ function renderMarkdown(markdown) {
130
120
  try {
131
121
  let transformed = transformTsDocLinks(markdown);
132
122
  transformed = transformed.split(/\n\n+/).map((para) => para.replace(/\s+/g, " ").trim()).filter((para) => para.length > 0).join("\n\n");
133
- const mdast = fromMarkdown(transformed);
134
- 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));
135
129
  if (hast && "children" in hast) {
136
130
  const children = hast.children;
137
131
  for (const child of children) addLinkClasses(child);
@@ -232,12 +226,63 @@ function renderMarkdownInline(markdown, context) {
232
226
  * blocks routed to the right one. Keys are sorted, so two configurations that
233
227
  * differ only in property order share an environment.
234
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
+ */
235
273
  function twoslashConfigKey(options) {
236
274
  const entries = Object.entries(options).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
237
275
  return JSON.stringify(entries);
238
276
  }
239
- var TwoslashManager = class TwoslashManager {
240
- static instance = null;
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 {
241
286
  /**
242
287
  * Transformers keyed by compiler-config fingerprint.
243
288
  *
@@ -265,7 +310,7 @@ var TwoslashManager = class TwoslashManager {
265
310
  * Resolved compiler options captured at initialize() time.
266
311
  * Returned by compilerOptionsSnapshot() for TwoslashCheckFailed events.
267
312
  */
268
- _resolvedCompilerOptions = DEFAULT_COMPILER_OPTIONS;
313
+ _resolvedCompilerOptions = toProgrammaticCompilerOptions(DEFAULT_COMPILER_OPTIONS);
269
314
  /**
270
315
  * Path of the file whose code block is currently being processed.
271
316
  * Used to attribute Twoslash error events to a source file. Defaults to
@@ -274,31 +319,20 @@ var TwoslashManager = class TwoslashManager {
274
319
  */
275
320
  currentFilePath = "unknown";
276
321
  /**
277
- * Private constructor to enforce singleton pattern
278
- */
279
- constructor() {}
280
- /**
281
- * Get the singleton instance of TwoslashManager
282
- */
283
- static getInstance() {
284
- if (!TwoslashManager.instance) TwoslashManager.instance = new TwoslashManager();
285
- return TwoslashManager.instance;
286
- }
287
- /**
288
- * Initialize the Twoslash transformer with a TypeScript environment cache.
289
- * This enables type-aware documentation with hover information and IntelliSense.
322
+ * Build an environment for a configuration, or return if one exists.
290
323
  *
291
- * @param vfs - Virtual file system mapping file paths to .d.ts content
292
- * @param _reserved - Reserved parameter (previously errorStatsCollector, now tracked via Effect Metrics)
293
- * @param _reserved2 - Reserved parameter (previously logger, now uses console)
294
- * @param tsEnvCache - TypeScript virtual environment cache for reusing language services
295
- * @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.
296
330
  */
297
- initialize(vfs, _reserved, _reserved2, tsEnvCache, compilerOptions, typesCache) {
331
+ registerEnvironment({ vfs, compilerOptions, typesCache }) {
298
332
  const extraFiles = {};
299
333
  for (const [path, content] of vfs.entries()) extraFiles[path] = content;
300
334
  this._vfsKeys = Array.from(vfs.keys());
301
- const resolvedOptions = compilerOptions ?? DEFAULT_COMPILER_OPTIONS;
335
+ const resolvedOptions = toProgrammaticCompilerOptions(compilerOptions ?? DEFAULT_COMPILER_OPTIONS);
302
336
  this._resolvedCompilerOptions = resolvedOptions;
303
337
  const configKey = twoslashConfigKey(resolvedOptions);
304
338
  if (this.defaultConfigKey === null) this.defaultConfigKey = configKey;
@@ -312,7 +346,6 @@ var TwoslashManager = class TwoslashManager {
312
346
  renderMarkdown,
313
347
  renderMarkdownInline
314
348
  }),
315
- ...tsEnvCache != null ? { cache: tsEnvCache } : {},
316
349
  ...typesCache != null ? { typesCache } : {},
317
350
  twoslashOptions: {
318
351
  extraFiles,
@@ -332,7 +365,7 @@ var TwoslashManager = class TwoslashManager {
332
365
  * under, so its code blocks are type-checked with that configuration.
333
366
  */
334
367
  registerScope(apiScope, compilerOptions) {
335
- this.scopeConfigs.set(apiScope, twoslashConfigKey(compilerOptions));
368
+ this.scopeConfigs.set(apiScope, twoslashConfigKey(toProgrammaticCompilerOptions(compilerOptions)));
336
369
  }
337
370
  /**
338
371
  * Get the Twoslash transformer for an API scope.
@@ -342,7 +375,7 @@ var TwoslashManager = class TwoslashManager {
342
375
  * route, and type-checking it under some configuration beats not checking it.
343
376
  * Returns null before any environment is initialized.
344
377
  */
345
- getTransformer(apiScope) {
378
+ transformerFor(apiScope) {
346
379
  const key = (apiScope != null ? this.scopeConfigs.get(apiScope) : void 0) ?? this.defaultConfigKey;
347
380
  return key != null ? this.environments.get(key) ?? null : null;
348
381
  }
@@ -363,12 +396,6 @@ var TwoslashManager = class TwoslashManager {
363
396
  this.scopeConfigs.clear();
364
397
  this.defaultConfigKey = null;
365
398
  }
366
- /**
367
- * Reset the singleton instance (useful for testing)
368
- */
369
- static reset() {
370
- TwoslashManager.instance = null;
371
- }
372
399
  /** Returns VFS keys snapshotted at initialize() time. Empty array before initialize(). */
373
400
  vfsKeysSnapshot() {
374
401
  return this._vfsKeys;
@@ -381,9 +408,9 @@ var TwoslashManager = class TwoslashManager {
381
408
  const message = error instanceof Error ? error.message : String(error);
382
409
  const match = /TS(\d+)/.exec(message);
383
410
  const tsCode = match ? Number(match[1]) : 0;
384
- emitEvent(PluginEvent.TwoslashDiagnostic({
411
+ emitSync(PluginEvent.TwoslashDiagnostic({
385
412
  ctx: {
386
- buildId: currentBuildId,
413
+ buildId: syncBuildId(),
387
414
  file
388
415
  },
389
416
  level: "warn",
@@ -394,9 +421,9 @@ var TwoslashManager = class TwoslashManager {
394
421
  message,
395
422
  snippet: ""
396
423
  }));
397
- emitEvent(PluginEvent.TwoslashCheckFailed({
424
+ emitSync(PluginEvent.TwoslashCheckFailed({
398
425
  ctx: {
399
- buildId: currentBuildId,
426
+ buildId: syncBuildId(),
400
427
  file
401
428
  },
402
429
  level: "trace",
@@ -410,34 +437,30 @@ var TwoslashManager = class TwoslashManager {
410
437
  * Test seam: drive `handleTwoslashError` directly without going through the Shiki transformer.
411
438
  * @internal
412
439
  */
413
- handleTwoslashErrorForTest(error, code, file) {
440
+ reportErrorForTest(error, code, file) {
414
441
  this.handleTwoslashError(error, code, file);
415
442
  }
416
- /**
417
- * Set the type routes map for resolving link references in hover docs.
418
- * This should be called before initialize() to enable type linking.
419
- *
420
- * @param routes - Map of type names to their documentation URLs
421
- */
422
- static setTypeRoutes(routes) {
423
- typeRoutes = routes;
424
- }
425
- /**
426
- * Add routes to the existing type routes map.
427
- * Useful for adding routes from multiple packages.
428
- *
429
- * @param routes - Map of type names to their documentation URLs
430
- */
431
- static addTypeRoutes(routes) {
432
- for (const [name, route] of routes) typeRoutes.set(name, route);
433
- }
434
- /**
435
- * Clear the type routes map (useful for testing)
436
- */
437
- static clearTypeRoutes() {
438
- typeRoutes.clear();
439
- }
440
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
+ }
441
464
 
442
465
  //#endregion
443
- 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,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 };