diff --git a/deno.json b/deno.json index c768de762f..422cd49846 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "veryfront", - "version": "0.1.1153", + "version": "0.1.1154", "license": "Apache-2.0", "nodeModulesDir": "auto", "minimumDependencyAge": { diff --git a/src/cache/dependency-graph.ts b/src/cache/dependency-graph.ts index 3dfdaf9dda..8037457f55 100644 --- a/src/cache/dependency-graph.ts +++ b/src/cache/dependency-graph.ts @@ -211,7 +211,7 @@ export function normalizeSpecifierToPath( projectDir: string, ): string { if (specifier.startsWith("@/")) { - return normalizeExtension(`${projectDir}/${specifier.slice(2)}`); + return normalizeDependencyPath(`${projectDir}/${specifier.slice(2)}`, fromFile); } if (specifier.startsWith("./") || specifier.startsWith("../")) { @@ -223,16 +223,28 @@ export function normalizeSpecifierToPath( else if (part !== ".") parts.push(part); } - return normalizeExtension(`/${parts.join("/")}`); + return normalizeDependencyPath(`/${parts.join("/")}`, fromFile); } if (specifier.startsWith("file://")) { - return normalizeExtension(specifier.slice(7)); + return normalizeDependencyPath(specifier.slice(7), fromFile); } return specifier; } +function normalizeDependencyPath(path: string, fromFile: string): string { + if ( + fromFile.endsWith(".src") && + !path.endsWith(".src") && + /\.(?:[cm]?[jt]sx?|mdx?)$/.test(path) + ) { + return `${path}.src`; + } + + return normalizeExtension(path); +} + function normalizeExtension(path: string): string { return path.replace(/\.(tsx?|jsx)$/, ".js"); } diff --git a/src/cache/dependency-tracking.test.ts b/src/cache/dependency-tracking.test.ts index 9a7eeae430..be887dd14f 100644 --- a/src/cache/dependency-tracking.test.ts +++ b/src/cache/dependency-tracking.test.ts @@ -103,6 +103,35 @@ describe("Dependency tracking cache invalidation", () => { expect(hash1).not.toBe(hash2); }); + it("should hash dependencies stored as compiled framework .src files", async () => { + const entryPath = "/framework/dist/framework-src/react/context/index.tsx.src"; + const dependencyPath = "/framework/dist/framework-src/react/runtime/core.ts.src"; + const entryCode = + `import { core } from "../runtime/core.ts";\nexport const context = core;\n`; + + const filesV1 = new Map([ + [entryPath, entryCode], + [dependencyPath, `export const core = "v1";\n`], + ]); + const filesV2 = new Map([ + [entryPath, entryCode], + [dependencyPath, `export const core = "v2";\n`], + ]); + + const hash1 = await computeDepsHash( + entryPath, + createGetContent(filesV1), + "/project", + ); + const hash2 = await computeDepsHash( + entryPath, + createGetContent(filesV2), + "/project", + ); + + expect(hash1).not.toBe(hash2); + }); + it("should reuse cached content for overlapping dependency graphs", async () => { const files = new Map([ [ diff --git a/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.test.ts b/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.test.ts index 4c5d5d5700..c17d45ee47 100644 --- a/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.test.ts +++ b/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.test.ts @@ -144,6 +144,33 @@ describe("extractAllFilePaths", () => { assertEquals(extractAllFilePaths(code), ["/app/.cache/markdown.tsx"]); }); + it("preserves compiled framework .src cache paths", () => { + const code = [ + `import context from "file:///tmp/deno-compile-veryfront/dist/framework-src/react/context/index.tsx.src";`, + `import core from "file:///tmp/deno-compile-veryfront/dist/framework-src/react/runtime/core.ts.src?v=42";`, + ].join("\n"); + + assertEquals(extractAllFilePaths(code), [ + "/tmp/deno-compile-veryfront/dist/framework-src/react/context/index.tsx.src", + "/tmp/deno-compile-veryfront/dist/framework-src/react/runtime/core.ts.src", + ]); + }); + + it("does not truncate unsupported file URL suffixes into valid-looking paths", () => { + const code = [ + `import source from "file:///tmp/project/Button.ts.source";`, + `import sourceMap from "file:///tmp/project/Button.js.map";`, + ].join("\n"); + + assertEquals(extractAllFilePaths(code), []); + }); + + it("ignores file URLs with a host component", () => { + const code = `import remote from "file://cache-host/tmp/project/Button.js";`; + + assertEquals(extractAllFilePaths(code), []); + }); + it("strips query parameters from extracted paths", () => { const code = `import a from "file:///tmp/project/Button.tsx?v=123";`; assertEquals(extractAllFilePaths(code), ["/tmp/project/Button.tsx"]); diff --git a/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.ts b/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.ts index 9138ef4be3..2b896f72db 100644 --- a/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.ts +++ b/src/modules/react-loader/ssr-module-loader/http-bundle-helpers.ts @@ -142,16 +142,17 @@ export function extractHttpBundlePaths(code: string): Array<{ path: string; hash */ export function extractAllFilePaths(code: string): string[] { // Create regex per call to avoid shared lastIndex state across concurrent calls. - const allFilePathsPattern = /file:\/\/([^"'\s]+\.(?:mjs|js|tsx|ts|jsx)(?:\?[^"'\s]*)?)/gi; + const allFilePathsPattern = /file:\/\/(\/[^"'\s]+)/gi; + const supportedPathPattern = /\.(?:mjs|js|tsx|ts|jsx)(?:\.src)?$/i; const paths: string[] = []; const seen = new Set(); let match: RegExpExecArray | null; while ((match = allFilePathsPattern.exec(code)) !== null) { - const path = match[1]?.replace(/\?.*$/, ""); + const path = match[1]?.replace(/[?#].*$/, ""); - if (!path || seen.has(path)) continue; + if (!path || !supportedPathPattern.test(path) || seen.has(path)) continue; seen.add(path); paths.push(path); diff --git a/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.test.ts b/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.test.ts index 2b0d2808e9..6c252f1334 100644 --- a/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.test.ts +++ b/src/modules/react-loader/ssr-module-loader/ssr-cache-manager.test.ts @@ -1,7 +1,7 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertNotEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; -import { join } from "#veryfront/compat/path/index.ts"; +import { join, toFileUrl } from "#veryfront/compat/path/index.ts"; import { denoAdapter } from "#veryfront/platform/adapters/runtime/deno/index.ts"; import { makeTempDir, @@ -157,6 +157,47 @@ describe("SSRCacheManager", { sanitizeResources: false, sanitizeOps: false }, () } }); + it("accepts cache entries that reference existing compiled framework sources", async () => { + const projectDir = await makeTempDir({ prefix: "vf-ssr-cache-manager-" }); + const embeddedSourcePath = join( + projectDir, + "dist", + "framework-src", + "react", + "runtime", + "core.ts.src", + ); + + try { + await mkdir(join(projectDir, "dist", "framework-src", "react", "runtime"), { + recursive: true, + }); + await writeTextFile(embeddedSourcePath, `export const core = "compiled";`); + + const cacheManager = new SSRCacheManager({ + projectDir, + projectId: `project-${crypto.randomUUID()}`, + contentSourceId: `preview-${crypto.randomUUID()}`, + adapter: denoAdapter, + dev: true, + }); + + const isValid = await cacheManager.validateCachedCode( + `import { core } from "${toFileUrl(embeddedSourcePath).href}"; export default core;`, + join(projectDir, "pages", "index.tsx"), + "memory-cache", + { + checkLocalPaths: true, + checkInvalidEsmShPath: false, + }, + ); + + assertEquals(isValid, true); + } finally { + await remove(projectDir, { recursive: true }); + } + }); + it("rejects redis cache entries with nested legacy .cache TSX imports inside vfmods", async () => { const projectDir = await makeTempDir({ prefix: "vf-ssr-cache-manager-" }); const projectId = `project-${crypto.randomUUID()}`; diff --git a/src/modules/react-loader/ssr-module-loader/tmp-paths.test.ts b/src/modules/react-loader/ssr-module-loader/tmp-paths.test.ts index a3fde2f8b2..b1ae06ccf2 100644 --- a/src/modules/react-loader/ssr-module-loader/tmp-paths.test.ts +++ b/src/modules/react-loader/ssr-module-loader/tmp-paths.test.ts @@ -62,6 +62,21 @@ describe("modules/react-loader/ssr-module-loader/tmp-paths", () => { ); }); + it("builds hashed JavaScript paths for compiled framework .src files", () => { + const tempPath = buildTempModulePath( + "/cache/mdx/v0-1-1154/project/source", + "/tmp/deno-compile-veryfront/dist/framework-src/react/runtime/core.ts.src", + "/project", + "0.1.1154", + "deadbeefcafebabe", + ); + + assertEquals( + tempPath, + "/cache/mdx/v0-1-1154/project/source/tmp/deno-compile-veryfront/dist/framework-src/react/runtime/core.v0-1-1154.deadbeef.js", + ); + }); + it("keeps absolute path structure when file is outside project dir", () => { const projectHash = hashCodeHex("my/project"); const tempPath = buildTempModulePath( diff --git a/src/modules/react-loader/ssr-module-loader/tmp-paths.ts b/src/modules/react-loader/ssr-module-loader/tmp-paths.ts index 34f78bf160..90bd06ea26 100644 --- a/src/modules/react-loader/ssr-module-loader/tmp-paths.ts +++ b/src/modules/react-loader/ssr-module-loader/tmp-paths.ts @@ -47,6 +47,6 @@ export function buildTempModulePath( const hashSuffix = contentHash ? `.v${versionPrefix}.${contentHash.slice(0, 8)}` : `.v${versionPrefix}`; - const jsPath = relativePath.replace(/\.(tsx?|jsx|mdx)$/, `${hashSuffix}.js`); + const jsPath = relativePath.replace(/\.(tsx?|jsx|mdx)(?:\.src)?$/, `${hashSuffix}.js`); return join(tmpDir, jsPath); } diff --git a/src/utils/version-constant.ts b/src/utils/version-constant.ts index a99452f0eb..6e2532d1b5 100644 --- a/src/utils/version-constant.ts +++ b/src/utils/version-constant.ts @@ -1,4 +1,4 @@ // Keep in sync with deno.json version. // scripts/release.ts updates this constant during releases. /** Shared version value. */ -export const VERSION = "0.1.1153"; +export const VERSION = "0.1.1154";