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
+ }
+}