From 96671cb4b0b80110c002b896e4da62a223238bde Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Thu, 2 Jul 2026 02:08:51 +0300 Subject: [PATCH 1/2] fix: preserve docs json cloud init --- packages/docs/src/cli/cloud.test.ts | 82 +++++++++++++++++++ packages/docs/src/cli/cloud.ts | 119 +++++++++++++++++++++++----- 2 files changed, 183 insertions(+), 18 deletions(-) diff --git a/packages/docs/src/cli/cloud.test.ts b/packages/docs/src/cli/cloud.test.ts index 8542862b..53f8d0a4 100644 --- a/packages/docs/src/cli/cloud.test.ts +++ b/packages/docs/src/cli/cloud.test.ts @@ -111,6 +111,88 @@ describe("cloud cli", () => { expect(docsJson.cloud.preview).toBeUndefined(); }); + it("treats existing frameworkless docs.json as the cloud init source of truth", async () => { + mkdirSync(path.join(tmpDir, "docs"), { recursive: true }); + writeFileSync(path.join(tmpDir, "docs", "index.mdx"), "# Hello\n", "utf-8"); + writeFileSync( + path.join(tmpDir, "docs.json"), + JSON.stringify( + { + version: 1, + docs: { + mode: "frameworkless", + runtime: "nextjs", + root: ".docs/site", + }, + content: { + docsRoot: "docs", + apiReferenceRoot: "api-reference", + }, + cloud: { + publish: { mode: "direct-commit", baseBranch: "stable" }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + const result = await initCloudConfig({ + rootDir: tmpDir, + apiKeyEnv: "ACME_DOCS_CLOUD_KEY", + }); + const docsJson = JSON.parse(readFileSync(path.join(tmpDir, "docs.json"), "utf-8")); + + expect(result).toMatchObject({ + configPath: path.join(tmpDir, "docs.json"), + docsJsonPath: path.join(tmpDir, "docs.json"), + apiKeyEnv: "ACME_DOCS_CLOUD_KEY", + configCreated: false, + configUpdated: false, + docsJsonCreated: false, + docsJsonUpdated: true, + }); + expect(existsSync(path.join(tmpDir, "docs.config.ts"))).toBe(false); + expect(docsJson).toMatchObject({ + docs: { + mode: "frameworkless", + runtime: "nextjs", + root: ".docs/site", + }, + content: { + docsRoot: "docs", + apiReferenceRoot: "api-reference", + }, + cloud: { + apiKey: { env: "ACME_DOCS_CLOUD_KEY" }, + deploy: { enabled: true }, + analytics: { + enabled: true, + console: false, + includeInputs: false, + }, + publish: { + mode: "direct-commit", + baseBranch: "stable", + }, + }, + }); + + const secondResult = await initCloudConfig({ + rootDir: tmpDir, + apiKeyEnv: "ACME_DOCS_CLOUD_KEY", + }); + + expect(secondResult).toMatchObject({ + configCreated: false, + configUpdated: false, + docsJsonCreated: false, + docsJsonUpdated: false, + }); + expect(existsSync(path.join(tmpDir, "docs.config.ts"))).toBe(false); + }); + it("initializes Docs Cloud config, analytics, and docs.json", async () => { writePackageJson(); mkdirSync(path.join(tmpDir, "app", "docs"), { recursive: true }); diff --git a/packages/docs/src/cli/cloud.ts b/packages/docs/src/cli/cloud.ts index 8f896246..db97ec42 100644 --- a/packages/docs/src/cli/cloud.ts +++ b/packages/docs/src/cli/cloud.ts @@ -1146,8 +1146,12 @@ function resolveDocsBlock( existing?: ManagedDocsJson, docsInfraProfile?: ConnectedDocsProfile, ): ManagedDocsJson["docs"] { - const detectedFramework = detectFramework(rootDir); const existingDocs = existing?.docs; + if (!snapshot.path && existingDocs) { + return existingDocs; + } + + const detectedFramework = detectFramework(rootDir); const runtime = detectedFramework ?? docsInfraProfile?.runtime ?? existingDocs?.runtime ?? "nextjs"; const hasFrameworkConfig = Boolean(snapshot.path || detectedFramework); @@ -1227,37 +1231,89 @@ export function serializeMaterializedDocsJson(config: ManagedDocsJson): string { return `${JSON.stringify(config, null, 2)}\n`; } -export async function materializeCloudConfig( - options: CloudCommandOptions = {}, -): Promise { - const rootDir = options.rootDir ?? process.cwd(); - const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE); - const existing = readExistingDocsJson(docsJsonPath); - const snapshot = await loadDocsConfigSnapshot(rootDir, options.configPath); +function withCloudInitDefaults( + cloud: DocsCloudConfig | undefined, + existingCloud: DocsCloudConfig | undefined, + apiKeyEnv: string, +): DocsCloudConfig { + const normalized = normalizeCloudConfig(cloud); + + if (!existingCloud?.apiKey?.env) { + normalized.apiKey = { env: apiKeyEnv }; + } + + if (!normalized.deploy) { + normalized.deploy = { enabled: true }; + } + + if (typeof normalized.analytics === "undefined") { + normalized.analytics = { + enabled: true, + console: false, + includeInputs: false, + }; + } + + return normalizeCloudConfig(normalized); +} + +function writeMaterializedCloudConfig(params: { + rootDir: string; + docsJsonPath: string; + existing?: ManagedDocsJson; + snapshot: DocsConfigSnapshot; + docsInfraProfile?: ConnectedDocsProfile; + cloudInitApiKeyEnv?: string; +}): MaterializeCloudConfigResult { const config = materializeDocsJsonObject({ - rootDir, - snapshot, - existing, - docsInfraProfile: options.docsInfraProfile, + rootDir: params.rootDir, + snapshot: params.snapshot, + existing: params.existing, + docsInfraProfile: params.docsInfraProfile, }); + + if (params.cloudInitApiKeyEnv) { + config.cloud = withCloudInitDefaults( + config.cloud, + params.existing?.cloud, + params.cloudInitApiKeyEnv, + ); + } + const serialized = serializeMaterializedDocsJson(config); - const previous = existing ? fs.readFileSync(docsJsonPath, "utf-8") : undefined; + const previous = params.existing ? fs.readFileSync(params.docsJsonPath, "utf-8") : undefined; const updated = previous !== serialized; if (updated) { - fs.writeFileSync(docsJsonPath, serialized, "utf-8"); + fs.writeFileSync(params.docsJsonPath, serialized, "utf-8"); } return { - configPath: snapshot.path ?? docsJsonPath, - docsJsonPath, + configPath: params.snapshot.path ?? params.docsJsonPath, + docsJsonPath: params.docsJsonPath, config, apiKeyEnv: config.cloud?.apiKey?.env ?? DOCS_CLOUD_DEFAULT_API_KEY_ENV, - created: !existing, + created: !params.existing, updated, }; } +export async function materializeCloudConfig( + options: CloudCommandOptions = {}, +): Promise { + const rootDir = options.rootDir ?? process.cwd(); + const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE); + const existing = readExistingDocsJson(docsJsonPath); + const snapshot = await loadDocsConfigSnapshot(rootDir, options.configPath); + return writeMaterializedCloudConfig({ + rootDir, + docsJsonPath, + existing, + snapshot, + docsInfraProfile: options.docsInfraProfile, + }); +} + function readCombinedEnv(rootDir: string): Record { const env: Record = { ...loadProjectEnv(rootDir), @@ -2380,11 +2436,38 @@ export async function syncCloudConfig(options: CloudCommandOptions = {}) { export async function initCloudConfig(options: CloudCommandOptions = {}): Promise { const rootDir = options.rootDir ?? process.cwd(); + const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE); + const existingDocsJson = readExistingDocsJson(docsJsonPath); const apiKeyEnv = normalizeEnvName(options.apiKeyEnv, DOCS_CLOUD_DEFAULT_API_KEY_ENV); const existingConfigPath = tryResolveDocsConfigPath(rootDir, options.configPath); + const useDocsJsonAsSource = Boolean(existingDocsJson && !existingConfigPath); const docsInfraProfile = options.docsInfraProfile ?? - (existingConfigPath ? undefined : detectConnectedFumadocsProfile(rootDir)); + (existingConfigPath || existingDocsJson ? undefined : detectConnectedFumadocsProfile(rootDir)); + + if (useDocsJsonAsSource) { + const materialized = writeMaterializedCloudConfig({ + rootDir, + docsJsonPath, + existing: existingDocsJson, + snapshot: {}, + docsInfraProfile, + cloudInitApiKeyEnv: apiKeyEnv, + }); + + return { + configPath: materialized.configPath, + docsJsonPath: materialized.docsJsonPath, + apiKeyEnv: materialized.apiKeyEnv, + analyticsProjectIdEnv: DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV, + configCreated: false, + configUpdated: false, + docsJsonCreated: false, + docsJsonUpdated: materialized.updated, + ...(docsInfraProfile ? { docsInfraProfile } : {}), + }; + } + const configUpdate = ensureDocsConfigCloudInit({ rootDir, configPath: options.configPath, From 807776586e02ffc88e917dd309eab4bd2c5c0ef1 Mon Sep 17 00:00:00 2001 From: Kinfe123 Date: Thu, 2 Jul 2026 02:38:00 +0300 Subject: [PATCH 2/2] fix: keep docs json source from fumadocs detection --- packages/docs/src/cli/cloud.test.ts | 57 +++++++++++++++++++++++++++++ packages/docs/src/cli/cloud.ts | 38 +++++++++++++------ 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/packages/docs/src/cli/cloud.test.ts b/packages/docs/src/cli/cloud.test.ts index 53f8d0a4..8c9c92fc 100644 --- a/packages/docs/src/cli/cloud.test.ts +++ b/packages/docs/src/cli/cloud.test.ts @@ -193,6 +193,63 @@ describe("cloud cli", () => { expect(existsSync(path.join(tmpDir, "docs.config.ts"))).toBe(false); }); + it("does not infer Fumadocs connect mode when docs.json is already the source", async () => { + writePackageJson({ + next: "16.0.0", + "fumadocs-core": "16.7.16", + "fumadocs-ui": "16.7.16", + }); + mkdirSync(path.join(tmpDir, "docs"), { recursive: true }); + mkdirSync(path.join(tmpDir, "content", "docs"), { recursive: true }); + writeFileSync(path.join(tmpDir, "docs", "index.mdx"), "# Atomic docs.json\n", "utf-8"); + writeFileSync( + path.join(tmpDir, "content", "docs", "index.mdx"), + "# Fumadocs signal\n", + "utf-8", + ); + writeFileSync(path.join(tmpDir, "source.config.ts"), "export default {};\n", "utf-8"); + writeFileSync( + path.join(tmpDir, "docs.json"), + JSON.stringify( + { + version: 1, + docs: { + mode: "frameworkless", + runtime: "nextjs", + root: ".docs/site", + }, + content: { + docsRoot: "docs", + }, + cloud: { + apiKey: { env: "EXISTING_DOCS_CLOUD_KEY" }, + }, + }, + null, + 2, + ), + "utf-8", + ); + + const result = await initCloudConfig({ rootDir: tmpDir }); + const materialized = await materializeCloudConfig({ rootDir: tmpDir }); + const docsJson = JSON.parse(readFileSync(path.join(tmpDir, "docs.json"), "utf-8")); + + expect(result.docsInfraProfile).toBeUndefined(); + expect(result.configCreated).toBe(false); + expect(result.configPath).toBe(path.join(tmpDir, "docs.json")); + expect(materialized.updated).toBe(false); + expect(existsSync(path.join(tmpDir, "docs.config.ts"))).toBe(false); + expect(docsJson.content.docsRoot).toBe("docs"); + expect(docsJson.extensions?.docsInfraProfile).toBeUndefined(); + expect(docsJson.docs).toEqual({ + mode: "frameworkless", + runtime: "nextjs", + root: ".docs/site", + }); + expect(docsJson.cloud.apiKey.env).toBe("EXISTING_DOCS_CLOUD_KEY"); + }); + it("initializes Docs Cloud config, analytics, and docs.json", async () => { writePackageJson(); mkdirSync(path.join(tmpDir, "app", "docs"), { recursive: true }); diff --git a/packages/docs/src/cli/cloud.ts b/packages/docs/src/cli/cloud.ts index db97ec42..7ffe2925 100644 --- a/packages/docs/src/cli/cloud.ts +++ b/packages/docs/src/cli/cloud.ts @@ -1073,10 +1073,7 @@ function resolveConnectedDocsProfile(params: { rootDir: string; snapshot: DocsConfigSnapshot; existing?: ManagedDocsJson; - explicit?: ConnectedDocsProfile; }): ConnectedDocsProfile | undefined { - if (params.explicit) return params.explicit; - const shouldResolveConnectProfile = params.snapshot.content?.includes(FUMADOCS_CONNECT_MARKER) || !params.snapshot.path; if (!shouldResolveConnectProfile) return undefined; @@ -1170,10 +1167,13 @@ function resolveDocsBlock( function resolveExtensions( existing: ManagedDocsJson | undefined, docsInfraProfile: ConnectedDocsProfile | undefined, + options: { dropStaleDocsInfraProfile?: boolean } = {}, ): JsonRecord | undefined { const existingExtensions = toJsonRecord(existing?.extensions); if (!docsInfraProfile) { - if (!existingExtensions?.docsInfraProfile) return existingExtensions; + if (!existingExtensions?.docsInfraProfile || options.dropStaleDocsInfraProfile === false) { + return existingExtensions; + } const { docsInfraProfile: _staleDocsInfraProfile, ...rest } = existingExtensions; return Object.keys(rest).length > 0 ? rest : undefined; @@ -1190,14 +1190,19 @@ function materializeDocsJsonObject(params: { snapshot: DocsConfigSnapshot; existing?: ManagedDocsJson; docsInfraProfile?: ConnectedDocsProfile; + detectConnectedDocsProfile?: boolean; + dropStaleDocsInfraProfile?: boolean; }): ManagedDocsJson { const cloud = resolveCloudConfig(params.snapshot, params.existing); - const docsInfraProfile = resolveConnectedDocsProfile({ - rootDir: params.rootDir, - snapshot: params.snapshot, - existing: params.existing, - explicit: params.docsInfraProfile, - }); + const docsInfraProfile = + params.docsInfraProfile ?? + (params.detectConnectedDocsProfile === false + ? undefined + : resolveConnectedDocsProfile({ + rootDir: params.rootDir, + snapshot: params.snapshot, + existing: params.existing, + })); const docsRoot = resolveDocsRoot( params.rootDir, params.snapshot, @@ -1207,7 +1212,9 @@ function materializeDocsJsonObject(params: { const apiReferenceRoot = resolveApiReferenceRoot(params.snapshot); const existingContent = toJsonRecord(params.existing?.content); const site = resolveSiteConfig(params.rootDir, params.snapshot, params.existing); - const extensions = resolveExtensions(params.existing, docsInfraProfile); + const extensions = resolveExtensions(params.existing, docsInfraProfile, { + dropStaleDocsInfraProfile: params.dropStaleDocsInfraProfile, + }); const content: ManagedDocsJson["content"] = { ...existingContent, @@ -1264,12 +1271,16 @@ function writeMaterializedCloudConfig(params: { snapshot: DocsConfigSnapshot; docsInfraProfile?: ConnectedDocsProfile; cloudInitApiKeyEnv?: string; + detectConnectedDocsProfile?: boolean; + dropStaleDocsInfraProfile?: boolean; }): MaterializeCloudConfigResult { const config = materializeDocsJsonObject({ rootDir: params.rootDir, snapshot: params.snapshot, existing: params.existing, docsInfraProfile: params.docsInfraProfile, + detectConnectedDocsProfile: params.detectConnectedDocsProfile, + dropStaleDocsInfraProfile: params.dropStaleDocsInfraProfile, }); if (params.cloudInitApiKeyEnv) { @@ -1305,12 +1316,15 @@ export async function materializeCloudConfig( const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE); const existing = readExistingDocsJson(docsJsonPath); const snapshot = await loadDocsConfigSnapshot(rootDir, options.configPath); + const useDocsJsonAsSource = Boolean(existing && !snapshot.path); return writeMaterializedCloudConfig({ rootDir, docsJsonPath, existing, snapshot, docsInfraProfile: options.docsInfraProfile, + detectConnectedDocsProfile: !useDocsJsonAsSource, + dropStaleDocsInfraProfile: !useDocsJsonAsSource, }); } @@ -2453,6 +2467,8 @@ export async function initCloudConfig(options: CloudCommandOptions = {}): Promis snapshot: {}, docsInfraProfile, cloudInitApiKeyEnv: apiKeyEnv, + detectConnectedDocsProfile: false, + dropStaleDocsInfraProfile: false, }); return {