diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml index b5e18b9a..2a6fd8ad 100644 --- a/.github/actions/setup-node-pnpm/action.yml +++ b/.github/actions/setup-node-pnpm/action.yml @@ -10,10 +10,18 @@ inputs: description: pnpm version required: false default: "10.33.0" + registry-url: + description: Optional npm registry URL to configure for npm publish/auth steps + required: false + default: "" install: description: Whether to run pnpm install required: false default: "true" + install-filter: + description: Optional pnpm filter selector to limit install scope + required: false + default: "" runs: using: composite @@ -29,8 +37,16 @@ runs: node-version: ${{ inputs.node-version }} cache: pnpm cache-dependency-path: pnpm-lock.yaml + registry-url: ${{ inputs.registry-url }} - name: Install workspace dependencies - if: inputs.install == 'true' + if: inputs.install == 'true' && inputs['install-filter'] == '' shell: bash run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Install filtered workspace dependencies + if: inputs.install == 'true' && inputs['install-filter'] != '' + shell: bash + run: pnpm install --frozen-lockfile --ignore-scripts --filter "$INSTALL_FILTER" + env: + INSTALL_FILTER: ${{ inputs['install-filter'] }} diff --git a/.github/workflows/deploy-doc.yml b/.github/workflows/deploy-doc.yml index 564f896a..024b9448 100644 --- a/.github/workflows/deploy-doc.yml +++ b/.github/workflows/deploy-doc.yml @@ -6,6 +6,13 @@ on: - main paths: - doc/** + - .github/actions/setup-node-pnpm/action.yml + - .github/workflows/deploy-doc.yml + - .github/workflows/pull-request-doc.yml + - package.json + - pnpm-lock.yaml + - pnpm-workspace.yaml + - turbo.json workflow_dispatch: permissions: @@ -25,9 +32,11 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/actions/setup-node-pnpm + with: + install-filter: "@truenine/memory-sync-docs..." - name: Preflight Vercel secrets shell: bash @@ -49,11 +58,20 @@ jobs: exit 1 fi + - name: Lint docs + run: pnpm -C doc run lint + + - name: Typecheck docs + run: pnpm -C doc run typecheck + - name: Pull Vercel production settings + working-directory: ./doc run: pnpm dlx vercel@latest pull --yes --environment=production --token="$VERCEL_TOKEN" - name: Build docs on Vercel + working-directory: ./doc run: pnpm dlx vercel@latest build --prod --token="$VERCEL_TOKEN" - name: Deploy docs to Vercel production + working-directory: ./doc run: pnpm dlx vercel@latest deploy --prebuilt --prod --token="$VERCEL_TOKEN" diff --git a/.github/workflows/pull-request-doc.yml b/.github/workflows/pull-request-doc.yml new file mode 100644 index 00000000..2ef3e98d --- /dev/null +++ b/.github/workflows/pull-request-doc.yml @@ -0,0 +1,47 @@ +name: Pull Request Docs + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened, ready_for_review] + paths: + - doc/** + - .github/actions/setup-node-pnpm/action.yml + - .github/workflows/deploy-doc.yml + - .github/workflows/pull-request-doc.yml + - package.json + - pnpm-lock.yaml + - pnpm-workspace.yaml + - turbo.json + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + check-doc: + if: github.event.pull_request.draft == false + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/setup-node-pnpm + with: + install-filter: "@truenine/memory-sync-docs..." + + - name: Validate docs content + run: pnpm -C doc run validate:content + + - name: Lint docs + run: pnpm -C doc run lint + + - name: Typecheck docs + run: pnpm -C doc run typecheck + + - name: Build docs + run: pnpm -C doc run build diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c1b5c33f..cec915b5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -14,6 +14,9 @@ on: - CODE_OF_CONDUCT.md - LICENSE - SECURITY.md + - doc/** + - .github/workflows/deploy-doc.yml + - .github/workflows/pull-request-doc.yml permissions: contents: read diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 413ca103..8b4518b4 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -1,7 +1,6 @@ name: Release Packages env: - NODE_VERSION: "25.6.1" NPM_REGISTRY_URL: https://registry.npmjs.org/ NPM_PUBLISH_VERIFY_ATTEMPTS: "90" NPM_PUBLISH_VERIFY_DELAY_SECONDS: "10" @@ -57,9 +56,9 @@ jobs: version: ${{ steps.check.outputs.version }} steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: ./.github/actions/setup-node-pnpm with: - node-version: ${{ env.NODE_VERSION }} + install: "false" - name: Check if should publish id: check @@ -240,10 +239,6 @@ jobs: - uses: ./.github/actions/setup-node-pnpm with: install: "true" - - name: Setup npm registry - uses: actions/setup-node@v6 - with: - node-version: ${{ env.NODE_VERSION }} registry-url: https://registry.npmjs.org/ - name: Preflight npm auth shell: bash @@ -464,10 +459,7 @@ jobs: steps: - uses: actions/checkout@v6 - uses: ./.github/actions/setup-node-pnpm - - name: Setup npm registry - uses: actions/setup-node@v6 with: - node-version: ${{ env.NODE_VERSION }} registry-url: https://registry.npmjs.org/ - name: Preflight npm auth shell: bash @@ -590,10 +582,7 @@ jobs: steps: - uses: actions/checkout@v6 - uses: ./.github/actions/setup-node-pnpm - - name: Setup npm registry - uses: actions/setup-node@v6 with: - node-version: ${{ env.NODE_VERSION }} registry-url: https://registry.npmjs.org/ - name: Preflight npm auth shell: bash diff --git a/doc/app/docs/[[...mdxPath]]/layout.tsx b/doc/app/docs/[[...mdxPath]]/layout.tsx index 8f8be510..75f132d3 100644 --- a/doc/app/docs/[[...mdxPath]]/layout.tsx +++ b/doc/app/docs/[[...mdxPath]]/layout.tsx @@ -16,7 +16,7 @@ export default async function DocsLayout({ const firstSegment = params.mdxPath?.[0] const section = firstSegment != null && isDocSectionName(firstSegment) ? firstSegment - : undefined + : void 0 const pageMap = await getPageMap(section ? `/docs/${section}` : '/docs') return ( diff --git a/doc/app/layout.tsx b/doc/app/layout.tsx index 773de2c7..b7398e36 100644 --- a/doc/app/layout.tsx +++ b/doc/app/layout.tsx @@ -1,55 +1,55 @@ -import type { Metadata } from "next"; -import { Inter, JetBrains_Mono } from "next/font/google"; -import { getSiteUrl, siteConfig } from "../lib/site"; -import "nextra-theme-docs/style.css"; -import "./globals.scss"; +import type {Metadata} from 'next' +import {Inter, JetBrains_Mono} from 'next/font/google' +import {getSiteUrl, siteConfig} from '../lib/site' +import 'nextra-theme-docs/style.css' +import './globals.scss' const sans = Inter({ - variable: "--font-sans", + variable: '--font-sans', preload: true, - subsets: ["latin"], -}); + subsets: ['latin'] +}) const mono = JetBrains_Mono({ - variable: "--font-mono", - subsets: ["latin"], - preload: true, -}); + variable: '--font-mono', + subsets: ['latin'], + preload: true +}) export const metadata: Metadata = { metadataBase: getSiteUrl(), title: { default: siteConfig.title, - template: `%s | ${siteConfig.productName}`, + template: `%s | ${siteConfig.productName}` }, description: siteConfig.description, applicationName: siteConfig.shortName, alternates: { - canonical: "/", + canonical: '/' }, - category: "developer tools", - manifest: "/manifest.webmanifest", + category: 'developer tools', + manifest: '/manifest.webmanifest', openGraph: { - type: "website", - url: "/", + type: 'website', + url: '/', title: siteConfig.title, description: siteConfig.description, siteName: siteConfig.title, - locale: "zh_CN", + locale: 'zh_CN' }, twitter: { - card: "summary_large_image", + card: 'summary_large_image', title: siteConfig.title, - description: siteConfig.description, - }, -}; + description: siteConfig.description + } +} -export default function RootLayout({ children }: { readonly children: React.ReactNode }) { +export default function RootLayout({children}: {readonly children: React.ReactNode}) { return ( - + {children} - ); + ) } diff --git a/doc/components/docs-callout.tsx b/doc/components/docs-callout.tsx index ec688b20..ab3aecf9 100644 --- a/doc/components/docs-callout.tsx +++ b/doc/components/docs-callout.tsx @@ -6,6 +6,7 @@ type CalloutTone = 'note' | 'tip' | 'important' | 'warning' | 'caution' type BlockquoteProps = ComponentPropsWithoutRef<'blockquote'> const CALLOUT_PATTERN = /^\s*\[!(note|tip|important|warning|caution)\]\s*/i +const CALLOUT_TONES = new Set(['note', 'tip', 'important', 'warning', 'caution']) const CALLOUT_LABELS: Record = { note: 'Note', @@ -43,46 +44,51 @@ function getMeaningfulChildren(children: ReactNode): ReactNode[] { function stripMarkerFromChildren(children: ReactNode): ReactNode { const items = getMeaningfulChildren(children) + const strippedItems: ReactNode[] = [] - return items.map((item, index) => { + for (const [index, item] of items.entries()) { if (index !== 0) { - return item + strippedItems.push(item) + continue } if (typeof item === 'string') { - return item.replace(CALLOUT_PATTERN, '') + strippedItems.push(item.replace(CALLOUT_PATTERN, '')) + continue } if (!isValidElement(item)) { - return item + strippedItems.push(item) + continue } const element = item as ReactElement<{children?: ReactNode}> const text = extractText(element.props.children) if (!CALLOUT_PATTERN.test(text)) { - return item + strippedItems.push(item) + continue } - return cloneElement(element, { + strippedItems.push(cloneElement(element, { ...element.props, children: text.replace(CALLOUT_PATTERN, '') - }) - }) + })) + } + + return strippedItems +} + +function isCalloutTone(value: string | undefined): value is CalloutTone { + return value != null && CALLOUT_TONES.has(value as CalloutTone) } function resolveCalloutTone(children: ReactNode): CalloutTone | null { const firstChild = getMeaningfulChildren(children)[0] const firstText = extractText(firstChild).trimStart() - const matched = firstText.match(CALLOUT_PATTERN)?.[1]?.toLowerCase() - - if ( - matched === 'note' - || matched === 'tip' - || matched === 'important' - || matched === 'warning' - || matched === 'caution' - ) { + const matched = CALLOUT_PATTERN.exec(firstText)?.[1]?.toLowerCase() + + if (isCalloutTone(matched)) { return matched } diff --git a/doc/components/home-contributors.tsx b/doc/components/home-contributors.tsx index db3448e5..d83072b6 100644 --- a/doc/components/home-contributors.tsx +++ b/doc/components/home-contributors.tsx @@ -1,6 +1,7 @@ -import {siteConfig} from '../lib/site' import {execFileSync} from 'node:child_process' import path from 'node:path' +import process from 'node:process' +import {siteConfig} from '../lib/site' interface GitHubContributor { readonly avatar_url: string @@ -44,10 +45,12 @@ const CONTRIBUTORS_PER_PAGE = 100 const MAX_CONTRIBUTOR_PAGES = 10 const CONTRIBUTORS_REVALIDATE_SECONDS = 60 * 60 * 12 const REPO_ROOT = path.resolve(process.cwd(), '..') +const LEADING_SLASHES_PATTERN = /^\/+/ +const CO_AUTHOR_PREFIX = 'co-authored-by:' function getRepoCoordinates(repoUrl: string) { const url = new URL(repoUrl) - const [owner, repo] = url.pathname.replace(/^\/+/, '').split('/') + const [owner, repo] = url.pathname.replace(LEADING_SLASHES_PATTERN, '').split('/') if (!owner || !repo) { throw new Error(`Invalid GitHub repository URL: ${repoUrl}`) @@ -58,11 +61,15 @@ function getRepoCoordinates(repoUrl: string) { function getGitHubHeaders() { const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN + const headers: Record = { + Accept: 'application/vnd.github+json' + } - return { - Accept: 'application/vnd.github+json', - ...(token ? {Authorization: `Bearer ${token}`} : {}) + if (token != null && token !== '') { + headers.Authorization = `Bearer ${token}` } + + return headers } async function fetchContributorsPage(page: number): Promise { @@ -117,20 +124,36 @@ async function fetchGitHubUser(login: string): Promise$/gim) const coAuthors: CoAuthorIdentity[] = [] - for (const match of matches) { - const [, name, email] = match + for (const rawLine of message.split('\n')) { + const line = rawLine.trim() + + if (!line.toLowerCase().startsWith(CO_AUTHOR_PREFIX)) { + continue + } + + const footer = line.slice(CO_AUTHOR_PREFIX.length).trim() + const openAngleBracketIndex = footer.lastIndexOf('<') + const closeAngleBracketIndex = footer.endsWith('>') + ? footer.length - 1 + : -1 - if (!name || !email) { + if (openAngleBracketIndex <= 0 || closeAngleBracketIndex <= openAngleBracketIndex) { + continue + } + + const name = footer.slice(0, openAngleBracketIndex).trim() + const email = footer.slice(openAngleBracketIndex + 1, closeAngleBracketIndex).trim() + + if (name === '' || email === '') { continue } coAuthors.push({ count: 1, - email: email.trim(), - name: name.trim() + email, + name }) } @@ -167,7 +190,7 @@ function getCoAuthorSearchQueries(identity: CoAuthorIdentity) { queries.push(`${identity.name} in:login`) - return Array.from(new Set(queries)) + return [...new Set(queries)] } function getKnownCoAuthorProfile(identity: CoAuthorIdentity): KnownCoAuthorProfile | null { @@ -252,7 +275,7 @@ async function getCoAuthors() { } } - return Array.from(coAuthors.values()).sort((left, right) => right.count - left.count) + return [...coAuthors.values()].sort((left, right) => right.count - left.count) } async function resolveCoAuthor(identity: CoAuthorIdentity) { @@ -355,34 +378,33 @@ async function getContributorCards() { cards.set(key, value) } - return Array.from(cards.values()) - .map(contributor => { - if (contributor.htmlUrl === 'https://github.com/cursoragent') { - return { - ...contributor, - kind: 'agent' as const, - label: 'cursoragent' - } + return Array.from(cards.values(), contributor => { + if (contributor.htmlUrl === 'https://github.com/cursoragent') { + return { + ...contributor, + kind: 'agent' as const, + label: 'cursoragent' } + } - if (contributor.htmlUrl === 'https://github.com/anthropics-claude-code') { - return { - ...contributor, - kind: 'agent' as const, - label: 'Claude Code' - } + if (contributor.htmlUrl === 'https://github.com/anthropics-claude-code') { + return { + ...contributor, + kind: 'agent' as const, + label: 'Claude Code' } + } - if (contributor.htmlUrl === 'https://github.com/windsurf') { - return { - ...contributor, - kind: 'agent' as const, - label: 'Windsurf' - } + if (contributor.htmlUrl === 'https://github.com/windsurf') { + return { + ...contributor, + kind: 'agent' as const, + label: 'Windsurf' } + } - return contributor - }) + return contributor + }) .sort((left, right) => right.sortWeight - left.sortWeight) } diff --git a/doc/components/mermaid.tsx b/doc/components/mermaid.tsx index 4c6e2651..91da8243 100644 --- a/doc/components/mermaid.tsx +++ b/doc/components/mermaid.tsx @@ -153,7 +153,6 @@ export function Mermaid({chart, title}: MermaidProps) { }, 1800) } - const hasTitle = title !== void 0 && title !== '' const hasSvg = svg !== void 0 && svg !== '' const hasError = error !== void 0 && error !== '' let diagramBody: ReactNode =
Rendering diagram...
diff --git a/doc/components/package-manager-tabs.tsx b/doc/components/package-manager-tabs.tsx index ddb387b9..bbaa4793 100644 --- a/doc/components/package-manager-tabs.tsx +++ b/doc/components/package-manager-tabs.tsx @@ -39,6 +39,8 @@ export function PackageManagerTabs({ }: PackageManagerTabsProps): ReactNode { const instanceId = useId() const [selectedManager, setSelectedManager] = useState(defaultManager) + const hasTitle = title !== void 0 && title !== '' + const hasDescription = description !== void 0 && description !== null && description !== false const tabs: PackageManagerTab[] = TAB_ORDER.map(id => ({ command: commands[id], id, @@ -55,12 +57,14 @@ export function PackageManagerTabs({ return (
- {title || description ? ( -
- {title ?

{title}

: null} - {description ?

{description}

: null} -
- ) : null} + {hasTitle || hasDescription + ? ( +
+ {hasTitle ?

{title}

: null} + {hasDescription ?

{description}

: null} +
+ ) + : null}
{tabs.map(tab => { const isSelected = tab.id === activeTab.id @@ -80,9 +84,9 @@ export function PackageManagerTabs({ }} > {tab.label} - {tab.id === recommendedManager ? ( - 推荐 - ) : null} + {tab.id === recommendedManager + ? 推荐 + : null} ) })} diff --git a/doc/content/sdk/_meta.ts b/doc/content/sdk/_meta.ts index be04a93e..01d60bc6 100644 --- a/doc/content/sdk/_meta.ts +++ b/doc/content/sdk/_meta.ts @@ -1,3 +1,3 @@ export default { - 'index': '概览' + index: '概览' } diff --git a/doc/mdx-components.tsx b/doc/mdx-components.tsx index 73c84880..98451b50 100644 --- a/doc/mdx-components.tsx +++ b/doc/mdx-components.tsx @@ -1,8 +1,8 @@ import type {ComponentPropsWithoutRef, ReactElement, ReactNode} from 'react' import {useMDXComponents as getDocsMDXComponents} from 'nextra-theme-docs' import {isValidElement} from 'react' -import {DocsBlockquote} from './components/docs-callout' import {CommandReference, FeatureMatrix, PlatformGrid, SupportMatrix} from './components/doc-widgets' +import {DocsBlockquote} from './components/docs-callout' import {DocsCodeBlock} from './components/docs-code-block' import {Mermaid} from './components/mermaid' import {PackageManagerTabs} from './components/package-manager-tabs' diff --git a/doc/package.json b/doc/package.json index 37e729c0..2de04ab0 100644 --- a/doc/package.json +++ b/doc/package.json @@ -12,7 +12,7 @@ "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind", "check": "run-p lint typecheck", "validate:content": "tsx scripts/validate-content.ts", - "typecheck": "next typegen && tsc --noEmit --incremental false", + "typecheck": "next typegen && tsc --project tsconfig.typecheck.json --noEmit --incremental false", "start": "next start", "lint": "pnpm run validate:content && eslint ." }, diff --git a/doc/tsconfig.json b/doc/tsconfig.json index 10d890c8..540f9a5c 100644 --- a/doc/tsconfig.json +++ b/doc/tsconfig.json @@ -33,8 +33,8 @@ "**/*.tsx", "**/*.mdx", "**/_meta.ts", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" + ".next/dev/types/**/*.ts", + ".next/types/**/*.ts" ], "exclude": [ "node_modules" diff --git a/doc/tsconfig.typecheck.json b/doc/tsconfig.typecheck.json new file mode 100644 index 00000000..12dce863 --- /dev/null +++ b/doc/tsconfig.typecheck.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "next-env.d.ts", + "**/*.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mdx", + "**/_meta.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules", + ".next/types/**/*.ts" + ] +} diff --git a/doc/vercel.json b/doc/vercel.json new file mode 100644 index 00000000..87056607 --- /dev/null +++ b/doc/vercel.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "git": { + "deploymentEnabled": false + } +}