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
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veryfront",
"version": "0.1.1143",
"version": "0.1.1144",
"license": "Apache-2.0",
"nodeModulesDir": "auto",
"minimumDependencyAge": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function createDeps(
validateCachedBundlesByManifestOrCode: () => {
throw new Error("validateCachedBundlesByManifestOrCode was not configured");
},
findMissingFrameworkBundlePaths: () => Promise.resolve([]),
getHttpBundleCacheDir: () => "/tmp/vf-http-bundles",
setCachedTransformAsync: () => Promise.resolve(),
runPipeline: () => {
Expand Down Expand Up @@ -71,8 +72,8 @@ describe("module-loader/module-transform-cache", () => {
});

it("re-transforms cached code when HTTP bundle validation fails", async () => {
const setCalls: Array<{ key: string; code: string; hash: string; ttl: number }> = [];
let transformCalls = 0;
let validatorCalls = 0;

const result = await transformModuleCodeWithCache({
fileContent: "export const page = 1;",
Expand All @@ -83,12 +84,16 @@ describe("module-loader/module-transform-cache", () => {
adapter: {} as RuntimeAdapter,
ttlSeconds: 123,
deps: createDeps({
getOrComputeTransform: (_key, _compute) =>
Promise.resolve({
getOrComputeTransform: async (_key, compute, _ttl, _onProgress, _signal, validator) => {
validatorCalls++;
const cacheEntry = {
code: 'import x from "file:///tmp/veryfront-http-bundle/http-deadbeef.mjs";',
cacheHit: true,
bundleManifestId: "manifest-abc",
}),
};
if (await validator?.(cacheEntry)) return cacheEntry;
return { code: await compute(), cacheHit: false };
},
validateCachedBundlesByManifestOrCode: (code, manifestId, cacheDir) => {
assertEquals(code.includes("deadbeef"), true);
assertEquals(manifestId, "manifest-abc");
Expand All @@ -104,19 +109,60 @@ describe("module-loader/module-transform-cache", () => {
transformCalls++;
return Promise.resolve("export const page = 1;");
},
setCachedTransformAsync: (key, code, hash, ttl) => {
setCalls.push({ key, code, hash, ttl: ttl ?? -1 });
return Promise.resolve();
},
}),
});

assertEquals(result.code, "export const page = 1;");
assertEquals(transformCalls, 1);
assertEquals(setCalls.length, 1);
assertEquals(setCalls[0]!.code, "export const page = 1;");
assertEquals(setCalls[0]!.hash, hashCodeHex("export const page = 1;"));
assertEquals(setCalls[0]!.ttl, 123);
assertEquals(validatorCalls, 1);
});

it("re-transforms cached code when a referenced framework file URL is missing", async () => {
const missingFrameworkPath =
"/tmp/.cache/veryfront/veryfront-mdx-esm/framework/vfmod-vf-framework-deadbeef.mjs";
const freshCode = "export const page = 3;";
let transformCalls = 0;
let validatorCalls = 0;

const result = await transformModuleCodeWithCache({
fileContent: "export const page = 3;",
filePath: "/project/app/page.tsx",
projectDir: "/project",
effectiveProjectId: "project-3",
mode: "production",
adapter: {} as RuntimeAdapter,
ttlSeconds: 789,
deps: createDeps({
getOrComputeTransform: async (_key, compute, _ttl, _onProgress, _signal, validator) => {
validatorCalls++;
const cacheEntry = {
code: `import helper from "file://${missingFrameworkPath}";\nexport default helper;`,
cacheHit: true,
bundleManifestId: "manifest-valid",
};
if (await validator?.(cacheEntry)) return cacheEntry;
return { code: await compute(), cacheHit: false };
},
validateCachedBundlesByManifestOrCode: () =>
Promise.resolve({
valid: true,
failedHashes: [],
source: "manifest",
}),
findMissingFrameworkBundlePaths: (code) => {
assertEquals(code.includes(missingFrameworkPath), true);
return Promise.resolve([missingFrameworkPath]);
},
transformToESM: () => {
transformCalls++;
return Promise.resolve(freshCode);
},
}),
});

assertEquals(result.code, freshCode);
assertEquals(transformCalls, 1);
assertEquals(validatorCalls, 1);
});

it("retries through the transform pipeline when cached code has unresolved _vf_modules imports", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import {
getOrComputeTransform,
initializeTransformCache,
setCachedTransformAsync,
type TransformCachedEntryValidator,
} from "#veryfront/transforms/esm/transform-cache.ts";
import { validateCachedBundlesByManifestOrCode } from "#veryfront/transforms/esm/cached-bundle-validation.ts";
import { exists } from "#veryfront/platform/compat/fs.ts";
import { findMissingFrameworkBundlePaths } from "#veryfront/transforms/shared/framework-bundle-paths.ts";
import { getHttpBundleCacheDir } from "#veryfront/utils/cache-dir.ts";
import { TRANSFORM_DISTRIBUTED_TTL_SEC } from "#veryfront/utils/constants/cache.ts";
import { REACT_DEFAULT_VERSION } from "#veryfront/utils/constants/cdn.ts";
Expand Down Expand Up @@ -64,6 +67,7 @@ export interface ModuleTransformCacheDeps {
ttlSeconds: number,
onProgress?: TransformProgressListener,
signal?: AbortSignal,
validateCachedEntry?: TransformCachedEntryValidator,
) => Promise<TransformCacheResult>;
transformToESM: (
code: string,
Expand All @@ -77,6 +81,7 @@ export interface ModuleTransformCacheDeps {
bundleManifestId: string | undefined,
cacheDir: string,
) => Promise<BundleValidationResult>;
findMissingFrameworkBundlePaths: (code: string) => Promise<string[]>;
getHttpBundleCacheDir: typeof getHttpBundleCacheDir;
setCachedTransformAsync: typeof setCachedTransformAsync;
runPipeline: (
Expand All @@ -92,6 +97,15 @@ const defaultDeps: ModuleTransformCacheDeps = {
getOrComputeTransform,
transformToESM,
validateCachedBundlesByManifestOrCode,
findMissingFrameworkBundlePaths: (code) =>
findMissingFrameworkBundlePaths(code, exists, {
onError: (path, error) => {
logger.error("Framework bundle validation error", {
path,
error: error instanceof Error ? error.message : String(error),
});
},
}),
getHttpBundleCacheDir,
setCachedTransformAsync,
runPipeline: async (code, filePath, projectDir, options) => {
Expand All @@ -114,6 +128,37 @@ export interface TransformModuleCodeWithCacheInput {
deps?: ModuleTransformCacheDeps;
}

function createCachedTransformValidator(
filePath: string,
deps: ModuleTransformCacheDeps,
): TransformCachedEntryValidator {
return async (entry) => {
const [httpValidation, missingFrameworkBundles] = await Promise.all([
deps.validateCachedBundlesByManifestOrCode(
entry.code,
entry.bundleManifestId,
deps.getHttpBundleCacheDir(),
),
deps.findMissingFrameworkBundlePaths(entry.code),
]);

if (httpValidation.valid && missingFrameworkBundles.length === 0) {
return true;
}

logger.warn("Cached transform dependency validation failed, re-transforming", {
filePath,
manifestId: entry.bundleManifestId?.slice(0, 12),
failedHashes: httpValidation.failedHashes,
reason: httpValidation.valid ? "framework_bundle_missing" : httpValidation.reason,
source: httpValidation.valid ? "framework-bundles" : httpValidation.source,
missingFrameworkBundleCount: missingFrameworkBundles.length,
firstMissingFrameworkBundle: missingFrameworkBundles[0]?.split("/").pop(),
});
return false;
};
}

/** Transform module source through the shared cache and stale-cache retry checks. */
export async function transformModuleCodeWithCache(
input: TransformModuleCodeWithCacheInput,
Expand Down Expand Up @@ -160,50 +205,13 @@ export async function transformModuleCodeWithCache(
ttlSeconds,
input.onProgress,
input.signal,
createCachedTransformValidator(input.filePath, deps),
);

input.signal?.throwIfAborted();

let transformedCode = transformResult.code;

if (transformResult.cacheHit) {
const validation = await deps.validateCachedBundlesByManifestOrCode(
transformedCode,
transformResult.bundleManifestId,
deps.getHttpBundleCacheDir(),
);
input.signal?.throwIfAborted();
if (!validation.valid) {
logger.warn("Cached HTTP bundle validation failed, re-transforming", {
filePath: input.filePath,
manifestId: transformResult.bundleManifestId?.slice(0, 12),
failedHashes: validation.failedHashes,
reason: validation.reason,
source: validation.source,
});

transformedCode = await deps.transformToESM(
input.fileContent,
input.filePath,
input.projectDir,
input.adapter,
transformOptions,
);

deps.setCachedTransformAsync(
cacheKey,
transformedCode,
contentHash,
ttlSeconds,
).catch((error) => {
logger.debug("Failed to update transform cache after re-transform", {
filePath: input.filePath,
error,
});
});
}
}

// CRITICAL: Validate that no unresolved /_vf_modules/ imports remain after transform.
// These imports should have been resolved to file:// paths by ssrVfModulesPlugin.
// If they're still present, retry the transform bypassing all caches.
Expand Down
Loading