From 67e600e87c14a0692fc0eb81c223463ae1332989 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 Oct 2026 08:10:02 -0500 Subject: [PATCH 1/3] feat(pacer): Add missing framework options --- src/libraries/libraries.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libraries/libraries.ts b/src/libraries/libraries.ts index c8e5b5e1f..1c2e26632 100644 --- a/src/libraries/libraries.ts +++ b/src/libraries/libraries.ts @@ -541,7 +541,19 @@ export const pacer: LibrarySlim = { "Optimize your application's performance with TanStack Pacer's core primitives: Debouncing, Throttling, Rate Limiting, Queuing, and Batching.", badge: 'beta', repo: 'tanstack/pacer', - frameworks: ['react', 'preact', 'solid', 'angular', 'vanilla'], + frameworks: [ + 'react', + 'preact', + 'solid', + 'angular', + 'vue', + 'svelte', + 'lit', + 'alpine', + 'ember', + 'octane', + 'vanilla', + ], corePackageName: '@tanstack/pacer', npmPackageNames: ['@tanstack/pacer', '@tanstack/pacer-lite'], legacyPackages: ['@tanstack/pacer-lite'], From 0fc668d3c1478b9d882e62a08decf4354044400b Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 Oct 2026 08:27:19 -0500 Subject: [PATCH 2/3] fix(docs): Read local directory manifests through the dev bridge --- src/utils/documents.server.ts | 109 +++++----------------------- src/utils/local-repo-tree.server.ts | 103 ++++++++++++++++++++++++++ tests/local-docs-tree.test.ts | 43 +++++++++++ vite.config.ts | 28 +++++-- 4 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 src/utils/local-repo-tree.server.ts create mode 100644 tests/local-docs-tree.test.ts diff --git a/src/utils/documents.server.ts b/src/utils/documents.server.ts index d409da703..2f5eb1b8b 100644 --- a/src/utils/documents.server.ts +++ b/src/utils/documents.server.ts @@ -21,6 +21,7 @@ import { localDocsDevPath, localDocsDevTokenHeader, } from './local-repo-path.server' +import { readLocalRepoTree } from './local-repo-tree.server' import { normalizeRedirectFrom } from './redirects' import { isValidRepoPath } from './repo-path' import { multiSortBy, removeLeadingSlash } from './utils' @@ -300,7 +301,11 @@ async function fetchFs(repo: string, filepath: string) { return null } -async function fetchFsFromDevServer(repo: string, filepath: string) { +async function fetchFsFromDevServer( + repo: string, + filepath: string, + kind: 'file' | 'tree' = 'file', +) { let request: Request try { @@ -316,6 +321,7 @@ async function fetchFsFromDevServer(repo: string, filepath: string) { const url = new URL(localDocsDevPath, request.url) url.searchParams.set('repo', repo) url.searchParams.set('path', filepath) + url.searchParams.set('kind', kind) const response = await fetch(url, { headers: { @@ -1443,6 +1449,16 @@ async function fetchApiContentsFs( ): Promise | null> { const [_, repo] = repoPair.split('/') + if (isIsolateRuntime()) { + const text = await fetchFsFromDevServer(repo, startingPath, 'tree') + if (text === null) return null + const tree: unknown = JSON.parse(text) + if (!isGitHubFileNodeArray(tree)) { + throw new Error('Invalid local docs directory tree') + } + return tree + } + const base = getLocalRepoBaseDirs(repo).find((candidate) => fs.existsSync(path.join(candidate, removeLeadingSlash(startingPath))), ) @@ -1451,96 +1467,7 @@ async function fetchApiContentsFs( return null } - const resolvedBase = base - const fsStartPath = path.join(resolvedBase, removeLeadingSlash(startingPath)) - - const dirsAndFilesToIgnore = [ - 'node_modules', - '.git', - 'dist', - 'test-results', - '.output', - '.netlify', - '.vercel', - '.DS_Store', - '.nitro', - '.tanstack-start/build', - ] - - async function getContentsForPath( - filePath: string, - ): Promise> { - try { - const list = await fsp.readdir(filePath, { withFileTypes: true }) - return list - .filter((item) => !dirsAndFilesToIgnore.includes(item.name)) - .map((item) => { - return { - name: item.name, - path: path.join(filePath, item.name), - type: item.isDirectory() ? 'dir' : 'file', - _links: { - self: path.join(filePath, item.name), - }, - } - }) - } catch (error) { - if ( - error instanceof Error && - 'code' in error && - error.code === 'ENOENT' - ) { - return [] - } - throw error - } - } - - const data = await getContentsForPath(fsStartPath) - - if (data.length === 0) { - return null - } - - async function buildFileTree( - nodes: Array | undefined, - depth: number, - parentPath: string, - ) { - const result: Array = [] - - const sortedNodes = sortApiContents(nodes ?? []) - - for (const node of sortedNodes) { - const file: GitHubFileNode = { - ...node, - depth, - parentPath, - } - - if (file.type === 'dir' && depth <= API_CONTENTS_MAX_DEPTH) { - const directoryFiles = await getContentsForPath(file._links.self) - file.children = await buildFileTree( - directoryFiles, - depth + 1, - `${parentPath}${file.path}/`, - ) - } - - // This replacement is only being done to more accurately mock the GitHub API response - file.path = removeLeadingSlash(file.path.replace(resolvedBase, '')) - file._links.self = removeLeadingSlash( - file._links.self.replace(resolvedBase, ''), - ) - - result.push(file) - } - - return result - } - - const fileTree = await buildFileTree(data, 0, '') - return fileTree + return readLocalRepoTree(base, startingPath) } async function fetchApiContentsRemote( diff --git a/src/utils/local-repo-tree.server.ts b/src/utils/local-repo-tree.server.ts new file mode 100644 index 000000000..971bea370 --- /dev/null +++ b/src/utils/local-repo-tree.server.ts @@ -0,0 +1,103 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import type { GitHubFile, GitHubFileNode } from './documents.server' +import { multiSortBy, removeLeadingSlash } from './utils' + +export async function readLocalRepoTree( + resolvedBase: string, + startingPath: string, +): Promise | null> { + const fsStartPath = path.join(resolvedBase, removeLeadingSlash(startingPath)) + + const dirsAndFilesToIgnore = [ + 'node_modules', + '.git', + 'dist', + 'test-results', + '.output', + '.netlify', + '.vercel', + '.DS_Store', + '.nitro', + '.tanstack-start/build', + ] + + async function getContentsForPath( + filePath: string, + ): Promise> { + try { + const list = await fs.readdir(filePath, { withFileTypes: true }) + return list + .filter((item) => !dirsAndFilesToIgnore.includes(item.name)) + .map((item) => { + return { + name: item.name, + path: path.join(filePath, item.name), + type: item.isDirectory() ? 'dir' : 'file', + _links: { + self: path.join(filePath, item.name), + }, + } + }) + } catch (error) { + if ( + error instanceof Error && + 'code' in error && + error.code === 'ENOENT' + ) { + return [] + } + throw error + } + } + + const data = await getContentsForPath(fsStartPath) + + if (data.length === 0) { + return null + } + + async function buildFileTree( + nodes: Array | undefined, + depth: number, + parentPath: string, + ) { + const result: Array = [] + + const sortedNodes = multiSortBy(nodes ?? [], [ + (node) => (node.type === 'dir' ? -1 : 1), + (node) => (node.name.startsWith('.') ? -1 : 1), + (node) => node.name, + ]) + + for (const node of sortedNodes) { + const file: GitHubFileNode = { + ...node, + depth, + parentPath, + } + + if (file.type === 'dir' && depth <= 3) { + const directoryFiles = await getContentsForPath(file._links.self) + file.children = await buildFileTree( + directoryFiles, + depth + 1, + `${parentPath}${file.path}/`, + ) + } + + // This replacement is only being done to more accurately mock the GitHub API response + file.path = removeLeadingSlash(file.path.replace(resolvedBase, '')) + file._links.self = removeLeadingSlash( + file._links.self.replace(resolvedBase, ''), + ) + + result.push(file) + } + + return result + } + + const fileTree = await buildFileTree(data, 0, '') + return fileTree +} diff --git a/tests/local-docs-tree.test.ts b/tests/local-docs-tree.test.ts new file mode 100644 index 000000000..269c47396 --- /dev/null +++ b/tests/local-docs-tree.test.ts @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict' +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { test } from 'node:test' +import { readLocalRepoTree } from '../src/utils/local-repo-tree.server' +import { resolveDocsPathRedirect } from '../src/utils/docs-redirects' +import type { GitHubFileNode } from '../src/utils/documents.server' + +test('local framework guides remain renderable before they exist upstream', async () => { + const root = await mkdtemp(path.join(os.tmpdir(), 'local-docs-')) + try { + const guide = 'framework/svelte/guides/debouncing' + for (const doc of [guide, 'guides/debouncing']) { + const file = path.join(root, 'docs', `${doc}.md`) + await mkdir(path.dirname(file), { recursive: true }) + await writeFile(file, '# Debouncing') + } + const tree = await readLocalRepoTree(root, 'docs') + assert.ok(tree) + function paths(nodes: Array): Array { + return nodes.flatMap((node) => [ + ...(node.type === 'file' + ? [node.path.replace(/^docs\//, '').replace(/\.md$/, '')] + : []), + ...paths(node.children ?? []), + ]) + } + const manifest = { paths: paths(tree), redirects: {} } + assert.ok(manifest.paths.includes(guide)) + assert.deepEqual( + resolveDocsPathRedirect({ + defaultDocs: 'overview', + docsPath: guide, + frameworks: ['svelte'], + manifest, + }), + { type: 'render', docsPath: guide }, + ) + } finally { + await rm(root, { recursive: true, force: true }) + } +}) diff --git a/vite.config.ts b/vite.config.ts index eda40a8d1..3eefb5fea 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,6 +19,7 @@ import { localDocsDevPath, localDocsDevTokenHeader, } from './src/utils/local-repo-path.server' +import { readLocalRepoTree } from './src/utils/local-repo-tree.server' import { startLocalMcpEgress } from './scripts/local-mcp-egress' import { localBuilderAi } from './scripts/local-builder-ai-vite' @@ -55,9 +56,11 @@ function localDocsDevFiles(): PluginOption { const repo = url.searchParams.get('repo') const filepath = url.searchParams.get('path') + const kind = url.searchParams.get('kind') ?? 'file' if ( !repo || + !['file', 'tree'].includes(kind) || !/^[a-zA-Z0-9._-]+$/.test(repo) || !filepath || !isContainedRepoPath(filepath) @@ -80,7 +83,7 @@ function localDocsDevFiles(): PluginOption { ]), ) - const localFilePath = repoDirs + const localTarget = repoDirs .map((repoDir) => ({ filepath: path.resolve(repoDir, filepath), repoDir, @@ -89,20 +92,33 @@ function localDocsDevFiles(): PluginOption { (candidate) => isPathInside(candidate.repoDir, candidate.filepath) && fs.existsSync(candidate.filepath) && - fs.statSync(candidate.filepath).isFile(), - )?.filepath + (kind === 'tree' + ? fs.statSync(candidate.filepath).isDirectory() + : fs.statSync(candidate.filepath).isFile()), + ) - if (!localFilePath) { + if (!localTarget) { response.statusCode = 404 response.end() return } try { - const content = await fs.promises.readFile(localFilePath) + const content = + kind === 'tree' + ? JSON.stringify( + (await readLocalRepoTree(localTarget.repoDir, filepath)) ?? + [], + ) + : await fs.promises.readFile(localTarget.filepath) response.statusCode = 200 response.setHeader('Cache-Control', 'no-store') - response.setHeader('Content-Type', 'text/plain; charset=utf-8') + response.setHeader( + 'Content-Type', + kind === 'tree' + ? 'application/json; charset=utf-8' + : 'text/plain; charset=utf-8', + ) response.end(content) } catch (error) { next(error) From 8be246b40c8c455a2798c4aa69d54dedb20e9c8f Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 Oct 2026 09:40:10 -0500 Subject: [PATCH 3/3] chore(docs): Keep Pacer website PR scoped to frameworks --- src/utils/documents.server.ts | 109 +++++++++++++++++++++++----- src/utils/local-repo-tree.server.ts | 103 -------------------------- tests/local-docs-tree.test.ts | 43 ----------- vite.config.ts | 28 ++----- 4 files changed, 97 insertions(+), 186 deletions(-) delete mode 100644 src/utils/local-repo-tree.server.ts delete mode 100644 tests/local-docs-tree.test.ts diff --git a/src/utils/documents.server.ts b/src/utils/documents.server.ts index 2f5eb1b8b..d409da703 100644 --- a/src/utils/documents.server.ts +++ b/src/utils/documents.server.ts @@ -21,7 +21,6 @@ import { localDocsDevPath, localDocsDevTokenHeader, } from './local-repo-path.server' -import { readLocalRepoTree } from './local-repo-tree.server' import { normalizeRedirectFrom } from './redirects' import { isValidRepoPath } from './repo-path' import { multiSortBy, removeLeadingSlash } from './utils' @@ -301,11 +300,7 @@ async function fetchFs(repo: string, filepath: string) { return null } -async function fetchFsFromDevServer( - repo: string, - filepath: string, - kind: 'file' | 'tree' = 'file', -) { +async function fetchFsFromDevServer(repo: string, filepath: string) { let request: Request try { @@ -321,7 +316,6 @@ async function fetchFsFromDevServer( const url = new URL(localDocsDevPath, request.url) url.searchParams.set('repo', repo) url.searchParams.set('path', filepath) - url.searchParams.set('kind', kind) const response = await fetch(url, { headers: { @@ -1449,16 +1443,6 @@ async function fetchApiContentsFs( ): Promise | null> { const [_, repo] = repoPair.split('/') - if (isIsolateRuntime()) { - const text = await fetchFsFromDevServer(repo, startingPath, 'tree') - if (text === null) return null - const tree: unknown = JSON.parse(text) - if (!isGitHubFileNodeArray(tree)) { - throw new Error('Invalid local docs directory tree') - } - return tree - } - const base = getLocalRepoBaseDirs(repo).find((candidate) => fs.existsSync(path.join(candidate, removeLeadingSlash(startingPath))), ) @@ -1467,7 +1451,96 @@ async function fetchApiContentsFs( return null } - return readLocalRepoTree(base, startingPath) + const resolvedBase = base + const fsStartPath = path.join(resolvedBase, removeLeadingSlash(startingPath)) + + const dirsAndFilesToIgnore = [ + 'node_modules', + '.git', + 'dist', + 'test-results', + '.output', + '.netlify', + '.vercel', + '.DS_Store', + '.nitro', + '.tanstack-start/build', + ] + + async function getContentsForPath( + filePath: string, + ): Promise> { + try { + const list = await fsp.readdir(filePath, { withFileTypes: true }) + return list + .filter((item) => !dirsAndFilesToIgnore.includes(item.name)) + .map((item) => { + return { + name: item.name, + path: path.join(filePath, item.name), + type: item.isDirectory() ? 'dir' : 'file', + _links: { + self: path.join(filePath, item.name), + }, + } + }) + } catch (error) { + if ( + error instanceof Error && + 'code' in error && + error.code === 'ENOENT' + ) { + return [] + } + throw error + } + } + + const data = await getContentsForPath(fsStartPath) + + if (data.length === 0) { + return null + } + + async function buildFileTree( + nodes: Array | undefined, + depth: number, + parentPath: string, + ) { + const result: Array = [] + + const sortedNodes = sortApiContents(nodes ?? []) + + for (const node of sortedNodes) { + const file: GitHubFileNode = { + ...node, + depth, + parentPath, + } + + if (file.type === 'dir' && depth <= API_CONTENTS_MAX_DEPTH) { + const directoryFiles = await getContentsForPath(file._links.self) + file.children = await buildFileTree( + directoryFiles, + depth + 1, + `${parentPath}${file.path}/`, + ) + } + + // This replacement is only being done to more accurately mock the GitHub API response + file.path = removeLeadingSlash(file.path.replace(resolvedBase, '')) + file._links.self = removeLeadingSlash( + file._links.self.replace(resolvedBase, ''), + ) + + result.push(file) + } + + return result + } + + const fileTree = await buildFileTree(data, 0, '') + return fileTree } async function fetchApiContentsRemote( diff --git a/src/utils/local-repo-tree.server.ts b/src/utils/local-repo-tree.server.ts deleted file mode 100644 index 971bea370..000000000 --- a/src/utils/local-repo-tree.server.ts +++ /dev/null @@ -1,103 +0,0 @@ -import fs from 'node:fs/promises' -import path from 'node:path' -import type { GitHubFile, GitHubFileNode } from './documents.server' -import { multiSortBy, removeLeadingSlash } from './utils' - -export async function readLocalRepoTree( - resolvedBase: string, - startingPath: string, -): Promise | null> { - const fsStartPath = path.join(resolvedBase, removeLeadingSlash(startingPath)) - - const dirsAndFilesToIgnore = [ - 'node_modules', - '.git', - 'dist', - 'test-results', - '.output', - '.netlify', - '.vercel', - '.DS_Store', - '.nitro', - '.tanstack-start/build', - ] - - async function getContentsForPath( - filePath: string, - ): Promise> { - try { - const list = await fs.readdir(filePath, { withFileTypes: true }) - return list - .filter((item) => !dirsAndFilesToIgnore.includes(item.name)) - .map((item) => { - return { - name: item.name, - path: path.join(filePath, item.name), - type: item.isDirectory() ? 'dir' : 'file', - _links: { - self: path.join(filePath, item.name), - }, - } - }) - } catch (error) { - if ( - error instanceof Error && - 'code' in error && - error.code === 'ENOENT' - ) { - return [] - } - throw error - } - } - - const data = await getContentsForPath(fsStartPath) - - if (data.length === 0) { - return null - } - - async function buildFileTree( - nodes: Array | undefined, - depth: number, - parentPath: string, - ) { - const result: Array = [] - - const sortedNodes = multiSortBy(nodes ?? [], [ - (node) => (node.type === 'dir' ? -1 : 1), - (node) => (node.name.startsWith('.') ? -1 : 1), - (node) => node.name, - ]) - - for (const node of sortedNodes) { - const file: GitHubFileNode = { - ...node, - depth, - parentPath, - } - - if (file.type === 'dir' && depth <= 3) { - const directoryFiles = await getContentsForPath(file._links.self) - file.children = await buildFileTree( - directoryFiles, - depth + 1, - `${parentPath}${file.path}/`, - ) - } - - // This replacement is only being done to more accurately mock the GitHub API response - file.path = removeLeadingSlash(file.path.replace(resolvedBase, '')) - file._links.self = removeLeadingSlash( - file._links.self.replace(resolvedBase, ''), - ) - - result.push(file) - } - - return result - } - - const fileTree = await buildFileTree(data, 0, '') - return fileTree -} diff --git a/tests/local-docs-tree.test.ts b/tests/local-docs-tree.test.ts deleted file mode 100644 index 269c47396..000000000 --- a/tests/local-docs-tree.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import assert from 'node:assert/strict' -import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises' -import os from 'node:os' -import path from 'node:path' -import { test } from 'node:test' -import { readLocalRepoTree } from '../src/utils/local-repo-tree.server' -import { resolveDocsPathRedirect } from '../src/utils/docs-redirects' -import type { GitHubFileNode } from '../src/utils/documents.server' - -test('local framework guides remain renderable before they exist upstream', async () => { - const root = await mkdtemp(path.join(os.tmpdir(), 'local-docs-')) - try { - const guide = 'framework/svelte/guides/debouncing' - for (const doc of [guide, 'guides/debouncing']) { - const file = path.join(root, 'docs', `${doc}.md`) - await mkdir(path.dirname(file), { recursive: true }) - await writeFile(file, '# Debouncing') - } - const tree = await readLocalRepoTree(root, 'docs') - assert.ok(tree) - function paths(nodes: Array): Array { - return nodes.flatMap((node) => [ - ...(node.type === 'file' - ? [node.path.replace(/^docs\//, '').replace(/\.md$/, '')] - : []), - ...paths(node.children ?? []), - ]) - } - const manifest = { paths: paths(tree), redirects: {} } - assert.ok(manifest.paths.includes(guide)) - assert.deepEqual( - resolveDocsPathRedirect({ - defaultDocs: 'overview', - docsPath: guide, - frameworks: ['svelte'], - manifest, - }), - { type: 'render', docsPath: guide }, - ) - } finally { - await rm(root, { recursive: true, force: true }) - } -}) diff --git a/vite.config.ts b/vite.config.ts index 3eefb5fea..eda40a8d1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,7 +19,6 @@ import { localDocsDevPath, localDocsDevTokenHeader, } from './src/utils/local-repo-path.server' -import { readLocalRepoTree } from './src/utils/local-repo-tree.server' import { startLocalMcpEgress } from './scripts/local-mcp-egress' import { localBuilderAi } from './scripts/local-builder-ai-vite' @@ -56,11 +55,9 @@ function localDocsDevFiles(): PluginOption { const repo = url.searchParams.get('repo') const filepath = url.searchParams.get('path') - const kind = url.searchParams.get('kind') ?? 'file' if ( !repo || - !['file', 'tree'].includes(kind) || !/^[a-zA-Z0-9._-]+$/.test(repo) || !filepath || !isContainedRepoPath(filepath) @@ -83,7 +80,7 @@ function localDocsDevFiles(): PluginOption { ]), ) - const localTarget = repoDirs + const localFilePath = repoDirs .map((repoDir) => ({ filepath: path.resolve(repoDir, filepath), repoDir, @@ -92,33 +89,20 @@ function localDocsDevFiles(): PluginOption { (candidate) => isPathInside(candidate.repoDir, candidate.filepath) && fs.existsSync(candidate.filepath) && - (kind === 'tree' - ? fs.statSync(candidate.filepath).isDirectory() - : fs.statSync(candidate.filepath).isFile()), - ) + fs.statSync(candidate.filepath).isFile(), + )?.filepath - if (!localTarget) { + if (!localFilePath) { response.statusCode = 404 response.end() return } try { - const content = - kind === 'tree' - ? JSON.stringify( - (await readLocalRepoTree(localTarget.repoDir, filepath)) ?? - [], - ) - : await fs.promises.readFile(localTarget.filepath) + const content = await fs.promises.readFile(localFilePath) response.statusCode = 200 response.setHeader('Cache-Control', 'no-store') - response.setHeader( - 'Content-Type', - kind === 'tree' - ? 'application/json; charset=utf-8' - : 'text/plain; charset=utf-8', - ) + response.setHeader('Content-Type', 'text/plain; charset=utf-8') response.end(content) } catch (error) { next(error)