Skip to content
51 changes: 50 additions & 1 deletion docs/api-reference/veryfront/fs.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
---
title: "veryfront/fs"
description: "Public filesystem, path, and cwd utilities."
description: "Runtime-native filesystem, path, and cwd utilities."
order: 11
---

## Runtime boundary

`veryfront/fs` uses the native process filesystem selected for Deno, Node, or Bun.
It does not delegate to the `runtime.get().fs` adapter. Custom adapters
configured with `runtime.set()` affect adapter-consuming APIs, not these
compatibility functions.

`veryfront/fs` does not add a project-root sandbox, block `.env` or other
secret-file names, or validate paths from untrusted input. Relative paths
resolve from `cwd()`. Absolute paths and `..` segments can reach any location
that the runtime permits.

Runtime permissions remain the outer boundary. Hosted project secrets are
supplied through request-owned environment data rather than `.env` files.
Isolated Pages route `ctx.fs` is a separate, read-only, project-confined
capability. Those protections do not change the contract of `veryfront/fs`.

Canonicalize a trusted root and candidate with `realPath`, then use
`validateLexicalPath` from `veryfront/security` before reading a
user-influenced path. Canonicalization follows existing symlinks before the
containment check.

Path admission is not an operating-system sandbox.
The trusted root must not be writable by untrusted or project code while a
validated path is in use. Otherwise, concurrent filesystem changes can
create a time-of-check/time-of-use race between validation and reading.

## Import

```ts
Expand Down Expand Up @@ -39,6 +66,28 @@ import { cwd, resolve } from "veryfront/fs";
const configPath = resolve(cwd(), "veryfront.config.ts");
```

### Confine an untrusted path

```ts
import { cwd, readTextFile, realPath, resolve } from "veryfront/fs";
import { validateLexicalPath } from "veryfront/security";

const publicFilesDir = await realPath(resolve(cwd(), "public-data"));

export async function readPublicFile(requestedPath: string): Promise<string> {
const candidate = resolve(publicFilesDir, requestedPath);
const canonicalPath = await realPath(candidate);
const admitted = validateLexicalPath(canonicalPath, {
baseDir: publicFilesDir,
allowAbsolute: true,
});
if (!admitted.valid || !admitted.canonicalPath) {
throw new Error("Invalid path");
}
return await readTextFile(admitted.canonicalPath);
}
```

## Exports

### Functions
Expand Down
148 changes: 148 additions & 0 deletions scripts/docs/barrel-jsdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
export interface BarrelJSDoc {
description: string;
moduleName: string;
remarks: string;
examples: Array<{ title: string; code: string }>;
}

const EMPTY_BARREL_JSDOC: BarrelJSDoc = {
description: "",
moduleName: "",
remarks: "",
examples: [],
};

