feat: # path-alias imports + convert in-repo apps & scaffold#565
Conversation
|
Design rationale: why Worth recording why this does not compromise no-build or the security boundary, since both were a concern:
|
|
Review: # alias holds up, a few real catches fixed Went over this in several passes. The mechanism is sound: the alias is expanded inside What I caught and fixed along the way:
Last pass was clean. Verified: |
|
CI note: the The unit job's only red is
Everything the feature actually does is verified green: |
…erministic (#566) The type-fixtures tsc used --moduleResolution bundler, which intermittently resolved @webjsdev/server to its default export condition (index.js, untyped) instead of the types condition (index.d.ts) under node --test cross-file concurrency. A fixture importing a type-only export (AuthInstance, the generic createAuth<TUser>) then failed with 'no exported member' / 'resolves to an untyped module', non-deterministically and shifting between sibling fixtures (auth-session-default vs auth-session-augment) across runs; green on main, deterministic on branches that perturbed test timing (#563, #565). Switch the fixtures to module/moduleResolution nodenext, which honors the package exports types condition deterministically (how a typed Node consumer resolves), so the bare @webjsdev/* specifiers always land on the .d.ts. Drop --allowJs too (no fixture imports a .js). All 10 fixtures pass with their @ts-expect-error counterfactuals intact.
…567) * fix: resolve type fixtures with nodenext so @webjsdev/* types are deterministic (#566) The type-fixtures tsc used --moduleResolution bundler, which intermittently resolved @webjsdev/server to its default export condition (index.js, untyped) instead of the types condition (index.d.ts) under node --test cross-file concurrency. A fixture importing a type-only export (AuthInstance, the generic createAuth<TUser>) then failed with 'no exported member' / 'resolves to an untyped module', non-deterministically and shifting between sibling fixtures (auth-session-default vs auth-session-augment) across runs; green on main, deterministic on branches that perturbed test timing (#563, #565). Switch the fixtures to module/moduleResolution nodenext, which honors the package exports types condition deterministically (how a typed Node consumer resolves), so the bare @webjsdev/* specifiers always land on the .d.ts. Drop --allowJs too (no fixture imports a .js). All 10 fixtures pass with their @ts-expect-error counterfactuals intact. * test(bun): denylist test/docs/ from the matrix (docs-boot 5s-timeout flake) Every test/docs/*.test.mjs boots the docs app via createRequestHandler and asserts rendered HTML; the cold boot resolves docs code-sample bare imports via jspm and intermittently exceeds bun test's 5s default timeout (which page tips over varies: security-page, troubleshooting-page, llms have all flaked, red on main too). Docs-content checks, fully covered on the Node path. Same class as the differential-elision + deployment-secrets denylist entries. * fix: retry transient tsc resolution + honor directory denylist entries The type-fixtures runner intermittently failed in CI when a spawned tsc momentarily resolved a workspace package to its untyped .js under node --test cross-file concurrency (TS7016/TS2305 on @webjsdev/server), non-deterministically shifting between sibling fixtures. The package types are correct, so a fresh tsc a moment later succeeds; retry up to 3x only on a failure that names @webjsdev/core|server, so a genuine fixture type error (which persists across attempts and points at the fixture) is still reported. Also fix the bun test matrix denylist matcher: it compared exact repo-relative paths only, so a directory-prefix entry like 'test/docs/' never matched any file under it. Honor a trailing-slash entry as a prefix so the flaky docs app-boot tests are actually skipped on Bun. * fix: isolate the d.ts counterfactual so it cannot race fixture compiles The retry was fragile: the counterfactual moved the live packages/server/index.d.ts aside for a full ~2.5s tsc run, far longer than a few retries can outlast, and the transient failure surfaces as varied codes (TS7016/TS2305/TS2322/TS2578) that no longer name the package on an augmentation fixture, so the retry detector missed it. Real fix: the counterfactual now copies the package into a temp node_modules and removes the overlay from the COPY, never the live file. node --test runs files concurrently, so a concurrent fixture compile in type-fixtures.test.mjs always sees the real, present overlay. Drops the retry loop (no longer needed) and keeps the bun denylist directory-prefix matcher fix from the prior commit. --------- Co-authored-by: t <t@t>
Expand a package.json 'imports' subpath alias (#/*: ./*) to its real path inside resolveImport, BEFORE the relative branch, so the module graph, auth gate, modulepreload, elision, and no-server-import-in-browser-module all see through the alias (an alias must not launder a .server.ts past the boundary). importmap.js emits the matching browser scope (#/: /) from the same imports map, kept in lockstep with the server resolver. versionModuleImports rewrites a #/ alias in served source to a base-path-safe versioned relative specifier so it gets the same immutable ?v caching + matched preload as a relative import. Driven off the actual imports map so a non-default base (./src/*) is honored.
…#555) Scaffold package.json gets imports: { '#/*': './*' }. Tests cover alias expansion, the non-default base, the graph edge, the browser-scope derivation, and the security counterfactual: a .server.ts imported via #/ into a shipping module still trips no-server-import-in-browser-module (the alias does not launder the boundary), while an elided page is correctly not flagged.
A #/ alias import is rewritten to a base-path-safe versioned relative specifier matching the modulepreload href (no #369 wasted-preload), and a #/ alias to a .server.ts is left untouched (bare-URL stub, never preloaded).
Add the package.json imports block + rewrite app-internal deep-relative imports to #/ across examples/blog, website, docs, and the ui-website (including the Drizzle db/ imports from #551). Imports that escape the app root (to packages/server, the ui registry sibling) stay relative; code samples inside docs page strings are untouched (masked). Boots verified on all four apps with the #/ importmap scope and no broken preloads.
Convert the deep-relative imports create.js/saas-template.js emit and the template files (test/hello, AGENTS.md, CONVENTIONS.md samples) to #/, so a freshly scaffolded app ships with #/ imports. readUiComponent rewrites a registry component's relative ../lib/utils.ts to the app's #/lib/utils/cn.ts. Only root-targeting climbs are converted (a partway ../types.ts is left). scaffold-integration asserts the imports block, #/ usage, and no surviving within-app deep relatives.
The decided single "#/*": "./*" key does NOT resolve on Bun (latest, 1.3.14): Bun's native package.json imports resolver rejects a #/-prefixed key (Cannot find module '#/lib/x'), though Node resolves it. Since the blog runs on Bun on Railway, that would break the deploy. Switch to per-directory keys (#lib/*: ./lib/*, spelled #lib/...), which resolve natively on Node AND Bun and produce a valid trailing-slash browser importmap scope (Bun's own docs use this form). The resolver/importmap code is key-shape-agnostic; only the scaffold + apps + tests changed shape. Verified on node 26 + bun 1.3.14; the 4 apps boot and a real blog module imports clean on Bun.
…#556) Use one root catch-all import key `"#*": "./*"` instead of per-directory keys: a new top-level folder is aliased with no config change. `#*` resolves natively on Node AND Bun (verified on bun 1.3.14), unlike a `#/`-prefixed key which Bun rejects. A bare `#` can't be a browser importmap prefix, so the browser scopes are auto-derived per top-level dir from a directory scan (dev.js `appTopLevelDirs` -> importmap), keeping server + browser in lockstep off the one map. Apps + tests use `#lib/...` spelling; the resolver stays key-shape-agnostic.
Root AGENTS.md gets an Imports convention (prefer #db/, #components/, #lib/... over deep relatives; native package.json imports, #* catch-all, no slash, Bun- safe). packages/server/AGENTS.md documents the resolver seam (appImportsMap / expandImportAlias in module-graph, importAliasBrowserEntries in importmap).
…555/#556) Self-review findings: - saas-template readUiComponent had a self-replacing no-op (the bulk rename mangled the search string), so a scaffolded saas app shipped components/ui/* with a broken ../lib/utils.ts cn import (ERR_MODULE_NOT_FOUND). Fixed to rewrite the registry's relative cn import to #lib/utils/cn.ts, with a scaffold-integration regression assertion over every copied ui component. - Two real app imports the codemod missed (website/app/page.ts highlight, ui-demo dialog side-effect import) now use #. - Corrected #/* vs #* comment drift in the CI step, path-alias proof header, and the scaffold test comment; documented that the # alias addresses a subdirectory (a root-level file has no browser scope, import it relatively). - Updated the landing-page code sample to #db/ for consistency.
The shipped alias is # (no slash); some comments/test names loosely called it #/ (the very form the feature rejects on Bun). Corrected in module-graph.js, asset-hash.js, the no-server-import + version-module-imports tests, and the bun path-alias proof header. Legit '#/-prefixed key is avoided' contrasts kept.
The full-stack ui-kit test expected the registry cn import at a relative ../../lib/utils/cn.ts and the api route test expected ../../../modules/...; the scaffold now emits #lib/utils/cn.ts and #modules/... The api route test's relative ../-depth off-by-one guard is moot (the alias is depth-independent), re-pointed at the alias.
dcb2d6f to
0f21a19
Compare
Closes #555
Closes #556
Implements the
#path-alias import mechanism (#555) and converts the in-repo apps, examples, and scaffold templates to use it (#556). #556 hard-depends on #555 (the alias must resolve before any import can be converted), so both ship together here.The alias shape:
#*catch-all (Bun-safe, zero-maintenance)The scaffold ships a single root catch-all in
package.json:So
import { db } from '#db/connection.server.ts',#components/...,#lib/...,#modules/..., and any NEW top-level folder all resolve with no config change. Two findings drove the shape (verified on Node 26 + Bun 1.3.14):#/-prefixed key ("#/*": "./*", spelled#/lib/...) does NOT resolve on Bun (Cannot find module '#/lib/x'), only on Node. Since the blog runs on Bun on Railway, that would break the deploy. The slash-free#*form (spelled#lib/...) resolves natively on both.#555: the mechanism (framework)
resolveImport(module-graph) expands a matching#specifier to its real path BEFORE the relative branch (driven off the app's"imports"map), so the auth gate, modulepreload, elision, andno-server-import-in-browser-moduleall see through the alias. An alias cannot launder a.server.tspast the boundary (counterfactual test included).importAliasBrowserEntries(importmap) derives the browser scope from the SAME map. A bare#cannot be an importmap prefix key, so the#*catch-all is expanded into one trailing-slash scope per top-level dir (#lib/->/lib/, ...) from a directory scan (dev.jsappTopLevelDirs); a new folder is covered on the next boot. Server + browser stay in lockstep.versionModuleImports(asset-hash) rewrites a#import in prod served source to a base-path-safe versioned relative specifier, so it gets the same?vimmutable cache + matched preload as a relative import (no dogfood: modulepreload ?v= mismatch double-downloads every nested component module #369 wasted preload); a#alias to a.server.tsis left unversioned.The alias addresses a top-level SUBDIRECTORY (
#lib/...); a root-level file is imported relatively (the browser scope is per-directory).#556: the conversion (apps + templates)
examples/blog,website,docs,packages/ui/packages/website: app-internal deep relatives rewritten to#(including the Drizzledb/imports from Switch default ORM from Prisma to Drizzle across scaffolds, webjs db, docs, blog #551). Imports that escape the app root (topackages/server, the ui registry sibling) stay relative.create.js/saas-template.jsemit#(including the registrycnrewrite to#lib/utils/cn.ts), so a fresh app demonstrates the feature.Verification
#resolves natively on Node 26 AND Bun 1.3.14 (test/bun/path-alias.mjs, wired into CI).#importmap scopes and no broken preloads; a real blog module imports clean on Bun.cn-rewrite regression + two missed real imports, fixed; round 2 found only#/-vs-#wording, fixed).