Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8199211
Define the compiled-binary CJS loader for rewritten route dependency …
kojiwakayama Nov 5, 2026
67372a6
Prepend the route require prologue after bundling on every runtime
kojiwakayama Nov 5, 2026
ffb8fc0
Name missing npm packages and native addons in the compiled CJS loader
kojiwakayama Nov 5, 2026
003421f
Address review: private loader scope, nested resolution, namespace in…
kojiwakayama Nov 5, 2026
15f4a64
Address review: declaration-only collisions, full namespace interop, …
kojiwakayama Nov 5, 2026
1c7aaee
fix(api-routes): rewrite CJS imports from lexer spans with collision-…
kojiwakayama Nov 5, 2026
3383a23
fix(api-routes): match Node CommonJS loading more closely in the comp…
kojiwakayama Nov 5, 2026
ad56761
fix(api-routes): cache module records and tighten local resolution in…
kojiwakayama Nov 5, 2026
e3d3945
fix(api-routes): handle more CommonJS import forms and module APIs
kojiwakayama Nov 5, 2026
50069a4
fix(routes): link CJS module parents, honor exports for prefix-only n…
kojiwakayama Nov 5, 2026
c304f57
fix(routes): sealed CJS namespaces, parent.require, regex and brace-a…
kojiwakayama Nov 6, 2026
a4e0d40
fix(routes): isolate CJS shim intrinsics, lazy namespace getters, bui…
kojiwakayama Nov 6, 2026
2e9196b
fix(routes): lexer-bounded dynamic imports, terminated rewrites, URL …
kojiwakayama Nov 6, 2026
fae2d59
fix(routes): contain require.resolve, canonical lookup paths, CJS imp…
kojiwakayama Nov 6, 2026
4896c2a
fix(routes): CJS import options and helper naming, captured statics, …
kojiwakayama Nov 6, 2026
ef0bddc
fix(routes): comment/escape-aware CJS import scan, one-pass shim plac…
kojiwakayama Nov 6, 2026
3077b93
fix(routes): control-flow regex scan, import option validation, requi…
kojiwakayama Nov 6, 2026
53da924
fix(routes): private #import, attribute type checks, nested import ed…
kojiwakayama Nov 6, 2026
e224578
fix(routing): resolve relative require.resolve paths from cwd; read /…
kojiwakayama Nov 6, 2026
76f2685
fix(routing): keep the CJS loader independent of replaceable built-in…
kojiwakayama Nov 6, 2026
3346cd7
fix(routing): object-literal division, package imports and hoisted fa…
kojiwakayama Nov 6, 2026
a118734
fix(routing): read import options before the await, bound package loo…
kojiwakayama Nov 6, 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
29 changes: 29 additions & 0 deletions docs/guides/api-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,35 @@ export function GET() {
}
```

## npm packages

API routes can import packages listed in the project's `package.json`. The
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
package must be installed in the project's `node_modules` where the server
runs. CommonJS packages support default, named, namespace and dynamic
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
`import()` forms. A namespace import or a dynamic `import()` exposes the
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
package's `module.exports` as `default`, alongside its named properties.

Import packages by their root name or by a file path inside the package. Subpath
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
names defined only in a package's `exports` map, or a package whose `exports`
field is a single string, may not resolve. A package is treated as CommonJS or
ESM as a whole, so an `.mjs` file inside a CommonJS package fails with
`ES module file "<path>" cannot be loaded with require`. Re-export named
values from a CommonJS package (`export { name } from "pkg"`); `export * from
"pkg"` is not supported, because its names are only known at run time.
Inside a CommonJS package, `import("./file.mjs")` with a literal relative or
absolute path loads that file relative to the package; a computed specifier
does not.

Packages that ship a native addon (`.node` file) are not supported. Loading one
fails with `native addon "<path>" cannot be loaded`. A package that is listed
but not installed fails with `npm package "<name>" is not installed in this
runtime's node_modules`.

A static `import` resolves when the route module loads, so either error makes
every request to the route fail until the dependency is changed. A dynamic
`import()` resolves when it runs: only requests that reach it fail, and the
handler can catch the rejected promise.

## Chat endpoint

The most common API route pattern in Veryfront connects a chat UI to an agent:
Expand Down
288 changes: 265 additions & 23 deletions src/routing/api/module-loader/external-import-rewriter.test.ts

Large diffs are not rendered by default.

1,034 changes: 980 additions & 54 deletions src/routing/api/module-loader/external-import-rewriter.ts

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/routing/api/module-loader/loader-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ describe("routing/api/module-loader helpers", () => {
);
assertEquals(toCjsDestructureBindings("{ foo, bar }"), "{ foo, bar }");
assertEquals(toCjsDestructureBindings("{ }"), "{}");
assertEquals(
toCjsDestructureBindings(`{ "a-b" as value, 'c,d' as other, e, }`),
`{ "a-b": value, 'c,d': other, e }`,
);
assertEquals(
toCjsDestructureBindings("{ café as c2, caf\\u00e9 }"),
"{ café: c2, caf\\u00e9 }",
);
});

