Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/cli/cli/changes/5.23.6/fix-docs-dev-hot-reload.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Fix `fern docs dev` hot reload not working for .mdx file changes. The backend
now updates the docs definition before notifying the browser to refresh, and
the reload handler properly recovers from errors instead of silently blocking
all future reloads.
type: fix
10 changes: 10 additions & 0 deletions packages/cli/cli/changes/5.24.0/seed-verify-pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Plumb `verify`, `verifyRunner`, and `verifyValidatorVersion` flags through
`GenerationRunner.RunArgs` so the seed runner can invoke
`PostGenerationPipeline` with `VerificationStep` and exercise the same
validator-container code path that `fern generate --local --verify` uses.
No customer-facing CLI behavior change — the flags are opt-in and used only
by the seed test runner today.
type: internal
22 changes: 22 additions & 0 deletions packages/cli/cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 5.24.0
changelogEntry:
- summary: |
Plumb `verify`, `verifyRunner`, and `verifyValidatorVersion` flags through
`GenerationRunner.RunArgs` so the seed runner can invoke
`PostGenerationPipeline` with `VerificationStep` and exercise the same
validator-container code path that `fern generate --local --verify` uses.
No customer-facing CLI behavior change — the flags are opt-in and used only
by the seed test runner today.
type: internal
createdAt: "2026-05-13"
irVersion: 66
- version: 5.23.6
changelogEntry:
- summary: |
Fix `fern docs dev` hot reload not working for .mdx file changes. The backend
now updates the docs definition before notifying the browser to refresh, and
the reload handler properly recovers from errors instead of silently blocking
all future reloads.
type: fix
createdAt: "2026-05-13"
irVersion: 66
- version: 5.23.5
changelogEntry:
- summary: |
Expand Down
88 changes: 51 additions & 37 deletions packages/cli/docs-preview/src/runAppPreviewServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,57 +1240,71 @@ export async function runAppPreviewServer({
void (async () => {
isReloading = true;

// Expand the list of files to include pages that depend on changed snippets
const filesToReload = snippetTracker.getFilesToReload(editedAbsoluteFilepaths);
const hasSnippetDependencies = snippetTracker.hasSnippetDependencies(editedAbsoluteFilepaths);

if (hasSnippetDependencies) {
context.logger.info(
`Snippet dependencies detected. Reloading ${filesToReload.length} files (${editedAbsoluteFilepaths.length} changed, ${filesToReload.length - editedAbsoluteFilepaths.length} dependent pages)`
);
}

sendData({
version: 1,
type: "startReload"
});

const reloadedPreviewResult = await reloadDocsDefinition(filesToReload);
try {
// Expand the list of files to include pages that depend on changed snippets
const filesToReload = snippetTracker.getFilesToReload(editedAbsoluteFilepaths);
const hasSnippetDependencies = snippetTracker.hasSnippetDependencies(editedAbsoluteFilepaths);

editedAbsoluteFilepaths.length = 0;
if (hasSnippetDependencies) {
context.logger.info(
`Snippet dependencies detected. Reloading ${filesToReload.length} files (${editedAbsoluteFilepaths.length} changed, ${filesToReload.length - editedAbsoluteFilepaths.length} dependent pages)`
);
}

isReloading = false;
const reloadedPreviewResult = await reloadDocsDefinition(filesToReload);

sendData({
version: 1,
type: "finishReload"
});
// Update the docs definition BEFORE notifying the browser,
// so the backend serves fresh data when the browser refreshes.
if (reloadedPreviewResult != null) {
// Detect slug changes before updating the docs definition
const slugChanges = slugTracker.updateAndDetectChanges(reloadedPreviewResult.docsDefinition);

if (reloadedPreviewResult != null) {
// Detect slug changes before updating the docs definition
const slugChanges = slugTracker.updateAndDetectChanges(reloadedPreviewResult.docsDefinition);
previewResult = reloadedPreviewResult;

previewResult = reloadedPreviewResult;
// Recompute translated definitions
translatedDefinitions = await computeTranslatedDefinitions(reloadedPreviewResult);
if (translatedDefinitions.size > 0) {
context.logger.debug(`Recomputed translations for ${translatedDefinitions.size} locale(s)`);
}

// Recompute translated definitions
translatedDefinitions = await computeTranslatedDefinitions(reloadedPreviewResult);
if (translatedDefinitions.size > 0) {
context.logger.debug(`Recomputed translations for ${translatedDefinitions.size} locale(s)`);
}
sendData({
version: 1,
type: "finishReload"
});

// Send navigateToSlug events for any slug changes
if (slugChanges.length > 0) {
slugChanges.forEach((change) => {
const eventData = {
version: 1,
type: "navigateToSlug",
oldSlug: change.oldSlug,
newSlug: change.newSlug
};

sendData(eventData);
// Send navigateToSlug events for any slug changes
if (slugChanges.length > 0) {
slugChanges.forEach((change) => {
const eventData = {
version: 1,
type: "navigateToSlug",
oldSlug: change.oldSlug,
newSlug: change.newSlug
};

sendData(eventData);
});
}
} else {
sendData({
version: 1,
type: "finishReload"
});
}
} catch (err) {
context.logger.error(`Reload failed: ${extractErrorMessage(err)}`);
sendData({
version: 1,
type: "finishReload"
});
} finally {
editedAbsoluteFilepaths.length = 0;
isReloading = false;
}
})();
}, RELOAD_DEBOUNCE_MS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import {
} from "@fern-api/api-workspace-commons";
import { SourceResolverImpl } from "@fern-api/cli-source-resolver";
import { generatorsYml, SNIPPET_JSON_FILENAME } from "@fern-api/configuration";
import { ContainerRunner } from "@fern-api/core-utils";
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { type PipelineLogger, PostGenerationPipeline } from "@fern-api/generator-cli/pipeline";
import { generateIntermediateRepresentation } from "@fern-api/ir-generator";
import { IntermediateRepresentation } from "@fern-api/ir-sdk";
import { CliError, TaskAbortSignal, TaskContext } from "@fern-api/task-context";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import chalk from "chalk";
import { assertVerifyPipelineSucceeded } from "./assertVerifyPipelineSucceeded.js";
import { generateDynamicSnippetTests } from "./dynamic-snippets/generateDynamicSnippetTests.js";
import { ExecutionEnvironment } from "./ExecutionEnvironment.js";
import { writeFilesToDiskAndRunGenerator } from "./runGenerator.js";
Expand All @@ -33,6 +36,33 @@ export declare namespace GenerationRunner {
ai: generatorsYml.AiServicesSchema | undefined;
skipFernignore?: boolean;
skipAutogenerationIfManualExamplesExist?: boolean;
/**
* When true, run `PostGenerationPipeline` with `VerificationStep` after the
* generator finishes writing files. Exercises the same validator-container
* code path that `fern generate --local --verify` (and Fiddle) use today, so
* seed CI catches regressions in the verify.sh + validator-image plumbing
* end-to-end.
*
* Currently wired only for the TypeScript SDK generator, because it is the
* only generator emitting `.fern/verify.sh` today (FER-9681 will extend
* emission to the remaining language generators).
*/
verify?: boolean;
/**
* Container runtime to use when `verify` is true. Defaults to "docker".
* Ignored when `verify` is false.
*/
verifyRunner?: ContainerRunner;
/**
* Optional override for the validator-image tag. `VerificationStep` derives
* the image as `{generatorName}-validator:{version}` from
* `generatorVersions[generatorName]`. Seed sets this to `"latest"` because
* the generator runs at the `:local` tag locally but no `:local` validator
* image is built today — the published `:latest` is the closest analog.
* When undefined, the generator invocation's own version is used (the
* behavior `runLocalGenerationForWorkspace` relies on).
*/
verifyValidatorVersion?: string;
}
}

Expand All @@ -54,7 +84,10 @@ export class GenerationRunner {
skipUnstableDynamicSnippetTests,
inspect,
skipFernignore,
skipAutogenerationIfManualExamplesExist
skipAutogenerationIfManualExamplesExist,
verify,
verifyRunner,
verifyValidatorVersion
}: GenerationRunner.RunArgs): Promise<void> {
const results = await Promise.all(
generatorGroup.generators.map(async (generatorInvocation) => {
Expand Down Expand Up @@ -106,6 +139,17 @@ export class GenerationRunner {
`Skipping dynamic snippet tests; shouldGenerateDynamicSnippetTests: ${shouldGenerateDynamicSnippetTests}, language: ${generatorInvocation.language}`
);
}

if (verify === true) {
await runVerifyPipeline({
outputDir: generatorInvocation.absolutePathToLocalOutput,
generatorName: generatorInvocation.name,
generatorVersion: verifyValidatorVersion ?? generatorInvocation.version,
cliVersion: workspace.cliVersion,
runner: verifyRunner ?? "docker",
context: interactiveTaskContext
});
}
} catch (error) {
if (error instanceof TaskAbortSignal) {
// already logged by failAndThrow, nothing to do
Expand Down Expand Up @@ -228,3 +272,50 @@ export class GenerationRunner {
});
}
}

/**
* Mirrors the `verifyOnlyPipelineEnabled` branch of `runLocalGenerationForWorkspace`:
* instantiates `PostGenerationPipeline` with only `VerificationStep` enabled so the
* same validator-container + image-derivation + `execInContainer` flow used by
* `fern generate --local --verify` is exercised end-to-end by the seed runner.
*
* `VerificationStep` no-ops when `.fern/verify.sh` is absent, so wiring a generator
* that does not emit the script (today: anything other than the TypeScript SDK)
* is a safe no-op rather than a hard failure.
*/
async function runVerifyPipeline({
outputDir,
generatorName,
generatorVersion,
cliVersion,
runner,
context
}: {
outputDir: AbsoluteFilePath;
generatorName: string;
generatorVersion: string;
cliVersion: string | undefined;
runner: ContainerRunner;
context: TaskContext;
}): Promise<void> {
const pipelineLogger: PipelineLogger = {
debug: (msg) => context.logger.debug(msg),
info: (msg) => context.logger.info(msg),
warn: (msg) => context.logger.warn(msg),
error: (msg) => context.logger.error(msg)
};

const pipeline = new PostGenerationPipeline(
{
outputDir,
verify: { enabled: true, runner },
cliVersion: cliVersion ?? "unknown",
generatorVersions: { [generatorName]: generatorVersion },
generatorName
},
pipelineLogger
);

const pipelineResult = await pipeline.run();
assertVerifyPipelineSucceeded(pipelineResult, generatorName);
}
Loading
Loading