From 16ab58b0bcd01c0105c2a5695ebd8277b803c13c Mon Sep 17 00:00:00 2001 From: Christian Legnitto Date: Mon, 27 Oct 2025 14:12:16 -0400 Subject: [PATCH] Disable file watching when running contentlayer build `contentlayer2 build` currently spins up chokidar watchers even though the command is supposed to run once and exit. In sandboxed environments (e.g. Codex seatbelt or some CI agents) that watcher never reports readiness, so the process and any scripts that invoke it hang indefinitely. Thread a new watch flag through the source-files fetcher and set it to false for generateDotpkg and dynamic builds while keeping watch=true for dev mode. That preserves incremental updates in dev while letting one-shot builds finish reliably. --- .../@contentlayer/core/src/dynamic-build.ts | 2 +- .../core/src/generation/generate-dotpkg.ts | 2 +- packages/@contentlayer/core/src/plugin.ts | 1 + .../source-files/src/fetchData/index.ts | 31 ++++++++++++------- .../@contentlayer/source-files/src/index.ts | 3 +- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/@contentlayer/core/src/dynamic-build.ts b/packages/@contentlayer/core/src/dynamic-build.ts index 72cf965e..9865e8f0 100644 --- a/packages/@contentlayer/core/src/dynamic-build.ts +++ b/packages/@contentlayer/core/src/dynamic-build.ts @@ -103,7 +103,7 @@ export const dynamicBuild = ({ config, verbose }: { config: Config; verbose: boo const cache = yield* $( pipe( - config.source.fetchData({ schemaDef, verbose, skipCachePersistence: true }), + config.source.fetchData({ schemaDef, verbose, skipCachePersistence: true, watch: false }), S.runHead, T.map(O.getUnsafe), T.chain((_) => T.fromEither(() => _)), diff --git a/packages/@contentlayer/core/src/generation/generate-dotpkg.ts b/packages/@contentlayer/core/src/generation/generate-dotpkg.ts index 14cbe0c2..46056f9e 100644 --- a/packages/@contentlayer/core/src/generation/generate-dotpkg.ts +++ b/packages/@contentlayer/core/src/generation/generate-dotpkg.ts @@ -101,7 +101,7 @@ export const generateDotpkgStream = ({ S.fromEffect(resolveParams), S.chainMapEitherRight(({ schemaDef, targetPath }) => pipe( - config.source.fetchData({ schemaDef, verbose }), + config.source.fetchData({ schemaDef, verbose, watch: isDev }), S.mapEffectEitherRight((cache) => pipe( writeFilesForCache({ config, schemaDef, targetPath, cache, generationOptions, writtenFilesCache, isDev }), diff --git a/packages/@contentlayer/core/src/plugin.ts b/packages/@contentlayer/core/src/plugin.ts index 8b469152..4d1ad341 100644 --- a/packages/@contentlayer/core/src/plugin.ts +++ b/packages/@contentlayer/core/src/plugin.ts @@ -129,6 +129,7 @@ export type FetchData = (_: { schemaDef: SchemaDef verbose: boolean skipCachePersistence?: boolean + watch?: boolean }) => S.Stream< OT.HasTracer & HasClock & HasCwd & HasConsole & fs.HasFs, never, diff --git a/packages/@contentlayer/source-files/src/fetchData/index.ts b/packages/@contentlayer/source-files/src/fetchData/index.ts index 2fbefc92..7a6a0ddd 100644 --- a/packages/@contentlayer/source-files/src/fetchData/index.ts +++ b/packages/@contentlayer/source-files/src/fetchData/index.ts @@ -24,6 +24,7 @@ export const fetchData = ({ contentDirExclude, skipCachePersistence = false, verbose, + watch = true, }: { coreSchemaDef: core.SchemaDef documentTypeDefs: LocalSchema.DocumentTypeDef[] @@ -38,6 +39,12 @@ export const fetchData = ({ */ skipCachePersistence?: boolean verbose: boolean + /** + * Whether file system changes should be watched for incremental updates. + * Disable this for one-off builds to avoid hanging on environments without + * file watcher support (e.g. sandboxed CI agents). + */ + watch?: boolean }): S.Stream< OT.HasTracer & HasCwd & HasConsole & fs.HasFs, never, @@ -48,18 +55,18 @@ export const fetchData = ({ const initEvent: CustomUpdateEventInit = { _tag: 'init' } - const watchPaths = contentDirInclude.length > 0 ? contentDirInclude : ['.'] - - const fileUpdatesStream = pipe( - FSWatch.makeAndSubscribe(watchPaths, { - cwd: contentDirPath, - ignoreInitial: true, - ignored: contentDirExclude as unknown as string[], // NOTE type cast needed because of readonly array - // Unfortunately needed in order to avoid race conditions - awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }, - }), - S.mapEitherRight(chokidarAllEventToCustomUpdateEvent), - ) + const fileUpdatesStream = watch + ? pipe( + FSWatch.makeAndSubscribe(contentDirInclude.length > 0 ? contentDirInclude : ['.'], { + cwd: contentDirPath, + ignoreInitial: true, + ignored: contentDirExclude as unknown as string[], // NOTE type cast needed because of readonly array + // Unfortunately needed in order to avoid race conditions + awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 }, + }), + S.mapEitherRight(chokidarAllEventToCustomUpdateEvent), + ) + : S.fromIterable>([]) const resolveParams = pipe( skipCachePersistence diff --git a/packages/@contentlayer/source-files/src/index.ts b/packages/@contentlayer/source-files/src/index.ts index 48dd1e44..13cfa7c5 100644 --- a/packages/@contentlayer/source-files/src/index.ts +++ b/packages/@contentlayer/source-files/src/index.ts @@ -105,7 +105,7 @@ export const makeSource: core.MakeSourcePlugin = (args) => async (sourceKe makeCoreSchema({ documentTypeDefs, options, esbuildHash }), T.mapError((error) => new SourceProvideSchemaError({ error })), ), - fetchData: ({ schemaDef, verbose, skipCachePersistence }) => + fetchData: ({ schemaDef, verbose, skipCachePersistence, watch }) => pipe( S.fromEffect(core.getCwd), S.chain((cwd) => { @@ -127,6 +127,7 @@ export const makeSource: core.MakeSourcePlugin = (args) => async (sourceKe contentDirInclude, verbose, skipCachePersistence, + watch, }) }), ),