export function parseBarrelJSDoc(content: string): BarrelJSDoc {
const trimmed = content.trimStart();
if (!trimmed.startsWith("/**")) {
return EMPTY_BARREL_JSDOC;
}

const endIdx = trimmed.indexOf("*/");
if (endIdx === -1) {
return EMPTY_BARREL_JSDOC;
}

const block = trimmed.slice(3, endIdx);
const lines = block.split("\n").map((line) => line.replace(/^\s*\*\s?/, ""));

let moduleName = "";
const descLines: string[] = [];
const remarkLines: string[] = [];
const examples: Array<{ title: string; code: string }> = [];
let inExample = false;
let inRemarks = false;
let exampleTitle = "";
let exampleLines: string[] = [];
let codeFence: { marker: "`" | "~"; length: number } | null = null;

const finishExample = (): void => {
if (exampleLines.length > 0) {
examples.push({ title: exampleTitle, code: exampleLines.join("\n") });
}
exampleTitle = "";
exampleLines = [];
codeFence = null;
};

const updateCodeFence = (line: string): void => {
const fenceMatch = line.match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
if (!fenceMatch) return;
const fence = fenceMatch[2];
const marker = fence[0] as "`" | "~";
if (codeFence === null) {
codeFence = { marker, length: fence.length };
} else if (
marker === codeFence.marker &&
fence.length >= codeFence.length &&
fenceMatch[3].trim() === ""
) {
codeFence = null;
}
};

for (const line of lines) {
if (inExample || inRemarks) updateCodeFence(line);

if (codeFence === null && line.startsWith("@module")) {
moduleName = line.replace("@module", "").trim();
inRemarks = false;
continue;
}

if (codeFence === null && line.startsWith("@remarks")) {
finishExample();
inExample = false;
inRemarks = true;
const inlineRemarks = line.replace("@remarks", "").trim();
if (inlineRemarks) remarkLines.push(inlineRemarks);
continue;
}

if (codeFence === null && line.startsWith("@example")) {
finishExample();
exampleTitle = line.replace("@example", "").trim();
exampleLines = [];
inExample = true;
inRemarks = false;
codeFence = null;
continue;
}

if (codeFence === null && line.startsWith("@")) {
finishExample();
inExample = false;
inRemarks = false;
continue;
}

if (inExample) {
exampleLines.push(line);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else if (inRemarks) {
remarkLines.push(line);
Comment thread
kojiwakayama marked this conversation as resolved.
} else if (!moduleName || descLines.length > 0 || line.trim()) {
if (!line.startsWith("@")) {
descLines.push(line);
}
}
}

finishExample();

const description = normalizePublicDocText(
descLines.join(" ").replace(/\s+/g, " ").trim(),
);
const remarks = remarkLines.join("\n").trim();
return { description, moduleName, remarks, examples };
}

export function normalizePublicDocText(text: string): string {
const withoutInlineJsDocLinks = text.replace(
/\{@(?:link|linkcode|linkplain)\s+([^}]+)\}/g,
(_match, rawTarget: string) => {
const target = rawTarget.trim();
const pipeIndex = target.indexOf("|");
const display = pipeIndex >= 0
? target.slice(pipeIndex + 1).trim()
: target.match(/^\S+\s+(.+)$/)?.[1]?.trim() || target;
const longestBacktickRun = Math.max(
0,
...(display.match(/`+/g) ?? []).map((run) => run.length),
);
const delimiter = "`".repeat(longestBacktickRun + 1);
const needsPadding = display.startsWith("`") || display.endsWith("`");
return `${delimiter}${
needsPadding ? ` ${display} ` : display
}${delimiter}`;
},
);

return withoutInlineJsDocLinks
.replace(/(`+)[\s\S]*?\1|[<>]/g, (token) => {
if (token.startsWith("`")) return token;
return token === "<" ? "&lt;" : "&gt;";
})
.replace(/[\u2013\u2014]/g, "-")
.replace(/\s+/g, " ")
.trim();
}
148 changes: 147 additions & 1 deletion scripts/docs/generate-api-reference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "#veryfront/testing/assert.ts";
import { describe, it } from "#std/testing/bdd";
import { compile } from "npm:@mdx-js/mdx@3.1.1";
import { parseBarrelJSDoc } from "./barrel-jsdoc.ts";

const CHECK_TEMP_PREFIX = "veryfront-api-reference-check-";

Expand Down Expand Up @@ -82,6 +83,98 @@ async function assertGeneratedReferenceIsFormatted(
}

describe("generate-api-reference", () => {
it("preserves JSDoc tags inside fenced examples", () => {
const parsed = parseBarrelJSDoc(`/**
* Example module.
*
* @module example
*
* @example Decorated class
* \`\`\`ts
* @sealed
* class Example {}
* \`\`\`
*
* @remarks
* The example remains intact.
*/`);

assertEquals(parsed.examples, [{
title: "Decorated class",
code: "```ts\n@sealed\nclass Example {}\n```\n",
}]);
assertEquals(parsed.remarks, "The example remains intact.");
});

it("preserves JSDoc tags inside tilde-fenced examples", () => {
const parsed = parseBarrelJSDoc(`/**
* @module example
* @example Decorated class
* ~~~ts
* @sealed
* class Example {}
* ~~~
* @remarks
* The example remains intact.
*/`);

assertEquals(parsed.examples, [{
title: "Decorated class",
code: "~~~ts\n@sealed\nclass Example {}\n~~~",
}]);
assertEquals(parsed.remarks, "The example remains intact.");
});

it("preserves JSDoc tags inside fenced remarks", () => {
const parsed = parseBarrelJSDoc(`/**
* @module example
* @remarks
* The literal tag stays in this example:
* ~~~md
* @example This is Markdown content
* ~~~
* @example Actual example
* ~~~ts
* export const value = true;
* ~~~
* @returns Nothing.
*/`);

assertEquals(
parsed.remarks,
"The literal tag stays in this example:\n~~~md\n@example This is Markdown content\n~~~",
);
assertEquals(parsed.examples, [{
title: "Actual example",
code: "~~~ts\nexport const value = true;\n~~~",
}]);
});

it("does not close a fence on a delimiter indented by four spaces", () => {
const parsed = parseBarrelJSDoc(`/**
* @module example
* @example Nested Markdown
* ~~~md
* ~~~
* @literal
* ~~~
* @returns Nothing.
*/`);

assertEquals(parsed.examples, [{
title: "Nested Markdown",
code: "~~~md\n ~~~\n@literal\n~~~",
}]);
});

it("uses a safe Markdown code span for linked labels containing backticks", () => {
const parsed = parseBarrelJSDoc(
"/**\n * Uses {@link Example|C:\\path<T>`name}.\n */",
);

assertEquals(parsed.description, "Uses ``C:\\path<T>`name``.");
});

it("removes check output when generation fails", async () => {
const sandboxRoot = await Deno.makeTempDir();
const emptyRoot = `${sandboxRoot}/cwd`;
Expand Down Expand Up @@ -160,6 +253,9 @@ describe("generate-api-reference", () => {
const chatReference = await Deno.readTextFile(
`${outputDir}/veryfront/chat.md`,
);
const fsReference = await Deno.readTextFile(
`${outputDir}/veryfront/fs.md`,
);
const agentReference = await Deno.readTextFile(
`${outputDir}/veryfront/agent.md`,
);
Expand All @@ -182,13 +278,63 @@ describe("generate-api-reference", () => {
false,
"generated client reference must not expose internal import specifiers",
);
assertStringIncludes(fsReference, "## Runtime boundary");
assertStringIncludes(
fsReference,
"uses the native process filesystem selected for Deno, Node, or Bun",
);
assertStringIncludes(
fsReference,
"does not delegate to the `runtime.get().fs` adapter",
);
assertMatch(
fsReference,
/does not add a\s+project-root sandbox, block `\.env` or other\s+secret-file names/,
);
assertMatch(
fsReference,
/Isolated Pages route `ctx\.fs` is a separate, read-only, project-confined\s+capability/,
);
assertStringIncludes(
fsReference,
'import { cwd, readTextFile, realPath, resolve } from "veryfront/fs";',
);
assertStringIncludes(
fsReference,
'import { validateLexicalPath } from "veryfront/security";',
);
assertEquals(
fsReference.includes('from "veryfront/platform"'),
false,
"the copyable example must use only published package exports",
);
assertStringIncludes(
fsReference,
'const publicFilesDir = await realPath(resolve(cwd(), "public-data"));',
);
assertMatch(
fsReference,
/const candidate = resolve\(publicFilesDir, requestedPath\);[\s\S]*?const canonicalPath = await realPath\(candidate\);[\s\S]*?validateLexicalPath\(canonicalPath, \{[\s\S]*?baseDir: publicFilesDir/,
"the confinement example must validate the physically resolved path",
);
assertStringIncludes(
fsReference,
"return await readTextFile(admitted.canonicalPath);",
"the confinement example must read the admitted canonical path",
);
assertEquals(
fsReference.indexOf("## Runtime boundary") <
fsReference.indexOf("## Import"),
true,
"runtime boundary guidance must precede imports",
);
assertMatch(
uiReference,
/^\|\s*`AppShellProps`\s*\|\s*Props accepted by `AppShell`\.\s*\|/m,
);
assertMatch(
uiReference,
/import type \{\s*DisclosureParts,\s*DisclosureProps,\s*MultipleToggleGroupRootProps,?\s*\} from "veryfront\/ui\/adapter";/m,
/import type \{\s*[A-Za-z][A-Za-z0-9]*(?:,\s*[A-Za-z][A-Za-z0-9]*){0,2},?\s*\} from "veryfront\/ui\/adapter";/m,
"type-only deep exports must use a copyable type import",
);
for (
Expand Down
Loading