Skip to content
Closed
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b02c1e3
fix(cli): run Windows post-processing on cached bundles
devin-ai-integration[bot] Apr 8, 2026
581738f
chore(ci): add docs preview smoke test workflow
devin-ai-integration[bot] Apr 8, 2026
500a7e3
fix(ci): remove hardcoded pnpm version in smoke test workflow
devin-ai-integration[bot] Apr 8, 2026
af57ae8
fix(ci): use prod CLI build, install native deps, add playwright pack…
devin-ai-integration[bot] Apr 8, 2026
807c34d
fix(ci): download CLI artifact outside repo tree to avoid catalog: pr…
devin-ai-integration[bot] Apr 8, 2026
b2f02a0
fix(ci): use /tmp for CLI artifact to fully isolate from repo package…
devin-ai-integration[bot] Apr 8, 2026
0727316
fix(ci): resolve pnpm catalog: versions in CLI build artifact
devin-ai-integration[bot] Apr 8, 2026
4884ac3
fix(ci): remove flaky API reference pages from smoke test
devin-ai-integration[bot] Apr 8, 2026
779e3cc
feat(ci): add windows-latest to docs preview smoke test matrix
devin-ai-integration[bot] Apr 8, 2026
8b2687c
fix(ci): log warning instead of silently swallowing errors in loadPnp…
devin-ai-integration[bot] Apr 8, 2026
822289e
fix(ci): fix biome noConsole lint error in loadPnpmCatalog
devin-ai-integration[bot] Apr 8, 2026
e9114db
fix(ci): increase server startup timeout to 300s for Windows bundle i…
devin-ai-integration[bot] Apr 8, 2026
a2b258f
fix(ci): add diagnostic curl step to capture Windows 500 response body
devin-ai-integration[bot] Apr 8, 2026
75862d8
fix(ci): prevent diagnostic curl SIGPIPE failure in smoke test
devin-ai-integration[bot] Apr 8, 2026
e615642
fix(cli): resolve Windows symlinks as junctions/copies after bundle e…
devin-ai-integration[bot] Apr 8, 2026
59d498a
fix(cli): fix biome import ordering and formatting
devin-ai-integration[bot] Apr 8, 2026
f5716c7
fix(ci): add Windows debug step to dump CLI debug log and check symli…
devin-ai-integration[bot] Apr 8, 2026
ba5e63d
fix(cli): resolve symlinks before pnpm i esbuild to prevent target pr…
devin-ai-integration[bot] Apr 8, 2026
6d9c272
fix(cli): fix TS2532 undefined check on path parts array
devin-ai-integration[bot] Apr 8, 2026
164a4f6
fix(cli): two-phase backup/restore for Windows file-traced packages
devin-ai-integration[bot] Apr 8, 2026
e7b8e20
fix(cli): remove backup/restore workaround and debug steps, fix at so…
devin-ai-integration[bot] Apr 9, 2026
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
138 changes: 90 additions & 48 deletions packages/cli/docs-preview/src/downloadLocalDocsBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,89 @@ const PNPMFILE_CJS_CONTENTS = `module.exports = {

const NPMRC_CONTENTS = "@fern-fern:registry=https://npm.buildwithfern.com\n";

// Marker file written after Windows post-processing completes so we can skip
// the (slow) pnpm install on subsequent cached-bundle runs.
const WINDOWS_POST_PROCESSED_MARKER = ".windows-post-processed";

function getPathToWindowsPostProcessedMarker({ app = false }: { app?: boolean }): AbsoluteFilePath {
return join(getPathToStandaloneFolder({ app }), RelativeFilePath.of(WINDOWS_POST_PROCESSED_MARKER));
}

/**
* Runs Windows-specific post-processing on the extracted bundle.
* The tar bundle contains Unix symlinks that don't work on Windows, so we
* write helper config files and run `pnpm install` in the standalone directory
* to recreate the missing node_modules entries.
*
* This is idempotent — it checks for existing files before writing and uses a
* marker file to skip the expensive `pnpm install` on subsequent runs.
*/
async function postProcessWindowsBundle({ app, logger }: { app: boolean; logger: Logger }): Promise<void> {
const absPathToStandalone = getPathToStandaloneFolder({ app });
if (!(await doesPathExist(absPathToStandalone))) {
logger.debug("Standalone folder does not exist, skipping Windows post-processing");
return;
}

// If the marker file exists, post-processing was already completed.
const markerPath = getPathToWindowsPostProcessedMarker({ app });
if (await doesPathExist(markerPath)) {
logger.debug("Windows post-processing already completed (marker file exists), skipping");
return;
}

const absPathToInstrumentationJs = getPathToInstrumentationJs({ app });
const pnpmWorkspacePath = getPathToPnpmWorkspaceYaml({ app });
const pnpmfilePath = getPathToPnpmfileCjs({ app });
const npmrcPath = getPathToNpmrc({ app });

// Check all paths in parallel
const [pnpmWorkspaceExists, pnpmfileExists, npmrcExists, instrumentationJsExists] = await Promise.all([
doesPathExist(pnpmWorkspacePath),
doesPathExist(pnpmfilePath),
doesPathExist(npmrcPath),
doesPathExist(absPathToInstrumentationJs)
]);

// Warn if pnpm-workspace.yaml does not exist
if (!pnpmWorkspaceExists) {
logger.warn(
`Expected pnpm-workspace.yaml at ${pnpmWorkspacePath} but it does not exist. If you are experiencing issues, please contact support@buildwithfern.com.`
);
}

// Write pnpmfile.cjs if it does not exist
if (!pnpmfileExists) {
logger.debug(`Writing pnpmfile.cjs at ${pnpmfilePath}`);
await writeFile(pnpmfilePath, PNPMFILE_CJS_CONTENTS);
}
// Write .npmrc if it does not exist
if (!npmrcExists) {
logger.debug(`Writing .npmrc at ${npmrcPath}`);
await writeFile(npmrcPath, NPMRC_CONTENTS);
}
// Remove instrumentation.js if it exists
if (instrumentationJsExists) {
logger.debug(`Removing instrumentation.js at ${absPathToInstrumentationJs}`);
await rm(absPathToInstrumentationJs);
}

try {
// pnpm install within standalone
logger.debug("Running pnpm install within standalone");
await loggingExeca(logger, "pnpm", ["install"], {
cwd: absPathToStandalone,
doNotPipeOutput: true
});
} catch (error) {
throw contactFernSupportError(`Failed to install required package due to error: ${error}`);
}

// Write marker file so we skip this on future cached-bundle runs
await writeFile(markerPath, new Date().toISOString());
logger.debug("Windows post-processing completed");
}

export async function downloadBundle({
bucketUrl,
logger,
Expand Down Expand Up @@ -142,7 +225,12 @@ export async function downloadBundle({
}
if (currentETag != null && currentETag === eTag) {
logger.debug("ETag matches. Using already downloaded bundle");
// The bundle is already downloaded
// The bundle is already downloaded, but on Windows we may still
// need to run post-processing (e.g. when the bundle was
// pre-deployed by CI or extracted externally).
if (PLATFORM_IS_WINDOWS && app) {
await postProcessWindowsBundle({ app, logger });
}
return {
type: "success"
};
Expand Down Expand Up @@ -362,53 +450,7 @@ export async function downloadBundle({
}

if (PLATFORM_IS_WINDOWS) {
const absPathToStandalone = getPathToStandaloneFolder({ app });
const absPathToInstrumentationJs = getPathToInstrumentationJs({ app });
const pnpmWorkspacePath = getPathToPnpmWorkspaceYaml({ app });
const pnpmfilePath = getPathToPnpmfileCjs({ app });
const npmrcPath = getPathToNpmrc({ app });

// Check all paths in parallel
const [pnpmWorkspaceExists, pnpmfileExists, npmrcExists, instrumentationJsExists] = await Promise.all([
doesPathExist(pnpmWorkspacePath),
doesPathExist(pnpmfilePath),
doesPathExist(npmrcPath),
doesPathExist(absPathToInstrumentationJs)
]);

// Warn if pnpm-workspace.yaml does not exist
if (!pnpmWorkspaceExists) {
logger.warn(
`Expected pnpm-workspace.yaml at ${pnpmWorkspacePath} but it does not exist. If you are experiencing issues, please contact support@buildwithfern.com.`
);
}

// Write pnpmfile.cjs if it does not exist
if (!pnpmfileExists) {
logger.debug(`Writing pnpmfile.cjs at ${pnpmfilePath}`);
await writeFile(pnpmfilePath, PNPMFILE_CJS_CONTENTS);
}
// Write .npmrc if it does not exist
if (!npmrcExists) {
logger.debug(`Writing .npmrc at ${npmrcPath}`);
await writeFile(npmrcPath, NPMRC_CONTENTS);
}
// Remove instrumentation.js if it exists
if (instrumentationJsExists) {
logger.debug(`Removing instrumentation.js at ${absPathToInstrumentationJs}`);
await rm(absPathToInstrumentationJs);
}

try {
// pnpm install within standalone
logger.debug("Running pnpm install within standalone");
await loggingExeca(logger, "pnpm", ["install"], {
cwd: absPathToStandalone,
doNotPipeOutput: true
});
} catch (error) {
throw contactFernSupportError(`Failed to install required package due to error: ${error}`);
}
await postProcessWindowsBundle({ app, logger });
}
}

Expand Down
Loading