diff --git a/deno.lock b/deno.lock index 3d5970098a8..0867329d019 100644 --- a/deno.lock +++ b/deno.lock @@ -9,7 +9,7 @@ "jsr:@deno/esbuild-plugin@^1.2.0": "1.2.1", "jsr:@deno/graph@0.86": "0.86.9", "jsr:@deno/graph@~0.82.3": "0.82.3", - "jsr:@deno/loader@0.4": "0.4.0", + "jsr:@deno/loader@0.5": "0.5.0", "jsr:@deno/loader@~0.3.10": "0.3.14", "jsr:@marvinh-test/fresh-island@^0.0.3": "0.0.3", "jsr:@marvinh-test/import-json@^0.0.1": "0.0.1", @@ -191,8 +191,8 @@ "@deno/loader@0.3.14": { "integrity": "97bc63a6cc2d27a60bcdc953f588c5213331d866d44212eebb24cebfb9b011ca" }, - "@deno/loader@0.4.0": { - "integrity": "6c1b18cfa18592740613ce79e15625c24268d60dbfc54bebfb5153bf512c536b" + "@deno/loader@0.5.0": { + "integrity": "a6d94408de5e6bacac404f8f6963c8b8cc278cfd1a878aa2f06b34a083d6bfee" }, "@marvinh-test/fresh-island@0.0.3": { "integrity": "6d06b6009b7dfba9bba28e941e03e6ff652c4ef4f2fbfdf4b78741abd6c6c1c6", @@ -6078,7 +6078,7 @@ }, "packages/plugin-vite": { "dependencies": [ - "jsr:@deno/loader@0.4", + "jsr:@deno/loader@0.5", "jsr:@fresh/core@2", "jsr:@marvinh-test/import-json@^0.0.1", "npm:@babel/core@^7.28.0", diff --git a/packages/fresh/src/dev/dev_build_cache.ts b/packages/fresh/src/dev/dev_build_cache.ts index 3678d0ffba4..69e9c3e2e47 100644 --- a/packages/fresh/src/dev/dev_build_cache.ts +++ b/packages/fresh/src/dev/dev_build_cache.ts @@ -622,8 +622,19 @@ import { app } from "${serverEntry}"; const root = ${rootPath}; setBuildCache(app, new ProdBuildCache(root, snapshot), "production"); +// In dev, plugins may mutate app-level hooks such as the error interceptor +// after this module loads. Keep the fetch target refreshable so the server +// entry can pick up the latest app.handler() when that happens. +let handler = app.handler(); + +export function refreshHandler() { + handler = app.handler(); +} + export default { - fetch: app.handler() + fetch(req, info) { + return handler(req, info); + } }; `; } diff --git a/packages/plugin-vite/deno.json b/packages/plugin-vite/deno.json index 57af17393e6..b7f5c93940a 100644 --- a/packages/plugin-vite/deno.json +++ b/packages/plugin-vite/deno.json @@ -23,7 +23,7 @@ "imports": { "@babel/core": "npm:@babel/core@^7.28.0", "@babel/preset-react": "npm:@babel/preset-react@^7.27.1", - "@deno/loader": "jsr:@deno/loader@^0.4.0", + "@deno/loader": "jsr:@deno/loader@^0.5.0", "@marvinh-test/import-json": "jsr:@marvinh-test/import-json@^0.0.1", "@remix-run/node-fetch-server": "npm:@remix-run/node-fetch-server@^0.12.0", "@prefresh/vite": "npm:@prefresh/vite@^2.4.8", diff --git a/packages/plugin-vite/src/plugins/deno.ts b/packages/plugin-vite/src/plugins/deno.ts index 6cefed7e463..b873dd79e09 100644 --- a/packages/plugin-vite/src/plugins/deno.ts +++ b/packages/plugin-vite/src/plugins/deno.ts @@ -2,6 +2,7 @@ import type { Plugin } from "vite"; import { type Loader, MediaType, + type ModuleLoadResponse, RequestedModuleType, ResolutionMode, Workspace, @@ -17,6 +18,8 @@ const { default: babelReact } = await import("@babel/preset-react"); const BUILTINS = new Set(builtinModules); +const decoder = new TextDecoder(); + interface DenoState { type: RequestedModuleType; } @@ -180,7 +183,7 @@ export function deno(): Plugin { return null; } - const code = new TextDecoder().decode(result.code); + const { code, map } = decodeLoadResult(result); const maybeJsx = babelTransform({ ssr: this.environment.config.consumer === "server", @@ -188,6 +191,7 @@ export function deno(): Plugin { code, id: specifier, isDev, + inputSourceMap: map, }); if (maybeJsx !== null) { return maybeJsx; @@ -195,6 +199,7 @@ export function deno(): Plugin { return { code, + map, }; } @@ -224,7 +229,7 @@ export function deno(): Plugin { return null; } - const code = new TextDecoder().decode(result.code); + const { code, map } = decodeLoadResult(result); const maybeJsx = babelTransform({ ssr: this.environment.config.consumer === "server", @@ -232,6 +237,7 @@ export function deno(): Plugin { id, code, isDev, + inputSourceMap: map, }); if (maybeJsx) { return maybeJsx; @@ -239,6 +245,7 @@ export function deno(): Plugin { return { code, + map, }; }, transform: { @@ -279,10 +286,11 @@ export function deno(): Plugin { return; } - const code = new TextDecoder().decode(result.code); + const { code, map } = decodeLoadResult(result); return { code, + map, }; }, }, @@ -375,13 +383,14 @@ function babelTransform( code: string; id: string; isDev: boolean; + inputSourceMap: babel.TransformOptions["inputSourceMap"]; }, ) { if (!isJsMediaType(options.media)) { return null; } - const { ssr, code, id, isDev } = options; + const { ssr, code, id, isDev, inputSourceMap } = options; const presets: babel.PluginItem[] = []; if ( @@ -400,6 +409,7 @@ function babelTransform( const result = babel.transformSync(code, { filename: id, babelrc: false, + inputSourceMap, sourceMaps: "both", presets: presets, plugins: [httpAbsolute(url)], @@ -415,3 +425,24 @@ function babelTransform( return null; } + +function decodeLoadResult( + result: Extract, +): { + code: string; + map: babel.TransformOptions["inputSourceMap"]; +} { + const map = result.sourceMap && JSON.parse(decoder.decode(result.sourceMap)); + // If we pass a separate sourcemap object to Vite, remove the loader's + // inline data URL so the module only has one sourcemap source of truth. + const code = !map + ? decoder.decode(result.code) + : decoder.decode(result.code).replace( + /\r?\n\/\/# sourceMappingURL=data:[^\r\n]+$/, + "", + ); + return { + code, + map, + }; +} diff --git a/packages/plugin-vite/src/plugins/server_entry.ts b/packages/plugin-vite/src/plugins/server_entry.ts index d4b1dec8726..ce2a37a8dd6 100644 --- a/packages/plugin-vite/src/plugins/server_entry.ts +++ b/packages/plugin-vite/src/plugins/server_entry.ts @@ -97,6 +97,7 @@ ${code} export function setErrorInterceptor(fn) { internalErrorIntercept(app, fn); + refreshHandler(); } if (import.meta.hot) import.meta.hot.accept();`; } diff --git a/packages/plugin-vite/tests/build_test.ts b/packages/plugin-vite/tests/build_test.ts index e4d8354e059..2163d6e1196 100644 --- a/packages/plugin-vite/tests/build_test.ts +++ b/packages/plugin-vite/tests/build_test.ts @@ -1,3 +1,5 @@ +import { createLogger } from "vite"; +import type { BabelFileResult } from "@babel/core"; import { expect } from "@std/expect"; import { walk } from "@std/fs/walk"; import { @@ -13,6 +15,7 @@ import { launchProd, usingEnv, } from "./test_utils.ts"; +import { toPosix } from "fresh/internal-dev"; import * as path from "@std/path"; import { FRESH_CSS_PLACEHOLDER } from "../src/plugins/server_snapshot.ts"; @@ -592,9 +595,13 @@ integrationTest( "vite build - custom rollup entryFileNames in server.js", async () => { await using res = await buildVite(DEMO_DIR, { - rollupOutput: { - entryFileNames: "[hash].mjs", - chunkFileNames: "[hash].mjs", + build: { + rollupOptions: { + output: { + entryFileNames: "[hash].mjs", + chunkFileNames: "[hash].mjs", + }, + }, }, }); @@ -828,3 +835,79 @@ integrationTest( ); }, ); + +integrationTest( + "vite build - ssr sourcemap should be generated collectly", + async () => { + await using tmp = await buildVite(DEMO_DIR, { + environments: { + ssr: { + build: { + sourcemap: true, + }, + }, + }, + }); + + const serverAssetsDir = path.join(tmp.tmp, "_fresh", "server", "assets"); + for await ( + const entry of walk(serverAssetsDir, { + exts: [".mjs"], + includeDirs: false, + }) + ) { + const js = await Deno.readTextFile(entry.path); + const match = js.match(/\/\/# sourceMappingURL=(.+)$/m); + const mapPath = path.join(path.dirname(entry.path), match![1]); + const mapText = await Deno.readTextFile(mapPath); + const map: NonNullable = JSON.parse(mapText); + + // check a specific sourcemap file which contains + // the reference of original source file + if (entry.name.includes("_fresh-route___tests_feed-")) { + expect( + map.sources.some((source) => + toPosix(source).endsWith("demo/routes/tests/feed.tsx") + ), + ).toBe(true); + } + } + }, +); + +// rollup specific test +// https://rollupjs.org/troubleshooting/#warning-sourcemap-is-likely-to-be-incorrect +// this test could be broke if it will migrate to rolldown +integrationTest( + "vite build - ssr sourcemap should be generated without warnings", + async () => { + const warnMsgs = new Set(); + const customLogger = createLogger("error"); + customLogger.warn = (msg) => { + customLogger.hasWarned = true; + warnMsgs.add(msg); + }; + customLogger.warnOnce = (msg) => { + customLogger.hasWarned = true; + warnMsgs.add(msg); + }; + + await using _ = await buildVite(DEMO_DIR, { + logLevel: "warn", + clearScreen: true, + customLogger, + environments: { + ssr: { + build: { + sourcemap: true, + }, + }, + }, + }); + + const sourceMapIncorrectMsg = warnMsgs.has( + "[plugin deno] Sourcemap is likely to be incorrect: a plugin (deno) was used to transform files, but didn't generate a sourcemap for the transformation. Consult the plugin documentation for help", + ); + expect(sourceMapIncorrectMsg).not.toBeTruthy(); + }, +); diff --git a/packages/plugin-vite/tests/test_utils.ts b/packages/plugin-vite/tests/test_utils.ts index 2d7a4d5c6b6..27a8385c002 100644 --- a/packages/plugin-vite/tests/test_utils.ts +++ b/packages/plugin-vite/tests/test_utils.ts @@ -1,4 +1,4 @@ -import { createBuilder } from "vite"; +import { createBuilder, mergeConfig } from "vite"; import * as path from "@std/path"; import { walk } from "@std/fs/walk"; import { integrationTest, withTmpDir } from "../../fresh/src/test_utils.ts"; @@ -135,24 +135,16 @@ export async function withDevServer( export async function buildVite( fixtureDir: string, - options?: { - base?: string; - rollupOutput?: { - entryFileNames?: string; - chunkFileNames?: string; - assetFileNames?: string; - }; - }, + config?: Parameters[0], ) { const tmp = await withTmpDir({ dir: path.join(import.meta.dirname!, ".."), prefix: "tmp_vite_", }); - const builder = await createBuilder({ + const defaults = { logLevel: "error", root: fixtureDir, - base: options?.base, build: { emptyOutDir: true, }, @@ -160,9 +152,6 @@ export async function buildVite( ssr: { build: { outDir: path.join(tmp.dir, "_fresh", "server"), - rollupOptions: options?.rollupOutput - ? { output: options.rollupOutput } - : undefined, }, }, client: { @@ -171,7 +160,10 @@ export async function buildVite( }, }, }, - }); + } satisfies Parameters[0]; + const builder = await createBuilder( + mergeConfig(defaults, config ?? {}), + ); await builder.buildApp(); return {