You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Next.js apps use @/ path aliases (import { x } from '@/lib/utils') instead of deep relative paths (../../../lib/utils). In Next this is just a TypeScript path alias (compilerOptions.paths: { "@/*": ["./*"] }, often with a configurable base like src or src/app) that its bundler honors at build time. webjs scaffolds currently use deep relative imports, and an alias like @/ would meaningfully improve DX (especially for the modules architecture, where ../../../lib/... is common).
The wrinkle is that webjs is buildless: there is no bundler rewriting specifiers. Source .ts/.js is served as native ES modules after type-stripping, so a bare @/lib/foo specifier must resolve at RUNTIME (Node 24+ and Bun on the server, the browser importmap on the client), not just in the editor. So the Next approach (tsconfig paths + bundler) only solves half of it; the runtime half needs explicit webjs wiring.
The case to cover: directory-prefix wildcard with a configurable base (the literal Next @/lib/db)
The alias must cover the DIRECTORY-PREFIX WILDCARD, not just single-file aliases. In Next, @ maps to a configurable base (root, src, or src/app), so @/lib/db resolves to <base>/lib/db for ANY path under that base, one mapping, every file reachable. The research must answer whether webjs can do this general case (configurable base + @/* wildcard), not only specific single-file aliases like #db -> ./db/connection.server.ts.
Critical constraint that makes the @ sigil harder than #:
Node's native package.json "imports" field REQUIRES every key to start with #. So "@/*" is NOT a legal imports key. A native "#/*": "./*" (or "#app/*": "./src/app/*") wildcard works on Node AND Bun with zero webjs code, but it spells #, not @.
To get the literal @/* server-side, webjs must install a CUSTOM module-resolution hook: Node's module.registerHooks / customization-hooks loader and Bun's resolver plugin, mapping @/ -> the base dir at resolve time. Research whether such a hook is viable and acceptable in webjs's buildless model on BOTH runtimes: it must register before any app module loads, compose with the existing TS-strip loader (Support both Bun and Node runtimes (first-class create + run) #508), and resolve to the REAL path so the .server.ts boundary + no-server-import-in-browser-module / elision checks still fire.
The browser side is easier: an importmap trailing-slash scope ("@/": "/" or "@/": "/src/app/") DOES express the wildcard prefix natively, so the client half of @/* is feasible via importmap injection. The asymmetry is purely server-side resolution.
So the real question for the @ sigil: is a cross-runtime custom resolver hook (Node + Bun) worth it for muscle-memory parity with Next, versus shipping #-prefixed native wildcards that read #lib/db / #app/lib/db instead of @/lib/db? Decide with base-dir configurability in mind (root vs src vs src/app).
Design / approach
Research whether and how to support path aliases without a build step. Two layers to satisfy:
Editor + typecheck (easy half):compilerOptions.paths ("@/*": ["./*"] or a base) in the scaffold tsconfig.json gives @/ autocomplete and webjs typecheck resolution, same as Next. Just a tsconfig setting.
Runtime (the real question): since nothing rewrites the specifier, the runtime must resolve it. Options to evaluate:
Node/Bun native package.json "imports" field (the # subpath-imports convention with a wildcard, e.g. "#lib/*": "./lib/*" or "#/*": "./*"). The buildless-native answer: works on BOTH Node and Bun with zero build step and zero resolver hook, the standards-track mechanism for internal aliases. Tradeoff: the sigil is #, not @.
@/ via a custom resolver hook + importmap. Server: a Node/Bun module-resolution hook mapping @/ -> base. Client: an importmap trailing-slash scope ("@/": "/..."). Evaluate the hook's viability/cost.
A hybrid: tsconfig paths for the editor + the runtime mechanism, generated together so they cannot drift.
Decide the sigil (@ for Next muscle-memory at the cost of a custom server resolver, vs # native and zero-hook), the configurable base, and scaffold-default vs opt-in.
Implementation notes (for the implementing agent)
Runtime resolver surfaces: node_modules/@webjsdev/server/src/importmap.js (importmap generator / client resolver) and vendor.js (vendor pinning into .webjs/vendor/importmap.json). A @/ alias flows through here for the browser side; an importmap trailing-slash scope expresses the wildcard.
Server side: native "imports" (# only) is resolved by Node/Bun with no webjs code, which is why # is the low-effort path. A @ sigil needs a custom hook (Node module.registerHooks, Bun resolver plugin) registered at boot before app modules load; verify it composes with the Support both Bun and Node runtimes (first-class create + run) #508 TS stripper and that Bun honors "imports" identically (cross-runtime assert).
Scaffold tsconfig is generated in packages/cli/lib/create.js (no paths/baseUrl today); the editor half is a change here plus the templates. Pick the base (root vs src).
Invariants: stays buildless (no bundler, no webjs build); server-only code stays in .server.{js,ts}, the alias must resolve to the REAL path so it cannot become a way to import server-only modules into browser files and dodge the elision / no-server-import-in-browser-module checks.
This is a RESEARCH task: the output is a recommendation (sigil, base, mechanism, default-vs-opt-in). Per project convention the research record is a PR (title + description + comments) then closed, NOT a doc under agent-docs/. Implementation, if any, follows as its own issue.
Acceptance criteria
A written recommendation: sigil (@ vs #), configurable base, runtime mechanism (native "imports" wildcard vs custom resolver hook + importmap vs hybrid), and scaffold-default vs opt-in.
The recommendation covers the directory-prefix WILDCARD with a configurable base (@/lib/db -> <base>/lib/db for ANY path), not just single-file aliases.
If @ is chosen: proof a custom Node + Bun resolver hook maps @/* at runtime, registers before app modules load, composes with the TS stripper, and resolves to the real path so the .server.ts boundary + elision still fire. If # is chosen: proof the native "imports" wildcard covers the same ergonomics on Node AND Bun.
Proof the chosen mechanism resolves at runtime on BOTH Node 24+ and Bun, AND in the browser importmap (an alias that works in SSR but 404s client-side is a fail).
Confirmation the editor/webjs typecheck half (tsconfig paths) aligns with the runtime half so they cannot drift.
The server-only-import boundary + elision checks still resolve the real path through the alias (no bypass).
Problem
Next.js apps use
@/path aliases (import { x } from '@/lib/utils') instead of deep relative paths (../../../lib/utils). In Next this is just a TypeScript path alias (compilerOptions.paths: { "@/*": ["./*"] }, often with a configurable base likesrcorsrc/app) that its bundler honors at build time. webjs scaffolds currently use deep relative imports, and an alias like@/would meaningfully improve DX (especially for the modules architecture, where../../../lib/...is common).The wrinkle is that webjs is buildless: there is no bundler rewriting specifiers. Source
.ts/.jsis served as native ES modules after type-stripping, so a bare@/lib/foospecifier must resolve at RUNTIME (Node 24+ and Bun on the server, the browser importmap on the client), not just in the editor. So the Next approach (tsconfig paths + bundler) only solves half of it; the runtime half needs explicit webjs wiring.The case to cover: directory-prefix wildcard with a configurable base (the literal Next
@/lib/db)The alias must cover the DIRECTORY-PREFIX WILDCARD, not just single-file aliases. In Next,
@maps to a configurable base (root,src, orsrc/app), so@/lib/dbresolves to<base>/lib/dbfor ANY path under that base, one mapping, every file reachable. The research must answer whether webjs can do this general case (configurable base +@/*wildcard), not only specific single-file aliases like#db -> ./db/connection.server.ts.Critical constraint that makes the
@sigil harder than#:package.json "imports"field REQUIRES every key to start with#. So"@/*"is NOT a legal imports key. A native"#/*": "./*"(or"#app/*": "./src/app/*") wildcard works on Node AND Bun with zero webjs code, but it spells#, not@.@/*server-side, webjs must install a CUSTOM module-resolution hook: Node'smodule.registerHooks/ customization-hooks loader and Bun's resolver plugin, mapping@/-> the base dir at resolve time. Research whether such a hook is viable and acceptable in webjs's buildless model on BOTH runtimes: it must register before any app module loads, compose with the existing TS-strip loader (Support both Bun and Node runtimes (first-class create + run) #508), and resolve to the REAL path so the.server.tsboundary +no-server-import-in-browser-module/ elision checks still fire."@/": "/"or"@/": "/src/app/") DOES express the wildcard prefix natively, so the client half of@/*is feasible via importmap injection. The asymmetry is purely server-side resolution.So the real question for the
@sigil: is a cross-runtime custom resolver hook (Node + Bun) worth it for muscle-memory parity with Next, versus shipping#-prefixed native wildcards that read#lib/db/#app/lib/dbinstead of@/lib/db? Decide with base-dir configurability in mind (root vssrcvssrc/app).Design / approach
Research whether and how to support path aliases without a build step. Two layers to satisfy:
compilerOptions.paths("@/*": ["./*"]or a base) in the scaffoldtsconfig.jsongives@/autocomplete andwebjs typecheckresolution, same as Next. Just a tsconfig setting.package.json "imports"field (the#subpath-imports convention with a wildcard, e.g."#lib/*": "./lib/*"or"#/*": "./*"). The buildless-native answer: works on BOTH Node and Bun with zero build step and zero resolver hook, the standards-track mechanism for internal aliases. Tradeoff: the sigil is#, not@.@/via a custom resolver hook + importmap. Server: a Node/Bun module-resolution hook mapping@/-> base. Client: an importmap trailing-slash scope ("@/": "/..."). Evaluate the hook's viability/cost.pathsfor the editor + the runtime mechanism, generated together so they cannot drift.@for Next muscle-memory at the cost of a custom server resolver, vs#native and zero-hook), the configurable base, and scaffold-default vs opt-in.Implementation notes (for the implementing agent)
node_modules/@webjsdev/server/src/importmap.js(importmap generator / client resolver) andvendor.js(vendor pinning into.webjs/vendor/importmap.json). A@/alias flows through here for the browser side; an importmap trailing-slash scope expresses the wildcard."imports"(#only) is resolved by Node/Bun with no webjs code, which is why#is the low-effort path. A@sigil needs a custom hook (Nodemodule.registerHooks, Bun resolver plugin) registered at boot before app modules load; verify it composes with the Support both Bun and Node runtimes (first-class create + run) #508 TS stripper and that Bun honors"imports"identically (cross-runtime assert).packages/cli/lib/create.js(nopaths/baseUrltoday); the editor half is a change here plus the templates. Pick the base (root vssrc).<script type="importmap">. The type stripper does not rewrite specifiers, so do NOT assume a build step.webjs build); server-only code stays in.server.{js,ts}, the alias must resolve to the REAL path so it cannot become a way to import server-only modules into browser files and dodge the elision /no-server-import-in-browser-modulechecks.#db/@/dbstyle imports for the db module are the same mechanism; land a consistent answer.This is a RESEARCH task: the output is a recommendation (sigil, base, mechanism, default-vs-opt-in). Per project convention the research record is a PR (title + description + comments) then closed, NOT a doc under
agent-docs/. Implementation, if any, follows as its own issue.Acceptance criteria
@vs#), configurable base, runtime mechanism (native"imports"wildcard vs custom resolver hook + importmap vs hybrid), and scaffold-default vs opt-in.@/lib/db-><base>/lib/dbfor ANY path), not just single-file aliases.@is chosen: proof a custom Node + Bun resolver hook maps@/*at runtime, registers before app modules load, composes with the TS stripper, and resolves to the real path so the.server.tsboundary + elision still fire. If#is chosen: proof the native"imports"wildcard covers the same ergonomics on Node AND Bun.webjs typecheckhalf (tsconfig paths) aligns with the runtime half so they cannot drift.