diff --git a/scripts/lint/client-bundle-baseline.json b/scripts/lint/client-bundle-baseline.json index d7c9f2d674..6f62923081 100644 --- a/scripts/lint/client-bundle-baseline.json +++ b/scripts/lint/client-bundle-baseline.json @@ -1,8 +1,6 @@ { "note": "Known server modules reachable from a browser entrypoint (#3670). Burn down, never grow. Regenerate with: deno run --allow-read --allow-write scripts/lint/audit-client-bundle.ts --update", "entrypoints": { - "src/index.client.ts": [ - "src/platform/adapters/fs/veryfront/request-context.ts" - ] + "src/index.client.ts": [] } } diff --git a/src/config/loader.ts b/src/config/loader.ts index 9b36ed7845..6d921fde27 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -29,7 +29,7 @@ import { getHostEnv } from "#veryfront/platform/compat/process/env.ts"; import { LRUCache } from "#veryfront/utils/lru-wrapper.ts"; import { registerLRUCache } from "#veryfront/cache/registry.ts"; import { VERYFRONT_CONFIG_FILES } from "./config-files.ts"; -import { getCurrentRequestContext } from "#veryfront/platform/adapters/fs/veryfront/request-context.ts"; +import { currentRequestContext } from "#veryfront/platform/request-context-access.ts"; import type { ModuleLexer } from "#veryfront/extensions/bundler/module-lexer.ts"; import { tryResolve as tryResolveContract } from "#veryfront/extensions/contracts.ts"; import { importFirstPartyExtensionModule } from "#veryfront/extensions/first-party-import.ts"; @@ -1935,7 +1935,7 @@ interface InternalGetConfigOptions extends GetConfigOptions { } function getVirtualConfigSourceContext(): VirtualConfigSourceContext | undefined { - const source = getCurrentRequestContext(); + const source = currentRequestContext(); if (!source) return undefined; return { @@ -2026,7 +2026,7 @@ function assertMatchingVirtualConfigSource( function assertMatchingHostedProjectIdentity( cacheKey: string, - actual: ReturnType, + actual: ReturnType, ): void { if (!actual) { throw CACHE_INVARIANT_VIOLATION.create({ @@ -2144,7 +2144,7 @@ function getConfigInternal( assertMatchingVirtualConfigSource(options.sourceContext, ambientSourceContext); } if (hostedMultiProjectFilesystem) { - assertMatchingHostedProjectIdentity(options!.cacheKey!, getCurrentRequestContext()); + assertMatchingHostedProjectIdentity(options!.cacheKey!, currentRequestContext()); } const sourceContext = hasQualifiedCacheIdentity ? options.sourceContext ?? ambientSourceContext diff --git a/src/platform/adapters/fs/veryfront/request-context.ts b/src/platform/adapters/fs/veryfront/request-context.ts index e1c58faaa1..686ce70c17 100644 --- a/src/platform/adapters/fs/veryfront/request-context.ts +++ b/src/platform/adapters/fs/veryfront/request-context.ts @@ -1,5 +1,7 @@ import { AsyncLocalStorage } from "node:async_hooks"; +import { registerRequestContextAccessor } from "#veryfront/platform/request-context-access.ts"; + export interface RequestContext { projectSlug: string; projectId?: string; @@ -25,6 +27,10 @@ export function getCurrentRequestContext(): RequestContext | null { return asyncLocalStorage.getStore() ?? null; } +// Shared client/server code reads the context through the client-safe holder; +// loading this module is what makes the real accessor available there. +registerRequestContextAccessor(getCurrentRequestContext); + /** * Wraps a callback to preserve the current AsyncLocalStorage context. * diff --git a/src/platform/request-context-access.ts b/src/platform/request-context-access.ts new file mode 100644 index 0000000000..727ead4ece --- /dev/null +++ b/src/platform/request-context-access.ts @@ -0,0 +1,35 @@ +/** + * Client-safe access to the hosted request context. + * + * The real context lives in an AsyncLocalStorage inside + * `adapters/fs/veryfront/request-context.ts`, whose module-scope + * `node:async_hooks` import must stay out of browser bundles. Shared + * client/server code (the config loader's hosted-identity assertions) reads + * the context through this holder instead: the server module registers its + * accessor when it loads, and in the browser nothing ever registers, so + * `currentRequestContext()` returns null — the correct answer there, since a + * hosted request context only exists while the server VFS adapter runs a + * request. + * + * Registration cannot be observed "too early": the only writer of the + * context (`multi-project-adapter.ts`'s `asyncLocalStorage.run`) imports the + * server module, so any populated context implies the accessor is in place. + */ + +import type { RequestContext } from "#veryfront/platform/adapters/fs/veryfront/request-context.ts"; + +export type { RequestContext }; + +let accessor: (() => RequestContext | null) | undefined; + +/** Called by the server request-context module when it loads. */ +export function registerRequestContextAccessor( + fn: () => RequestContext | null, +): void { + accessor = fn; +} + +/** The current hosted request context, or null outside a server request. */ +export function currentRequestContext(): RequestContext | null { + return accessor?.() ?? null; +}