0.39.1 (2026-10-31)#
Native file URL conversion#
Added synchronous FileRef.toURL(), which converts the referenced filesystem
path to a file: URL using the running host's native pathToFileURL semantics.
Relative paths resolve from the current working directory, and native escaping
is preserved for reserved characters. Browser imports remain safe;
BrowserFileRef.toURL() throws the existing unavailable-filesystem error when
called.
0.39.0 (2026-10-28)#
This minor release updates the FileRef path contract and introduces explicit
directory, empty-file, and symbolic-link creation. The path changes replace
debase() with relativeTo(base) and make normalization opt-in. Creation
replaces create() with createDirectory(), adds exclusive createFile() and
symbolic createLink(), and keeps write()'s automatic parent creation.
Both sets of changes require migration; see the examples below.
FileRef path contract and migration#
FileRef is a path reference, not an open file handle or a canonical filesystem
identity. fs.ref() preserves ordinary native path spelling. join() preserves
. and .., rejects absolute/drive-qualified/URL operands, and treats empty
operands as no-ops. Call normalize() explicitly to collapse lexical dot
segments. This matters when traversing symlinks: normalizing before opening a
path may identify a different file. fs.resolve() remains cwd-dependent.
debase() is replaced by relativeTo(base), which returns a string with
/ separators, "." for the same lexical location, and errors for mismatched
roots or implicit context. It does not certify filesystem containment.
file.debase(root, "/").path;
// → file.relativeTo(root)
file.debase(root).path.replace(/^[\\/]/, "");
// → file.relativeTo(root)
guide.debase(base).path.slice(1).split("/");
// → guide.relativeTo(base).split("/")
file.debase(root).bare().path.slice(1);
// → file.bare().relativeTo(root)
file.debase(client_dir).path.replaceAll("\\", "/");
// → file.relativeTo(client_dir)
`/${file.relativeTo(static_dir)}`; // explicit asset prefix, if neededIf the old code expected an empty string for identical paths, compare with
"." instead. The result is already a portable relative string: split it on
/, or rejoin it with root.join(file.relativeTo(root)). Do not use this
operation to enforce a directory boundary; verify paths using filesystem
identity and symlink-aware checks where security depends on containment.
fs.ref(new URL("file:///tmp/x")) and fs.ref("file:///tmp/x") remain valid
complete inputs on supported runtimes; neither is a join operand. Browser
references support slash-based lexical work but not disk or cwd operations.
For slash paths, explicit normalization preserves a trailing slash even when
the path collapses to the current directory ("a/../" becomes "./").
core now uses the final filename, not its directory: fs.ref("a/.env").core
is "", the same as fs.ref(".env").core. This corrects a prior
directory-dependent result for names beginning with a dot.
At this release, @rcompat/path was retained without deprecation. Its subsequent
workspace removal is recorded in the repository changelog; migration guidance
is in this package's README. Native Windows syntax uses the runtime's path
facilities; it has not been run on a Windows host and needs separate
validation.
Root layouts and incompatible locations#
const directory = page.directory.relativeTo(pages_base);
// Previously the root layout compared directory === "".
const applies = directory === "." ||
own_page.relativeTo(pages_base).startsWith(`${directory}/`);
// Prefix text is not a directory boundary: "docs/" cannot match
// "documentation/page.ts". relativeTo() does not enforce containment.
fs.ref("/alphabet/page.ts").relativeTo("/alpha");
// "../alphabet/page.ts"
fs.ref("/app/page.ts").relativeTo("app"); // throws: absolute vs relative
fs.ref("a").relativeTo("../b"); // throws: unknown cwd child nameOn Windows hosts only, native node:path handles drive and UNC syntax:
fs.ref("C:\\app\\routes\\docs\\index.ts")
.relativeTo("c:\\app\\routes"); // "docs/index.ts"
fs.ref("C:\\app\\routes").join("docs", "index.ts").path;
// "C:\\app\\routes\\docs\\index.ts" (joining preserves spelling)
fs.ref("C:\\app\\routes\\docs\\..\\index.ts").normalize().path;
// "C:\\app\\routes\\index.ts"
fs.ref("C:\\app\\routes").relativeTo("D:\\app\\routes");
// throws: incompatible drivesNative Windows execution was unavailable for this task; the examples explain
the intended migration, not a Windows compatibility certification. Browser
paths use / regardless of the host and treat \\ as filename text.
Primate migration fixtures#
src/private/migration.spec.ts tests replacement expressions without changing
Primate. The grouped fixtures correspond to the 24 inspected call sites in
Primate (paths below are relative to its checkout):
| Fixture | Inspected call sites |
|---|---|
| Route IDs and extensions | packages/go/src/private/default.ts:125,134; packages/python/src/private/default.ts:90; packages/ruby/src/private/default.ts:91; packages/core/src/private/request/router.ts:27; packages/core/src/private/build/App.ts:200; packages/core/src/private/build/client/plugin/routes.spec.ts:58 |
| Route/view/component keys | packages/webc/src/private/default.ts:36; packages/core/src/private/frontend.ts:491,515,521; packages/core/src/private/build/server/plugin/views.ts:43,73; packages/core/src/private/build/server/plugin/view.ts:34; packages/core/src/private/build/client/plugin/route-view.ts:33 |
| Root layout and descendants | packages/core/src/private/frontend.ts:477,481 |
| Client/static asset keys | packages/core/src/private/serve/App.ts:604; packages/core/src/private/build/client/index.ts:208; packages/core/src/private/build/server/plugin/assets.ts:33,53 |
| Guides, test paths and filters | apps/website/config/Website.ts:19; packages/test/src/private/setup.ts:68; packages/tailwind/src/private/collect.ts:27 |
For custom extensions, remove the selected suffix after computing the
relative string. Removing only the final extension with bare() is different
for files like index.route.ts. Prefix asset keys explicitly with / rather
than relying on debase() producing one; this also makes root-layout equality
checks use "." rather than the former empty string.
Explicit filesystem creation#
create() is removed from FileRef and fs, without a deprecated or forwarding
alias. Rename directory creation without changing its options or its default
recursive/idempotent behavior:
await directory.create(options);
// → await directory.createDirectory(options);
await fs.create(path, options);
// → await fs.createDirectory(path, options);FileRef.directory continues to return the parent reference and does not
create it. Existing write(content) still creates its parent directories;
the two new operations below do not:
await file.createFile();
await fs.createFile("existing/empty.txt");
// Exclusive, empty regular files only; not a replacement for write(content).
await link.createLink(target);
await link.createLink(target, { type: "symbolic" });
await fs.createLink(link, target, { type: "symbolic" });
// Same operation: link is the NEW link location, target is what it stores.createFile() refuses occupied locations (including dangling links), never
truncates or follows them, and fails on missing parents. createLink() also
refuses all occupied locations and missing parents, but accepts dangling target
paths. Relative targets are stored as provided, including ./ and ../,
and are interpreted from the link's directory when followed. Absolute targets
and FileRefs are accepted; URL strings and objects are not target operands,
though fs.ref(new URL("file:///tmp/link")) is a valid link location.
Native errors (such as EEXIST and ENOENT) remain available to callers.
Only symbolic links are implemented. options.type names the link kind;
omitting it is equivalent to "symbolic", while "hard" rejects. A separate
targetType: "file" | "directory" is an optional Windows target-kind hint;
POSIX ignores it. Windows may require a directory hint or privileges, and its
native symlink behavior was not executed here. Browser references remain safe
to import; createDirectory, createFile and createLink throw on invocation.
The earlier proposed symlink() name was never shipped.