veryfront 0.1.95 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/html/schemas/html.schema.d.ts +2 -2
  3. package/esm/src/jobs/runtime-env.d.ts +5 -0
  4. package/esm/src/jobs/runtime-env.d.ts.map +1 -0
  5. package/esm/src/jobs/runtime-env.js +101 -0
  6. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  7. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +32 -1
  8. package/esm/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.d.ts.map +1 -1
  9. package/esm/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.js +31 -2
  10. package/esm/src/modules/react-loader/ssr-module-loader/vf-module-resolver.d.ts.map +1 -1
  11. package/esm/src/modules/react-loader/ssr-module-loader/vf-module-resolver.js +1 -0
  12. package/esm/src/rendering/orchestrator/module-loader/index.d.ts.map +1 -1
  13. package/esm/src/rendering/orchestrator/module-loader/index.js +6 -1
  14. package/esm/src/rendering/page-rendering.d.ts +8 -0
  15. package/esm/src/rendering/page-rendering.d.ts.map +1 -1
  16. package/esm/src/rendering/page-rendering.js +29 -18
  17. package/esm/src/task/runner.d.ts.map +1 -1
  18. package/esm/src/task/runner.js +2 -6
  19. package/esm/src/transforms/mdx/esm-module-loader/cache/index.d.ts +5 -1
  20. package/esm/src/transforms/mdx/esm-module-loader/cache/index.d.ts.map +1 -1
  21. package/esm/src/transforms/mdx/esm-module-loader/cache/index.js +18 -2
  22. package/esm/src/transforms/mdx/esm-module-loader/cache-format.d.ts +2 -1
  23. package/esm/src/transforms/mdx/esm-module-loader/cache-format.d.ts.map +1 -1
  24. package/esm/src/transforms/mdx/esm-module-loader/cache-format.js +12 -5
  25. package/esm/src/transforms/mdx/esm-module-loader/loader-helpers.d.ts.map +1 -1
  26. package/esm/src/transforms/mdx/esm-module-loader/loader-helpers.js +1 -0
  27. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.d.ts +1 -1
  28. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.d.ts.map +1 -1
  29. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.js +2 -2
  30. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.d.ts +23 -0
  31. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.d.ts.map +1 -0
  32. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.js +112 -0
  33. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.d.ts +2 -2
  34. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.d.ts.map +1 -1
  35. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.js +33 -4
  36. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/framework-validator.d.ts +6 -1
  37. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/framework-validator.d.ts.map +1 -1
  38. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/framework-validator.js +31 -1
  39. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts +1 -0
  40. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.d.ts.map +1 -1
  41. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/index.js +15 -6
  42. package/esm/src/transforms/mdx/esm-module-loader/types.d.ts +1 -0
  43. package/esm/src/transforms/mdx/esm-module-loader/types.d.ts.map +1 -1
  44. package/esm/src/utils/version.d.ts +1 -1
  45. package/esm/src/utils/version.js +1 -1
  46. package/esm/src/workflow/executor/workflow-executor.d.ts.map +1 -1
  47. package/esm/src/workflow/executor/workflow-executor.js +7 -1
  48. package/esm/src/workflow/types.d.ts +1 -0
  49. package/esm/src/workflow/types.d.ts.map +1 -1
  50. package/esm/src/workflow/worker/dynamic-job-entrypoint.d.ts.map +1 -1
  51. package/esm/src/workflow/worker/dynamic-job-entrypoint.js +18 -1
  52. package/esm/src/workflow/worker/job-entrypoint.d.ts.map +1 -1
  53. package/esm/src/workflow/worker/job-entrypoint.js +18 -1
  54. package/package.json +1 -1
  55. package/src/deno.js +1 -1
  56. package/src/src/jobs/runtime-env.ts +132 -0
  57. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +34 -0
  58. package/src/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.ts +34 -2
  59. package/src/src/modules/react-loader/ssr-module-loader/vf-module-resolver.ts +1 -0
  60. package/src/src/rendering/orchestrator/module-loader/index.ts +12 -1
  61. package/src/src/rendering/page-rendering.ts +64 -39
  62. package/src/src/task/runner.ts +2 -8
  63. package/src/src/transforms/mdx/esm-module-loader/cache/index.ts +18 -1
  64. package/src/src/transforms/mdx/esm-module-loader/cache-format.ts +33 -1
  65. package/src/src/transforms/mdx/esm-module-loader/loader-helpers.ts +1 -0
  66. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/cache-keys.ts +2 -1
  67. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.ts +173 -0
  68. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.ts +43 -3
  69. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/framework-validator.ts +37 -0
  70. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +26 -11
  71. package/src/src/transforms/mdx/esm-module-loader/types.ts +1 -0
  72. package/src/src/utils/version.ts +1 -1
  73. package/src/src/workflow/executor/workflow-executor.ts +7 -1
  74. package/src/src/workflow/types.ts +1 -0
  75. package/src/src/workflow/worker/dynamic-job-entrypoint.ts +19 -1
  76. package/src/src/workflow/worker/job-entrypoint.ts +19 -1
