snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
276
scripts/create-github-release.mjs
Normal file
276
scripts/create-github-release.mjs
Normal file
@@ -0,0 +1,276 @@
|
||||
// @ts-nocheck
|
||||
import fs from 'fs'
|
||||
import path from 'node:path'
|
||||
import { globSync } from 'node:fs'
|
||||
import { execSync, execFileSync } from 'node:child_process'
|
||||
import { tmpdir } from 'node:os'
|
||||
|
||||
const rootDir = path.join(import.meta.dirname, '..')
|
||||
const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
||||
|
||||
// Resolve GitHub usernames from commit author emails
|
||||
const usernameCache = {}
|
||||
async function resolveUsername(email) {
|
||||
if (!ghToken || !email) return null
|
||||
if (usernameCache[email] !== undefined) return usernameCache[email]
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/search/users?q=${email}`, {
|
||||
headers: { Authorization: `token ${ghToken}` },
|
||||
})
|
||||
const data = await res.json()
|
||||
const login = data?.items?.[0]?.login || null
|
||||
usernameCache[email] = login
|
||||
return login
|
||||
} catch {
|
||||
usernameCache[email] = null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve author from a PR number via GitHub API
|
||||
const prAuthorCache = {}
|
||||
async function resolveAuthorForPR(prNumber) {
|
||||
if (prAuthorCache[prNumber] !== undefined) return prAuthorCache[prNumber]
|
||||
|
||||
if (!ghToken) {
|
||||
prAuthorCache[prNumber] = null
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/TanStack/query/pulls/${prNumber}`,
|
||||
{ headers: { Authorization: `token ${ghToken}` } },
|
||||
)
|
||||
const data = await res.json()
|
||||
const login = data?.user?.login || null
|
||||
prAuthorCache[prNumber] = login
|
||||
return login
|
||||
} catch {
|
||||
prAuthorCache[prNumber] = null
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Get the previous release commit to diff against.
|
||||
// This script runs right after the "ci: Version Packages" PR is merged,
|
||||
// so HEAD is the release commit.
|
||||
const releaseLogs = execSync(
|
||||
'git log --oneline --grep="^ci: Version Packages" --grep="^ci: changeset release" --format=%H',
|
||||
)
|
||||
.toString()
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
|
||||
const currentRelease = releaseLogs[0] || 'HEAD'
|
||||
const previousRelease = releaseLogs[1]
|
||||
|
||||
// Find packages that were actually bumped by comparing versions
|
||||
const packagesDir = path.join(rootDir, 'packages')
|
||||
const allPkgJsonPaths = globSync('*/package.json', { cwd: packagesDir })
|
||||
|
||||
const bumpedPackages = []
|
||||
for (const relPath of allPkgJsonPaths) {
|
||||
const fullPath = path.join(packagesDir, relPath)
|
||||
const currentPkg = JSON.parse(fs.readFileSync(fullPath, 'utf-8'))
|
||||
if (currentPkg.private) continue
|
||||
|
||||
// Get the version from the previous release commit
|
||||
if (previousRelease) {
|
||||
try {
|
||||
const prevContent = execFileSync(
|
||||
'git',
|
||||
['show', `${previousRelease}:packages/${relPath}`],
|
||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },
|
||||
)
|
||||
const prevPkg = JSON.parse(prevContent)
|
||||
if (prevPkg.version !== currentPkg.version) {
|
||||
bumpedPackages.push({
|
||||
name: currentPkg.name,
|
||||
version: currentPkg.version,
|
||||
prevVersion: prevPkg.version,
|
||||
dir: path.dirname(relPath),
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Package didn't exist in previous release — it's new
|
||||
bumpedPackages.push({
|
||||
name: currentPkg.name,
|
||||
version: currentPkg.version,
|
||||
prevVersion: null,
|
||||
dir: path.dirname(relPath),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// No previous release — include all non-private packages
|
||||
bumpedPackages.push({
|
||||
name: currentPkg.name,
|
||||
version: currentPkg.version,
|
||||
prevVersion: null,
|
||||
dir: path.dirname(relPath),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bumpedPackages.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Build changelog from git log between releases (conventional commits)
|
||||
const rangeFrom = previousRelease || `${currentRelease}~1`
|
||||
const rawLog = execSync(
|
||||
`git log ${rangeFrom}..${currentRelease} --pretty=format:"%h %ae %s" --no-merges`,
|
||||
{ encoding: 'utf-8' },
|
||||
).trim()
|
||||
|
||||
const typeOrder = [
|
||||
'breaking',
|
||||
'feat',
|
||||
'fix',
|
||||
'perf',
|
||||
'refactor',
|
||||
'docs',
|
||||
'chore',
|
||||
'test',
|
||||
'ci',
|
||||
]
|
||||
const typeLabels = {
|
||||
breaking: '⚠️ Breaking Changes',
|
||||
feat: 'Features',
|
||||
fix: 'Fix',
|
||||
perf: 'Performance',
|
||||
refactor: 'Refactor',
|
||||
docs: 'Documentation',
|
||||
chore: 'Chore',
|
||||
test: 'Tests',
|
||||
ci: 'CI',
|
||||
}
|
||||
const typeIndex = (t) => {
|
||||
const i = typeOrder.indexOf(t)
|
||||
return i === -1 ? 99 : i
|
||||
}
|
||||
|
||||
const groups = {}
|
||||
const commits = rawLog ? rawLog.split('\n') : []
|
||||
|
||||
for (const line of commits) {
|
||||
const match = line.match(/^(\w+)\s+(\S+)\s+(.*)$/)
|
||||
if (!match) continue
|
||||
const [, hash, email, subject] = match
|
||||
|
||||
// Skip release commits
|
||||
if (
|
||||
subject.startsWith('ci: Version Packages') ||
|
||||
subject.startsWith('ci: changeset release')
|
||||
)
|
||||
continue
|
||||
|
||||
// Parse conventional commit: type(scope)!: message
|
||||
const conventionalMatch = subject.match(/^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.*)$/)
|
||||
const type = conventionalMatch ? conventionalMatch[1] : 'other'
|
||||
const isBreaking = conventionalMatch ? !!conventionalMatch[3] : false
|
||||
const scope = conventionalMatch ? conventionalMatch[2] || '' : ''
|
||||
const message = conventionalMatch ? conventionalMatch[4] : subject
|
||||
|
||||
// Only include user-facing change types
|
||||
if (!['feat', 'fix', 'perf', 'refactor', 'build', 'chore'].includes(type))
|
||||
continue
|
||||
|
||||
// Extract PR number if present
|
||||
const prMatch = message.match(/\(#(\d+)\)/)
|
||||
const prNumber = prMatch ? prMatch[1] : null
|
||||
|
||||
const bucket = isBreaking ? 'breaking' : type
|
||||
if (!groups[bucket]) groups[bucket] = []
|
||||
groups[bucket].push({ hash, email, scope, message, prNumber })
|
||||
}
|
||||
|
||||
// Build markdown grouped by conventional commit type
|
||||
const sortedTypes = Object.keys(groups).sort(
|
||||
(a, b) => typeIndex(a) - typeIndex(b),
|
||||
)
|
||||
|
||||
let changelogMd = ''
|
||||
for (const type of sortedTypes) {
|
||||
const label = typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1)
|
||||
changelogMd += `### ${label}\n\n`
|
||||
|
||||
for (const commit of groups[type]) {
|
||||
const scopePrefix = commit.scope ? `${commit.scope}: ` : ''
|
||||
const cleanMessage = commit.message.replace(/\s*\(#\d+\)/, '')
|
||||
const prRef = commit.prNumber ? ` (#${commit.prNumber})` : ''
|
||||
const username = commit.prNumber
|
||||
? await resolveAuthorForPR(commit.prNumber)
|
||||
: await resolveUsername(commit.email)
|
||||
const authorSuffix = username ? ` by @${username}` : ''
|
||||
|
||||
changelogMd += `- ${scopePrefix}${cleanMessage}${prRef} (${commit.hash})${authorSuffix}\n`
|
||||
}
|
||||
changelogMd += '\n'
|
||||
}
|
||||
|
||||
if (!changelogMd.trim()) {
|
||||
changelogMd = '- No changelog entries\n\n'
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const date = now.toISOString().slice(0, 10)
|
||||
const time = now.toISOString().slice(11, 16).replace(':', '')
|
||||
const tagName = `release-${date}-${time}`
|
||||
const titleDate = `${date} ${now.toISOString().slice(11, 16)}`
|
||||
|
||||
const isPrerelease = process.argv.includes('--prerelease')
|
||||
const isLatest = process.argv.includes('--latest')
|
||||
|
||||
const body = `Release ${titleDate}
|
||||
|
||||
## Changes
|
||||
|
||||
${changelogMd}
|
||||
## Packages
|
||||
|
||||
${bumpedPackages.map((p) => `- ${p.name}@${p.version}`).join('\n')}
|
||||
`
|
||||
|
||||
// Create the release
|
||||
// Check if tag already exists — if so, try to create the release for it
|
||||
// (handles retries where the tag was pushed but release creation failed)
|
||||
let tagExists = false
|
||||
try {
|
||||
execSync(`git rev-parse ${tagName}`, { stdio: 'ignore' })
|
||||
tagExists = true
|
||||
} catch {
|
||||
// Tag doesn't exist yet
|
||||
}
|
||||
|
||||
if (!tagExists) {
|
||||
execSync(`git tag -a -m "${tagName}" ${tagName}`)
|
||||
execSync('git push --tags')
|
||||
}
|
||||
|
||||
const prereleaseFlag = isPrerelease ? '--prerelease' : ''
|
||||
const latestFlag = isLatest ? ' --latest' : ''
|
||||
const tmpFile = path.join(tmpdir(), `release-notes-${tagName}.md`)
|
||||
fs.writeFileSync(tmpFile, body)
|
||||
|
||||
try {
|
||||
execSync(
|
||||
`gh release create ${tagName} ${prereleaseFlag} --title "Release ${titleDate}" --notes-file ${tmpFile}${latestFlag}`,
|
||||
{ stdio: 'inherit' },
|
||||
)
|
||||
console.info(`GitHub release ${tagName} created.`)
|
||||
} catch (err) {
|
||||
// Clean up the tag if we created it but release failed
|
||||
if (!tagExists) {
|
||||
console.info(`Release creation failed, cleaning up tag ${tagName}...`)
|
||||
try {
|
||||
execSync(`git push --delete origin ${tagName}`, { stdio: 'ignore' })
|
||||
execSync(`git tag -d ${tagName}`, { stdio: 'ignore' })
|
||||
} catch {
|
||||
// Best effort cleanup
|
||||
}
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
fs.unlinkSync(tmpFile)
|
||||
}
|
||||
350
scripts/generate-docs.ts
Normal file
350
scripts/generate-docs.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
const require = createRequire(import.meta.url)
|
||||
const typedocConfigPackageJson =
|
||||
require.resolve('@tanstack/typedoc-config/package.json')
|
||||
const typedocConfigDir = dirname(typedocConfigPackageJson)
|
||||
const typedocConfigRequire = createRequire(typedocConfigPackageJson)
|
||||
const TypeDoc = await import(typedocConfigRequire.resolve('typedoc'))
|
||||
|
||||
type PackageReferenceDocsConfig = {
|
||||
entryPoints: Array<string>
|
||||
tsconfig: string
|
||||
outputDir: string
|
||||
exclude?: Array<string>
|
||||
excludeExternals?: boolean
|
||||
simplifyLitQueriesControllerTypes?: boolean
|
||||
trimGeneratedMarkdown?: boolean
|
||||
// Maps a generated page's path (relative to outputDir, no extension) to the flat
|
||||
// `docs/framework/<framework>/reference/<name>.md` URLs it replaced, so old links/bookmarks redirect
|
||||
// instead of falling through to the framework docs index. See https://github.com/TanStack/query/issues/11371
|
||||
redirectFrom?: Record<string, Array<string>>
|
||||
}
|
||||
|
||||
type TypeDocReflectionWithSignatures = {
|
||||
name: string
|
||||
children?: Array<TypeDocReflectionWithSignatures>
|
||||
signatures?: Array<{
|
||||
typeParameters?: Array<{
|
||||
name: string
|
||||
default?: unknown
|
||||
}>
|
||||
}>
|
||||
}
|
||||
|
||||
function simplifyLitQueriesControllerTypes(
|
||||
project: TypeDocReflectionWithSignatures,
|
||||
) {
|
||||
const stack: Array<TypeDocReflectionWithSignatures> = [project]
|
||||
|
||||
for (const reflection of stack) {
|
||||
stack.push(...(reflection.children ?? []))
|
||||
|
||||
if (reflection.name !== 'createQueriesController') {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const signature of reflection.signatures ?? []) {
|
||||
const combinedResult = signature.typeParameters?.find(
|
||||
(typeParameter) => typeParameter.name === 'TCombinedResult',
|
||||
)
|
||||
|
||||
if (!combinedResult?.default) {
|
||||
continue
|
||||
}
|
||||
|
||||
const queryOptionsType = TypeDoc.ReferenceType.createBrokenReference(
|
||||
'TQueryOptions',
|
||||
project,
|
||||
undefined,
|
||||
)
|
||||
queryOptionsType.refersToTypeParameter = true
|
||||
|
||||
// CreateQueriesResults is internal; render it as plain text, not a link.
|
||||
const queriesResultsType = TypeDoc.ReferenceType.createBrokenReference(
|
||||
'CreateQueriesResults',
|
||||
project,
|
||||
undefined,
|
||||
)
|
||||
queriesResultsType.typeArguments = [queryOptionsType]
|
||||
|
||||
combinedResult.default = queriesResultsType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function trimTrailingWhitespaceInMarkdown(outputDir: string) {
|
||||
const entries = await readdir(outputDir, { withFileTypes: true })
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const path = resolve(outputDir, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await trimTrailingWhitespaceInMarkdown(path)
|
||||
return
|
||||
}
|
||||
|
||||
if (!entry.isFile() || !path.endsWith('.md')) {
|
||||
return
|
||||
}
|
||||
|
||||
const markdown = await readFile(path, 'utf8')
|
||||
const trimmed = markdown.replace(/[ \t]+$/gm, '')
|
||||
|
||||
if (trimmed !== markdown) {
|
||||
await writeFile(path, trimmed)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function addRedirectFromToFileFrontmatter(
|
||||
filePath: string,
|
||||
fromPaths: Array<string>,
|
||||
) {
|
||||
const markdown = await readFile(filePath, 'utf8')
|
||||
|
||||
const frontmatterMatch = markdown.match(/^---\n([\s\S]*?)\n---\n/)
|
||||
if (!frontmatterMatch) {
|
||||
throw new Error(`Expected frontmatter in ${filePath}`)
|
||||
}
|
||||
|
||||
const redirectLines = fromPaths
|
||||
.map((fromPath) => ` - ${fromPath}`)
|
||||
.join('\n')
|
||||
const updatedFrontmatter = `---\n${frontmatterMatch[1]}\nredirect_from:\n${redirectLines}\n---\n`
|
||||
|
||||
await writeFile(
|
||||
filePath,
|
||||
updatedFrontmatter + markdown.slice(frontmatterMatch[0].length),
|
||||
)
|
||||
}
|
||||
|
||||
async function addRedirectFromToFrontmatter(
|
||||
outputDir: string,
|
||||
redirectFrom: Record<string, Array<string>>,
|
||||
) {
|
||||
for (const [pagePath, fromPaths] of Object.entries(redirectFrom)) {
|
||||
await addRedirectFromToFileFrontmatter(
|
||||
resolve(outputDir, `${pagePath}.md`),
|
||||
fromPaths,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePackageReferenceDocs(pkg: PackageReferenceDocsConfig) {
|
||||
const outputDir = pkg.outputDir
|
||||
await rm(outputDir, { recursive: true, force: true })
|
||||
await mkdir(outputDir, { recursive: true })
|
||||
|
||||
const app = await TypeDoc.Application.bootstrapWithPlugins({
|
||||
plugin: [
|
||||
'typedoc-plugin-markdown',
|
||||
'typedoc-plugin-frontmatter',
|
||||
resolve(typedocConfigDir, './src/typedoc-custom-settings.js'),
|
||||
],
|
||||
hideGenerator: true,
|
||||
readme: 'none',
|
||||
entryFileName: 'index',
|
||||
hideBreadcrumbs: true,
|
||||
hidePageHeader: true,
|
||||
hidePageTitle: true,
|
||||
useCodeBlocks: true,
|
||||
excludePrivate: true,
|
||||
excludeInternal: true,
|
||||
excludeExternals: pkg.excludeExternals,
|
||||
sourceLinkTemplate:
|
||||
'https://github.com/TanStack/query/blob/{gitRevision}/{path}#L{line}',
|
||||
gitRevision: 'main',
|
||||
entryPoints: pkg.entryPoints,
|
||||
tsconfig: pkg.tsconfig,
|
||||
...(pkg.exclude && { exclude: pkg.exclude }),
|
||||
out: outputDir,
|
||||
})
|
||||
|
||||
const project = await app.convert()
|
||||
|
||||
if (project) {
|
||||
if (pkg.simplifyLitQueriesControllerTypes) {
|
||||
simplifyLitQueriesControllerTypes(project)
|
||||
}
|
||||
|
||||
await app.generateOutputs(project)
|
||||
|
||||
if (pkg.trimGeneratedMarkdown) {
|
||||
await trimTrailingWhitespaceInMarkdown(outputDir)
|
||||
}
|
||||
|
||||
if (pkg.redirectFrom) {
|
||||
await addRedirectFromToFrontmatter(outputDir, pkg.redirectFrom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const packages: Array<PackageReferenceDocsConfig> = [
|
||||
{
|
||||
entryPoints: [
|
||||
resolve(__dirname, '../packages/angular-query-experimental/src/index.ts'),
|
||||
],
|
||||
tsconfig: resolve(
|
||||
__dirname,
|
||||
'../packages/angular-query-experimental/tsconfig.json',
|
||||
),
|
||||
outputDir: resolve(__dirname, '../docs/framework/angular/reference'),
|
||||
},
|
||||
{
|
||||
entryPoints: [resolve(__dirname, '../packages/svelte-query/src/index.ts')],
|
||||
tsconfig: resolve(__dirname, '../packages/svelte-query/tsconfig.json'),
|
||||
outputDir: resolve(__dirname, '../docs/framework/svelte/reference'),
|
||||
},
|
||||
{
|
||||
entryPoints: [resolve(__dirname, '../packages/solid-query/src/index.ts')],
|
||||
tsconfig: resolve(__dirname, '../packages/solid-query/tsconfig.json'),
|
||||
outputDir: resolve(__dirname, '../docs/framework/solid/reference'),
|
||||
redirectFrom: {
|
||||
'functions/infiniteQueryOptions': [
|
||||
'framework/solid/reference/infiniteQueryOptions',
|
||||
],
|
||||
'functions/mutationOptions': [
|
||||
'framework/solid/reference/mutationOptions',
|
||||
],
|
||||
'functions/queryOptions': ['framework/solid/reference/queryOptions'],
|
||||
'functions/useInfiniteQuery': [
|
||||
'framework/solid/reference/useInfiniteQuery',
|
||||
],
|
||||
'functions/useIsFetching': ['framework/solid/reference/useIsFetching'],
|
||||
'functions/useIsMutating': ['framework/solid/reference/useIsMutating'],
|
||||
'functions/useMutation': ['framework/solid/reference/useMutation'],
|
||||
'functions/useMutationState': [
|
||||
'framework/solid/reference/useMutationState',
|
||||
],
|
||||
'functions/useQueries': ['framework/solid/reference/useQueries'],
|
||||
'functions/useQuery': ['framework/solid/reference/useQuery'],
|
||||
},
|
||||
},
|
||||
{
|
||||
entryPoints: [resolve(__dirname, '../packages/vue-query/src/index.ts')],
|
||||
tsconfig: resolve(__dirname, '../packages/vue-query/tsconfig.json'),
|
||||
outputDir: resolve(__dirname, '../docs/framework/vue/reference'),
|
||||
},
|
||||
{
|
||||
entryPoints: [resolve(__dirname, '../packages/react-query/src/index.ts')],
|
||||
tsconfig: resolve(__dirname, '../packages/react-query/tsconfig.json'),
|
||||
outputDir: resolve(__dirname, '../docs/framework/react/reference'),
|
||||
redirectFrom: {
|
||||
'functions/infiniteQueryOptions': [
|
||||
'framework/react/reference/infiniteQueryOptions',
|
||||
],
|
||||
'functions/mutationOptions': [
|
||||
'framework/react/reference/mutationOptions',
|
||||
],
|
||||
'functions/QueryClientProvider': [
|
||||
'framework/react/reference/QueryClientProvider',
|
||||
],
|
||||
'functions/QueryErrorResetBoundary': [
|
||||
'framework/react/reference/QueryErrorResetBoundary',
|
||||
],
|
||||
'functions/queryOptions': ['framework/react/reference/queryOptions'],
|
||||
'functions/useInfiniteQuery': [
|
||||
'framework/react/reference/useInfiniteQuery',
|
||||
],
|
||||
'functions/useIsFetching': ['framework/react/reference/useIsFetching'],
|
||||
'functions/useIsMutating': ['framework/react/reference/useIsMutating'],
|
||||
'functions/useMutation': ['framework/react/reference/useMutation'],
|
||||
'functions/useMutationState': [
|
||||
'framework/react/reference/useMutationState',
|
||||
],
|
||||
'functions/usePrefetchInfiniteQuery': [
|
||||
'framework/react/reference/usePrefetchInfiniteQuery',
|
||||
],
|
||||
'functions/usePrefetchQuery': [
|
||||
'framework/react/reference/usePrefetchQuery',
|
||||
],
|
||||
'functions/useQueries': ['framework/react/reference/useQueries'],
|
||||
'functions/useQuery': ['framework/react/reference/useQuery'],
|
||||
'functions/useQueryClient': ['framework/react/reference/useQueryClient'],
|
||||
'functions/useQueryErrorResetBoundary': [
|
||||
'framework/react/reference/useQueryErrorResetBoundary',
|
||||
],
|
||||
'functions/useSuspenseInfiniteQuery': [
|
||||
'framework/react/reference/useSuspenseInfiniteQuery',
|
||||
],
|
||||
'functions/useSuspenseQueries': [
|
||||
'framework/react/reference/useSuspenseQueries',
|
||||
],
|
||||
'functions/useSuspenseQuery': [
|
||||
'framework/react/reference/useSuspenseQuery',
|
||||
],
|
||||
// Redirects from the legacy hand-written docs/reference/*.md pages, removed in favor of
|
||||
// this generated reference.
|
||||
'classes/QueryClient': [
|
||||
'reference/QueryClient',
|
||||
'framework/react/reference/QueryClient',
|
||||
],
|
||||
'classes/QueryCache': [
|
||||
'reference/QueryCache',
|
||||
'framework/react/reference/QueryCache',
|
||||
],
|
||||
'classes/MutationCache': [
|
||||
'reference/MutationCache',
|
||||
'framework/react/reference/MutationCache',
|
||||
],
|
||||
'classes/QueryObserver': [
|
||||
'reference/QueryObserver',
|
||||
'framework/react/reference/QueryObserver',
|
||||
],
|
||||
'classes/InfiniteQueryObserver': [
|
||||
'reference/InfiniteQueryObserver',
|
||||
'framework/react/reference/InfiniteQueryObserver',
|
||||
],
|
||||
'classes/QueriesObserver': [
|
||||
'reference/QueriesObserver',
|
||||
'framework/react/reference/QueriesObserver',
|
||||
],
|
||||
// focusManager/onlineManager/timeoutManager are class instances, not object literals, so
|
||||
// TypeDoc can't inline their methods onto the `variables/*` instance page — the method docs
|
||||
// that the legacy pages covered now live on the `interfaces/*` page for the class itself.
|
||||
'interfaces/FocusManager': [
|
||||
'reference/focusManager',
|
||||
'framework/react/reference/focusManager',
|
||||
],
|
||||
'interfaces/OnlineManager': [
|
||||
'reference/onlineManager',
|
||||
'framework/react/reference/onlineManager',
|
||||
],
|
||||
'interfaces/TimeoutManager': ['reference/timeoutManager'],
|
||||
'variables/notifyManager': [
|
||||
'reference/notifyManager',
|
||||
'framework/react/reference/notifyManager',
|
||||
],
|
||||
'variables/environmentManager': ['reference/environmentManager'],
|
||||
'functions/experimental_streamedQuery': ['reference/streamedQuery'],
|
||||
},
|
||||
},
|
||||
{
|
||||
entryPoints: [resolve(__dirname, '../packages/preact-query/src/index.ts')],
|
||||
tsconfig: resolve(__dirname, '../packages/preact-query/tsconfig.json'),
|
||||
outputDir: resolve(__dirname, '../docs/framework/preact/reference'),
|
||||
},
|
||||
{
|
||||
entryPoints: [resolve(__dirname, '../packages/lit-query/src/index.ts')],
|
||||
tsconfig: resolve(__dirname, '../packages/lit-query/tsconfig.json'),
|
||||
outputDir: resolve(__dirname, '../docs/framework/lit/reference'),
|
||||
excludeExternals: true,
|
||||
simplifyLitQueriesControllerTypes: true,
|
||||
trimGeneratedMarkdown: true,
|
||||
},
|
||||
]
|
||||
|
||||
for (const pkg of packages) {
|
||||
await generatePackageReferenceDocs(pkg)
|
||||
}
|
||||
|
||||
console.log('\n✅ All markdown files have been processed!')
|
||||
|
||||
process.exit(0)
|
||||
106
scripts/generate-labeler-config.ts
Normal file
106
scripts/generate-labeler-config.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import * as prettier from 'prettier'
|
||||
|
||||
/** Pairs of package labels and their corresponding paths */
|
||||
type LabelerPair = [string, string]
|
||||
|
||||
function readPairsFromFs(): Array<LabelerPair> {
|
||||
const ignored = new Set(['.DS_Store'])
|
||||
|
||||
const pairs: Array<LabelerPair> = []
|
||||
|
||||
// Add subfolders in the packages folder, i.e. packages/**
|
||||
fs.readdirSync(path.resolve('packages'))
|
||||
.filter((folder) => !ignored.has(folder))
|
||||
.forEach((folder) => {
|
||||
// Check if package.json exists for the folder before adding it
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.resolve(path.join('packages', folder, 'package.json')),
|
||||
)
|
||||
) {
|
||||
pairs.push([`package: ${folder}`, `packages/${folder}/**/*`])
|
||||
} else {
|
||||
console.info(
|
||||
`Skipping \`${folder}\` as it does not have a \`package.json\` file.`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by package name in alphabetical order
|
||||
pairs.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
async function generateLabelerYaml(pairs: Array<LabelerPair>): Promise<string> {
|
||||
function s(n = 1) {
|
||||
return ' '.repeat(n)
|
||||
}
|
||||
|
||||
// Convert the pairs into valid yaml
|
||||
const formattedPairs = pairs
|
||||
.map(([packageLabel, packagePath]) => {
|
||||
const result = [
|
||||
`'${packageLabel}':`,
|
||||
`${s(2)}-${s(1)}changed-files:`,
|
||||
`${s(4)}-${s(1)}any-glob-to-any-file:${s(1)}'${packagePath}'`,
|
||||
].join('\n')
|
||||
|
||||
return result
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// Get the location of the Prettier config file
|
||||
const prettierConfigPath = await prettier.resolveConfigFile()
|
||||
if (!prettierConfigPath) {
|
||||
throw new Error(
|
||||
'No Prettier config file found. Please ensure you have a Prettier config file in your project.',
|
||||
)
|
||||
}
|
||||
console.info('using prettier config file at:', prettierConfigPath)
|
||||
|
||||
// Resolve the Prettier config
|
||||
const prettierConfig = await prettier.resolveConfig(prettierConfigPath)
|
||||
console.info('using resolved prettier config:', prettierConfig)
|
||||
|
||||
// Format the YAML string using Prettier
|
||||
const formattedStr = await prettier.format(formattedPairs, {
|
||||
parser: 'yaml',
|
||||
...prettierConfig,
|
||||
})
|
||||
|
||||
return formattedStr
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.info('Generating labeler config...')
|
||||
|
||||
// Generate the pairs of package labels and their corresponding paths
|
||||
const pairs = readPairsFromFs()
|
||||
|
||||
// Always add the docs folder
|
||||
pairs.push(['documentation', 'docs/**/*'])
|
||||
|
||||
// Convert the pairs into valid yaml
|
||||
const yamlStr = await generateLabelerYaml(pairs)
|
||||
|
||||
// Write to 'labeler-config.yml'
|
||||
const configPath = path.resolve('labeler-config.yml')
|
||||
fs.writeFileSync(configPath, yamlStr, {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
|
||||
console.info(`Generated labeler config at \`${configPath}\`!`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
run().then(() => {
|
||||
process.exit(0)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error generating labeler config:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
40
scripts/getTsdownConfig.js
Normal file
40
scripts/getTsdownConfig.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {Object} opts - Options for building configurations.
|
||||
* @param {string[]} opts.entry - The entry array.
|
||||
* @returns {import('tsdown').UserConfig}
|
||||
*/
|
||||
export function modernConfig(opts) {
|
||||
return {
|
||||
entry: opts.entry,
|
||||
format: ['cjs', 'esm'],
|
||||
target: ['chrome91', 'firefox90', 'edge91', 'safari15', 'ios15', 'opera77'],
|
||||
// Rolldown lowers private fields for browser target arrays. Keep the
|
||||
// transform target at ES2022 to match the syntax that tsup emitted.
|
||||
inputOptions: { transform: { target: 'es2022' } },
|
||||
outDir: 'build/modern',
|
||||
dts: true,
|
||||
fixedExtension: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} opts - Options for building configurations.
|
||||
* @param {string[]} opts.entry - The entry array.
|
||||
* @returns {import('tsdown').UserConfig}
|
||||
*/
|
||||
export function legacyConfig(opts) {
|
||||
return {
|
||||
entry: opts.entry,
|
||||
format: ['cjs', 'esm'],
|
||||
target: ['es2020', 'node16'],
|
||||
outDir: 'build/legacy',
|
||||
dts: true,
|
||||
fixedExtension: false,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
}
|
||||
}
|
||||
17
scripts/getViteAliases.js
Normal file
17
scripts/getViteAliases.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @ts-check
|
||||
|
||||
import path from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
const tsconfig = ts.readConfigFile(
|
||||
path.resolve(__dirname, '..', 'tsconfig.json'),
|
||||
ts.sys.readFile,
|
||||
).config
|
||||
|
||||
export const dynamicAliases = Object.entries(
|
||||
tsconfig.compilerOptions.paths || {},
|
||||
).reduce((aliases, [key, [value]]) => {
|
||||
const aliasKey = key.replace('/*', '')
|
||||
aliases[aliasKey] = path.resolve(value.replace('/*', ''))
|
||||
return aliases
|
||||
}, /** @type {Record<string, string>} */ ({}))
|
||||
4
scripts/tsconfig.json
Normal file
4
scripts/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
130
scripts/verify-links.ts
Normal file
130
scripts/verify-links.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs'
|
||||
import { extname, resolve, sep } from 'node:path'
|
||||
import { glob } from 'tinyglobby'
|
||||
// @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'.
|
||||
import markdownLinkExtractor from 'markdown-link-extractor'
|
||||
|
||||
const errors: Array<{
|
||||
file: string
|
||||
link: string
|
||||
resolvedPath: string
|
||||
reason: string
|
||||
}> = []
|
||||
|
||||
function isRelativeLink(link: string) {
|
||||
return (
|
||||
!link.startsWith('/') &&
|
||||
!link.startsWith('http://') &&
|
||||
!link.startsWith('https://') &&
|
||||
!link.startsWith('//') &&
|
||||
!link.startsWith('#') &&
|
||||
!link.startsWith('mailto:')
|
||||
)
|
||||
}
|
||||
|
||||
/** Remove any trailing .md */
|
||||
function stripExtension(p: string): string {
|
||||
return p.replace(`${extname(p)}`, '')
|
||||
}
|
||||
|
||||
function relativeLinkExists(link: string, file: string): boolean {
|
||||
// Remove hash if present
|
||||
const linkWithoutHash = link.split('#')[0]
|
||||
// If the link is empty after removing hash, it's not a file
|
||||
if (!linkWithoutHash) return false
|
||||
|
||||
// Strip the file/link extensions
|
||||
const filePath = stripExtension(file)
|
||||
const linkPath = stripExtension(linkWithoutHash)
|
||||
|
||||
// Resolve the path relative to the markdown file's directory
|
||||
// Nav up a level to simulate how links are resolved on the web
|
||||
let absPath = resolve(filePath, '..', linkPath)
|
||||
|
||||
// Ensure the resolved path is within /docs
|
||||
const docsRoot = resolve('docs')
|
||||
if (!absPath.startsWith(docsRoot)) {
|
||||
errors.push({
|
||||
link,
|
||||
file,
|
||||
resolvedPath: absPath,
|
||||
reason: 'Path outside /docs',
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// `resolve` uses the platform separator, but the patterns below are written
|
||||
// with forward slashes, so match against a normalized copy of the path.
|
||||
// Node accepts forward slashes on Windows too, so the result stays usable.
|
||||
absPath = absPath.split(sep).join('/')
|
||||
|
||||
// Check if this is an example path
|
||||
const isExample = absPath.includes('/examples/')
|
||||
|
||||
let exists = false
|
||||
|
||||
if (isExample) {
|
||||
// Transform /docs/framework/{framework}/examples/ to /examples/{framework}/
|
||||
absPath = absPath.replace(
|
||||
/\/docs\/framework\/([^/]+)\/examples\//,
|
||||
'/examples/$1/',
|
||||
)
|
||||
// For examples, we want to check if the directory exists
|
||||
exists = existsSync(absPath) && statSync(absPath).isDirectory()
|
||||
} else {
|
||||
// For non-examples, we want to check if the .md file exists
|
||||
if (!absPath.endsWith('.md')) {
|
||||
absPath = `${absPath}.md`
|
||||
}
|
||||
exists = existsSync(absPath)
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
errors.push({
|
||||
link,
|
||||
file,
|
||||
resolvedPath: absPath,
|
||||
reason: 'Not found',
|
||||
})
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
async function verifyMarkdownLinks() {
|
||||
// Find all markdown files in docs directory
|
||||
const markdownFiles = await glob('docs/**/*.md', {
|
||||
ignore: ['**/node_modules/**'],
|
||||
})
|
||||
|
||||
console.log(`Found ${markdownFiles.length} markdown files\n`)
|
||||
|
||||
// Process each file
|
||||
for (const file of markdownFiles) {
|
||||
const content = readFileSync(file, 'utf-8')
|
||||
const links: Array<string> = markdownLinkExtractor(content)
|
||||
|
||||
const relativeLinks = links.filter((link: string) => {
|
||||
return isRelativeLink(link)
|
||||
})
|
||||
|
||||
if (relativeLinks.length > 0) {
|
||||
relativeLinks.forEach((link) => {
|
||||
relativeLinkExists(link, file)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(`\n❌ Found ${errors.length} broken links:`)
|
||||
errors.forEach((err) => {
|
||||
console.log(
|
||||
`${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`,
|
||||
)
|
||||
})
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log('\n✅ No broken links found!')
|
||||
}
|
||||
}
|
||||
|
||||
verifyMarkdownLinks().catch(console.error)
|
||||
Reference in New Issue
Block a user