blodemd 0.0.7 → 0.0.9

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 (61) hide show
  1. package/README.md +25 -9
  2. package/dev-server/app/[[...slug]]/page.tsx +1 -0
  3. package/dev-server/next.config.js +11 -13
  4. package/dev-server/package.json +1 -1
  5. package/dev-server/tsconfig.json +3 -0
  6. package/dist/cli.mjs +869 -184
  7. package/dist/cli.mjs.map +1 -1
  8. package/docs/components/api/api-playground.tsx +255 -80
  9. package/docs/components/api/api-reference.tsx +11 -1
  10. package/docs/components/docs/contextual-menu.tsx +227 -142
  11. package/docs/components/docs/copy-page-menu.tsx +132 -85
  12. package/docs/components/docs/doc-header.tsx +13 -3
  13. package/docs/components/docs/doc-shell.tsx +22 -11
  14. package/docs/components/docs/mobile-nav.tsx +0 -6
  15. package/docs/components/mdx/code-group.tsx +171 -62
  16. package/docs/components/mdx/tabs.tsx +131 -26
  17. package/docs/components/ui/input.tsx +0 -1
  18. package/docs/components/ui/search.tsx +241 -132
  19. package/docs/lib/content-root.ts +33 -0
  20. package/docs/lib/content-source.ts +70 -0
  21. package/docs/lib/contextual-options.ts +20 -0
  22. package/docs/lib/docs-runtime.tsx +595 -0
  23. package/docs/lib/edge-config.ts +95 -0
  24. package/docs/lib/env.ts +22 -0
  25. package/docs/lib/openapi-proxy.ts +88 -0
  26. package/docs/lib/platform-config.ts +6 -0
  27. package/docs/lib/routes.ts +39 -0
  28. package/docs/lib/supabase.ts +13 -0
  29. package/docs/lib/tenancy.ts +322 -0
  30. package/docs/lib/tenant-headers.ts +14 -0
  31. package/docs/lib/tenant-static.ts +529 -0
  32. package/docs/lib/tenant-utility-context.ts +62 -0
  33. package/docs/lib/tenants.ts +68 -0
  34. package/docs/lib/use-mobile.ts +19 -0
  35. package/package.json +3 -2
  36. package/packages/@repo/common/dist/index.d.ts +7 -0
  37. package/packages/@repo/common/dist/index.d.ts.map +1 -1
  38. package/packages/@repo/common/dist/index.js +42 -0
  39. package/packages/@repo/common/src/index.ts +50 -0
  40. package/packages/@repo/contracts/dist/project.d.ts +1 -1
  41. package/packages/@repo/contracts/dist/project.js +1 -1
  42. package/packages/@repo/contracts/src/project.ts +1 -1
  43. package/packages/@repo/models/dist/docs-config.d.ts +194 -29
  44. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  45. package/packages/@repo/models/dist/docs-config.js +3 -28
  46. package/packages/@repo/models/src/docs-config.ts +5 -31
  47. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  48. package/packages/@repo/previewing/dist/blob-source.js +7 -2
  49. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
  50. package/packages/@repo/previewing/dist/fs-source.js +2 -3
  51. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  52. package/packages/@repo/previewing/dist/index.js +1 -41
  53. package/packages/@repo/previewing/src/blob-source.ts +7 -4
  54. package/packages/@repo/previewing/src/fs-source.ts +2 -3
  55. package/packages/@repo/previewing/src/index.ts +3 -55
  56. package/packages/@repo/validation/dist/index.d.ts +2 -2
  57. package/packages/@repo/validation/dist/index.d.ts.map +1 -1
  58. package/packages/@repo/validation/dist/index.js +2 -2
  59. package/packages/@repo/validation/package.json +1 -0
  60. package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
  61. package/packages/@repo/validation/src/index.ts +4 -4