it("returns the expected file loaders and extensions", () => {
Expand Down
110 changes: 98 additions & 12 deletions src/routing/api/module-loader/loader-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,107 @@
return undefined;
}

export function toCjsDestructureBindings(bindings: string): string {
const inner = bindings.trim().replace(/^\{\s*/, "").trimEnd().replace(/\}$/, "");
if (!inner) return "{}";
/**
* A JavaScript identifier, including Unicode letters and `\u` escapes, as
* the source of a RegExp built with the `u` flag.
*/
export const JS_IDENTIFIER = "(?:[\\p{ID_Start}$_]|\\\\u[0-9a-fA-F]{4}|\\\\u\\{[0-9a-fA-F]+\\})" +

Check warning on line 53 in src/routing/api/module-loader/loader-helpers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=veryfront_veryfront-code&issues=AaGt8vUxQkYmMVwvnkPp&open=AaGt8vUxQkYmMVwvnkPp&pullRequest=5472
"(?:[\\p{ID_Continue}$\\u200C\\u200D]|\\\\u[0-9a-fA-F]{4}|\\\\u\\{[0-9a-fA-F]+\\})*";

Check warning on line 54 in src/routing/api/module-loader/loader-helpers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

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

/** A single- or double-quoted string literal, as the source of a RegExp. */
export const JS_STRING_LITERAL = `"(?:[^"\\\\\\r\\n]|\\\\.)*"|'(?:[^'\\\\\\r\\n]|\\\\.)*'`;

Check warning on line 57 in src/routing/api/module-loader/loader-helpers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

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

const SPECIFIER_ENTRY = new RegExp(
`\\s*(?:(${JS_STRING_LITERAL})|(${JS_IDENTIFIER}))` +

Check warning on line 60 in src/routing/api/module-loader/loader-helpers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=veryfront_veryfront-code&issues=AaGt8vUxQkYmMVwvnkPs&open=AaGt8vUxQkYmMVwvnkPs&pullRequest=5472
`(?:\\s+as\\s+(?:(${JS_STRING_LITERAL})|(${JS_IDENTIFIER})))?\\s*(?:,|$)`,

Check warning on line 61 in src/routing/api/module-loader/loader-helpers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=veryfront_veryfront-code&issues=AaGt8vUxQkYmMVwvnkPt&open=AaGt8vUxQkYmMVwvnkPt&pullRequest=5472
"uy",
);

/** One `name as alias` entry of an import or export clause, as source text. */
export interface ModuleSpecifier {
/** The name in the other module: an identifier or a string literal. */
readonly name: string;
readonly nameIsString: boolean;
/** The local or exported name, if it differs: an identifier or a string literal. */
readonly alias?: string;
readonly aliasIsString: boolean;
}

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;
/** Parse a `{ ... }` import or export clause. Throws on anything else. */
export function parseModuleSpecifiers(clause: string): ModuleSpecifier[] {
const trimmed = clause.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
throw new Error(`Unsupported import or export clause: ${clause}`);
}
const inner = trimmed.slice(1, -1);
const specifiers: ModuleSpecifier[] = [];
const entry = new RegExp(SPECIFIER_ENTRY.source, SPECIFIER_ENTRY.flags);
while (inner.slice(entry.lastIndex).trim() !== "") {
const start = entry.lastIndex;
const match = entry.exec(inner);
if (!match || entry.lastIndex === start) {
throw new Error(`Unsupported import or export clause: ${clause}`);
}
const [, nameString, nameIdent, aliasString, aliasIdent] = match;
specifiers.push({
name: (nameString ?? nameIdent)!,
nameIsString: nameString !== undefined,
alias: aliasString ?? aliasIdent,
aliasIsString: aliasString !== undefined,
});
}
return specifiers;
}

const SIMPLE_ESCAPES: Record<string, string> = {
b: "\b",
f: "\f",
n: "\n",
r: "\r",
t: "\t",
v: "\v",
"0": "\0",
};

/**
* The name a specifier's `name` denotes, with string-literal quotes removed
* and escapes decoded, so `default`, `"default"` and `\u0064efault` compare
* equal.
*/
export function moduleExportNameValue(specifier: ModuleSpecifier): string {
const source = specifier.nameIsString ? specifier.name.slice(1, -1) : specifier.name;
return source.replace(
/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\s\S]))/g,
(_match, codePoint?: string, unit?: string, byte?: string, other?: string) => {
const hex = codePoint ?? unit ?? byte;
if (hex !== undefined) return String.fromCodePoint(parseInt(hex, 16));

Check warning on line 122 in src/routing/api/module-loader/loader-helpers.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseInt` over `parseInt`.

See more on https://sonarcloud.io/project/issues?id=veryfront_veryfront-code&issues=AaGv-Sdo1VLkj8ktvSxQ&open=AaGv-Sdo1VLkj8ktvSxQ&pullRequest=5472
if (
other === "\n" || other === "\r" || other === "\r\n" || other === "\u2028" ||
other === "\u2029"
) {
return "";
}
return SIMPLE_ESCAPES[other!] ?? other!;
},
);
}

return `{ ${converted.join(", ")} }`;
/**
* Convert a named import clause (`{ a, b as c, "d-e" as f }`) to a
* destructuring pattern (`{ a, b: c, "d-e": f }`). A string-literal name
* needs an identifier alias, as in the import itself.
*/
export function toCjsDestructureBindings(bindings: string): string {
const converted: string[] = [];
for (const specifier of parseModuleSpecifiers(bindings)) {
if (specifier.aliasIsString || (specifier.nameIsString && specifier.alias === undefined)) {
throw new Error(`Unsupported named import clause: ${bindings}`);
}
converted.push(
specifier.alias === undefined ? specifier.name : `${specifier.name}: ${specifier.alias}`,
);
}
return converted.length === 0 ? "{}" : `{ ${converted.join(", ")} }`;
}

export function getLoaderForFile(filePath: string): "tsx" | "jsx" | "ts" | "js" | "json" {
Expand Down
Loading
Loading