Skip to content
Open
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
8 changes: 2 additions & 6 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/fresh/src/runtime/client/dev_hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IS_BROWSER } from "../shared.ts";
let ws: WebSocket;
let revision = 0;

let reconnectTimer: number;
let reconnectTimer: ReturnType<typeof setTimeout>;
const backoff = [
// Wait 100ms initially, because we could also be
// disconnected because of a form submit.
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-vite/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.3.10",
"@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",
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-vite/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ export function fresh(config?: FreshViteConfig): Plugin[] {
},
},
ssr: {
resolve: {
builtins: [],
},
build: {
manifest: true,
emitAssets: true,
Expand Down
183 changes: 164 additions & 19 deletions packages/plugin-vite/src/plugins/deno.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import type { Plugin } from "vite";
import {
type Loader,
MediaType,
RequestedModuleType,
ResolutionMode,
Workspace,
import type {
Loader,
MediaType as DenoMediaType,
RequestedModuleType as DenoRequestedModuleType,
} from "@deno/loader";
import * as path from "@std/path";
import * as babel from "@babel/core";
Expand All @@ -16,9 +14,17 @@
const { default: babelReact } = await import("@babel/preset-react");

const BUILTINS = new Set(builtinModules);
const NODE_BUILTIN_PREFIX = "\0fresh-node-builtin::";

type LoaderModule = typeof import("@deno/loader");

let MediaType: LoaderModule["MediaType"];
let RequestedModuleType: LoaderModule["RequestedModuleType"];
let ResolutionMode: LoaderModule["ResolutionMode"];
let Workspace: LoaderModule["Workspace"];

interface DenoState {
type: RequestedModuleType;
type: DenoRequestedModuleType;
}

export function deno(): Plugin {
Expand All @@ -38,6 +44,12 @@
isDev = env.command === "serve";
},
async configResolved() {
const loaderModule = await import("@deno/loader");
MediaType = loaderModule.MediaType;
RequestedModuleType = loaderModule.RequestedModuleType;
ResolutionMode = loaderModule.ResolutionMode;
Workspace = loaderModule.Workspace;

// TODO: Pass conditions
ssrLoader = await new Workspace({
platform: "node",
Expand All @@ -54,11 +66,13 @@
return true;
},
async resolveId(id, importer, options) {
if (BUILTINS.has(id)) {
// `node:` prefix is not included in builtins list.
if (!id.startsWith("node:")) {
id = `node:${id}`;
const builtin = id.startsWith("node:") ? id.slice("node:".length) : id;
if (BUILTINS.has(builtin)) {
id = id.startsWith("node:") ? id : `node:${id}`;
if (this.environment.config.consumer === "server") {
return NODE_BUILTIN_PREFIX + id;
}
// `node:` prefix is not included in builtins list.
return {
id,
external: true,
Expand Down Expand Up @@ -168,6 +182,10 @@
}
},
async load(id) {
if (id.startsWith(NODE_BUILTIN_PREFIX)) {
return nodeBuiltinModule(id.slice(NODE_BUILTIN_PREFIX.length));
}

const loader = this.environment.config.consumer === "server"
? ssrLoader
: browserLoader;
Expand All @@ -190,12 +208,32 @@
isDev,
});
if (maybeJsx !== null) {
if (maybeJsx.map) {
// Babel reads the loader's inline source map but inherits its
// `sources` (relative path). Rewrite to the absolute specifier
// with an empty `sourceRoot` so stack frames show the real URL
// instead of the `\0deno::…` virtual ID and don't double the cwd.
maybeJsx.map.sources = [specifier];
maybeJsx.map.sourceRoot = "";
}
// Babel emits its own inline `//# sourceMappingURL=` comment via
// `sourceMaps: "both"`. Rewrite that one too so V8 stack traces
// (which read the inline map natively) point at the specifier.
maybeJsx.code = rewriteInlineSourceMapSources(
maybeJsx.code,
specifier,
);
return maybeJsx;
}

return {
code,
};
// For non-JS media (JSON, CSS, …) the loaded code is not JavaScript,
// so appending a `//# sourceMappingURL=` comment would corrupt it.
// Those modules don't show up in JS stack traces, so leave them alone.
if (!isJsMediaType(result.mediaType)) {
return { code };
}

return rewriteLoadedSourceMap(code, specifier);
}

if (id.startsWith("\0")) {
Expand Down Expand Up @@ -289,7 +327,7 @@
};
}

function isJsMediaType(media: MediaType): boolean {
function isJsMediaType(media: DenoMediaType): boolean {
switch (media) {
case MediaType.JavaScript:
case MediaType.Jsx:
Expand Down Expand Up @@ -322,13 +360,13 @@
return typeof str === "string" && str.startsWith("\0deno::");
}

function toDenoSpecifier(spec: string, type: RequestedModuleType) {
function toDenoSpecifier(spec: string, type: DenoRequestedModuleType) {
return `\0deno::${type}::${spec}`;
}

function parseDenoSpecifier(
spec: DenoSpecifier,
): { type: RequestedModuleType; specifier: string } {
): { type: DenoRequestedModuleType; specifier: string } {
const match = spec.match(/^\0deno::([^:]+)::(.*)$/)!;

let specifier = match[2];
Expand All @@ -352,7 +390,7 @@
return { type: +match[1], specifier };
}

function getDenoType(id: string, type: string): RequestedModuleType {
function getDenoType(id: string, type: string): DenoRequestedModuleType {
switch (type) {
case "json":
return RequestedModuleType.Json;
Expand All @@ -368,9 +406,116 @@
}
}

async function nodeBuiltinModule(id: string) {
const names = Object.keys(await import(id));

Check warning on line 410 in packages/plugin-vite/src/plugins/deno.ts

View workflow job for this annotation

GitHub Actions / test (v2.x, ubuntu-latest)

unable to analyze dynamic import
return [
`const mod = await Function("id", "return import(id)")(${
JSON.stringify(id)
});`,
"const requireValue = mod.default ?? mod;",
"export { requireValue as __require };",
"export default requireValue;",
...names
.filter((name) =>
/^[$_\p{ID_Start}][$_\u200c\u200d\p{ID_Continue}]*$/u
.test(name)
)
.filter((name) => name !== "default")
.map((name) => `export const ${name} = mod[${JSON.stringify(name)}];`),
].join("\n");
}

// Builds a 1:1 (line-by-line, column 0) source map so that Vite/V8 can
// rewrite stack frames from `\0deno::{type}::{specifier}` virtual IDs back
// to the original specifier. Uses an absolute URL/path in `sources` combined
// with an empty `sourceRoot` to avoid Vite resolving sources relative to the
// cwd, which would produce doubled paths like
// `packages/fresh/src/packages/fresh/src/segments.ts`.
export function identitySourceMap(source: string, code: string) {
const lineCount = code.split("\n").length;
// VLQ "AAAA" → [genCol=0, srcIdx=0, srcLine=0, srcCol=0]
// ";AACA" → newline, then deltas [0, 0, +1, 0]
let mappings = "AAAA";
for (let i = 1; i < lineCount; i++) {
mappings += ";AACA";
}
return {
version: 3,
sources: [source],
sourcesContent: [code],
names: [] as string[],
mappings,
sourceRoot: "",
};
}

const INLINE_SOURCE_MAP_RE =
/\n?\/\/# sourceMappingURL=data:application\/json(?:;charset=[^;]+)?;base64,([A-Za-z0-9+/=]+)\s*$/;

// `ssrLoader.load()` returns code with an inline `//# sourceMappingURL=` data
// URL whose `sources` array contains a path relative to the cwd. Without
// fixing this up, stack traces either leak the `\0deno::…` virtual module ID
// (when V8 falls back to the module ID) or display doubled cwd paths like
// `packages/fresh/src/packages/fresh/src/segments.ts` (the caveat described
// in denoland/fresh#3464).
//
// Rewrites the inline source map (if any) so `sources` is the absolute
// specifier with an empty `sourceRoot`. The inline comment itself is kept in
// place so that V8 picks it up natively for stack-trace translation. When the
// loader did not emit a source map, an identity map is appended so the
// virtual ID is still replaced in stack traces. The same map is also
// returned alongside the code so Rollup (production builds) sees consistent
// `sources` during source-map chaining.
export function rewriteLoadedSourceMap(code: string, source: string) {
const match = code.match(INLINE_SOURCE_MAP_RE);
if (match !== null) {
try {
const parsed = JSON.parse(atob(match[1]));
parsed.sources = [source];
parsed.sourceRoot = "";
const start = match.index!;
const reencoded = btoa(JSON.stringify(parsed));
const newCode = code.slice(0, start) +
`\n//# sourceMappingURL=data:application/json;base64,${reencoded}`;
return { code: newCode, map: parsed };
} catch {
// fall through to identity map
}
}
const map = identitySourceMap(source, code);
const encoded = btoa(JSON.stringify(map));
return {
code:
`${code}\n//# sourceMappingURL=data:application/json;base64,${encoded}`,
map,
};
}

// Rewrites just the `sources` of an existing inline `//# sourceMappingURL=`
// comment in JS code, without touching mappings or appending a new comment
// when none exists. Used after Babel has already produced its own source
// map and we only need to fix the specifier.
export function rewriteInlineSourceMapSources(
code: string,
source: string,
): string {
const match = code.match(INLINE_SOURCE_MAP_RE);
if (match === null) return code;
try {
const parsed = JSON.parse(atob(match[1]));
parsed.sources = [source];
parsed.sourceRoot = "";
const reencoded = btoa(JSON.stringify(parsed));
return code.slice(0, match.index!) +
`\n//# sourceMappingURL=data:application/json;base64,${reencoded}`;
} catch {
return code;
}
}

function babelTransform(
options: {
media: MediaType;
media: DenoMediaType;
ssr: boolean;
code: string;
id: string;
Expand Down
Loading
Loading