jamdesk 1.1.70 → 1.1.72
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/dist/__tests__/integration/validate.integration.test.js +21 -0
- package/dist/__tests__/integration/validate.integration.test.js.map +1 -1
- package/dist/__tests__/unit/dev-loading-server.test.js +30 -14
- package/dist/__tests__/unit/dev-loading-server.test.js.map +1 -1
- package/dist/__tests__/unit/extract-hooks.test.js +102 -1
- package/dist/__tests__/unit/extract-hooks.test.js.map +1 -1
- package/dist/__tests__/unit/mdx-validator.test.js +22 -0
- package/dist/__tests__/unit/mdx-validator.test.js.map +1 -1
- package/dist/__tests__/unit/migrate-mdx.test.js +121 -1
- package/dist/__tests__/unit/migrate-mdx.test.js.map +1 -1
- package/dist/__tests__/unit/relative-mdx-imports.test.d.ts +2 -0
- package/dist/__tests__/unit/relative-mdx-imports.test.d.ts.map +1 -0
- package/dist/__tests__/unit/relative-mdx-imports.test.js +452 -0
- package/dist/__tests__/unit/relative-mdx-imports.test.js.map +1 -0
- package/dist/__tests__/unit/relocate-snippets.test.d.ts +2 -0
- package/dist/__tests__/unit/relocate-snippets.test.d.ts.map +1 -0
- package/dist/__tests__/unit/relocate-snippets.test.js +542 -0
- package/dist/__tests__/unit/relocate-snippets.test.js.map +1 -0
- package/dist/__tests__/unit/run-build-script.test.js +23 -1
- package/dist/__tests__/unit/run-build-script.test.js.map +1 -1
- package/dist/__tests__/unit/vendored-sync.test.js +5 -0
- package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
- package/dist/__tests__/unit/warn-relative-imports.test.d.ts +2 -0
- package/dist/__tests__/unit/warn-relative-imports.test.d.ts.map +1 -0
- package/dist/__tests__/unit/warn-relative-imports.test.js +44 -0
- package/dist/__tests__/unit/warn-relative-imports.test.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +16 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/migrate/convert-mdx.d.ts +5 -1
- package/dist/commands/migrate/convert-mdx.d.ts.map +1 -1
- package/dist/commands/migrate/convert-mdx.js +19 -6
- package/dist/commands/migrate/convert-mdx.js.map +1 -1
- package/dist/commands/migrate/extract-hooks.d.ts +26 -0
- package/dist/commands/migrate/extract-hooks.d.ts.map +1 -1
- package/dist/commands/migrate/extract-hooks.js +71 -12
- package/dist/commands/migrate/extract-hooks.js.map +1 -1
- package/dist/commands/migrate/fix-mdx-syntax.d.ts +38 -0
- package/dist/commands/migrate/fix-mdx-syntax.d.ts.map +1 -0
- package/dist/commands/migrate/fix-mdx-syntax.js +80 -0
- package/dist/commands/migrate/fix-mdx-syntax.js.map +1 -0
- package/dist/commands/migrate/index.d.ts.map +1 -1
- package/dist/commands/migrate/index.js +165 -3
- package/dist/commands/migrate/index.js.map +1 -1
- package/dist/commands/migrate/relocate-snippets.d.ts +30 -0
- package/dist/commands/migrate/relocate-snippets.d.ts.map +1 -0
- package/dist/commands/migrate/relocate-snippets.js +357 -0
- package/dist/commands/migrate/relocate-snippets.js.map +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +31 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +7 -5
- package/dist/lib/deps.js.map +1 -1
- package/dist/lib/docs-config.d.ts +1 -0
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +53 -6
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/mdx-syntax-fixes.d.ts +14 -0
- package/dist/lib/mdx-syntax-fixes.d.ts.map +1 -0
- package/dist/lib/mdx-syntax-fixes.js +38 -0
- package/dist/lib/mdx-syntax-fixes.js.map +1 -0
- package/dist/lib/mdx-validator.d.ts +11 -2
- package/dist/lib/mdx-validator.d.ts.map +1 -1
- package/dist/lib/mdx-validator.js +41 -4
- package/dist/lib/mdx-validator.js.map +1 -1
- package/dist/lib/relative-mdx-imports.d.ts +44 -0
- package/dist/lib/relative-mdx-imports.d.ts.map +1 -0
- package/dist/lib/relative-mdx-imports.js +145 -0
- package/dist/lib/relative-mdx-imports.js.map +1 -0
- package/dist/lib/run-build-script.d.ts.map +1 -1
- package/dist/lib/run-build-script.js +7 -0
- package/dist/lib/run-build-script.js.map +1 -1
- package/dist/lib/warn-relative-imports.d.ts +2 -0
- package/dist/lib/warn-relative-imports.d.ts.map +1 -0
- package/dist/lib/warn-relative-imports.js +29 -0
- package/dist/lib/warn-relative-imports.js.map +1 -0
- package/package.json +2 -2
- package/vendored/app/(jd-system)/jd-inactive/BrandedInactive.tsx +118 -0
- package/vendored/app/(jd-system)/jd-inactive/layout.tsx +12 -0
- package/vendored/app/(jd-system)/jd-inactive/page.tsx +40 -0
- package/vendored/app/(unlock)/jd/unlock/page.tsx +32 -0
- package/vendored/app/[[...slug]]/page.tsx +63 -15
- package/vendored/app/globals.css +5 -0
- package/vendored/app/layout.tsx +36 -0
- package/vendored/components/navigation/Header.tsx +4 -2
- package/vendored/components/navigation/SocialFooter.tsx +4 -18
- package/vendored/lib/branding-url.ts +9 -0
- package/vendored/lib/build/error-parser.ts +26 -0
- package/vendored/lib/docs-isr.ts +33 -19
- package/vendored/lib/docs-types.ts +1 -1
- package/vendored/lib/email-notifier.ts +1 -1
- package/vendored/lib/isr-build-executor.ts +1 -1
- package/vendored/lib/layout-helpers.tsx +54 -2
- package/vendored/lib/middleware-helpers.ts +46 -8
- package/vendored/lib/preprocess-mdx.ts +62 -15
- package/vendored/lib/redis.ts +86 -0
- package/vendored/lib/revalidation-trigger.ts +29 -15
- package/vendored/lib/validate-config.ts +68 -7
- package/vendored/schema/docs-schema.json +1 -1
- package/vendored/scripts/compile-snippets.cjs +50 -7
- package/vendored/themes/index.ts +6 -4
- package/vendored/workspace-package-lock.json +118 -133
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.72",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"nspell": "^2.1.5",
|
|
116
116
|
"open": "^11.0.0",
|
|
117
117
|
"ora": "^9.4.0",
|
|
118
|
-
"tar": "^7.5.
|
|
118
|
+
"tar": "^7.5.14"
|
|
119
119
|
},
|
|
120
120
|
"devDependencies": {
|
|
121
121
|
"@mdx-js/mdx": "^3.1.1",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface BrandedInactiveProps {
|
|
4
|
+
projectName?: string;
|
|
5
|
+
logoUrl?: string;
|
|
6
|
+
hasCustomDomain?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BrandedInactive({
|
|
10
|
+
projectName, logoUrl, hasCustomDomain = false,
|
|
11
|
+
}: BrandedInactiveProps): React.ReactElement {
|
|
12
|
+
const showBrandMark = Boolean(logoUrl || projectName);
|
|
13
|
+
return (
|
|
14
|
+
<div style={styles.shell}>
|
|
15
|
+
<main style={styles.main}>
|
|
16
|
+
<div style={styles.card}>
|
|
17
|
+
{showBrandMark ? (
|
|
18
|
+
<div style={styles.brandMark}>
|
|
19
|
+
{logoUrl ? (
|
|
20
|
+
<img
|
|
21
|
+
src={logoUrl}
|
|
22
|
+
alt={projectName ?? ""}
|
|
23
|
+
width={120}
|
|
24
|
+
height={32}
|
|
25
|
+
style={styles.logo}
|
|
26
|
+
/>
|
|
27
|
+
) : null}
|
|
28
|
+
{projectName ? (
|
|
29
|
+
<span style={styles.projectName}>{projectName}</span>
|
|
30
|
+
) : null}
|
|
31
|
+
</div>
|
|
32
|
+
) : null}
|
|
33
|
+
<h1 style={styles.heading}>This site is currently paused</h1>
|
|
34
|
+
<p style={styles.body}>
|
|
35
|
+
The owner needs to reactivate their subscription to bring
|
|
36
|
+
this documentation back online.
|
|
37
|
+
</p>
|
|
38
|
+
{hasCustomDomain ? (
|
|
39
|
+
<p style={styles.bodySubtle}>
|
|
40
|
+
If you’re the owner, sign in to restore access.
|
|
41
|
+
</p>
|
|
42
|
+
) : null}
|
|
43
|
+
</div>
|
|
44
|
+
</main>
|
|
45
|
+
<footer style={styles.footer}>
|
|
46
|
+
<a href="https://jamdesk.com" style={styles.poweredBy}>
|
|
47
|
+
Powered by{" "}
|
|
48
|
+
<strong style={styles.brand} translate="no">Jamdesk</strong>
|
|
49
|
+
</a>
|
|
50
|
+
</footer>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
56
|
+
shell: {
|
|
57
|
+
minHeight: "100vh",
|
|
58
|
+
display: "grid",
|
|
59
|
+
gridTemplateRows: "1fr auto",
|
|
60
|
+
background:
|
|
61
|
+
"radial-gradient(ellipse 80% 50% at 50% 30%, " +
|
|
62
|
+
"rgba(255,54,33,0.07), transparent 70%), #0a0a0d",
|
|
63
|
+
color: "#e8e8ee",
|
|
64
|
+
fontFamily:
|
|
65
|
+
"'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",
|
|
66
|
+
padding:
|
|
67
|
+
"env(safe-area-inset-top) env(safe-area-inset-right) " +
|
|
68
|
+
"env(safe-area-inset-bottom) env(safe-area-inset-left)",
|
|
69
|
+
},
|
|
70
|
+
main: {
|
|
71
|
+
display: "flex", flexDirection: "column",
|
|
72
|
+
alignItems: "center", justifyContent: "center",
|
|
73
|
+
padding: "2rem", textAlign: "center",
|
|
74
|
+
},
|
|
75
|
+
card: {maxWidth: "32rem", width: "100%"},
|
|
76
|
+
brandMark: {
|
|
77
|
+
display: "inline-flex", alignItems: "center",
|
|
78
|
+
gap: "0.75rem", marginBottom: "1.25rem",
|
|
79
|
+
},
|
|
80
|
+
logo: {
|
|
81
|
+
height: "auto", maxHeight: "2rem", maxWidth: "8rem",
|
|
82
|
+
display: "block", opacity: 0.95,
|
|
83
|
+
},
|
|
84
|
+
projectName: {
|
|
85
|
+
fontSize: "clamp(1.75rem, 4.5vw, 2.25rem)",
|
|
86
|
+
fontWeight: 600, color: "#e8e8ee",
|
|
87
|
+
lineHeight: 1.2,
|
|
88
|
+
letterSpacing: "-0.01em",
|
|
89
|
+
textWrap: "balance",
|
|
90
|
+
},
|
|
91
|
+
heading: {
|
|
92
|
+
fontSize: "1rem",
|
|
93
|
+
fontWeight: 400, lineHeight: 1.6,
|
|
94
|
+
margin: "0 0 0.5rem",
|
|
95
|
+
color: "#e8e8ee",
|
|
96
|
+
textWrap: "balance",
|
|
97
|
+
},
|
|
98
|
+
body: {
|
|
99
|
+
fontSize: "1rem", color: "#b4b4bf",
|
|
100
|
+
lineHeight: 1.6, margin: "0 0 0.75rem",
|
|
101
|
+
textWrap: "pretty",
|
|
102
|
+
},
|
|
103
|
+
bodySubtle: {
|
|
104
|
+
fontSize: "0.875rem", color: "#8a8a96",
|
|
105
|
+
margin: 0, textWrap: "pretty",
|
|
106
|
+
},
|
|
107
|
+
footer: {
|
|
108
|
+
display: "flex", justifyContent: "center",
|
|
109
|
+
padding: "1.5rem",
|
|
110
|
+
},
|
|
111
|
+
poweredBy: {
|
|
112
|
+
color: "#6a6a76",
|
|
113
|
+
fontSize: "0.8125rem",
|
|
114
|
+
textDecoration: "none",
|
|
115
|
+
letterSpacing: "0.02em",
|
|
116
|
+
},
|
|
117
|
+
brand: {color: "#ff3621", fontWeight: 600},
|
|
118
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {Metadata} from "next";
|
|
2
|
+
|
|
3
|
+
// Root layout's `x-jd-layout: placeholder` branch owns the shell.
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "Docs Site Unavailable",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function InactiveLayout(
|
|
9
|
+
{children}: {children: React.ReactNode},
|
|
10
|
+
) {
|
|
11
|
+
return children;
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {headers} from "next/headers";
|
|
2
|
+
import {notFound} from "next/navigation";
|
|
3
|
+
import {isUrlSafe} from "@/lib/url-safety";
|
|
4
|
+
import {BrandedInactive} from "./BrandedInactive";
|
|
5
|
+
|
|
6
|
+
// Read project data from middleware-set headers per-request. We can't
|
|
7
|
+
// use force-static because the proxy sets different headers for each
|
|
8
|
+
// paused project (name, logo, custom-domain flag). The response is
|
|
9
|
+
// still trivially cacheable at the edge with a short TTL.
|
|
10
|
+
export const dynamic = "force-dynamic";
|
|
11
|
+
|
|
12
|
+
export default async function InactivePage() {
|
|
13
|
+
const h = await headers();
|
|
14
|
+
// Direct hits to /jd-inactive (without a proxy rewrite) must 404 rather
|
|
15
|
+
// than render the generic placeholder. Otherwise anyone could probe
|
|
16
|
+
// <slug>.jamdesk.app/jd-inactive and learn whether the slug is hosted
|
|
17
|
+
// here, plus see the "site paused" copy on slugs that are actually
|
|
18
|
+
// healthy. The proxy always sets x-jd-layout=placeholder when it
|
|
19
|
+
// rewrites for the inactive flag — its absence means we got here from
|
|
20
|
+
// the user typing the URL.
|
|
21
|
+
if (h.get("x-jd-layout") !== "placeholder") {
|
|
22
|
+
notFound();
|
|
23
|
+
}
|
|
24
|
+
const projectName = h.get("x-jd-project-name") ?? undefined;
|
|
25
|
+
// Logo URL is customer-supplied (flows from Redis through middleware
|
|
26
|
+
// headers). isUrlSafe blocks data:/javascript:, plus SSRF vectors
|
|
27
|
+
// (private IPs, encoded IPv4 bypasses, IPv6 loopback) — same threat
|
|
28
|
+
// model as the API-playground proxy uses. Mirror in proxy/lib/
|
|
29
|
+
// inactive-html.ts via sanitizeLogoUrl.
|
|
30
|
+
const rawLogo = h.get("x-jd-project-logo");
|
|
31
|
+
const logoUrl = rawLogo && isUrlSafe(rawLogo) ? rawLogo : undefined;
|
|
32
|
+
const hasCustomDomain = h.get("x-jd-custom-domain") === "1";
|
|
33
|
+
return (
|
|
34
|
+
<BrandedInactive
|
|
35
|
+
projectName={projectName}
|
|
36
|
+
logoUrl={logoUrl}
|
|
37
|
+
hasCustomDomain={hasCustomDomain}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -2,6 +2,7 @@ import { headers } from 'next/headers';
|
|
|
2
2
|
import { getDocsConfig } from '@/lib/docs-isr';
|
|
3
3
|
import { normalizeLogo } from '@/lib/docs-types';
|
|
4
4
|
import { sanitizeFrom } from '@/lib/sanitize-from';
|
|
5
|
+
import { getBrandingUrl } from '@/lib/branding-url';
|
|
5
6
|
import UnlockForm from './UnlockForm';
|
|
6
7
|
|
|
7
8
|
interface UnlockPageProps {
|
|
@@ -29,14 +30,19 @@ export default async function UnlockPage({ searchParams }: UnlockPageProps) {
|
|
|
29
30
|
const from = sanitizeFrom(rawFrom ?? null);
|
|
30
31
|
const showError = params.error === '1';
|
|
31
32
|
|
|
33
|
+
const showBranding = process.env.NEXT_PUBLIC_SHOW_BRANDING !== 'false';
|
|
34
|
+
const brandingUrl = getBrandingUrl(slug);
|
|
35
|
+
|
|
32
36
|
return (
|
|
33
37
|
<main
|
|
34
38
|
style={{
|
|
35
39
|
minHeight: '100vh',
|
|
36
40
|
display: 'flex',
|
|
41
|
+
flexDirection: 'column',
|
|
37
42
|
alignItems: 'center',
|
|
38
43
|
justifyContent: 'center',
|
|
39
44
|
padding: '24px 16px',
|
|
45
|
+
gap: 24,
|
|
40
46
|
}}
|
|
41
47
|
>
|
|
42
48
|
<section
|
|
@@ -152,6 +158,32 @@ export default async function UnlockPage({ searchParams }: UnlockPageProps) {
|
|
|
152
158
|
</p>
|
|
153
159
|
)}
|
|
154
160
|
</section>
|
|
161
|
+
|
|
162
|
+
{showBranding && (
|
|
163
|
+
<a
|
|
164
|
+
href={brandingUrl}
|
|
165
|
+
target="_blank"
|
|
166
|
+
rel="noopener noreferrer"
|
|
167
|
+
style={{
|
|
168
|
+
display: 'inline-flex',
|
|
169
|
+
alignItems: 'center',
|
|
170
|
+
gap: 6,
|
|
171
|
+
fontSize: 13,
|
|
172
|
+
color: '#9ca3af',
|
|
173
|
+
textDecoration: 'none',
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
Powered by
|
|
177
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
178
|
+
<img
|
|
179
|
+
src="/_jd/branding/jamdesk-wordmark.svg"
|
|
180
|
+
alt="Jamdesk"
|
|
181
|
+
width={58}
|
|
182
|
+
height={15}
|
|
183
|
+
style={{ display: 'inline-block', verticalAlign: 'middle' }}
|
|
184
|
+
/>
|
|
185
|
+
</a>
|
|
186
|
+
)}
|
|
155
187
|
</main>
|
|
156
188
|
);
|
|
157
189
|
}
|
|
@@ -25,54 +25,102 @@ interface PageProps {
|
|
|
25
25
|
}>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Mirror of the CLI's detector regex in cli/src/lib/relative-mdx-imports.ts.
|
|
29
|
+
// Duplicated intentionally — build-service uses bundler module resolution
|
|
30
|
+
// and shouldn't reach into cli/src/. Keep the regex itself in sync with
|
|
31
|
+
// that file. Two intentional simplifications vs the CLI version:
|
|
32
|
+
// - no `g` flag (boolean test, not iteration)
|
|
33
|
+
// - fence stripping replaces matches with empty string instead of
|
|
34
|
+
// blank lines of equal count — line numbers don't matter here, only
|
|
35
|
+
// a yes/no skip decision (the CLI preserves them for warning text).
|
|
36
|
+
const PARENT_RELATIVE_MDX_IMPORT_RE =
|
|
37
|
+
/^[ \t]*import\s+(?:type\s+)?[\w$*{}, \n\r]+\s+from\s+["']\.{1,2}\/[^"']+\.mdx["']\s*;?/m;
|
|
38
|
+
|
|
39
|
+
const FENCED_CODE_BLOCK_RE =
|
|
40
|
+
/^( *)(```+|~~~+)[^\n]*\n([\s\S]*?)\n\1\2\s*$/gm;
|
|
41
|
+
|
|
42
|
+
export function pageHasRelativeMdxImport(filePath: string): boolean {
|
|
43
|
+
try {
|
|
44
|
+
// Read the full file — MDX imports can appear at any top-level position
|
|
45
|
+
// (after a long prose intro or table of contents), and a slice was
|
|
46
|
+
// missing real imports past byte ~8192 in 70-94 KB customer pages.
|
|
47
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
48
|
+
// Strip fenced code blocks so documentation examples (e.g.
|
|
49
|
+
// ```mdx\nimport X from "../snippets/foo.mdx";\n```) don't false-trigger.
|
|
50
|
+
return PARENT_RELATIVE_MDX_IMPORT_RE.test(content.replace(FENCED_CODE_BLOCK_RE, ''));
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface CollectedPaths {
|
|
57
|
+
supported: string[];
|
|
58
|
+
skipped: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getAllDocPaths(): CollectedPaths {
|
|
29
62
|
const contentDir = getContentDir();
|
|
30
|
-
const
|
|
63
|
+
const supported: string[] = [];
|
|
64
|
+
const skipped: string[] = [];
|
|
31
65
|
|
|
32
66
|
function traverseDir(dir: string, basePath: string = '') {
|
|
33
67
|
if (!fs.existsSync(dir)) return;
|
|
34
68
|
const files = fs.readdirSync(dir);
|
|
35
69
|
for (const file of files) {
|
|
36
|
-
// Skip dotfiles (.claude, .git, etc.)
|
|
37
70
|
if (file.startsWith('.')) continue;
|
|
38
71
|
const filePath = path.join(dir, file);
|
|
39
72
|
let stat;
|
|
40
73
|
try {
|
|
41
74
|
stat = fs.statSync(filePath);
|
|
42
75
|
} catch {
|
|
43
|
-
// Broken symlink or inaccessible — skip
|
|
44
76
|
continue;
|
|
45
77
|
}
|
|
46
78
|
if (stat.isDirectory()) {
|
|
47
79
|
traverseDir(filePath, path.join(basePath, file));
|
|
48
80
|
} else if (file.endsWith('.mdx')) {
|
|
49
81
|
const slug = path.join(basePath, file.replace(/\.mdx$/, ''));
|
|
50
|
-
|
|
82
|
+
if (pageHasRelativeMdxImport(filePath)) {
|
|
83
|
+
skipped.push(slug);
|
|
84
|
+
} else {
|
|
85
|
+
supported.push(slug);
|
|
86
|
+
}
|
|
51
87
|
}
|
|
52
88
|
}
|
|
53
89
|
}
|
|
54
90
|
|
|
55
91
|
traverseDir(contentDir);
|
|
56
|
-
return
|
|
92
|
+
return { supported, skipped };
|
|
57
93
|
}
|
|
58
94
|
|
|
95
|
+
// In `jamdesk dev`, Next.js calls generateStaticParams during initial
|
|
96
|
+
// compile AND for each lazily-compiled route (every page click). Without
|
|
97
|
+
// a guard the same multi-line warning floods the dev console after every
|
|
98
|
+
// navigation. Cache the prior skipped set and re-log only when it changes
|
|
99
|
+
// — that way a user who fixes one import still sees the list shrink, but
|
|
100
|
+
// repeated calls with no change stay quiet.
|
|
101
|
+
let lastSkippedKey: string | null = null;
|
|
102
|
+
|
|
59
103
|
export async function generateStaticParams() {
|
|
60
104
|
// ISR: pages generated on-demand, no build-time pre-render.
|
|
61
105
|
if (isIsrMode()) return [];
|
|
62
106
|
|
|
63
|
-
const
|
|
64
|
-
// next-mdx-remote can't compile relative MDX imports — skip those tests.
|
|
65
|
-
const unsupportedPatterns = ['deep-relative-test', 'relative-snippets-test'];
|
|
107
|
+
const { supported, skipped } = getAllDocPaths();
|
|
66
108
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
const skippedKey = skipped.length === 0 ? '' : skipped.slice().sort().join('\n');
|
|
110
|
+
if (skippedKey !== lastSkippedKey) {
|
|
111
|
+
lastSkippedKey = skippedKey;
|
|
112
|
+
if (skipped.length > 0) {
|
|
113
|
+
console.log(`[Build] Skipped ${skipped.length} page(s) with parent-relative MDX imports:`);
|
|
114
|
+
for (const slug of skipped) {
|
|
115
|
+
console.log(` - ${slug}`);
|
|
116
|
+
}
|
|
117
|
+
console.log(' Run "jamdesk migrate" to auto-fix moveable imports, or "jamdesk validate" for per-import detail.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
72
120
|
|
|
73
121
|
return [
|
|
74
122
|
{ slug: [] },
|
|
75
|
-
...
|
|
123
|
+
...supported.map((p) => ({ slug: p.split('/') })),
|
|
76
124
|
];
|
|
77
125
|
}
|
|
78
126
|
|
package/vendored/app/globals.css
CHANGED
|
@@ -34,4 +34,9 @@ img.inline-block { display: inline-block !important; }
|
|
|
34
34
|
.dark img.dark\:inline, .dark .dark\:inline { display: inline !important; }
|
|
35
35
|
.dark img.dark\:inline-block, .dark .dark\:inline-block { display: inline-block !important; }
|
|
36
36
|
|
|
37
|
+
/* SSR scroll lock — see lib/layout-helpers.tsx scrollLockBootstrap. */
|
|
38
|
+
html[data-scroll-locked] #content-scroll-container {
|
|
39
|
+
overflow-y: hidden !important;
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
/* Shiki handles syntax highlighting via CSS variables - no theme import needed */
|
package/vendored/app/layout.tsx
CHANGED
|
@@ -23,6 +23,15 @@ import { fetchCustomCss, fetchCustomJs } from '@/lib/r2-content';
|
|
|
23
23
|
import { DocsChrome, getLocalFileContent } from '@/lib/layout-helpers';
|
|
24
24
|
|
|
25
25
|
export async function generateMetadata(): Promise<Metadata> {
|
|
26
|
+
// The placeholder shell would otherwise render with the paused
|
|
27
|
+
// project's title/favicon (and pay an R2 round-trip for it) — the
|
|
28
|
+
// nested (jd-system)/jd-inactive layout supplies neutral metadata.
|
|
29
|
+
if (isIsrMode()) {
|
|
30
|
+
const placeholderCheck = await headers();
|
|
31
|
+
if (placeholderCheck.get('x-jd-layout') === 'placeholder') {
|
|
32
|
+
return {robots: {index: false, follow: false}};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
26
35
|
let config: DocsConfig;
|
|
27
36
|
if (isIsrMode()) {
|
|
28
37
|
const headersList = await headers();
|
|
@@ -58,6 +67,33 @@ export default async function RootLayout({
|
|
|
58
67
|
}) {
|
|
59
68
|
const headersList = await headers();
|
|
60
69
|
|
|
70
|
+
// Placeholder short-circuit: proxy sets x-jd-layout=placeholder when
|
|
71
|
+
// rewriting an inactive site to /jd-inactive. Skip docs chrome (would
|
|
72
|
+
// otherwise leak the customer's nav, search, sidebar around our
|
|
73
|
+
// paused-site placeholder) and render a minimal dark shell.
|
|
74
|
+
if (headersList.get('x-jd-layout') === 'placeholder') {
|
|
75
|
+
return (
|
|
76
|
+
<html lang="en" suppressHydrationWarning>
|
|
77
|
+
<head>
|
|
78
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
79
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
80
|
+
<meta name="theme-color" content="#0a0a0d" />
|
|
81
|
+
</head>
|
|
82
|
+
<body
|
|
83
|
+
style={{
|
|
84
|
+
margin: 0,
|
|
85
|
+
minHeight: '100vh',
|
|
86
|
+
backgroundColor: '#0a0a0d',
|
|
87
|
+
colorScheme: 'dark',
|
|
88
|
+
}}
|
|
89
|
+
suppressHydrationWarning
|
|
90
|
+
>
|
|
91
|
+
{children}
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
61
97
|
// Unlock-mode short-circuit: middleware sets x-jd-unlock-mode when
|
|
62
98
|
// rewriting to /jd/unlock. Skip docs chrome — the unlock page renders
|
|
63
99
|
// its own minimal shell via app/(unlock)/jd/unlock/page.tsx.
|
|
@@ -59,8 +59,10 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
59
59
|
// Determine effective tabsPosition (same logic as Sidebar)
|
|
60
60
|
const themeConfig = getTheme(config.theme);
|
|
61
61
|
|
|
62
|
-
// Nebula theme uses compact search (icon only) instead of full search bar
|
|
63
|
-
|
|
62
|
+
// Nebula theme uses compact search (icon only) instead of full search bar.
|
|
63
|
+
// Read from `themeConfig.name` (canonical-case from registry) rather than
|
|
64
|
+
// `config.theme` so uppercase docs.json values like "NEBULA" still match.
|
|
65
|
+
const useCompactSearch = themeConfig.name === 'nebula';
|
|
64
66
|
const effectiveTabsPosition: TabsPosition = config.tabsPosition || tabsPositionProp || themeConfig.defaultTabsPosition;
|
|
65
67
|
const showTabsInHeader = effectiveTabsPosition === 'top';
|
|
66
68
|
|
|
@@ -3,19 +3,9 @@
|
|
|
3
3
|
import Image from 'next/image';
|
|
4
4
|
import type { DocsConfig, SocialPlatform, FooterLinkColumn } from '@/lib/docs-types';
|
|
5
5
|
import { getIconClass } from '@/lib/icon-utils';
|
|
6
|
+
import { getBrandingUrl } from '@/lib/branding-url';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
// Uses NEXT_PUBLIC_ prefix so these are inlined during build for client components
|
|
9
|
-
const showBranding = process.env.NEXT_PUBLIC_SHOW_BRANDING !== 'false'; // Default true
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generate branding URL with UTM parameters for attribution tracking.
|
|
13
|
-
* projectSlug is passed as a prop (works in ISR multi-tenant mode).
|
|
14
|
-
* Falls back to NEXT_PUBLIC_PROJECT_SLUG env var (works in CLI dev mode).
|
|
15
|
-
*/
|
|
16
|
-
function getBrandingUrl(projectSlug: string): string {
|
|
17
|
-
return `https://www.jamdesk.com?utm_campaign=poweredBy&utm_medium=referral&utm_source=${projectSlug}`;
|
|
18
|
-
}
|
|
8
|
+
const showBranding = process.env.NEXT_PUBLIC_SHOW_BRANDING !== 'false';
|
|
19
9
|
|
|
20
10
|
interface SocialFooterProps {
|
|
21
11
|
config: DocsConfig;
|
|
@@ -150,23 +140,19 @@ export function SocialFooter({ config, hidden, projectSlug }: SocialFooterProps)
|
|
|
150
140
|
return null;
|
|
151
141
|
}
|
|
152
142
|
|
|
153
|
-
// Resolve slug: prop (ISR) → env var (CLI dev) → fallback
|
|
154
|
-
const slug = projectSlug || process.env.NEXT_PUBLIC_PROJECT_SLUG || 'docs';
|
|
155
|
-
|
|
156
143
|
return (
|
|
157
144
|
<footer className="mt-12 sm:mt-16 pt-6 sm:pt-8 border-t border-[var(--color-border)]">
|
|
158
145
|
{hasLinks && <LinkColumns columns={links} />}
|
|
159
|
-
{/* Desktop: row layout, Mobile: column layout */}
|
|
160
146
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
161
147
|
{hasSocials && <SocialIcons socials={socials} />}
|
|
162
148
|
{showBranding && (
|
|
163
149
|
<a
|
|
164
|
-
href={getBrandingUrl(
|
|
150
|
+
href={getBrandingUrl(projectSlug)}
|
|
165
151
|
target="_blank"
|
|
166
152
|
rel="noopener noreferrer"
|
|
167
153
|
className="group flex items-center gap-1 text-sm text-[#AEAEAE] hover:text-[#6A6D70] transition-colors whitespace-nowrap"
|
|
168
154
|
>
|
|
169
|
-
|
|
155
|
+
Powered by
|
|
170
156
|
<span className="relative inline-block w-[58px] h-[15px] top-[1px]">
|
|
171
157
|
<Image
|
|
172
158
|
src="/_jd/branding/jamdesk-wordmark.svg"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// `NEXT_PUBLIC_PROJECT_SLUG` is read inline so Next.js inlines the value at build
|
|
2
|
+
// time for client components (SocialFooter). Hoisting the read would defeat the
|
|
3
|
+
// inline. Server components (the unlock page) just see it as runtime env.
|
|
4
|
+
export function getBrandingUrl(projectSlug?: string | null): string {
|
|
5
|
+
const slug = encodeURIComponent(
|
|
6
|
+
projectSlug || process.env.NEXT_PUBLIC_PROJECT_SLUG || 'docs'
|
|
7
|
+
);
|
|
8
|
+
return `https://www.jamdesk.com?utm_campaign=poweredBy&utm_medium=referral&utm_source=${slug}`;
|
|
9
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ErrorDetails } from '../../shared/types.js';
|
|
2
2
|
import { DEPRECATED_COMPONENTS } from '../deprecated-components.js';
|
|
3
|
+
import { MIGRATION_DOCS_URL } from '../validate-config.js';
|
|
3
4
|
|
|
4
5
|
/** Format: ERR-XXXXXXXX (first 8 chars of buildId, uppercase) */
|
|
5
6
|
export function generateErrorRef(buildId: string): string {
|
|
@@ -108,6 +109,31 @@ export function parseErrorDetails(
|
|
|
108
109
|
// Extract error source information upfront - used by multiple error types
|
|
109
110
|
const errorSource = extractErrorSource(output, pageToFileMap);
|
|
110
111
|
|
|
112
|
+
// Mintlify migration needed — must come before the generic config_error branch
|
|
113
|
+
// so the migrate-specific suggestion wins. We match on the migration docs URL
|
|
114
|
+
// (which every Mintlify-detection branch in validate-config embeds) rather than
|
|
115
|
+
// a prose phrase, so future copy edits can't silently break the contract.
|
|
116
|
+
if (message.includes(MIGRATION_DOCS_URL)) {
|
|
117
|
+
return {
|
|
118
|
+
type: 'config_error',
|
|
119
|
+
message: 'Mintlify config detected — migration needed',
|
|
120
|
+
details: message,
|
|
121
|
+
suggestion:
|
|
122
|
+
'Your docs.json is still configured for Mintlify. Jamdesk includes a ' +
|
|
123
|
+
'tool that converts Mintlify projects automatically.\n\n' +
|
|
124
|
+
'From your project\'s root directory, run:\n\n' +
|
|
125
|
+
' npm install -g jamdesk\n' +
|
|
126
|
+
' jamdesk migrate\n\n' +
|
|
127
|
+
'The migration will:\n' +
|
|
128
|
+
'• Convert mint.json / docs.json to the Jamdesk format\n' +
|
|
129
|
+
'• Update the theme to "jam"\n' +
|
|
130
|
+
'• Convert Mintlify-only MDX components (e.g. <CardGroup> → <Columns>)\n' +
|
|
131
|
+
'• Rewrite parent-relative snippet imports\n\n' +
|
|
132
|
+
'Then commit the changes and push — the next build will pick them up.\n\n' +
|
|
133
|
+
'Full guide: https://jamdesk.com/docs/setup/migration',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
111
137
|
// Configuration validation errors (from validate phase)
|
|
112
138
|
if (message.includes('Missing docs.json') || message.includes('Invalid docs.json')) {
|
|
113
139
|
return {
|
package/vendored/lib/docs-isr.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import { cache } from 'react';
|
|
15
16
|
import {
|
|
16
17
|
fetchDocsConfig,
|
|
17
18
|
fetchMdxContent,
|
|
@@ -72,17 +73,19 @@ function walkMdx(dir: string): string[] {
|
|
|
72
73
|
return out;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
// React cache() — request-scoped memoization. Layered on top of Next.js
|
|
77
|
+
// Data Cache (which fetchDocsConfig already wraps via unstable_cache) so
|
|
78
|
+
// repeated calls within the same render pass don't re-enter the Data Cache
|
|
79
|
+
// either. Production fra1 logs showed 3 R2 fetches per request from
|
|
80
|
+
// layout.generateMetadata + layout render + content-loader.getConfig — when
|
|
81
|
+
// they all hit a Data Cache miss simultaneously (fresh region/cold cache),
|
|
82
|
+
// each pays a cross-continent round-trip (~300ms each). cache() collapses
|
|
83
|
+
// these to one in-flight promise.
|
|
84
|
+
//
|
|
85
|
+
// Caveat: cache() is documented to dedupe across generateMetadata + page
|
|
86
|
+
// render in the same request, but historical Next.js bugs (#50080, #67133)
|
|
87
|
+
// occasionally split contexts. In the worst case this still reduces 3→2.
|
|
88
|
+
async function getDocsConfigUncached(projectSlug: string): Promise<DocsConfig> {
|
|
86
89
|
requireIsrMode();
|
|
87
90
|
const config = await fetchDocsConfig(projectSlug);
|
|
88
91
|
if (config) return config;
|
|
@@ -99,6 +102,14 @@ export async function getDocsConfig(projectSlug: string): Promise<DocsConfig> {
|
|
|
99
102
|
throw new Error(`Project not found in R2: ${projectSlug}`);
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Get docs.json configuration for a project.
|
|
107
|
+
*
|
|
108
|
+
* Request-scoped (React cache) on top of Next.js Data Cache. Same projectSlug
|
|
109
|
+
* within one render → one R2 fetch.
|
|
110
|
+
*/
|
|
111
|
+
export const getDocsConfig = cache(getDocsConfigUncached);
|
|
112
|
+
|
|
102
113
|
/**
|
|
103
114
|
* Get all document paths for a project.
|
|
104
115
|
*
|
|
@@ -114,14 +125,7 @@ export async function getAllDocPaths(projectSlug: string): Promise<string[]> {
|
|
|
114
125
|
return listAllPaths(projectSlug);
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
* Get raw MDX content for a page.
|
|
119
|
-
*
|
|
120
|
-
* @param projectSlug - The project identifier
|
|
121
|
-
* @param pagePath - Path to the page (e.g., 'api/auth')
|
|
122
|
-
* @returns Raw MDX content string
|
|
123
|
-
*/
|
|
124
|
-
export async function getMdxContent(
|
|
128
|
+
async function getMdxContentUncached(
|
|
125
129
|
projectSlug: string,
|
|
126
130
|
pagePath: string
|
|
127
131
|
): Promise<string> {
|
|
@@ -141,6 +145,16 @@ export async function getMdxContent(
|
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Get raw MDX content for a page.
|
|
150
|
+
*
|
|
151
|
+
* Request-scoped (React cache) so generateMetadata's frontmatter read and
|
|
152
|
+
* the subsequent page render share one R2 fetch instead of paying it twice.
|
|
153
|
+
* Production fra1 logs showed every page making 2× fetchMdxContent calls
|
|
154
|
+
* for the same path (~600ms duplicated work per request).
|
|
155
|
+
*/
|
|
156
|
+
export const getMdxContent = cache(getMdxContentUncached);
|
|
157
|
+
|
|
144
158
|
/**
|
|
145
159
|
* Get snippet content.
|
|
146
160
|
*
|
|
@@ -758,7 +758,7 @@ export interface SpellcheckConfig {
|
|
|
758
758
|
export interface PasswordAuthConfig {
|
|
759
759
|
/** Opt in to password protection. Site is only gated when this is true AND a password has been set in the dashboard. */
|
|
760
760
|
enabled?: boolean;
|
|
761
|
-
/** Optional hint shown on the unlock page (e.g., "Ask
|
|
761
|
+
/** Optional hint shown on the unlock page (e.g., "Ask #docs-access on Slack"). Plain text, no HTML. */
|
|
762
762
|
hint?: string;
|
|
763
763
|
/** Paths or globs that bypass the password check. Supports '*' (one path segment) and '**' (recursive). */
|
|
764
764
|
public?: string[];
|
|
@@ -38,7 +38,7 @@ export async function sendInternalBuildFailureEmail(info: BuildFailureEmailInfo)
|
|
|
38
38
|
const result = await resend.emails.send({
|
|
39
39
|
from: 'Jamdesk <no-reply@mail.jamdesk.com>',
|
|
40
40
|
to: reportEmail,
|
|
41
|
-
subject: `
|
|
41
|
+
subject: `Jamdesk build failed: ${info.projectName || info.projectId}`,
|
|
42
42
|
html,
|
|
43
43
|
text,
|
|
44
44
|
});
|
|
@@ -294,7 +294,7 @@ export const ISR_PHASES = {
|
|
|
294
294
|
optimize_images: { label: 'Optimizing images...', weight: 5 },
|
|
295
295
|
r2_upload: { label: 'Uploading to CDN...', weight: 35 },
|
|
296
296
|
embeddings: { label: 'Indexing AI search + chat...', weight: 5 },
|
|
297
|
-
vercel_purge: { label: 'Refreshing cache...', weight:
|
|
297
|
+
vercel_purge: { label: 'Refreshing cache...', weight: 10 },
|
|
298
298
|
cleanup: { label: 'Cleaning up...', weight: 5 },
|
|
299
299
|
} as const;
|
|
300
300
|
|