@@ -0,0 +1,132 @@
1
+ import { logger as baseLogger } from "../utils/index.js";
2
+
3
+ const logger = baseLogger.component("job-runtime-env");
4
+
5
+ export const INJECTED_TASK_ENV_JSON = "VERYFRONT_TASK_ENV_JSON";
6
+
7
+ const UNSAFE_INJECTED_ENV_KEYS = new Set(["__proto__", "constructor", "prototype"]);
8
+
9
+ const HIDDEN_TASK_CONTEXT_ENV_KEYS = new Set([
10
+ "VERYFRONT_API_TOKEN",
11
+ "VERYFRONT_API_URL",
12
+ "VERYFRONT_PROJECT_API_URL",
13
+ "VERYFRONT_API_BASE_URL",
14
+ "VERYFRONT_PROJECT_ID",
15
+ "VERYFRONT_PROJECT_SLUG",
16
+ "VERYFRONT_BRANCH_REF",
17
+ "VERYFRONT_API_USER",
18
+ "VERYFRONT_API_PASS",
19
+ "VERYFRONT_JOB_RESULT_PATH",
20
+ INJECTED_TASK_ENV_JSON,
21
+ ]);
22
+
23
+ const DISALLOWED_INJECTED_ENV_KEYS = new Set([
24
+ "VERYFRONT_API_TOKEN",
25
+ "VERYFRONT_API_URL",
26
+ "VERYFRONT_PROJECT_API_URL",
27
+ "VERYFRONT_API_BASE_URL",
28
+ "VERYFRONT_PROJECT_ID",
29
+ "VERYFRONT_PROJECT_SLUG",
30
+ "VERYFRONT_BRANCH_REF",
31
+ "VERYFRONT_API_USER",
32
+ "VERYFRONT_API_PASS",
33
+ "VERYFRONT_JOB_RESULT_PATH",
34
+ INJECTED_TASK_ENV_JSON,
35
+ ]);
36
+
37
+ function isHiddenTaskContextEnvKey(key: string): boolean {
38
+ return key.startsWith("TENANT_") || HIDDEN_TASK_CONTEXT_ENV_KEYS.has(key);
39
+ }
40
+
41
+ function isDisallowedInjectedEnvKey(key: string): boolean {
42
+ return key.startsWith("TENANT_") || DISALLOWED_INJECTED_ENV_KEYS.has(key);
43
+ }
44
+
45
+ function filterExistingProjectEnv(
46
+ env: Record<string, string> | undefined,
47
+ ): Record<string, string> {
48
+ const filtered: Record<string, string> = {};
49
+ for (const [key, value] of Object.entries(env ?? {})) {
50
+ if (
51
+ UNSAFE_INJECTED_ENV_KEYS.has(key) ||
52
+ isDisallowedInjectedEnvKey(key) ||
53
+ typeof value !== "string"
54
+ ) {
55
+ continue;
56
+ }
57
+ filtered[key] = value;
58
+ }
59
+ return filtered;
60
+ }
61
+
62
+ export function readInjectedProjectEnv(
63
+ allEnv: Record<string, string>,
64
+ ): Record<string, string> {
65
+ const raw = allEnv[INJECTED_TASK_ENV_JSON];
66
+ if (!raw) {
67
+ return {};
68
+ }
69
+
70
+ try {
71
+ const parsed = JSON.parse(raw);
72
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
73
+ return {};
74
+ }
75
+
76
+ const injectedEnv = Object.create(null) as Record<string, string>;
77
+ for (const [key, value] of Object.entries(parsed)) {
78
+ if (
79
+ UNSAFE_INJECTED_ENV_KEYS.has(key) ||
80
+ isDisallowedInjectedEnvKey(key) ||
81
+ typeof value !== "string"
82
+ ) {
83
+ continue;
84
+ }
85
+ injectedEnv[key] = value;
86
+ }
87
+
88
+ return injectedEnv;
89
+ } catch {
90
+ logger.warn(`Ignoring invalid ${INJECTED_TASK_ENV_JSON}`);
91
+ return {};
92
+ }
93
+ }
94
+
95
+ export function buildTaskContextEnv(
96
+ allEnv: Record<string, string>,
97
+ envAllowlist?: string[],
98
+ ): Record<string, string> {
99
+ const allowlistedEnvKeys = envAllowlist ? new Set(envAllowlist) : null;
100
+ const env: Record<string, string> = {};
101
+
102
+ for (const [key, value] of Object.entries(allEnv)) {
103
+ if (isHiddenTaskContextEnvKey(key)) {
104
+ continue;
105
+ }
106
+ if (allowlistedEnvKeys && !allowlistedEnvKeys.has(key)) {
107
+ continue;
108
+ }
109
+ env[key] = value;
110
+ }
111
+
112
+ for (const [key, value] of Object.entries(readInjectedProjectEnv(allEnv))) {
113
+ if (allowlistedEnvKeys && !allowlistedEnvKeys.has(key)) {
114
+ continue;
115
+ }
116
+ env[key] = value;
117
+ }
118
+
119
+ return env;
120
+ }
121
+
122
+ export function mergeInjectedWorkflowEnv(
123
+ existingEnv: Record<string, string> | undefined,
124
+ allEnv: Record<string, string>,
125
+ ): Record<string, string> | undefined {
126
+ const mergedEnv = {
127
+ ...filterExistingProjectEnv(existingEnv),
128
+ ...readInjectedProjectEnv(allEnv),
129
+ };
130
+
131
+ return Object.keys(mergedEnv).length > 0 ? mergedEnv : undefined;
132
+ }
@@ -54,6 +54,7 @@ import { preflightLocalImports } from "./preflight-imports.js";
54
54
  import { resolveVfModuleImports } from "./vf-module-resolver.js";
