Browse packages

Changelog · v0.39.1

fs

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.

js
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 needed

If 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#

js
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 name

On Windows hosts only, native node:path handles drive and UNC syntax:

js
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 drives

Native 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:

js
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:

js
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.