@@ -0,0 +1,88 @@
1
+ import type { Tenant } from "@repo/models";
2
+ import { loadSiteConfig } from "@repo/previewing";
3
+
4
+ import { getTenantContentSource } from "@/lib/content-source";
5
+ import { getDocsCollectionWithNavigation } from "@/lib/docs-collection";
6
+ import { loadOpenApiRegistry } from "@/lib/openapi";
7
+ import { createTimedPromiseCache } from "@/lib/server-cache";
8
+
9
+ const OPENAPI_PROXY_CACHE_TTL_MS = 30 * 60 * 1000;
10
+
11
+ interface OpenApiProxyConfig {
12
+ allowedHosts: string[];
13
+ enabled: boolean;
14
+ }
15
+
16
+ const openApiProxyConfigCache = createTimedPromiseCache<
17
+ string,
18
+ OpenApiProxyConfig | null
19
+ >({
20
+ maxEntries: 512,
21
+ ttlMs: OPENAPI_PROXY_CACHE_TTL_MS,
22
+ });
23
+
24
+ const getOpenApiProxyCacheKey = (tenant: Tenant) =>
25
+ [
26
+ tenant.id,
27
+ tenant.slug,
28
+ tenant.activeDeploymentId ?? "",
29
+ tenant.activeDeploymentManifestUrl ?? "",
30
+ tenant.docsPath ?? "",
31
+ ].join(":");
32
+
33
+ const normalizeAllowedHosts = (hosts: string[]): string[] => [
34
+ ...new Set(hosts.map((host) => host.trim().toLowerCase()).filter(Boolean)),
35
+ ];
36
+
37
+ export const clearOpenApiProxyConfigCache = () => {
38
+ openApiProxyConfigCache.clear();
39
+ };
40
+
41
+ export const clearOpenApiProxyConfigCacheForTenant = (tenantId: string) => {
42
+ openApiProxyConfigCache.deleteByPrefix(tenantId);
43
+ };
44
+
45
+ export const loadOpenApiProxyConfig = async (
46
+ tenant: Tenant
47
+ ): Promise<OpenApiProxyConfig | null> => {
48
+ const cacheKey = getOpenApiProxyCacheKey(tenant);
49
+
50
+ return await openApiProxyConfigCache.getOrCreate(cacheKey, async () => {
51
+ const contentSource = getTenantContentSource(tenant);
52
+ const configResult = await loadSiteConfig(contentSource);
53
+ if (!configResult.ok) {
54
+ return null;
55
+ }
56
+
57
+ const configuredHosts = normalizeAllowedHosts(
58
+ configResult.config.openapiProxy?.allowedHosts ?? []
59
+ );
60
+ if (!configResult.config.openapiProxy?.enabled || configuredHosts.length) {
61
+ return {
62
+ allowedHosts: configuredHosts,
63
+ enabled: configResult.config.openapiProxy?.enabled === true,
64
+ };
65
+ }
66
+
67
+ const registry = await loadOpenApiRegistry(
68
+ getDocsCollectionWithNavigation(configResult.config),
69
+ contentSource
70
+ );
71
+ const derivedHosts = normalizeAllowedHosts(
72
+ registry.entries.flatMap((entry) =>
73
+ (entry.spec.servers ?? []).flatMap((server) => {
74
+ try {
75
+ return [new URL(server.url).hostname];
76
+ } catch {
77
+ return [];
78
+ }
79
+ })
80
+ )
81
+ );
82
+
83
+ return {
84
+ allowedHosts: derivedHosts,
85
+ enabled: true,
86
+ };
87
+ });
88
+ };
@@ -0,0 +1,6 @@
1
+ import { platformAssetPrefix, platformRootDomain } from "./env";
2
+
3
+ export const platformConfig = {
4
+ assetPrefix: platformAssetPrefix,
5
+ rootDomain: platformRootDomain,
6
+ };
@@ -1,6 +1,20 @@
1
1
  import { normalizePath, withLeadingSlash } from "@repo/common";
2
2
 
3
3
  const ABSOLUTE_URL_REGEX = /^[a-z][a-z\d+.-]*:/i;
