Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6f63222
fix(routing): bundle CommonJS dependencies of compiled-binary API routes
kojiwakayama Nov 6, 2026
0a9f18d
fix(routing): resolve and load bundled dependencies as Node does
kojiwakayama Nov 6, 2026
f58844a
refactor(routing): remove the runtime CommonJS loader for compiled-bi…
kojiwakayama Nov 6, 2026
a7753e2
refactor(routing): address Sonar findings in compiled CJS bundling pr…
kojiwakayama Nov 6, 2026
22b529d
fix(api): keep CommonJS semantics for bundled route dependencies
kojiwakayama Nov 6, 2026
a41c0b0
test(api): run the CommonJS bundling test on every runtime
kojiwakayama Nov 6, 2026
52d7ee7
test(api): check ESM dependencies in the bundle, not at runtime
kojiwakayama Nov 6, 2026
088bf7e
fix(api): parse bundled CommonJS dependencies and resolve them as Nod…
kojiwakayama Nov 6, 2026
ab1a42d
fix(api): cache JSON loaded by a computed require in compiled routes
kojiwakayama Nov 6, 2026
f2b2de6
test(api): unit-cover compiled CommonJS bundling and fix the Bun shard
kojiwakayama Nov 6, 2026
a7ad943
fix(api): serve computed built-ins and bundle dual-package require ed…
kojiwakayama Nov 6, 2026
19c7713
test(api): unit-test dependency file parsing for compiled routes
kojiwakayama Nov 6, 2026
189546e
test(api): stop the bundler after each parsing test instead of disabl…
kojiwakayama Nov 6, 2026
d0532c2
fix(api): load computed dependency requires and keep compiled route o…
kojiwakayama Nov 6, 2026
c799b56
fix(api): keep require.ensure on the compiled runtime require
kojiwakayama Nov 6, 2026
e6431f3
fix(api): resolve packages from runtime-loaded dependencies
kojiwakayama Nov 6, 2026
48640ee
fix(api): share the CommonJS module cache and match Node's resolution…
kojiwakayama Nov 6, 2026
3d25226
test(api): avoid a fixture package name Bun treats as built-in
kojiwakayama Nov 7, 2026
37f518a
fix(api): resolve residual requires, package targets and nested exter…
kojiwakayama Nov 7, 2026
05ea7df
fix(api): follow Node's package resolution in the runtime require
kojiwakayama Nov 7, 2026
dd15aac
fix(module-loader): match Node for exports fallbacks, Windows ids and…
kojiwakayama Nov 7, 2026
e77ca40
test(module-loader): parse the helper-collision prologue instead of i…
kojiwakayama Nov 7, 2026
3d4664b
fix(module-loader): share Node's module cache with bundled CommonJS f…
kojiwakayama Nov 7, 2026
34e7bf1
test(module-loader): expect TypeScript stripping only where the runti…
kojiwakayama Nov 7, 2026
2706f75
fix(module-loader): keep Node's CommonJS scoping rules in bundled dep…
kojiwakayama Nov 7, 2026
c006f1c
fix(module-loader): match Node's module graph, package scopes and imp…
kojiwakayama Nov 7, 2026
4a652bd
fix(module-loader): bind bundled requires to their module and report …
kojiwakayama Nov 7, 2026
4a079aa
fix(routing): bind later literal requires and computed imports to the…
kojiwakayama Nov 7, 2026
0a2a12c
fix(routing): report package-map refusals with Node's error codes
kojiwakayama Nov 7, 2026
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
1 change: 1 addition & 0 deletions extensions/ext-bundler-esbuild/src/plugin-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function toEsbuildPlugin(
namespace: args.namespace,
suffix: args.suffix,
pluginData: args.pluginData,
with: args.with,
};
const result = await callback(loadArgs);
if (result == null) return result ?? null;
Expand Down
2 changes: 1 addition & 1 deletion scripts/lint/check-sanitizer-baseline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
// audits removed opt-outs. Platform/compat process and opaque-deps suites now
// restore the globals and child processes they touch instead of suppressing
// leak reports.
export const SANITIZER_OPT_OUT_BASELINE = 340;
export const SANITIZER_OPT_OUT_BASELINE = 338;

const OPT_OUT_PATTERN = /sanitize(?:Resources|Ops|Exit)\s*:\s*false/g;

Expand Down
2 changes: 2 additions & 0 deletions src/extensions/bundler/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ export interface OnLoadArgs {
namespace: string;
suffix?: string;
pluginData?: unknown;
/** Import attributes of the import that loads the file. */
with?: Record<string, string>;
}

