rspress-plugin-api-extractor 0.8.8 → 0.9.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.
- package/README.md +11 -11
- package/api-extracted-package.js +1 -1
- package/build-program.js +3 -3
- package/build-stages.js +30 -31
- package/config-helpers.js +61 -21
- package/errors.js +1 -7
- package/frontmatter.js +149 -0
- package/index.d.ts +11 -2
- package/layers/ConfigServiceLive.js +53 -39
- package/layers/TypeRegistryServiceLive.js +11 -6
- package/markdown/helpers.js +40 -69
- package/markdown/index.js +2 -2
- package/markdown/page-generators/class-page.js +40 -42
- package/markdown/page-generators/enum-page.js +15 -15
- package/markdown/page-generators/function-page.js +18 -20
- package/markdown/page-generators/interface-page.js +41 -43
- package/markdown/page-generators/namespace-page.js +22 -24
- package/markdown/page-generators/type-alias-page.js +14 -16
- package/markdown/page-generators/variable-page.js +14 -16
- package/markdown/prose-linker.js +22 -0
- package/model-loader.js +59 -113
- package/package.json +14 -8
- package/plugin.js +1 -5
- package/runtime/components/EnumMembersTable/index.css +18 -18
- package/runtime/components/EnumMembersTable/index.module.js +3 -3
- package/runtime/components/ExampleBlock/index.css +2 -2
- package/runtime/components/ExampleBlock/index.module.js +2 -2
- package/runtime/components/MemberSignature/index.css +6 -6
- package/runtime/components/MemberSignature/index.module.js +4 -4
- package/runtime/components/ParametersTable/index.css +19 -19
- package/runtime/components/ParametersTable/index.module.js +3 -3
- package/runtime/components/SignatureBlock/index.css +6 -6
- package/runtime/components/SignatureBlock/index.module.js +4 -4
- package/runtime/components/SignatureCode/index.css +9 -9
- package/runtime/components/SignatureCode/index.module.js +3 -3
- package/runtime/components/SignatureToolbar/index.css +18 -18
- package/runtime/components/SignatureToolbar/index.module.js +7 -7
- package/runtime/components/buttons/index.css +5 -5
- package/runtime/components/buttons/index.module.js +2 -2
- package/shiki-transformer.js +3 -3
- package/sync-node-fs.js +80 -0
- package/tsdoc-metadata.json +1 -1
- package/twoslash-transformer.js +1 -1
- package/content-hash.js +0 -79
- package/formatter.js +0 -69
- package/layers/SnapshotServiceLive.js +0 -92
- package/loader.js +0 -200
- package/markdown/cross-linker.js +0 -157
- package/migrations/001_create_snapshots.js +0 -25
- package/multi-entry-resolver.js +0 -70
- package/route-collisions.js +0 -44
- package/services/SnapshotService.js +0 -7
- package/synthetic-bases.js +0 -74
package/README.md
CHANGED
|
@@ -56,17 +56,17 @@ The plugin reads your `.api.json` model and writes one MDX page per public API i
|
|
|
56
56
|
|
|
57
57
|
## Documentation
|
|
58
58
|
|
|
59
|
-
- [Getting started](https://github.com/spencerbeggs/
|
|
60
|
-
- [Configuration](https://github.com/spencerbeggs/
|
|
61
|
-
- [Config helpers](https://github.com/spencerbeggs/
|
|
62
|
-
- [Single package](https://github.com/spencerbeggs/
|
|
63
|
-
- [Multi-package](https://github.com/spencerbeggs/
|
|
64
|
-
- [Versioned](https://github.com/spencerbeggs/
|
|
65
|
-
- [i18n](https://github.com/spencerbeggs/
|
|
66
|
-
- [Multi-entry points](https://github.com/spencerbeggs/
|
|
67
|
-
- [LLMs](https://github.com/spencerbeggs/
|
|
68
|
-
- [Runtime components](https://github.com/spencerbeggs/
|
|
69
|
-
- [Troubleshooting](https://github.com/spencerbeggs/
|
|
59
|
+
- [Getting started](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/01-getting-started.md) — Install, minimal config, first build.
|
|
60
|
+
- [Configuration](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/02-configuration.md) — Full plugin-options reference.
|
|
61
|
+
- [Config helpers](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/03-config-helpers.md) — `api.fromDir` and `apis.fromDir` for discovering config from package folders.
|
|
62
|
+
- [Single package](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/04-single-package.md) — The single-API recipe.
|
|
63
|
+
- [Multi-package](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/05-multi-package.md) — The multi-API portal recipe.
|
|
64
|
+
- [Versioned](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/06-versioned.md) — Documenting major versions side by side.
|
|
65
|
+
- [i18n](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/07-i18n.md) — Internationalized documentation.
|
|
66
|
+
- [Multi-entry points](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/08-multi-entry-points.md) — Deduplication, "Available from" and route collisions.
|
|
67
|
+
- [LLMs](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/09-llms.md) — Per-package `llms*.txt` files and assistant actions.
|
|
68
|
+
- [Runtime components](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/10-runtime-components.md) — The runtime components and live `with-api` code blocks.
|
|
69
|
+
- [Troubleshooting](https://github.com/spencerbeggs/tsdoctor/blob/main/docs/11-troubleshooting.md) — Route collisions, forgotten exports, Twoslash errors and stale caches.
|
|
70
70
|
|
|
71
71
|
## License
|
|
72
72
|
|
package/api-extracted-package.js
CHANGED
package/build-program.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { setProseLinker } from "./markdown/prose-linker.js";
|
|
2
2
|
import "./markdown/index.js";
|
|
3
|
-
import { SnapshotService } from "./services/SnapshotService.js";
|
|
4
3
|
import { buildPipelineForApi, cleanupAndCommit, prepareWorkItems, writeMetadata } from "./build-stages.js";
|
|
5
4
|
import { withPhase } from "./observability/spans.js";
|
|
6
5
|
import { TwoslashManager } from "./twoslash-transformer.js";
|
|
7
6
|
import { VfsRegistry } from "./vfs-registry.js";
|
|
8
7
|
import path from "node:path";
|
|
8
|
+
import { SnapshotService } from "@tsdoctor/snapshot";
|
|
9
9
|
import { Effect, FileSystem } from "effect";
|
|
10
10
|
|
|
11
11
|
//#region src/build-program.ts
|
|
@@ -44,7 +44,7 @@ function generateApiDocs(apiConfig, buildContext, fileContextMap) {
|
|
|
44
44
|
baseRoute,
|
|
45
45
|
packageName
|
|
46
46
|
})), thresholds);
|
|
47
|
-
|
|
47
|
+
setProseLinker(crossLinkData.routes);
|
|
48
48
|
const apiScope = baseRoute.replace(/^\//, "").split("/")[0] || packageName;
|
|
49
49
|
shikiCrossLinker.reinitialize(crossLinkData.routes, crossLinkData.kinds, apiScope);
|
|
50
50
|
TwoslashManager.addTypeRoutes(crossLinkData.routes);
|
package/build-stages.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseFrontmatter, stringifyFrontmatter } from "./frontmatter.js";
|
|
2
2
|
import { PluginEvent } from "./observability/events.js";
|
|
3
3
|
import { emit } from "./observability/EventBus.js";
|
|
4
4
|
import { BuildMetrics } from "./layers/build-metrics.js";
|
|
5
5
|
import "./layers/ObservabilityLive.js";
|
|
6
|
-
import { ApiParser } from "./loader.js";
|
|
7
6
|
import { generateFrontmatter } from "./markdown/helpers.js";
|
|
8
7
|
import { ClassPageGenerator } from "./markdown/page-generators/class-page.js";
|
|
9
8
|
import { EnumPageGenerator } from "./markdown/page-generators/enum-page.js";
|
|
@@ -14,15 +13,12 @@ import { NamespacePageGenerator } from "./markdown/page-generators/namespace-pag
|
|
|
14
13
|
import { TypeAliasPageGenerator } from "./markdown/page-generators/type-alias-page.js";
|
|
15
14
|
import { VariablePageGenerator } from "./markdown/page-generators/variable-page.js";
|
|
16
15
|
import "./markdown/index.js";
|
|
17
|
-
import { resolveEntryPoints } from "./multi-entry-resolver.js";
|
|
18
16
|
import { OpenGraphResolver } from "./og-resolver.js";
|
|
19
|
-
import { detectRouteCollisions, formatRouteCollisionError } from "./route-collisions.js";
|
|
20
|
-
import { SnapshotService } from "./services/SnapshotService.js";
|
|
21
|
-
import { BASE_CLASS_ANCHOR, detectSyntheticBases } from "./synthetic-bases.js";
|
|
22
17
|
import path from "node:path";
|
|
18
|
+
import { SnapshotService, hashContent, hashFrontmatter } from "@tsdoctor/snapshot";
|
|
23
19
|
import { Effect, FileSystem, Metric, Stream } from "effect";
|
|
24
20
|
import { ApiItemKind } from "@microsoft/api-extractor-model";
|
|
25
|
-
import
|
|
21
|
+
import { ApiItems, EntryPoints, Routes, SyntheticBases } from "@tsdoctor/model";
|
|
26
22
|
|
|
27
23
|
//#region src/build-stages.ts
|
|
28
24
|
/**
|
|
@@ -63,60 +59,60 @@ function setBuildStagesEventEmitter(fn, buildId = "") {
|
|
|
63
59
|
currentBuildId = buildId;
|
|
64
60
|
}
|
|
65
61
|
/**
|
|
66
|
-
* Sanitize a display name to create a valid HTML ID.
|
|
67
|
-
* Mirrors the logic in MarkdownCrossLinker.sanitizeId().
|
|
68
|
-
*/
|
|
69
|
-
function sanitizeId(displayName) {
|
|
70
|
-
return displayName.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/^-+|-+$/g, "");
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
62
|
* Prepare the flat list of WorkItems to process and the cross-link data maps.
|
|
74
63
|
*
|
|
75
64
|
* This function:
|
|
76
65
|
* 1. Categorizes API items from the model
|
|
77
|
-
* 2. Builds cross-link routes and kinds maps
|
|
66
|
+
* 2. Builds cross-link routes and kinds maps for the prose and Shiki cross-linkers
|
|
78
67
|
* 3. Extracts namespace members and adds their routes (with collision detection)
|
|
79
68
|
* 4. Flattens all items into a single WorkItem[]
|
|
80
69
|
*
|
|
81
|
-
* NOTE: This function does NOT
|
|
70
|
+
* NOTE: This function does NOT install the prose linker. The caller
|
|
82
71
|
* is responsible for passing the returned crossLinkData to the cross-linker and
|
|
83
72
|
* Shiki cross-linker as needed.
|
|
84
73
|
*/
|
|
85
74
|
function prepareWorkItems(input) {
|
|
86
75
|
const { apiPackage, categories, baseRoute } = input;
|
|
87
|
-
const resolvedItems =
|
|
88
|
-
const syntheticBases =
|
|
76
|
+
const resolvedItems = EntryPoints.resolve(apiPackage);
|
|
77
|
+
const syntheticBases = SyntheticBases.detect(resolvedItems.map((r) => r.item));
|
|
89
78
|
const docItems = syntheticBases.bases.size ? resolvedItems.filter((r) => !syntheticBases.bases.has(r.item)) : resolvedItems;
|
|
90
79
|
const resolvedLookup = /* @__PURE__ */ new Map();
|
|
91
80
|
for (const resolved of docItems) {
|
|
92
81
|
const key = `${resolved.item.displayName}::${resolved.item.kind}`;
|
|
93
82
|
resolvedLookup.set(key, resolved);
|
|
94
83
|
}
|
|
95
|
-
const items =
|
|
96
|
-
const
|
|
84
|
+
const { items, uncategorized } = ApiItems.categorize(docItems, categories);
|
|
85
|
+
for (const skipped of uncategorized) emitEvent(PluginEvent.ItemSkipped({
|
|
86
|
+
ctx: { buildId: currentBuildId },
|
|
87
|
+
item: skipped.displayName,
|
|
88
|
+
kind: String(skipped.kind),
|
|
89
|
+
reason: "uncategorized",
|
|
90
|
+
level: "warn"
|
|
91
|
+
}));
|
|
92
|
+
const namespaceMembers = ApiItems.namespaceMembers(docItems);
|
|
97
93
|
const candidates = [];
|
|
98
|
-
for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) candidates.push({
|
|
94
|
+
for (const [categoryKey, categoryConfig] of Object.entries(categories)) for (const item of items[categoryKey] || []) candidates.push(new Routes.RouteCandidate({
|
|
99
95
|
id: `${item.displayName}::${item.kind}`,
|
|
100
96
|
displayName: item.displayName,
|
|
101
97
|
folder: categoryConfig.folderName,
|
|
102
98
|
baseName: item.displayName.toLowerCase(),
|
|
103
99
|
kind: String(item.kind),
|
|
104
100
|
canonicalRef: item.canonicalReference?.toString() ?? item.displayName
|
|
105
|
-
});
|
|
101
|
+
}));
|
|
106
102
|
for (const nsMember of namespaceMembers) {
|
|
107
103
|
const nsCategoryEntry = Object.entries(categories).find(([, config]) => config.itemKinds?.includes(nsMember.item.kind));
|
|
108
104
|
if (!nsCategoryEntry) continue;
|
|
109
105
|
const [, nsCategoryConfig] = nsCategoryEntry;
|
|
110
|
-
candidates.push({
|
|
106
|
+
candidates.push(new Routes.RouteCandidate({
|
|
111
107
|
id: nsMember.qualifiedName,
|
|
112
108
|
displayName: nsMember.qualifiedName,
|
|
113
109
|
folder: nsCategoryConfig.folderName,
|
|
114
110
|
baseName: nsMember.qualifiedName.toLowerCase(),
|
|
115
111
|
kind: String(nsMember.item.kind),
|
|
116
112
|
canonicalRef: nsMember.item.canonicalReference?.toString() ?? nsMember.qualifiedName
|
|
117
|
-
});
|
|
113
|
+
}));
|
|
118
114
|
}
|
|
119
|
-
const collisions =
|
|
115
|
+
const collisions = Routes.detectCollisions(candidates);
|
|
120
116
|
if (collisions.length > 0) {
|
|
121
117
|
try {
|
|
122
118
|
for (const collision of collisions) emitEvent(PluginEvent.RouteCollisionDetected({
|
|
@@ -128,7 +124,10 @@ function prepareWorkItems(input) {
|
|
|
128
124
|
items: collision.items.map((item) => `${item.displayName} (${item.kind}) [${item.canonicalRef}]`)
|
|
129
125
|
}));
|
|
130
126
|
} catch {}
|
|
131
|
-
throw new
|
|
127
|
+
throw new Routes.RouteCollisionError({
|
|
128
|
+
baseRoute,
|
|
129
|
+
collisions
|
|
130
|
+
});
|
|
132
131
|
}
|
|
133
132
|
const routes = /* @__PURE__ */ new Map();
|
|
134
133
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -148,7 +147,7 @@ function prepareWorkItems(input) {
|
|
|
148
147
|
const itemWithMembers = item;
|
|
149
148
|
for (const member of itemWithMembers.members) {
|
|
150
149
|
const memberName = member.displayName;
|
|
151
|
-
const memberId = sanitizeId(memberName);
|
|
150
|
+
const memberId = Routes.sanitizeId(memberName);
|
|
152
151
|
const fullMemberName = `${item.displayName}.${memberName}`;
|
|
153
152
|
const memberRoute = `${itemRoute}#${memberId}`;
|
|
154
153
|
routes.set(fullMemberName, memberRoute);
|
|
@@ -181,7 +180,7 @@ function prepareWorkItems(input) {
|
|
|
181
180
|
const owner = syntheticBase.ownerClasses[0];
|
|
182
181
|
const ownerRoute = owner ? routes.get(owner.displayName) : void 0;
|
|
183
182
|
if (!ownerRoute) continue;
|
|
184
|
-
routes.set(baseName, `${ownerRoute}#${BASE_CLASS_ANCHOR}`);
|
|
183
|
+
routes.set(baseName, `${ownerRoute}#${SyntheticBases.BASE_CLASS_ANCHOR}`);
|
|
185
184
|
kinds.set(baseName, baseItem.kind);
|
|
186
185
|
}
|
|
187
186
|
const workItems = [];
|
|
@@ -339,7 +338,7 @@ function generateSinglePage(workItem, ctx) {
|
|
|
339
338
|
durationMs: Math.round(performance.now() - pageGenStart),
|
|
340
339
|
level: "debug"
|
|
341
340
|
}));
|
|
342
|
-
const parsed =
|
|
341
|
+
const parsed = parseFrontmatter(page.content);
|
|
343
342
|
const bodyContent = normalizeMarkdownSpacing(parsed.content);
|
|
344
343
|
const frontmatterData = parsed.data;
|
|
345
344
|
const relativePathWithExt = `${page.routePath.replace(baseRoute, "").replace(/^\//, "")}.mdx`;
|
|
@@ -354,7 +353,7 @@ function generateSinglePage(workItem, ctx) {
|
|
|
354
353
|
if (yield* fileSystem.exists(absolutePath).pipe(Effect.orElseSucceed(() => false))) {
|
|
355
354
|
const existingContent = yield* fileSystem.readFileString(absolutePath).pipe(Effect.orElseSucceed(() => null));
|
|
356
355
|
if (existingContent !== null) {
|
|
357
|
-
const { data: existingFrontmatter, content: existingBody } =
|
|
356
|
+
const { data: existingFrontmatter, content: existingBody } = parseFrontmatter(existingContent);
|
|
358
357
|
const normalizedExistingBody = normalizeMarkdownSpacing(existingBody);
|
|
359
358
|
const existingContentHash = hashContent(normalizedExistingBody);
|
|
360
359
|
const existingFrontmatterHash = hashFrontmatter(existingFrontmatter);
|
|
@@ -440,7 +439,7 @@ function writeSingleFile(result, ctx) {
|
|
|
440
439
|
routePath
|
|
441
440
|
};
|
|
442
441
|
}
|
|
443
|
-
let finalContent =
|
|
442
|
+
let finalContent = stringifyFrontmatter(bodyContent, frontmatter);
|
|
444
443
|
if (ogResolver && siteUrl && packageName) {
|
|
445
444
|
const ogImageMetadata = yield* Effect.promise(() => ogResolver.resolve(ogImage, packageName, apiName));
|
|
446
445
|
const ogMetadataOptions = {
|
package/config-helpers.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { normalizeBaseRoute
|
|
1
|
+
import { normalizeBaseRoute } from "./path-derivation.js";
|
|
2
|
+
import { SyncDiscoveryLayer } from "./sync-node-fs.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { Effect, Result } from "effect";
|
|
6
|
+
import { discoverBundle } from "@tsdoctor/bundle";
|
|
4
7
|
|
|
5
8
|
//#region src/config-helpers.ts
|
|
6
9
|
const PREFIX = "[rspress-plugin-api-extractor]";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
function discoverDir(dir) {
|
|
10
|
+
/**
|
|
11
|
+
* The adapter's strictness gate over `@tsdoctor/bundle`'s layer-0-only
|
|
12
|
+
* discovery: the plugin's public contract requires a `package.json` with a
|
|
13
|
+
* `name` (the bundle spec does not), so the gate runs FIRST and keeps the
|
|
14
|
+
* historical error messages. Returning the parsed name/version also lets the
|
|
15
|
+
* bundle discovery skip its api.json name fallback entirely (the model file
|
|
16
|
+
* is never parsed here, exactly as before).
|
|
17
|
+
*/
|
|
18
|
+
function requirePackageJson(dir) {
|
|
17
19
|
let stat;
|
|
18
20
|
try {
|
|
19
21
|
stat = fs.statSync(dir);
|
|
@@ -29,13 +31,34 @@ function discoverDir(dir) {
|
|
|
29
31
|
}
|
|
30
32
|
if (!pkg.name) throw new Error(`${PREFIX} api.fromDir: package.json in ${dir} has no "name" field`);
|
|
31
33
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
packageName: pkg.name,
|
|
35
|
-
version: pkg.version ?? "",
|
|
36
|
-
modelPath: discoverModel(dir, pkg.name)
|
|
34
|
+
name: pkg.name,
|
|
35
|
+
...pkg.version !== void 0 ? { version: pkg.version } : {}
|
|
37
36
|
};
|
|
38
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Run `@tsdoctor/bundle`'s `discoverBundle` synchronously (over the sync
|
|
40
|
+
* `FileSystem` bridge — see `sync-node-fs.ts`) and translate its typed
|
|
41
|
+
* failures into the plugin's historical error messages.
|
|
42
|
+
*/
|
|
43
|
+
function discoverDescriptor(dir, name, version) {
|
|
44
|
+
const result = Effect.runSync(Effect.result(discoverBundle(dir, { overrides: {
|
|
45
|
+
name,
|
|
46
|
+
...version !== void 0 ? { version } : {}
|
|
47
|
+
} }).pipe(Effect.provide(SyncDiscoveryLayer))));
|
|
48
|
+
if (Result.isSuccess(result)) return result.success;
|
|
49
|
+
const error = result.failure;
|
|
50
|
+
if (error._tag === "BundleDiscoveryError") switch (error.reason) {
|
|
51
|
+
case "noApiModel": throw new Error(`${PREFIX} api.fromDir: no *.api.json model found in ${dir}. Pass an explicit \`model\` to override.`);
|
|
52
|
+
case "ambiguousApiModel": {
|
|
53
|
+
const facts = (error.detail ?? "multiple *.api.json files").split(";")[0];
|
|
54
|
+
throw new Error(`${PREFIX} api.fromDir: ${facts} in ${dir}. Pass an explicit \`model\`.`);
|
|
55
|
+
}
|
|
56
|
+
case "notFound": throw new Error(`${PREFIX} api.fromDir: directory not found: ${dir}`);
|
|
57
|
+
case "notADirectory": throw new Error(`${PREFIX} api.fromDir: not a directory: ${dir}`);
|
|
58
|
+
default: throw new Error(`${PREFIX} api.fromDir: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`${PREFIX} api.fromDir: ${String(error)}`);
|
|
61
|
+
}
|
|
39
62
|
function resolveBaseRoute(baseRoute, info) {
|
|
40
63
|
const interpolated = (typeof baseRoute === "function" ? baseRoute(info) : baseRoute).replace(/\{dirname\}/g, info.dirname).replace(/\{packageName\}/g, info.packageName);
|
|
41
64
|
return normalizeBaseRoute(interpolated);
|
|
@@ -46,21 +69,36 @@ function resolveBaseRoute(baseRoute, info) {
|
|
|
46
69
|
* `ApiExtractorPlugin.api.fromDir`; the returned config can be passed to the
|
|
47
70
|
* single-API `api:` option or used as an element of the multi-API `apis:` array.
|
|
48
71
|
*
|
|
72
|
+
* Discovery (model-file selection, unscoped-name disambiguation, tsconfig
|
|
73
|
+
* detection) delegates to `@tsdoctor/bundle`'s `discoverBundle`; the
|
|
74
|
+
* RSPress-specific concerns — `baseRoute` templating and `MultiApiConfig`
|
|
75
|
+
* assembly — stay here, as does the plugin's stricter contract that the
|
|
76
|
+
* folder carry a named `package.json` (the bundle spec itself accepts
|
|
77
|
+
* layer-0-only folders).
|
|
78
|
+
*
|
|
49
79
|
* `baseRoute` is intentionally left unset unless overridden, so the plugin
|
|
50
80
|
* applies its own context-aware default (`/api` under `api:`,
|
|
51
81
|
* `/{packageName}/api` under `apis:`). See {@link BaseRoute}.
|
|
52
82
|
*/
|
|
53
83
|
function fromDir(dir, overrides = {}) {
|
|
54
84
|
const { baseRoute, cwd, ...rest } = overrides;
|
|
55
|
-
const
|
|
56
|
-
const
|
|
85
|
+
const abs = path.resolve(cwd ?? process.cwd(), dir);
|
|
86
|
+
const pkg = requirePackageJson(abs);
|
|
87
|
+
const descriptor = discoverDescriptor(abs, pkg.name, pkg.version);
|
|
88
|
+
const info = {
|
|
89
|
+
dir: descriptor.dir,
|
|
90
|
+
dirname: descriptor.dirname,
|
|
91
|
+
packageName: descriptor.name,
|
|
92
|
+
version: descriptor.version ?? "",
|
|
93
|
+
modelPath: descriptor.modelPath
|
|
94
|
+
};
|
|
57
95
|
return {
|
|
58
96
|
packageName: info.packageName,
|
|
59
97
|
name: info.packageName,
|
|
60
98
|
model: info.modelPath,
|
|
61
99
|
packageJson: path.join(info.dir, "package.json"),
|
|
62
100
|
...baseRoute !== void 0 ? { baseRoute: resolveBaseRoute(baseRoute, info) } : {},
|
|
63
|
-
...
|
|
101
|
+
...descriptor.tsconfigPath !== void 0 ? { tsconfig: descriptor.tsconfigPath } : {},
|
|
64
102
|
...rest
|
|
65
103
|
};
|
|
66
104
|
}
|
|
@@ -76,8 +114,10 @@ function isModelFolder(dir) {
|
|
|
76
114
|
* Strictly scan a parent directory of package folders and build one
|
|
77
115
|
* `MultiApiConfig` per subfolder. Exposed as `ApiExtractorPlugin.apis.fromDir`;
|
|
78
116
|
* the returned array is intended for the multi-API `apis:` option. Every
|
|
79
|
-
* non-dotfile subdirectory MUST be a valid model folder
|
|
80
|
-
* `
|
|
117
|
+
* non-dotfile subdirectory MUST be a valid model folder — including a
|
|
118
|
+
* `package.json`, which is this adapter's stricter contract over the bundle
|
|
119
|
+
* spec's layer-0-only discovery. `options` (minus `cwd`) is applied as shared
|
|
120
|
+
* defaults to each `api.fromDir` call.
|
|
81
121
|
*/
|
|
82
122
|
function fromParentDir(parentDir, options = {}) {
|
|
83
123
|
const { cwd, ...rest } = options;
|
package/errors.js
CHANGED
|
@@ -8,12 +8,6 @@ var ConfigValidationError = class extends ConfigValidationErrorBase {
|
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
10
|
const ApiModelLoadErrorBase = Data.TaggedError("ApiModelLoadError");
|
|
11
|
-
const SnapshotDbErrorBase = Data.TaggedError("SnapshotDbError");
|
|
12
|
-
var SnapshotDbError = class extends SnapshotDbErrorBase {
|
|
13
|
-
get message() {
|
|
14
|
-
return `Snapshot DB error during '${this.operation}' at '${this.dbPath}': ${this.reason}`;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
11
|
const PathDerivationErrorBase = Data.TaggedError("PathDerivationError");
|
|
18
12
|
const TypeRegistryErrorBase = Data.TaggedError("TypeRegistryError");
|
|
19
13
|
var TypeRegistryError = class extends TypeRegistryErrorBase {
|
|
@@ -26,4 +20,4 @@ const TwoslashProcessingErrorBase = Data.TaggedError("TwoslashProcessingError");
|
|
|
26
20
|
const PrettierFormatErrorBase = Data.TaggedError("PrettierFormatError");
|
|
27
21
|
|
|
28
22
|
//#endregion
|
|
29
|
-
export { ConfigValidationError, ConfigValidationErrorBase,
|
|
23
|
+
export { ConfigValidationError, ConfigValidationErrorBase, TypeRegistryError, TypeRegistryErrorBase };
|
package/frontmatter.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { Yaml, YamlStringifyOptions } from "@effected/yaml";
|
|
3
|
+
|
|
4
|
+
//#region src/frontmatter.ts
|
|
5
|
+
/**
|
|
6
|
+
* Stringify options shared by both emit sites.
|
|
7
|
+
*
|
|
8
|
+
* `lineWidth: 0` disables wrapping so long titles/descriptions/URLs stay on
|
|
9
|
+
* one line, and `defaultScalarStyle: "double-quoted"` quotes every string
|
|
10
|
+
* value. The forced quoting matters for downstream consumers: RSPress parses
|
|
11
|
+
* the emitted frontmatter with js-yaml (YAML 1.1-flavored), where an unquoted
|
|
12
|
+
* ISO timestamp such as `2024-01-15T12:00:00.000Z` decodes to a `Date` object
|
|
13
|
+
* instead of a string. Quoting every value keeps the decoded representation
|
|
14
|
+
* identical across YAML 1.1 and 1.2 parsers.
|
|
15
|
+
*/
|
|
16
|
+
const STRINGIFY_OPTIONS = YamlStringifyOptions.make({
|
|
17
|
+
lineWidth: 0,
|
|
18
|
+
defaultScalarStyle: "double-quoted"
|
|
19
|
+
});
|
|
20
|
+
const OPEN_DELIMITER = "---";
|
|
21
|
+
const CLOSE_SEARCH = "\n---";
|
|
22
|
+
/**
|
|
23
|
+
* Split markdown source into frontmatter data and body content, preserving
|
|
24
|
+
* gray-matter's exact boundary semantics.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* This is a byte-for-byte port of the `gray-matter` split contract the
|
|
28
|
+
* snapshot system's hashes depend on (see `@tsdoctor/snapshot`
|
|
29
|
+
* `hashContent`/`hashFrontmatter` and the disk-fallback comparison in
|
|
30
|
+
* `build-stages.ts`), with `@effected/yaml` (`Yaml.parse`, YAML 1.2) as the
|
|
31
|
+
* YAML engine instead of js-yaml:
|
|
32
|
+
*
|
|
33
|
+
* - No opening `---` line at offset 0 → `data: {}` and the whole input as
|
|
34
|
+
* `content` (a leading BOM is stripped first, as gray-matter does).
|
|
35
|
+
* - The closing delimiter is the first `\n---` after the opening line
|
|
36
|
+
* (gray-matter uses a plain `indexOf`, so `\n----` also closes and the
|
|
37
|
+
* leftover `-` stays in the body — preserved deliberately).
|
|
38
|
+
* - Exactly one newline (`\n` or `\r\n`) immediately after the closing `---`
|
|
39
|
+
* is consumed; everything else is the body verbatim. A build's generated
|
|
40
|
+
* page (`---\n…\n---\n\n# Title`) therefore yields a body starting with a
|
|
41
|
+
* single `\n`, exactly as gray-matter returned it.
|
|
42
|
+
* - A block with no closing delimiter is all frontmatter and yields an empty
|
|
43
|
+
* body; an empty/blank block yields `data: {}`.
|
|
44
|
+
* - Invalid YAML throws (a defect), matching gray-matter's js-yaml throw.
|
|
45
|
+
*
|
|
46
|
+
* One deliberate delta: gray-matter treats text on the opening line
|
|
47
|
+
* (`---toml`) as an engine name and throws for unregistered engines; this
|
|
48
|
+
* split treats such input as "no frontmatter" instead. The plugin never emits
|
|
49
|
+
* or consumes language-tagged frontmatter.
|
|
50
|
+
*
|
|
51
|
+
* Representation parity with js-yaml is verified by characterization tests
|
|
52
|
+
* (`__test__/frontmatter.test.ts`) pinning hashes captured under gray-matter.
|
|
53
|
+
* The one input where the engines disagree — an *unquoted* ISO timestamp
|
|
54
|
+
* (js-yaml: `Date`, YAML 1.2: string) — is unreachable from this plugin's
|
|
55
|
+
* emitters, which always quote timestamp values, and hashes identically
|
|
56
|
+
* anyway because `hashFrontmatter` JSON-serializes (a `Date` serializes to
|
|
57
|
+
* the same ISO string).
|
|
58
|
+
*
|
|
59
|
+
* @param source - The markdown source, with or without a frontmatter block
|
|
60
|
+
* @returns The decoded frontmatter data and the body content
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
function parseFrontmatter(source) {
|
|
65
|
+
const text = source.charCodeAt(0) === 65279 ? source.slice(1) : source;
|
|
66
|
+
if (!text.startsWith(OPEN_DELIMITER)) return {
|
|
67
|
+
data: {},
|
|
68
|
+
content: text
|
|
69
|
+
};
|
|
70
|
+
const afterOpen = text.charAt(3);
|
|
71
|
+
if (text === OPEN_DELIMITER) return {
|
|
72
|
+
data: {},
|
|
73
|
+
content: ""
|
|
74
|
+
};
|
|
75
|
+
if (afterOpen !== "\n" && !(afterOpen === "\r" && text.charAt(4) === "\n")) return {
|
|
76
|
+
data: {},
|
|
77
|
+
content: text
|
|
78
|
+
};
|
|
79
|
+
const fmStart = afterOpen === "\r" ? 5 : 4;
|
|
80
|
+
const closeIndex = text.indexOf(CLOSE_SEARCH, fmStart - 1);
|
|
81
|
+
let frontmatterText;
|
|
82
|
+
let content;
|
|
83
|
+
if (closeIndex === -1) {
|
|
84
|
+
frontmatterText = text.slice(fmStart);
|
|
85
|
+
content = "";
|
|
86
|
+
} else {
|
|
87
|
+
frontmatterText = closeIndex < fmStart ? "" : text.slice(fmStart, closeIndex);
|
|
88
|
+
let bodyStart = closeIndex + 4;
|
|
89
|
+
if (text.charAt(bodyStart) === "\r" && text.charAt(bodyStart + 1) === "\n") bodyStart += 2;
|
|
90
|
+
else if (text.charAt(bodyStart) === "\n") bodyStart += 1;
|
|
91
|
+
content = text.slice(bodyStart);
|
|
92
|
+
}
|
|
93
|
+
if (frontmatterText.trim() === "") return {
|
|
94
|
+
data: {},
|
|
95
|
+
content
|
|
96
|
+
};
|
|
97
|
+
const value = Effect.runSync(Yaml.parse(frontmatterText));
|
|
98
|
+
return {
|
|
99
|
+
data: value == null ? {} : value,
|
|
100
|
+
content
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Serialize frontmatter data and body content back into a markdown document,
|
|
105
|
+
* preserving gray-matter's `matter.stringify` contract.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* Emits `---\n<yaml>---\n<content>` with the body's trailing newline ensured,
|
|
109
|
+
* and returns the body unchanged (no fences) when `data` has no keys — both
|
|
110
|
+
* gray-matter behaviors the write path relied on. The YAML is emitted by
|
|
111
|
+
* `@effected/yaml` with every string value double-quoted (see
|
|
112
|
+
* `STRINGIFY_OPTIONS` for why); byte output differs from js-yaml's dump, but
|
|
113
|
+
* the decoded representation is identical, which is the invariant the
|
|
114
|
+
* snapshot hashes depend on. Unchanged pages are never rewritten, so the byte
|
|
115
|
+
* difference only ever lands in files that were being rewritten anyway.
|
|
116
|
+
*
|
|
117
|
+
* @param content - The body content
|
|
118
|
+
* @param data - The frontmatter data to serialize
|
|
119
|
+
* @returns The combined markdown document
|
|
120
|
+
*
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
function stringifyFrontmatter(content, data) {
|
|
124
|
+
const body = content.endsWith("\n") ? content : `${content}\n`;
|
|
125
|
+
if (Object.keys(data).length === 0) return body;
|
|
126
|
+
return `---\n${Effect.runSync(Yaml.stringify(data, STRINGIFY_OPTIONS))}---\n${body}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Serialize a data object to a YAML frontmatter block (fences included, plus
|
|
130
|
+
* the trailing blank line the page generators emit before the body).
|
|
131
|
+
*
|
|
132
|
+
* @remarks
|
|
133
|
+
* Used by `generateFrontmatter` (`markdown/helpers.ts`) as the emission half
|
|
134
|
+
* of the page generators' frontmatter. Every string value is double-quoted
|
|
135
|
+
* (see `STRINGIFY_OPTIONS`), so values that a YAML 1.1 consumer would
|
|
136
|
+
* otherwise coerce (timestamps, `yes`/`no`, numeric-looking strings) stay
|
|
137
|
+
* strings for RSPress's js-yaml parse.
|
|
138
|
+
*
|
|
139
|
+
* @param data - The frontmatter data to serialize
|
|
140
|
+
* @returns A `---`-fenced YAML block ending with a blank line
|
|
141
|
+
*
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
function emitFrontmatterBlock(data) {
|
|
145
|
+
return `---\n${Effect.runSync(Yaml.stringify(data, STRINGIFY_OPTIONS))}---\n\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
export { emitFrontmatterBlock, parseFrontmatter, stringifyFrontmatter };
|
package/index.d.ts
CHANGED
|
@@ -874,6 +874,13 @@ type FromDirOptions = Omit<Partial<MultiApiConfig>, "baseRoute"> & {
|
|
|
874
874
|
* `ApiExtractorPlugin.api.fromDir`; the returned config can be passed to the
|
|
875
875
|
* single-API `api:` option or used as an element of the multi-API `apis:` array.
|
|
876
876
|
*
|
|
877
|
+
* Discovery (model-file selection, unscoped-name disambiguation, tsconfig
|
|
878
|
+
* detection) delegates to `@tsdoctor/bundle`'s `discoverBundle`; the
|
|
879
|
+
* RSPress-specific concerns — `baseRoute` templating and `MultiApiConfig`
|
|
880
|
+
* assembly — stay here, as does the plugin's stricter contract that the
|
|
881
|
+
* folder carry a named `package.json` (the bundle spec itself accepts
|
|
882
|
+
* layer-0-only folders).
|
|
883
|
+
*
|
|
877
884
|
* `baseRoute` is intentionally left unset unless overridden, so the plugin
|
|
878
885
|
* applies its own context-aware default (`/api` under `api:`,
|
|
879
886
|
* `/{packageName}/api` under `apis:`). See {@link BaseRoute}.
|
|
@@ -883,8 +890,10 @@ declare function fromDir(dir: string, overrides?: FromDirOptions): MultiApiConfi
|
|
|
883
890
|
* Strictly scan a parent directory of package folders and build one
|
|
884
891
|
* `MultiApiConfig` per subfolder. Exposed as `ApiExtractorPlugin.apis.fromDir`;
|
|
885
892
|
* the returned array is intended for the multi-API `apis:` option. Every
|
|
886
|
-
* non-dotfile subdirectory MUST be a valid model folder
|
|
887
|
-
* `
|
|
893
|
+
* non-dotfile subdirectory MUST be a valid model folder — including a
|
|
894
|
+
* `package.json`, which is this adapter's stricter contract over the bundle
|
|
895
|
+
* spec's layer-0-only discovery. `options` (minus `cwd`) is applied as shared
|
|
896
|
+
* defaults to each `api.fromDir` call.
|
|
888
897
|
*/
|
|
889
898
|
declare function fromParentDir(parentDir: string, options?: FromDirOptions): MultiApiConfig[];
|
|
890
899
|
//#endregion
|