4
+ const MARKDOWN_EXPORT_EXTENSIONS = [".md", ".mdx"] as const;
5
+
6
+ export const stripBasePath = (value: string, basePath: string) => {
7
+ if (!basePath) {
8
+ return value;
9
+ }
10
+ if (value === basePath) {
11
+ return "/";
12
+ }
13
+ if (value.startsWith(`${basePath}/`)) {
14
+ return value.slice(basePath.length) || "/";
15
+ }
16
+ return value;
17
+ };
4
18
 
5
19
  export const toDocHref = (path: string, basePath = "") => {
6
20
  const clean = normalizePath(path);
@@ -11,6 +25,31 @@ export const toDocHref = (path: string, basePath = "") => {
11
25
  return `${base}/${clean}`.replaceAll(/\/+/g, "/");
12
26
  };
13
27
 
28
+ export const toMarkdownDocHref = (path: string, basePath = "") => {
29
+ const href = toDocHref(path, basePath);
30
+ return href === "/" ? "/.md" : `${href}.md`;
31
+ };
32
+
33
+ export const getMarkdownExportSourcePath = (pathname: string) => {
34
+ for (const extension of MARKDOWN_EXPORT_EXTENSIONS) {
35
+ if (pathname.endsWith(extension)) {
36
+ return pathname.slice(0, -extension.length) || "/";
37
+ }
38
+ }
39
+
40
+ return null;
41
+ };
42
+
43
+ export const getMarkdownExportSlug = (pathname: string, basePath = "") => {
44
+ const sourcePath = getMarkdownExportSourcePath(pathname);
45
+ if (sourcePath === null) {
46
+ return null;
47
+ }
48
+
49
+ const relativePath = stripBasePath(sourcePath, basePath);
50
+ return relativePath === "/" ? "" : relativePath.slice(1);
51
+ };
52
+
14
53
  export const isExternalHref = (href: string) =>
15
54
  ABSOLUTE_URL_REGEX.test(href) || href.startsWith("//");
16
55
 
@@ -0,0 +1,13 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+
3
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
4
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "";
5
+
6
+ let client: ReturnType<typeof createClient> | null = null;
7
+
8
+ export const createSupabaseClient = () => {
9
+ if (!client) {
10
+ client = createClient(supabaseUrl, supabaseAnonKey);
11
+ }
12
+ return client;
13
+ };
@@ -0,0 +1,322 @@
1
+ import { getLocalRootHostsFromEnv, normalizeHost } from "@repo/common";
2
+ import type { Tenant } from "@repo/contracts";
3
+ import { TenantResolutionSchema } from "@repo/contracts";
4
+
5
+ import {
6
+ getTenantEdgeHostRecord,
7
+ getTenantEdgeSlugRecord,
8
+ isEdgeConfigEnabled,
9
+ } from "./edge-config";
10
+ import { docsApiBase } from "./env";
11
+ import { platformConfig } from "./platform-config";
12
+ import { createTimedPromiseCache } from "./server-cache";
13
+
14
+ const DEFAULT_RESERVED_PATHS = [
15
+ "/_internal",
16
+ "/_next",
17
+ "/.well-known",
18
+ "/docs.json",
19
+ "/favicon.ico",
20
+ "/llms.txt",
21
+ "/oauth",
22
+ "/robots.txt",
23
+ "/sitemap.xml",
24
+ "/logos",
25
+ "/file-text.svg",
26
+ "/globe.svg",
27
+ "/next.svg",
28
+ "/turborepo-dark.svg",
29
+ "/turborepo-light.svg",
30
+ "/vercel.svg",
31
+ "/window.svg",
32
+ ];
33
+
34
+ const LOCAL_ROOT_HOSTS = new Set(["localhost", "127.0.0.1"]);
35
+ const TRAILING_SLASHES_REGEX = /\/+$/;
36
+ const LEADING_SLASHES_REGEX = /^\/+/;
37
+ const BACKSLASH_TO_SLASH_REGEX = /\\/g;
38
+ const DEFAULT_DOCS_BASE_PATH = "/docs";
39
+ const TENANT_RESOLUTION_REVALIDATE_SECONDS = 300;
40
+ const ROOT_TENANT_UTILITY_PATHS = new Set([
41
+ "/llms-full.txt",
42
+ "/llms.txt",
43
+ "/robots.txt",
44
+ "/sitemap.xml",
45
+ ]);
46
+
47
+ const slugifyPath = (value: string) => {
48
+ const trimmed = value
49
+ .replace(BACKSLASH_TO_SLASH_REGEX, "/")
50
+ .replace(TRAILING_SLASHES_REGEX, "");
51
+ return trimmed.replace(LEADING_SLASHES_REGEX, "");
52
+ };
53
+
54
+ const stripPrefix = (pathname: string, prefix: string | null) => {
55
+ if (!prefix) {
56
+ return slugifyPath(pathname);
57
+ }
58
+
59
+ const normalizedPath = slugifyPath(pathname);
60
+ const normalizedPrefix = slugifyPath(prefix);
61
+ if (!normalizedPrefix) {
62
+ return normalizedPath;
63
+ }
64
+
65
+ if (normalizedPath === normalizedPrefix) {
66
+ return "";
67
+ }
68
+
69
+ if (normalizedPath.startsWith(`${normalizedPrefix}/`)) {
70
+ return normalizedPath.slice(normalizedPrefix.length + 1);
71
+ }
72
+
73
+ return null;
74
+ };
75
+
76
+ const tenantResolutionCache = createTimedPromiseCache<
77
+ string,
78
+ Awaited<ReturnType<typeof fetchTenantResolution>>
79
+ >({
80
+ maxEntries: 512,
81
+ ttlMs: TENANT_RESOLUTION_REVALIDATE_SECONDS * 1000,
82
+ });
83
+
84
+ export const getRequestHost = (headerSource: Pick<Headers, "get">) => {
85
+ const forwardedHost = headerSource.get("x-forwarded-host");
86
+ return normalizeHost(
87
+ forwardedHost?.split(",")[0]?.trim() || headerSource.get("host") || ""
88
+ );
89
+ };
90
+
91
+ export const getRequestProtocol = (headerSource: Pick<Headers, "get">) =>
92
+ headerSource.get("x-forwarded-proto")?.split(",")[0]?.trim() || "https";
93
+
94
+ export const isRootRuntimeHost = (host: string) => {
95
+ const normalizedHost = normalizeHost(host);
96
+ return (
97
+ normalizedHost === platformConfig.rootDomain ||
98
+ LOCAL_ROOT_HOSTS.has(normalizedHost) ||
99
+ getLocalRootHostsFromEnv(process.env).has(normalizedHost)
100
+ );
101
+ };
102
+
103
+ export const isReservedPath = (pathname: string) => {
104
+ const { assetPrefix } = platformConfig;
105
+ if (assetPrefix && pathname.startsWith(assetPrefix)) {
106
+ return true;
107
+ }
108
+ return DEFAULT_RESERVED_PATHS.some((prefix) => pathname.startsWith(prefix));
109
+ };
110
+
111
+ const resolveSubdomainBasePath = (pathname: string): string => {
112
+ const normalizedPath = slugifyPath(pathname);
113
+
114
+ if (
115
+ normalizedPath === "docs" ||
116
+ normalizedPath.startsWith(`${slugifyPath(DEFAULT_DOCS_BASE_PATH)}/`)
117
+ ) {
118
+ return DEFAULT_DOCS_BASE_PATH;
119
+ }
120
+
121
+ return "";
122
+ };
123
+
124
+ const buildTenantPathResolution = (
125
+ tenant: Tenant,
126
+ strategy: "preview" | "subdomain" | "custom-domain",
127
+ host: string,
128
+ pathname: string,
129
+ basePath: string
130
+ ) => {
131
+ if (ROOT_TENANT_UTILITY_PATHS.has(pathname)) {
132
+ return {
133
+ basePath,
134
+ host,
135
+ rewrittenPath: `/sites/${tenant.slug}${pathname}`,
136
+ strategy,
137
+ tenant,
138
+ };
139
+ }
140
+
141
+ const slugPath = stripPrefix(pathname, basePath || null);
142
+ if (slugPath === null) {
143
+ return null;
144
+ }
145
+
146
+ const rewrittenPath = slugPath
147
+ ? `/sites/${tenant.slug}/${slugPath}`
148
+ : `/sites/${tenant.slug}/`;
149
+
150
+ return {
151
+ basePath,
152
+ host,
153
+ rewrittenPath,
154
+ strategy,
155
+ tenant,
156
+ };
157
+ };
158
+
159
+ const buildPathTenantResolution = (
160
+ tenant: Tenant,
161
+ host: string,
162
+ pathname: string
163
+ ) => {
164
+ const normalized = slugifyPath(pathname);
165
+ const parts = normalized ? normalized.split("/") : [];
166
+ const [projectSlug, ...rest] = parts;
167
+ if (!projectSlug || projectSlug !== tenant.slug) {
168
+ return null;
169
+ }
170
+
171
+ const remainder = rest.join("/");
172
+ return {
173
+ basePath: `/${tenant.slug}`,
174
+ host,
175
+ rewrittenPath: remainder
176
+ ? `/sites/${tenant.slug}/${remainder}`
177
+ : `/sites/${tenant.slug}/`,
178
+ strategy: "path" as const,
179
+ tenant,
180
+ };
181
+ };
182
+
183
+ const fetchTenantResolutionFromApi = async (host: string, pathname: string) => {
184
+ const url = new URL("/tenants/resolve", docsApiBase);
185
+ url.searchParams.set("host", normalizeHost(host));
186
+ url.searchParams.set("path", pathname);
187
+ let response: Response;
188
+ try {
189
+ response = await fetch(url.toString(), {
190
+ next: { revalidate: TENANT_RESOLUTION_REVALIDATE_SECONDS },
191
+ });
192
+ } catch {
193
+ return null;
194
+ }
195
+ if (!response.ok) {
196
+ return null;
197
+ }
198
+ const json = (await response.json()) as unknown;
199
+ const parsed = TenantResolutionSchema.safeParse(json);
200
+ if (!parsed.success) {
201
+ return null;
202
+ }
203
+ return parsed.data;
204
+ };
205
+
206
+ export const resolveTenantFromEdgeConfig = async (
207
+ host: string,
208
+ pathname: string
209
+ ) => {
210
+ const normalizedHost = normalizeHost(host);
211
+ const hostRecord = await getTenantEdgeHostRecord(normalizedHost);
212
+ if (hostRecord) {
213
+ const basePath =
214
+ hostRecord.strategy === "subdomain"
215
+ ? resolveSubdomainBasePath(pathname)
216
+ : (hostRecord.pathPrefix ?? "");
217
+
218
+ return buildTenantPathResolution(
219
+ hostRecord.tenant,
220
+ hostRecord.strategy,
221
+ normalizedHost,
222
+ pathname,
223
+ basePath
224
+ );
225
+ }
226
+
227
+ const previewPrefix = normalizedHost.includes("---")
228
+ ? normalizedHost.split("---")[0]
229
+ : null;
230
+ if (previewPrefix) {
231
+ const previewRecord = await getTenantEdgeSlugRecord(previewPrefix);
232
+ if (previewRecord) {
233
+ return buildTenantPathResolution(
234
+ previewRecord.tenant,
235
+ "preview",
236
+ normalizedHost,
237
+ pathname,
238
+ resolveSubdomainBasePath(pathname)
239
+ );
240
+ }
241
+ }
242
+
243
+ const localSuffixes = ["localhost", "127.0.0.1"];
244
+ const localRootHosts = getLocalRootHostsFromEnv(process.env);
245
+ const localSuffix = localRootHosts.has(normalizedHost)
246
+ ? null
247
+ : localSuffixes.find((suffix) => normalizedHost.endsWith(`.${suffix}`));
248
+ if (localSuffix) {
249
+ const subdomain = normalizedHost.slice(0, -1 * (localSuffix.length + 1));
250
+ if (subdomain) {
251
+ const subdomainRecord = await getTenantEdgeSlugRecord(subdomain);
252
+ if (subdomainRecord) {
253
+ return buildTenantPathResolution(
254
+ subdomainRecord.tenant,
255
+ "subdomain",
256
+ normalizedHost,
257
+ pathname,
258
+ resolveSubdomainBasePath(pathname)
259
+ );
260
+ }
261
+ }
262
+ }
263
+
264
+ if (normalizedHost.endsWith(`.${platformConfig.rootDomain}`)) {
265
+ const subdomain = normalizedHost.slice(
266
+ 0,
267
+ -1 * (platformConfig.rootDomain.length + 1)
268
+ );
269
+ if (
270
+ subdomain &&
271
+ !["www", "app", "admin", "dashboard"].includes(subdomain)
272
+ ) {
273
+ const subdomainRecord = await getTenantEdgeSlugRecord(subdomain);
274
+ if (subdomainRecord) {
275
+ return buildTenantPathResolution(
276
+ subdomainRecord.tenant,
277
+ "subdomain",
278
+ normalizedHost,
279
+ pathname,
280
+ resolveSubdomainBasePath(pathname)
281
+ );
282
+ }
283
+ }
284
+ }
285
+
286
+ if (isRootRuntimeHost(normalizedHost)) {
287
+ const normalizedPath = slugifyPath(pathname);
288
+ const [projectSlug] = normalizedPath.split("/");
289
+ if (projectSlug) {
290
+ const pathRecord = await getTenantEdgeSlugRecord(projectSlug);
291
+ if (pathRecord) {
292
+ return buildPathTenantResolution(
293
+ pathRecord.tenant,
294
+ normalizedHost,
295
+ pathname
296
+ );
297
+ }
298
+ }
299
+ }
300
+
301
+ return null;
302
+ };
303
+
304
+ const fetchTenantResolution = async (host: string, pathname: string) => {
305
+ if (isEdgeConfigEnabled()) {
306
+ return await resolveTenantFromEdgeConfig(host, pathname);
307
+ }
308
+
309
+ return await fetchTenantResolutionFromApi(host, pathname);
310
+ };
311
+
312
+ export const clearTenantResolutionCache = () => {
313
+ tenantResolutionCache.clear();
314
+ };
315
+
316
+ export const resolveTenant = async (host: string, pathname: string) => {
317
+ const cacheKey = `${normalizeHost(host)}:${pathname}`;
318
+ return await tenantResolutionCache.getOrCreate(
319
+ cacheKey,
320
+ async () => await fetchTenantResolution(host, pathname)
321
+ );
322
+ };
@@ -0,0 +1,14 @@
1
+ export const TENANT_HEADERS = {
2
+ BASE_PATH: "x-tenant-base-path",
3
+ CUSTOM_DOMAINS: "x-tenant-custom-domains",
4
+ DEPLOYMENT_ID: "x-tenant-deployment-id",
5
+ DOMAIN: "x-tenant-domain",
6
+ ID: "x-tenant-id",
7
+ MANIFEST_URL: "x-tenant-manifest-url",
8
+ NAME: "x-tenant-name",
9
+ PATH_PREFIX: "x-tenant-path-prefix",
10
+ PRIMARY_DOMAIN: "x-tenant-primary-domain",
11
+ SLUG: "x-tenant-slug",
12
+ STRATEGY: "x-tenant-strategy",
13
+ SUBDOMAIN: "x-tenant-subdomain",
14
+ } as const;