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
Implement path-alias imports so scaffolded apps can write import { db } from '#/lib/db.server.ts' / import { Button } from '#/components/button.ts' instead of deep relative paths (../../../lib/...). Decided sigil: # (native package.json "imports"), per the research in #549 (record: closed PR #553). The @ sigil was rejected (it adds two bespoke per-runtime resolver hooks + a tsconfig-paths drift surface purely for Next parity; # is native, standards-track, zero editor config).
Decided shape: a SINGLE root-mapped key "#/*": "./*" (not per-directory keys). So every import reads #/lib/..., #/components/..., #/modules/..., #/app/..., which is the exact Next analog (@/lib/... with # swapped for @), the closest preserved muscle memory. One key, zero maintenance when dirs are added, configurable base (a src-rooted app writes "#/*": "./src/*").
Security-critical finding (the load-bearing change)
webjs's resolveImport (node_modules/@webjsdev/server/src/module-graph.js, ~L290/L308) currently SKIPS any specifier not starting with . or /. So a #/lib/db.server.ts specifier is treated as a bare npm specifier and is INVISIBLE to the four things that walk the import graph: the browser-bound auth gate (reachableFromEntries), modulepreload (transitiveDeps), elision (analyzeElision + the serve-time stripper in component-elision.js), and the no-server-import-in-browser-module check (check.js). Without a fix, an alias would LAUNDER a .server.ts past all of them. The mandatory change: expand a matching #/-prefixed specifier to its real path INSIDE resolveImport, before the relative-resolution branch, so all four see through the alias.
module-graph.jsresolveImport: read the app's package.json "imports", expand a matching #/-prefixed specifier to the real path before the relative branch (the security-critical change above). Drive it off the actual "imports" map so a non-default base (./src/*) is honored, do NOT hardcode ./.
Ship as a scaffold default (the value is in the modules architecture where deep relatives proliferate); opt-out is simply using a plain relative import. An unused "imports" block costs nothing.
Implementation notes (for the implementing agent)
The full verified evidence (native "imports" wildcard incl. the bare #/* catch-all on Node + Bun, the importmap trailing-slash scope #/ in Chromium, the @-hook feasibility, the editor check) is in PR research: path-alias imports (@/ vs #) in a buildless runtime #553's description + comment. Read it first.
Bun: native "imports" is byte-identical to Node (verified), so no Bun-specific resolver code is needed for # (unlike @).
Invariant: the alias must resolve to the REAL path so the .server.ts boundary + elision + auth-gate still fire (the whole point of change Corrected backtick errors #1).
Acceptance criteria
import x from '#/lib/foo.server.ts' resolves at runtime on Node 24+ AND Bun (native, no build step) and serves correctly in the browser via the emitted importmap entry.
resolveImport expands #/-aliases so the auth gate, modulepreload, elision, and no-server-import-in-browser-module all see the real path.
COUNTERFACTUAL: a .server.ts imported via a #/-alias into a shipping browser module still trips no-server-import-in-browser-module (proves the alias does not launder the boundary).
The importmap and the server resolver agree (no alias that resolves in SSR but 404s in the browser), both derived from the one "imports" map.
Scaffold ships the single-key "imports": { "#/*": "./*" }; a freshly scaffolded app uses #/... imports and passes webjs check + boots.
A non-default base ("#/*": "./src/*") resolves correctly on both runtimes and in the browser (proves the mapping is not hardcoded to ./).
Tests at every layer: unit (alias expansion + the counterfactual), browser (a scaffolded app importing #/components/... hydrates), e2e (the aliased URL is in the auth gate and served, not 404), cross-runtime (Bun).
Problem
Implement path-alias imports so scaffolded apps can write
import { db } from '#/lib/db.server.ts'/import { Button } from '#/components/button.ts'instead of deep relative paths (../../../lib/...). Decided sigil:#(nativepackage.json "imports"), per the research in #549 (record: closed PR #553). The@sigil was rejected (it adds two bespoke per-runtime resolver hooks + a tsconfig-paths drift surface purely for Next parity;#is native, standards-track, zero editor config).Decided shape: a SINGLE root-mapped key
"#/*": "./*"(not per-directory keys). So every import reads#/lib/...,#/components/...,#/modules/...,#/app/..., which is the exact Next analog (@/lib/...with#swapped for@), the closest preserved muscle memory. One key, zero maintenance when dirs are added, configurable base (asrc-rooted app writes"#/*": "./src/*").Security-critical finding (the load-bearing change)
webjs's
resolveImport(node_modules/@webjsdev/server/src/module-graph.js, ~L290/L308) currently SKIPS any specifier not starting with.or/. So a#/lib/db.server.tsspecifier is treated as a bare npm specifier and is INVISIBLE to the four things that walk the import graph: the browser-bound auth gate (reachableFromEntries), modulepreload (transitiveDeps), elision (analyzeElision+ the serve-time stripper incomponent-elision.js), and theno-server-import-in-browser-modulecheck (check.js). Without a fix, an alias would LAUNDER a.server.tspast all of them. The mandatory change: expand a matching#/-prefixed specifier to its real path INSIDEresolveImport, before the relative-resolution branch, so all four see through the alias.Design / approach (scope, from the #553 research)
module-graph.jsresolveImport: read the app'spackage.json "imports", expand a matching#/-prefixed specifier to the real path before the relative branch (the security-critical change above). Drive it off the actual"imports"map so a non-default base (./src/*) is honored, do NOT hardcode./.importmap.js: emit the matching browser entry derived from the same"imports"block, a single trailing-slash scope"#/": "/"(or"#/": "/src/"for asrcbase), so browser and server agree. The Modulepreload hints emitted for server-only files the gate 404s #158/Import scanner counts imports shown as code inside template literals #159 preload-mismatch class is the landmine: the importmap and the server resolver MUST stay in lockstep, both derived from the one"imports"map.cli/lib/create.js: add the single-key"imports"block to the scaffoldpackage.json:"imports"BLOCK (the capability). Converting the template FILE contents to the#/...form is handled by Convert deep relative imports to #/ aliases across the in-repo apps + examples #556 (the codemod sweep over all existing deep-relative imports), so Implement # path-alias imports (package.json imports + resolveImport alias expansion) #555 stays purely the mechanism."imports"resolves underNodeNextwith ZEROcompilerOptions.paths(verified on TS 5.x in research: path-alias imports (@/ vs #) in a buildless runtime #553), so no tsconfig drift surface.Ship as a scaffold default (the value is in the modules architecture where deep relatives proliferate); opt-out is simply using a plain relative import. An unused
"imports"block costs nothing.Implementation notes (for the implementing agent)
"imports"wildcard incl. the bare#/*catch-all on Node + Bun, the importmap trailing-slash scope#/in Chromium, the@-hook feasibility, the editor check) is in PR research: path-alias imports (@/ vs #) in a buildless runtime #553's description + comment. Read it first.#/lib/db/connection.server.ts. Switch default ORM from Prisma to Drizzle across scaffolds, webjs db, docs, blog #551 ships with plain relative paths until then; this is additive."imports"is byte-identical to Node (verified), so no Bun-specific resolver code is needed for#(unlike@)..server.tsboundary + elision + auth-gate still fire (the whole point of change Corrected backtick errors #1).Acceptance criteria
import x from '#/lib/foo.server.ts'resolves at runtime on Node 24+ AND Bun (native, no build step) and serves correctly in the browser via the emitted importmap entry.resolveImportexpands#/-aliases so the auth gate, modulepreload, elision, andno-server-import-in-browser-moduleall see the real path..server.tsimported via a#/-alias into a shipping browser module still tripsno-server-import-in-browser-module(proves the alias does not launder the boundary)."imports"map."imports": { "#/*": "./*" }; a freshly scaffolded app uses#/...imports and passeswebjs check+ boots."#/*": "./src/*") resolves correctly on both runtimes and in the browser (proves the mapping is not hardcoded to./).#/components/...hydrates), e2e (the aliased URL is in the auth gate and served, not 404), cross-runtime (Bun).Decided in #549 (research). Record: #553.