Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ When making changes, preserve the product shape unless the owner explicitly want

### Do
- keep the core experience static-host friendly
- add a preceding `/** ... */` block for public exported functions/components in `src/lib/**` and `src/components/**`
- keep the fragment transport client-side
- prefer small, explicit protocol changes
- update docs when changing user-visible behavior or protocol semantics
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ npm run preview

Set `NEXT_PUBLIC_BASE_PATH` before `npm run build` when you want to preview a subpath deployment locally.

## Contributing

- Public exported functions/components in `src/lib/**` and `src/components/**` must have a preceding `/** ... */` JSDoc block.
- Internal helpers are intentionally excluded from this rule to keep documentation noise low.
- Run `npm run check:public-export-docs` (included in `npm run lint` and `npm run check`) before opening a PR.

## Verification

```bash
Expand Down
34 changes: 12 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "next build",
"preview": "node scripts/serve-export.mjs",
"start": "npm run preview",
"lint": "eslint .",
"lint": "eslint . && npm run check:public-export-docs",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
Expand All @@ -17,7 +17,8 @@
"test:browsers": "playwright install chromium",
"codec:poc": "node scripts/codec-poc.mjs",
"typecheck": "node scripts/ensure-next-types.mjs && tsc --noEmit",
"check": "npm run lint && npm run test && npm run typecheck && npm run build"
"check": "npm run lint && npm run test && npm run typecheck && npm run build",
"check:public-export-docs": "node scripts/check-public-export-docs.mjs"
},
"dependencies": {
"@codemirror/commands": "^6.10.1",
Expand Down
94 changes: 94 additions & 0 deletions scripts/check-public-export-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";

const ROOTS = ["src/lib", "src/components"];
const VALID_EXTENSIONS = new Set([".ts", ".tsx"]);
const EXPORT_FUNCTION_RE = /^export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/;

async function* walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield* walk(fullPath);
continue;
}

if (!VALID_EXTENSIONS.has(path.extname(entry.name))) {
continue;
}

if (/\.(test|spec)\.[mc]?[tj]sx?$/.test(entry.name)) {
continue;
}

yield fullPath;
}
}

function hasPrecedingJsDoc(lines, index) {
let lineIndex = index - 1;

while (lineIndex >= 0 && lines[lineIndex].trim() === "") {
lineIndex -= 1;
}

if (lineIndex < 0 || !lines[lineIndex].trim().endsWith("*/")) {
return false;
}

for (let cursor = lineIndex; cursor >= 0; cursor -= 1) {
const value = lines[cursor].trim();
if (value.startsWith("/**")) {
return true;
}

if (!value.startsWith("*") && !value.startsWith("/*") && !value.startsWith("//")) {
return false;
}
}

return false;
}

function findMissingDocs(filePath, source) {
const lines = source.split(/\r?\n/);
const missing = [];

for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(EXPORT_FUNCTION_RE);
if (!match) {
continue;
}

if (!hasPrecedingJsDoc(lines, index)) {
missing.push({
filePath,
line: index + 1,
name: match[1],
});
}
}

return missing;
}

const missingDocs = [];

for (const root of ROOTS) {
for await (const filePath of walk(root)) {
const source = await readFile(filePath, "utf8");
missingDocs.push(...findMissingDocs(filePath, source));
}
}

if (missingDocs.length > 0) {
console.error("Missing JSDoc on public exported functions/components:\n");
for (const item of missingDocs) {
console.error(`- ${item.filePath}:${item.line} (${item.name})`);
}
console.error("\nAdd a preceding /** ... */ block for each public export listed above.");
process.exit(1);
}

console.log("Public export documentation check passed.");
5 changes: 5 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export const metadata: Metadata = {
description: "A static, zero-retention artifact viewer shell for fragment-based markdown, code, diff, CSV, and JSON payloads.",
};

/**
* Root layout for the static shell that installs fonts and global theme context for all viewer states.
* Accepts `children` from Next.js app routing and wraps them with the shared `ThemeProvider`.
* Sets hydration-safe HTML/body structure used by lazy renderer mounts and fallback screens.
*/
export default function RootLayout({
children,
}: Readonly<{
Expand Down
5 changes: 5 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ViewerShell } from "@/components/viewer-shell";

/**
* Entry page for the exported shell, routing users into the fragment-aware viewer workflow.
* It renders `ViewerShell` with no props because artifact state is derived from URL fragment parsing.
* Keeps page composition minimal so renderer loading and fallback logic stay inside the shell.
*/
export default function HomePage() {
return <ViewerShell />;
}
5 changes: 5 additions & 0 deletions src/components/home/link-creator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ function getDraftSignature(draft: LinkCreatorDraft) {
return JSON.stringify(draft);
}

/**
* Builds shareable fragment links from pasted artifact content in the home empty state flow.
* Accepts `onPreviewHash` so the parent shell can preview the generated fragment before navigation.
* Generates links client-side with validation, and exposes inline copy/error/stale-result states.
*/
export function LinkCreator({ onPreviewHash }: LinkCreatorProps) {
const [draft, setDraft] = useState<LinkCreatorDraft>(defaultLinkCreatorDraft);
const [generatedLink, setGeneratedLink] = useState<GeneratedArtifactLink | null>(null);
Expand Down
Loading
Loading