55
55
  import { registerCSSImport } from "../css-import-collector.js";
56
56
  import { injectNodePositions } from "../../../transforms/plugins/babel-node-positions.js";
57
+ import { ensureMdxModuleDependencies } from "../../../transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.js";
57
58
 
58
59
  const logger = rendererLogger.component("ssr-module-loader");
59
60
 
@@ -230,6 +231,35 @@ export class SSRModuleLoader {
230
231
  }
231
232
 
232
233
  if (classifiedError.type === "module-not-found") {
234
+ if (this.options.contentSourceId) {
235
+ try {
236
+ const cachedCode = await this.cache.getFs().readTextFile(cacheEntry.tempPath);
237
+ const recovered = await ensureMdxModuleDependencies(cachedCode, {
238
+ projectId: this.options.projectId,
239
+ contentSourceId: this.options.contentSourceId,
240
+ log: logger,
241
+ });
242
+ if (recovered.missing.length === 0 && recovered.recovered.length > 0) {
243
+ const retryTempPath = cacheEntry.tempPath.replace(/\.mjs$/, "") +
244
+ `-recovered-${cacheEntry.contentHash}.mjs`;
245
+ await this.cache.getFs().writeTextFile(retryTempPath, cachedCode);
246
+ logger.info("Recovered vfmod dependencies for cached SSR module, retrying import", {
247
+ file: filePath.slice(-40),
248
+ recovered: recovered.recovered.slice(0, 5),
249
+ retryTempPath,
250
+ });
251
+ return (await import(
252
+ `file://${retryTempPath}?v=${cacheEntry.contentHash}&retry=1`
253
+ )) as Record<string, unknown>;
254
+ }
255
+ } catch (recoveryError) {
256
+ logger.debug("Failed to recover vfmod dependencies for cached SSR module", {
257
+ file: filePath.slice(-40),
258
+ error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
259
+ });
260
+ }
261
+ }
262
+
233
263
  logger.error(
234
264
  "[SSR-MODULE-LOADER] Cached module has missing dependency, invalidating cache",
235
265
  {
@@ -426,6 +456,10 @@ export class SSRModuleLoader {
426
456
  mdxCacheDir,
427
457
  this.options.projectDir,
428
458
  contentHash,
459
+ {
460
+ projectId: this.options.projectId,
461
+ contentSourceId: this.options.contentSourceId,
462
+ },
429
463
  );
430
464
 
431
465
  if (mdxCacheResult.status === "hit") {
@@ -26,6 +26,7 @@ import {
26
26
  } from "./http-bundle-helpers.js";
27
27
  import { buildTempModulePath, buildTmpDirPath, getTmpDirCacheKey } from "./tmp-paths.js";
28
28
  import type { ModuleCacheEntry, SSRModuleLoaderOptions } from "./types.js";
29
+ import { ensureMdxModuleDependencies } from "../../../transforms/mdx/esm-module-loader/module-fetcher/dependency-recovery.js";
29
30
 
30
31
  const logger = rendererLogger.component("ssr-module-loader");
31
32
 
@@ -169,7 +170,7 @@ export class SSRCacheManager {
169
170
  try {
170
171
  const cachedCode = await this.fs.readTextFile(cachedEntry.tempPath);
171
172
  const isValid = await this.validateCachedCode(cachedCode, filePath, "memory-cache", {
172
- checkLocalPaths: false,
173
+ checkLocalPaths: true,
173
174
  checkInvalidEsmShPath: false,
174
175
  });
175
176
  if (!isValid) {
@@ -237,11 +238,14 @@ export class SSRCacheManager {
237
238
 
238
239
  private async hasMissingLocalPaths(code: string, filePath: string): Promise<boolean> {
239
240
  const allPaths = extractAllFilePaths(code);
241
+ let hasMissingPath = false;
242
+
240
243
  for (const path of allPaths) {
241
244
  try {
242
245
  const stat = await this.fs.stat(path);
243
246
  if (!stat.isFile) {
244
- return true;
247
+ hasMissingPath = true;
248
+ break;
245
249
  }
246
250
  } catch (error) {
247
251
  logger.debug("Redis cache has invalid local path, re-transforming", {
@@ -249,6 +253,34 @@ export class SSRCacheManager {
249
253
  missingPath: path.slice(-60),
250
254
  error,
251
255
  });
256
+ hasMissingPath = true;
257
+ break;
258
+ }
259
+ }
260
+
261
+ if (
262
+ hasMissingPath &&
263
+ this.options.projectId &&
264
+ this.options.contentSourceId
265
+ ) {
266
+ const recovered = await ensureMdxModuleDependencies(code, {
267
+ projectId: this.options.projectId,
268
+ contentSourceId: this.options.contentSourceId,
269
+ log: logger,
270
+ });
271
+ if (recovered.recovered.length > 0) {
272
+ logger.debug("Recovered missing local vfmod dependencies for SSR cache entry", {
273
+ file: filePath.slice(-40),
274
+ recovered: recovered.recovered.slice(0, 5),
275
+ });
276
+ }
277
+ }
278
+
279
+ for (const path of allPaths) {
280
+ try {
281
+ const stat = await this.fs.stat(path);
282
+ if (!stat.isFile) return true;
283
+ } catch (_) {
252
284
  return true;
253
285
  }
254
286
  }
@@ -78,6 +78,7 @@ export async function resolveVfModuleImports(
78
78
  options.projectDir,
79
79
  options.projectId,
80
80
  {
81
+ contentSourceId: options.contentSourceId,
81
82
  reactVersion: options.reactVersion,
82
83
  projectSlug: options.projectId,
83
84
  strictMissingModules: false,
@@ -223,7 +223,18 @@ export async function transformModuleWithDeps(
223
223
  const sourceKey = encodeURIComponent(contentSourceId);
224
224
  const mdxCacheDir = join(baseCacheDir, projectKey, sourceKey);
225
225
 
226
- const mdxCacheResult = await lookupMdxEsmCache(filePath, mdxCacheDir, projectDir);
226
+ const mdxCacheResult = await lookupMdxEsmCache(
227
+ filePath,
228
+ mdxCacheDir,
229
+ projectDir,
230
+ undefined,
231
+ contentSourceId
232
+ ? {
233
+ projectId,
234
+ contentSourceId,
235
+ }
236
+ : undefined,
237
+ );
227
238
  if (mdxCacheResult.status === "hit") {
228
239
  moduleCache.set(cacheKey, mdxCacheResult.path);
229
240
  return mdxCacheResult.path;
@@ -15,6 +15,64 @@ interface MDXPageResult {
15
15
  collectedMetadata: Record<string, unknown>;
16
16
  }
17
17
 
18
+ interface PreparedMDXPageBundles {
19
+ pageBundle: PageBundle;
20
+ serverModuleCode: string;
21
+ }
22
+
23
+ export async function prepareMDXPageBundles(
24
+ pageInfo: EntityInfo,
25
+ projectDir: string,
26
+ options?: {
27
+ precompiledModule?: string;
28
+ studioEmbed?: boolean;
29
+ },
30
+ ): Promise<PreparedMDXPageBundles> {
31
+ const { frontmatter, content, path } = pageInfo.entity;
32
+ const fmArg = frontmatter && Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
33
+
34
+ const ssrBundle = await compileContent(
35
+ "development",
36
+ projectDir,
37
+ content,
38
+ fmArg,
39
+ path,
40
+ "server",
41
+ undefined,
42
+ options?.studioEmbed,
43
+ );
44
+
45
+ const pageBundle = ssrBundle as PageBundle;
46
+
47
+ if (options?.precompiledModule) {
48
+ pageBundle.clientModuleCode = options.precompiledModule;
49
+ } else {
50
+ const browserBundle = await compileContent(
51
+ "development",
52
+ projectDir,
53
+ content,
54
+ fmArg,
55
+ path,
56
+ "browser",
57
+ undefined,
58
+ options?.studioEmbed,
59
+ );
60
+ pageBundle.clientModuleCode = browserBundle.compiledCode;
61
+ }
62
+
63
+ const clientModuleCode = pageBundle.clientModuleCode;
64
+ if (!clientModuleCode) {
65
+ throw RENDER_ERROR.create({
66
+ detail: "MDX compilation produced no client module code",
67
+ });
68
+ }
69
+
70
+ return {
71
+ pageBundle,
72
+ serverModuleCode: ssrBundle.compiledCode,
73
+ };
74
+ }
75
+
18
76
  export function handleMDXPage(
19
77
  pageInfo: EntityInfo,
20
78
  slug: string,
@@ -42,49 +100,16 @@ export function handleMDXPage(
42
100
  return withSpan(
43
101
  "rendering.handleMDXPage",
44
102
  async () => {
45
- const { frontmatter, content, path } = pageInfo.entity;
46
- const fmArg = frontmatter && Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
47
-
48
- const ssrBundle = await compileContent(
49
- "development",
50
- projectDir,
51
- content,
52
- fmArg,
53
- path,
54
- "server",
55
- undefined,
56
- options?.studioEmbed,
57
- );
58
-
59
- const pageBundle = ssrBundle as PageBundle;
103
+ const { frontmatter, path } = pageInfo.entity;
104
+ const { pageBundle, serverModuleCode } = await prepareMDXPageBundles(pageInfo, projectDir, {
105
+ precompiledModule: options?.precompiledModule,
106
+ studioEmbed: options?.studioEmbed,
107
+ });
60
108
  let collectedMetadata: Record<string, unknown> = {};
61
109
 
62
110
  try {
63
- if (options?.precompiledModule) {
64
- pageBundle.clientModuleCode = options.precompiledModule;
65
- } else {
66
- const browserBundle = await compileContent(
67
- "development",
68
- projectDir,
69
- content,
70
- fmArg,
71
- path,
72
- "browser",
73
- undefined,
74
- options?.studioEmbed,
75
- );
76
- pageBundle.clientModuleCode = browserBundle.compiledCode;
77
- }
78
-
79
- const clientModuleCode = pageBundle.clientModuleCode;
80
- if (!clientModuleCode) {
81
- throw RENDER_ERROR.create({
82
- detail: "MDX compilation produced no client module code",
83
- });
84
- }
85
-
86
111
  const mod = (await mdxRenderer.loadModuleESM(
87
- clientModuleCode,
112
+ serverModuleCode,
88
113
  adapter,
89
114
  options?.projectId,
90
115
  projectDir,
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { logger as baseLogger } from "../utils/index.js";
9
9
  import { env as getProcessEnv } from "../platform/compat/process.js";
10
+ import { buildTaskContextEnv } from "../jobs/runtime-env.js";
10
11
  import type { DiscoveredTask } from "./discovery.js";
11
12
  import type { TaskContext } from "./types.js";
12
13
 
@@ -61,14 +62,7 @@ export async function runTask(options: RunTaskOptions): Promise<TaskRunResult> {
61
62
  }
62
63
 
63
64
  const allEnv = getProcessEnv();
64
- const env: Record<string, string> = envAllowlist
65
- ? Object.fromEntries(
66
- envAllowlist.flatMap((k) => {
67
- const value = allEnv[k];
68
- return value === undefined ? [] : [[k, value] as const];
69
- }),
70
- )
71
- : { ...allEnv };
65
+ const env = buildTaskContextEnv(allEnv, envAllowlist);
72
66
 
73
67
  const ctx: TaskContext = {
74
68
  env,
@@ -21,6 +21,7 @@ import {
21
21
  import { LOG_PREFIX_MDX_LOADER } from "../constants.js";
22
22
  import { LRUCache } from "../../../../utils/lru-wrapper.js";
23
23
  import { buildMdxEsmPathCacheKey, MDX_ESM_ALL_FILE_URL_PATTERN_SOURCE } from "../cache-format.js";
24
+ import { ensureMdxModuleDependencies } from "../module-fetcher/dependency-recovery.js";
24
25
 
25
26
  export type CacheLookupResult =
26
27
  | { status: "hit"; path: string }
@@ -341,6 +342,7 @@ export async function lookupMdxEsmCache(
341
342
  cacheDir: string,
342
343
  projectDir?: string,
343
344
  _contentHash?: string, // Intentionally unused - kept for API compatibility
345
+ recoveryOptions?: { projectId: string; contentSourceId: string },
344
346
  ): Promise<CacheLookupResult> {
345
347
  const cache = await getModulePathCache(cacheDir);
346
348
  const cacheKey = toMdxEsmCacheKey(filePath, projectDir);
@@ -408,7 +410,22 @@ export async function lookupMdxEsmCache(
408
410
  // CRITICAL: Check that all file:// dependencies actually exist on disk.
409
411
  // The distributed cache may contain code referencing file:// paths from other pods
410
412
  // that don't exist locally (e.g., HTTP bundles, MDX-ESM modules).
411
- const missingDeps = await findMissingFileDependencies(cachedCode);
413
+ let missingDeps = await findMissingFileDependencies(cachedCode);
414
+ if (missingDeps.length > 0 && recoveryOptions) {
415
+ const recovered = await ensureMdxModuleDependencies(cachedCode, {
416
+ ...recoveryOptions,
417
+ log: logger,
418
+ });
419
+ if (recovered.recovered.length > 0) {
420
+ logger.debug(`${LOG_PREFIX_MDX_LOADER} Recovered cached MDX-ESM dependencies`, {
421
+ filePath,
422
+ cachedPath,
423
+ recovered: recovered.recovered.slice(0, 5),
424
+ });
425
+ }
426
+ missingDeps = await findMissingFileDependencies(cachedCode);
427
+ }
428
+
412
429
  if (missingDeps.length > 0) {
413
430
  logger.warn(
414
431
  `${LOG_PREFIX_MDX_LOADER} Cached module has ${missingDeps.length} missing file dependencies, invalidating`,
@@ -10,10 +10,11 @@ const CACHE_NAMESPACE_SENTINEL = "__vf_cache_namespace__";
10
10
  function formatMdxEsmTransformCacheKey(
11
11
  namespace: string,
12
12
  projectId: string,
13
+ contentSourceId: string,
13
14
  normalizedPath: string,
14
15
  contentHash: string,
15
16
  ): string {
16
- return `${namespace}:${projectId}:${normalizedPath}:${contentHash}:ssr`;
17
+ return `${namespace}:${projectId}:${contentSourceId}:${normalizedPath}:${contentHash}:ssr`;
17
18
  }
18
19
 
19
20
  function formatMdxEsmPathCacheKey(namespace: string, normalizedPath: string): string {
@@ -24,6 +25,15 @@ function formatMdxEsmModuleFileName(namespace: string, contentHash: string): str
24
25
  return `vfmod-${namespace}-${contentHash}.mjs`;
25
26
  }
26
27
 
28
+ function formatMdxEsmModuleRecoveryCacheKey(
29
+ namespace: string,
30
+ projectId: string,
31
+ contentSourceId: string,
32
+ fileName: string,
33
+ ): string {
34
+ return `${namespace}:${projectId}:${contentSourceId}:${fileName}:vfmod`;
35
+ }
36
+
27
37
  function formatMdxJsxCacheFileName(namespace: string, filePath: string): string {
28
38
  return `jsx-${namespace}-${hashString(filePath)}.mjs`;
29
39
  }
@@ -42,11 +52,18 @@ function buildMdxEsmCacheSchemaSample() {
42
52
  transformKey: formatMdxEsmTransformCacheKey(
43
53
  CACHE_NAMESPACE_SENTINEL,
44
54
  "__vf_project__",
55
+ "preview-main",
45
56
  "_vf_modules/pages/index.js",
46
57
  "deadbeef",
47
58
  ),
48
59
  pathKey: formatMdxEsmPathCacheKey(CACHE_NAMESPACE_SENTINEL, "_vf_modules/pages/index.js"),
49
60
  moduleFile: formatMdxEsmModuleFileName(CACHE_NAMESPACE_SENTINEL, "deadbeef"),
61
+ moduleRecoveryKey: formatMdxEsmModuleRecoveryCacheKey(
62
+ CACHE_NAMESPACE_SENTINEL,
63
+ "__vf_project__",
64
+ "preview-main",
65
+ formatMdxEsmModuleFileName(CACHE_NAMESPACE_SENTINEL, "deadbeef"),
66
+ ),
50
67
  jsxFile: formatMdxJsxCacheFileName(CACHE_NAMESPACE_SENTINEL, "/tmp/project/Button.tsx"),
51
68
  unresolvedVfModulesPattern: UNRESOLVED_VF_MODULES_PATTERN.source,
52
69
  allFileUrlPattern: ALL_FILE_URL_PATTERN_SOURCE,
@@ -84,12 +101,14 @@ export const FRAMEWORK_VF_MODULE_CACHE_NAMESPACE = createCacheNamespace(
84
101
 
85
102
  export function buildMdxEsmTransformCacheKey(
86
103
  projectId: string,
104
+ contentSourceId: string,
87
105
  normalizedPath: string,
88
106
  contentHash: string,
89
107
  ): string {
90
108
  return formatMdxEsmTransformCacheKey(
91
109
  MDX_ESM_CACHE_NAMESPACE,
92
110
  projectId,
111
+ contentSourceId,
93
112
  normalizedPath,
94
113
  contentHash,
95
114
  );
@@ -103,6 +122,19 @@ export function buildMdxEsmModuleFileName(contentHash: string): string {
103
122
  return formatMdxEsmModuleFileName(MDX_ESM_CACHE_NAMESPACE, contentHash);
104
123
  }
105
124
 
125
+ export function buildMdxEsmModuleRecoveryCacheKey(
126
+ projectId: string,
127
+ contentSourceId: string,
128
+ fileName: string,
129
+ ): string {
130
+ return formatMdxEsmModuleRecoveryCacheKey(
131
+ MDX_ESM_CACHE_NAMESPACE,
132
+ projectId,
133
+ contentSourceId,
134
+ fileName,
135
+ );
136
+ }
137
+
106
138
  export function buildMdxJsxCacheFileName(filePath: string): string {
107
139
  return formatMdxJsxCacheFileName(MDX_ESM_CACHE_NAMESPACE, filePath);
108
140
  }
@@ -151,6 +151,7 @@ export async function processVfModuleImports(
151
151
  projectDir,
152
152
  context.projectId,
153
153
  {
154
+ contentSourceId: context.contentSourceId,
154
155
  reactVersion: context.reactVersion,
155
156
  projectSlug: context.projectSlug,
156
157
  logger: logger.child({
@@ -15,10 +15,11 @@ import { buildMdxEsmPathCacheKey, buildMdxEsmTransformCacheKey } from "../cache-
15
15
  */
16
16
  export function getTransformCacheKey(
17
17
  projectId: string,
18
+ contentSourceId: string,
18
19
  normalizedPath: string,
19
20
  contentHash: string,
20
21
  ): string {
21
- return buildMdxEsmTransformCacheKey(projectId, normalizedPath, contentHash);
22
+ return buildMdxEsmTransformCacheKey(projectId, contentSourceId, normalizedPath, contentHash);
22
23
  }
23
24
 
24
25
  export function getVersionedPathCacheKey(normalizedPath: string): string {