/** Result returned from an `onLoad` callback. */
Expand Down
1,969 changes: 1,969 additions & 0 deletions src/routing/api/module-loader/compiled-cjs-bundling.test.ts

Large diffs are not rendered by default.

2,023 changes: 2,023 additions & 0 deletions src/routing/api/module-loader/compiled-cjs-bundling.ts

Large diffs are not rendered by default.

69 changes: 7 additions & 62 deletions src/routing/api/module-loader/external-import-rewriter.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import "#veryfront/schemas/_test-setup.ts";
import {
assert,
assertEquals,
assertStringIncludes,
assertThrows,
} from "#veryfront/testing/assert.ts";
import { assertEquals, assertStringIncludes, assertThrows } from "#veryfront/testing/assert.ts";
import { describe, it } from "#veryfront/testing/bdd.ts";
import type { FileInfo } from "#veryfront/platform/adapters/base.ts";
import type { FileSystem } from "#veryfront/platform/compat/fs.ts";
import {
generateCompiledBinaryRequireShim,
getNodeExternalPackagesToResolve,
NODE_BUILTINS,
resolveEsmUserDependencies,
resolveVeryfrontPackageExport,
rewriteCompiledBinaryUserDependencyImports,
Expand Down Expand Up @@ -116,45 +109,12 @@ describe("external-import-rewriter", () => {
describe("rewriteCompiledBinaryUserDependencyImports", () => {
const deps = new Map([["lodash", "^4"]]);

it("rewrites a default import to interop require", () => {
const out = rewriteCompiledBinaryUserDependencyImports(
`import _ from "lodash";`,
deps,
);
assertEquals(out, `const _ = __vf_interopDefault(require("lodash"));`);
});

it("rewrites a namespace import to require", () => {
const out = rewriteCompiledBinaryUserDependencyImports(
`import * as _ from "lodash";`,
deps,
);
assertEquals(out, `const _ = require("lodash");`);
});

it("rewrites a named import to a destructuring require", () => {
const out = rewriteCompiledBinaryUserDependencyImports(
`import { merge } from "lodash";`,
deps,
);
assertStringIncludes(out, `= require("lodash")`);
assertStringIncludes(out, "merge");
});

it("rewrites a dynamic import to a resolved require promise", () => {
const out = rewriteCompiledBinaryUserDependencyImports(
`const m = import("lodash");`,
deps,
);
assertStringIncludes(out, `Promise.resolve(require("lodash"))`);
});

it("rewrites a subpath import including the subpath in require", () => {
const out = rewriteCompiledBinaryUserDependencyImports(
`import merge from "lodash/merge";`,
deps,
);
assertStringIncludes(out, `require("lodash/merge")`);
// The route build bundles CommonJS dependencies (inbox#4177), so an import
// of one never reaches this rewrite; it must not be turned into require().
it("leaves imports of CommonJS dependencies untouched", () => {
const code =
`import _ from "lodash";\nimport merge from "lodash/merge";\nconst m = import("lodash");`;
assertEquals(rewriteCompiledBinaryUserDependencyImports(code, deps), code);
});

it("leaves imports for packages not in userDeps untouched", () => {
Expand Down Expand Up @@ -383,21 +343,6 @@ describe("external-import-rewriter", () => {
});
});

describe("generateCompiledBinaryRequireShim", () => {
it("emits a self-contained CJS require shim referencing the project dir", () => {
const shim = generateCompiledBinaryRequireShim("/srv/app");
// Declares the require shim and embeds the project package.json path.
assertStringIncludes(shim, "__vf_createRequire");
assertStringIncludes(shim, "/srv/app/package.json");
// Embeds the node builtin set used for routing builtin requires.
assertStringIncludes(shim, JSON.stringify(NODE_BUILTINS));
// Defines a require function and an interop helper.
assertStringIncludes(shim, "var require = function(id)");
assertStringIncludes(shim, "__vf_interopDefault");
assert(shim.length > 0);
});
});

describe("Node Veryfront package imports", () => {
// An API route that imports `veryfront/tool` must reach the SAME module
// instance the running server uses, or `toolRegistry.get()` looks into a
Expand Down
103 changes: 0 additions & 103 deletions src/routing/api/module-loader/external-import-rewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
type SourceReader = Pick<FileSystem, "readTextFile">;

/** Node.js built-in module names — shared across the CJS shim, esbuild externals, and Deno rewrites. */
export const NODE_BUILTINS = ROUTE_NODE_BUILTINS;

Check warning on line 29 in src/routing/api/module-loader/external-import-rewriter.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use `export…from` to re-export `NODE_BUILTINS`.

See more on https://sonarcloud.io/project/issues?id=veryfront_veryfront-code&issues=AaGyOvH6LjZQeDKjxmBR&open=AaGyOvH6LjZQeDKjxmBR&pullRequest=5477

function splitImportSuffix(specifier: string): { path: string; suffix: string } {
const queryIndex = specifier.indexOf("?");
Expand All @@ -50,109 +50,6 @@
return await readProjectDependenciesForRoute(projectDir, fs);
}

/**
* Generates a CJS module loader shim for compiled Deno binaries.
*
* In compiled binaries, `createRequire()` can resolve module paths and load
* built-in modules (fs, path, etc.), but cannot load CJS files from disk
* (loadMaybeCjs fails with "path not found"). This shim works around that
* limitation by using `Deno.readTextFileSync` to read CJS files and
* `new Function` to evaluate them in a proper CJS wrapper with require,
* exports, module, __filename, and __dirname bindings.
*/
export function generateCompiledBinaryRequireShim(projectDir: string): string {
const builtinSet = JSON.stringify(NODE_BUILTINS);
const safeProjectDir = JSON.stringify(projectDir + "/package.json");
const safeProjectRoot = JSON.stringify(pathHelper.resolve(projectDir));

return `
import { createRequire as __vf_createRequire } from "node:module";
import { dirname as __vf_dirname, resolve as __vf_resolve } from "node:path";
var __vf_builtinRequire = __vf_createRequire(${safeProjectDir});
var __vf_builtinSet = new Set(${builtinSet});
var __vf_projectRoot = ${safeProjectRoot};
// VULN-FS-5: Canonicalize the project root so containment checks using
// Deno.realPathSync(resolved) compare canonical-vs-canonical. Without this,
// when the project itself is opened via a symlink, the realpath'd resolved
// module path has a different prefix than the non-canonical projectRoot and
// legitimate dependencies would be rejected.
try { __vf_projectRoot = Deno.realPathSync(__vf_projectRoot); } catch (_) { /* expected: projectRoot may not exist at shim init in some environments */ }
var __vf_cache = Object.create(null);
// Resolve symlinks so containment compares canonical paths. A module that does
// not exist yet cannot be canonicalized, so fall back to its parent directory,
// which is what carries a symlinked project root; only if that fails too is the
// plain resolved path used.
function __vf_canonicalize(candidate) {
try {
return Deno.realPathSync(candidate);
} catch (_) { /* expected: candidate may not exist */ }
try {
var parent = __vf_dirname(candidate);
return Deno.realPathSync(parent) + candidate.slice(parent.length);
} catch (_) { /* expected: parent may not exist either */ }
return __vf_resolve(candidate);
}
function __vf_assertContained(resolved) {
var norm = __vf_resolve(resolved).replace(/\\\\/g, "/");
var root = __vf_projectRoot.replace(/\\\\/g, "/");
if (!norm.startsWith(root + "/") && norm !== root) {
throw new Error("CJS loader blocked path outside project: " + resolved);
}
}
function __vf_loadCjs(id, parentDir) {
if (id.startsWith("node:")) return __vf_builtinRequire(id);
if (__vf_builtinSet.has(id)) return __vf_builtinRequire(id);
var slashIdx = id.indexOf("/");
if (slashIdx > 0 && __vf_builtinSet.has(id.slice(0, slashIdx))) return __vf_builtinRequire(id);
var resolved;
if (id.startsWith(".") || id.startsWith("/")) {
resolved = __vf_resolve(parentDir, id);
if (!resolved.match(/\\.[a-zA-Z0-9]+$/)) {
var exts = [".js", ".cjs", ".json", "/index.js", "/index.cjs", "/index.json"];
for (var i = 0; i < exts.length; i++) {
try { Deno.statSync(resolved + exts[i]); resolved += exts[i]; break; } catch (_) { /* expected: probing file extensions */ }
}
}
} else {
resolved = __vf_builtinRequire.resolve(id);
}
// VULN-FS-5: Canonicalize before asserting containment, so the candidate and
// the already-canonical project root are compared like with like, then assert
// once. Checking the non-canonical path first rejected dependencies genuinely
// inside a project whose root is reached through a symlink, which is the
// ordinary case for package managers, CI checkouts and macOS /tmp.
// Canonicalizing first also keeps the guard against symlinked node_modules
// entries pointing outside the root: the escape is only visible once resolved.
resolved = __vf_canonicalize(resolved);
__vf_assertContained(resolved);
if (resolved in __vf_cache) return __vf_cache[resolved];
var code = Deno.readTextFileSync(resolved);
if (resolved.endsWith(".json")) {
var json = JSON.parse(code);
__vf_cache[resolved] = json;
return json;
}
var mod = { exports: {} };
__vf_cache[resolved] = mod.exports;
var dir = __vf_dirname(resolved);
var childReq = function(childId) { return __vf_loadCjs(childId, dir); };
childReq.resolve = function(childId) {
if (childId.startsWith(".") || childId.startsWith("/")) return __vf_resolve(dir, childId);
return __vf_builtinRequire.resolve(childId);
};
childReq.ensure = function(mods, cb) { cb(); };
var fn = new Function("exports", "require", "module", "__filename", "__dirname", "global", "globalThis", "Worker", code);
fn(mod.exports, childReq, mod, resolved, dir, globalThis, globalThis, undefined);
__vf_cache[resolved] = mod.exports;
return mod.exports;
}
function __vf_interopDefault(m) { return m && m.__esModule && m.default !== undefined ? m.default : m; }
var require = function(id) { return __vf_loadCjs(id, ${JSON.stringify(projectDir)}); };
require.resolve = function(id) { return __vf_builtinRequire.resolve(id); };
require.ensure = function(mods, cb) { cb(); };
`.trim();
}

export function getNodeExternalPackagesToResolve(userDeps: Map<string, string>): string[] {
return getNodeExternalPackagesToResolveForRoute(userDeps);
}
Expand Down
12 changes: 2 additions & 10 deletions src/routing/api/module-loader/loader-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
FILE_EXTENSIONS,
getLoaderForFile,
resolveExportEntry,
toCjsDestructureBindings,
validateModulePath,
} from "./loader-helpers.ts";

Expand Down Expand Up @@ -34,15 +33,6 @@ describe("routing/api/module-loader helpers", () => {
assertEquals(resolveExportEntry({ require: "./cjs.js" }), undefined);
});

it("converts CJS destructuring bindings", () => {
assertEquals(
toCjsDestructureBindings("{ parse as parsePdf, version }"),
"{ parse: parsePdf, version }",
);
assertEquals(toCjsDestructureBindings("{ foo, bar }"), "{ foo, bar }");
assertEquals(toCjsDestructureBindings("{ }"), "{}");
});

it("returns the expected file loaders and extensions", () => {
assertEquals(
FILE_EXTENSIONS,
Expand All @@ -54,6 +44,8 @@ describe("routing/api/module-loader helpers", () => {
assertEquals(getLoaderForFile("file.jsx"), "jsx", "jsx files must use the jsx loader");
assertEquals(getLoaderForFile("file.json"), "json", "json files must use the json loader");
assertEquals(getLoaderForFile("file.mjs"), "js", "mjs files must use the js loader");
assertEquals(getLoaderForFile("file.cts"), "ts", "cts files are CommonJS TypeScript");
assertEquals(getLoaderForFile("file.mts"), "ts", "mts files are ESM TypeScript");
assertEquals(getLoaderForFile("noext"), "js", "unmapped extensions fall back to the js loader");
});
});
19 changes: 2 additions & 17 deletions src/routing/api/module-loader/loader-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const EXT_TO_LOADER: Record<string, "tsx" | "jsx" | "ts" | "js" | "json"> = {
tsx: "tsx",
jsx: "jsx",
ts: "ts",
mts: "ts",
cts: "ts",
json: "json",
};

Expand Down Expand Up @@ -46,23 +48,6 @@ export function resolveExportEntry(entry: unknown): string | undefined {
return undefined;
}

export function toCjsDestructureBindings(bindings: string): string {
const inner = bindings.trim().replace(/^\{\s*/, "").trimEnd().replace(/\}$/, "");
if (!inner) return "{}";

const converted = inner
.split(",")
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const aliasMatch = part.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/);
if (aliasMatch) return `${aliasMatch[1]}: ${aliasMatch[2]}`;
return part;
});

return `{ ${converted.join(", ")} }`;
}

export function getLoaderForFile(filePath: string): "tsx" | "jsx" | "ts" | "js" | "json" {
const ext = filePath.split(".").pop() ?? "";
return EXT_TO_LOADER[ext] ?? "js";
Expand Down
Loading
Loading