diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 06f1143b2a7f..4c51fb78c658 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -1,7 +1,7 @@ import type * as Preset from '@docusaurus/preset-classic'; import type * as PresetMermaid from '@docusaurus/theme-mermaid'; import type { Config } from '@docusaurus/types'; -import { GlobExcludeDefault } from '@docusaurus/utils'; +import { GlobExcludeDefault, getVcsPreset } from '@docusaurus/utils'; import { createRequire } from 'module'; import path from 'path'; import { themes } from 'prism-react-renderer'; @@ -18,12 +18,30 @@ require('./scripts/generateMonacoPreloads.cjs').ensureMonacoPreloadManifest(); const isDev = process.env.NODE_ENV === 'development'; // docs/core is shared by React (/docs) and Vue (/vue); see framework-docs/ +const { + docsInstance, + frameworkInstance, +} = require('./framework-docs/docsInstances.js'); const frameworkDocs = require('./framework-docs/index.js'); const remarkFramework = require('./framework-docs/remarkFramework.js'); // Non-Vue instances render React; :::vue reaches Vue agents via skill references const reactRemarkPlugins = [[remarkFramework, { framework: 'react' }]]; const vueDocs = frameworkDocs.generate('vue'); if (isDev) frameworkDocs.watch('vue'); +const vueInstance = frameworkInstance('vue'); +const gitVcs = getVcsPreset('default-v1'); +const editRoot = 'https://github.com/reactive/data-client/edit/master'; +/** Plugin options locating a docs instance (framework-docs/docsInstances.js) */ +const docsLocation = (id: string) => { + const { path: docsPath, routeBasePath } = docsInstance(id); + return { + id, + path: `../${docsPath}`, + routeBasePath, + editUrl: ({ docPath }: { docPath: string }) => + `${editRoot}/${docsPath}/${docPath}`, + }; +}; const config: Config = { title: 'Data Client', @@ -208,33 +226,32 @@ const config: Config = { repoUrl: 'https://github.com/reactive/data-client', }, onBrokenLinks: 'log', + future: { + // Generated Vue mirror pages have no git history; read their source's + experimental_vcs: { + ...gitVcs, + getFileCreationInfo: file => + gitVcs.getFileCreationInfo(vueDocs.sourceOf(file)), + getFileLastUpdateInfo: file => + gitVcs.getFileLastUpdateInfo(vueDocs.sourceOf(file)), + }, + }, presets: [ [ '@docusaurus/preset-classic', { docs: { - //id: 'core', - path: '../docs/core', + ...docsLocation('default'), // `exclude` replaces Docusaurus' defaults; keep them so `_` partials aren't published exclude: [ ...GlobExcludeDefault, 'getting-started/README.md', '**/*.vue.{md,mdx}', ], - //routeBasePath: 'core', sidebarPath: require.resolve('./framework-docs/sidebars-react.js'), beforeDefaultRemarkPlugins: reactRemarkPlugins, showLastUpdateAuthor: true, showLastUpdateTime: true, - editUrl: ({ locale, docPath }) => { - /*if (locale !== 'en') { - return `https://crowdin.com/project/docusaurus-v2/${locale}`; - }*/ - // We want users to submit doc updates to the upstream/next version! - // Otherwise we risk losing the update on the next release. - const nextVersionDocsDirPath = 'docs'; - return `https://github.com/reactive/data-client/edit/master/${nextVersionDocsDirPath}/${docPath}`; - }, lastVersion: 'current', includeCurrentVersion: true, versions: { @@ -271,47 +288,34 @@ const config: Config = { [ '@docusaurus/plugin-content-docs', { - id: 'vue', + ...docsLocation('vue'), path: vueDocs.outDir, exclude: [...GlobExcludeDefault, 'getting-started/README.md'], - routeBasePath: 'vue', sidebarPath: require.resolve('./framework-docs/sidebars-vue.js'), beforeDefaultRemarkPlugins: [ [ remarkFramework, { framework: 'vue', - routeBasePath: 'vue', + routeBasePath: vueInstance.routeBasePath, docIds: frameworkDocs.docIds('vue'), }, ], ], - // generated files have no git history - showLastUpdateAuthor: false, - showLastUpdateTime: false, + showLastUpdateAuthor: true, + showLastUpdateTime: true, editUrl: ({ docPath }) => - `https://github.com/reactive/data-client/edit/master/docs/core/${frameworkDocs.sourcePath('vue', docPath)}`, + `${editRoot}/${vueInstance.path}/${frameworkDocs.sourcePath('vue', docPath)}`, }, ], [ '@docusaurus/plugin-content-docs', { - id: 'rest', - path: '../docs/rest', - routeBasePath: 'rest', + ...docsLocation('rest'), sidebarPath: require.resolve('./sidebars-rest.js'), beforeDefaultRemarkPlugins: reactRemarkPlugins, showLastUpdateAuthor: true, showLastUpdateTime: true, - editUrl: ({ locale, docPath }) => { - /*if (locale !== 'en') { - return `https://crowdin.com/project/docusaurus-v2/${locale}`; - }*/ - // We want users to submit doc updates to the upstream/next version! - // Otherwise we risk losing the update on the next release. - const nextVersionDocsDirPath = 'docs/rest'; - return `https://github.com/reactive/data-client/edit/master/${nextVersionDocsDirPath}/${docPath}`; - }, lastVersion: 'current', includeCurrentVersion: true, versions: { @@ -325,22 +329,11 @@ const config: Config = { [ '@docusaurus/plugin-content-docs', { - id: 'graphql', - path: '../docs/graphql', - routeBasePath: 'graphql', + ...docsLocation('graphql'), sidebarPath: require.resolve('./sidebars-graphql.js'), beforeDefaultRemarkPlugins: reactRemarkPlugins, showLastUpdateAuthor: true, showLastUpdateTime: true, - editUrl: ({ locale, docPath }) => { - /*if (locale !== 'en') { - return `https://crowdin.com/project/docusaurus-v2/${locale}`; - }*/ - // We want users to submit doc updates to the upstream/next version! - // Otherwise we risk losing the update on the next release. - const nextVersionDocsDirPath = 'docs/graphql'; - return `https://github.com/reactive/data-client/edit/master/${nextVersionDocsDirPath}/${docPath}`; - }, lastVersion: 'current', includeCurrentVersion: true, versions: { @@ -356,7 +349,8 @@ const config: Config = { { // Vue docs briefly lived at /docs/vue createRedirects(existingPath: string) { - if (existingPath === '/vue' || existingPath.startsWith('/vue/')) + const vue = `/${vueInstance.routeBasePath}`; + if (existingPath === vue || existingPath.startsWith(`${vue}/`)) return `/docs${existingPath}`; }, redirects: [ @@ -424,16 +418,7 @@ const config: Config = { path.resolve(__dirname, './node-plugin'), path.resolve(__dirname, './profiling-plugin'), path.resolve(__dirname, './raw-plugin'), - [ - path.resolve(__dirname, './llms-plugin'), - { - frameworks: { - react: { id: 'default', path: '/', name: 'React' }, - vue: { id: 'vue', path: '/vue/', name: 'Vue' }, - }, - shared: { rest: 'REST', graphql: 'GraphQL' }, - }, - ], + path.resolve(__dirname, './llms-plugin'), ], themeConfig: { mermaid: { diff --git a/website/framework-docs/README.md b/website/framework-docs/README.md index 112a52f5ca37..6333411897d7 100644 --- a/website/framework-docs/README.md +++ b/website/framework-docs/README.md @@ -84,6 +84,9 @@ no text left for a framework (e.g. only `:react[...]`) is dropped from that fram - Docusaurus can't point two docs instances at one folder, so `index.js` mirrors `docs/core` into `docs/.core-vue` (gitignored; a sibling so relative imports into `docs/rest` keep working), applying `.vue.md` overrides, `vue_` front matter and `frameworks:` filtering. It runs on config load and re-syncs on change during `yarn start`. +- `docsInstances.js` lists every docs instance (id, source folder, route, `llms.txt` path). + `docusaurus.config.ts`, `docsToMarkdown.mjs`, `llms-plugin.js` and `remarkFramework.js` + (`FRAMEWORKS`) read routes and frameworks from it. - `FrameworkSelector` (in the breadcrumbs) switches to the same page in the other docs instance, and disables a framework when the page doesn't exist there. diff --git a/website/framework-docs/docsInstances.js b/website/framework-docs/docsInstances.js new file mode 100644 index 000000000000..b35978e3f86e --- /dev/null +++ b/website/framework-docs/docsInstances.js @@ -0,0 +1,55 @@ +/* global module */ +/** + * Every docs instance on the site, read by docusaurus.config.ts (plugin + * options), docsToMarkdown.mjs (routes) and llms-plugin.js (llms.txt). + * + * Instances with a `framework` render docs/core for that framework (Vue via + * the generated mirror, see index.js); the rest are shared by every framework. + * + * - id: docs plugin instance id + * - path: source folder, relative to the repo root (for Vue, the source its + * mirror is generated from) + * - routeBasePath: site route of the instance + * - llms: where that framework's llms.txt and llms-full.txt are served + */ +const DOCS_INSTANCES = [ + { + id: 'default', + framework: 'react', + name: 'React', + path: 'docs/core', + routeBasePath: 'docs', + llms: '/', + }, + { + id: 'vue', + framework: 'vue', + name: 'Vue', + path: 'docs/core', + routeBasePath: 'vue', + llms: '/vue/', + }, + { id: 'rest', name: 'REST', path: 'docs/rest', routeBasePath: 'rest' }, + { + id: 'graphql', + name: 'GraphQL', + path: 'docs/graphql', + routeBasePath: 'graphql', + }, +]; + +/** Instances rendering docs/core, one per framework */ +const FRAMEWORK_INSTANCES = DOCS_INSTANCES.filter(d => d.framework); +const FRAMEWORKS = FRAMEWORK_INSTANCES.map(d => d.framework); + +const docsInstance = id => DOCS_INSTANCES.find(d => d.id === id); +const frameworkInstance = framework => + FRAMEWORK_INSTANCES.find(d => d.framework === framework); + +module.exports = { + DOCS_INSTANCES, + FRAMEWORK_INSTANCES, + FRAMEWORKS, + docsInstance, + frameworkInstance, +}; diff --git a/website/framework-docs/docsToMarkdown.mjs b/website/framework-docs/docsToMarkdown.mjs index 181c1303c31e..c9b2b4058752 100644 --- a/website/framework-docs/docsToMarkdown.mjs +++ b/website/framework-docs/docsToMarkdown.mjs @@ -26,6 +26,7 @@ const require = createRequire(import.meta.url); const preprocessContent = require('@docusaurus/mdx-loader/lib/preprocessor').default; +const { DOCS_INSTANCES, frameworkInstance } = require('./docsInstances.js'); const { docIds, docIdOf, @@ -37,12 +38,13 @@ const remarkFramework = require('./remarkFramework.js'); export { ROOT, SITE, rel }; -/** docs folder -> route base per framework; keep in sync with docusaurus.config.ts */ -const ROUTES = [ - ['docs/core/', { react: '/docs/', vue: '/vue/' }], - ['docs/rest/', { react: '/rest/', vue: '/rest/' }], - ['docs/graphql/', { react: '/graphql/', vue: '/graphql/' }], -]; +/** Docs instance rendering `relPath` (from the repo root) for a framework */ +const instanceOf = (relPath, framework) => + DOCS_INSTANCES.find( + d => + relPath.startsWith(`${d.path}/`) && + (d.framework ?? framework) === framework, + ); const vueIds = docIds('vue'); const MD = /\.mdx?$/; @@ -107,16 +109,18 @@ const parse = memoize(file => { /** Site route (no host) of a doc for a framework */ export const routeOf = memoize((file, framework) => { const relPath = rel(file).replace(/\.(react|vue)(\.mdx?)$/, '$2'); - const match = ROUTES.find(([dir]) => relPath.startsWith(dir)); - if (!match) return; - const [dir, bases] = match; - const docId = docIdOf(relPath.slice(dir.length), contentFor(file, framework)); + const instance = instanceOf(relPath, framework); + if (!instance) return; + const docId = docIdOf( + relPath.slice(instance.path.length + 1), + contentFor(file, framework), + ); // Vue links to pages without a Vue version go to the React docs - const base = - dir === 'docs/core/' && framework === 'vue' && !vueIds.has(docId) ? - bases.react - : bases[framework]; - return `${base}${docId}`.replace(/\/index$/, '/'); + const { routeBasePath } = + instance.framework === 'vue' && !vueIds.has(docId) ? + frameworkInstance('react') + : instance; + return `/${routeBasePath}/${docId}`.replace(/\/index$/, '/'); }); /** Relative doc links become site routes; absolute ones are left to remarkFramework */ @@ -482,7 +486,11 @@ function render(file, framework, props = {}) { tree.children = convertAll(tree.children); // absolute /docs links point at this framework's docs, as on the site if (framework === 'vue') - remarkFramework({ framework, routeBasePath: 'vue', docIds: vueIds })(tree); + remarkFramework({ + framework, + routeBasePath: frameworkInstance('vue').routeBasePath, + docIds: vueIds, + })(tree); tree.title = frontMatterValue(content, 'title'); return tree; } diff --git a/website/framework-docs/index.js b/website/framework-docs/index.js index 3831008e481c..06b9ac2440b4 100644 --- a/website/framework-docs/index.js +++ b/website/framework-docs/index.js @@ -9,9 +9,9 @@ const fs = require('fs'); const path = require('path'); -const { FRAMEWORKS } = require('./remarkFramework.js'); +const { FRAMEWORKS, frameworkInstance } = require('./docsInstances.js'); -const SRC = path.resolve(__dirname, '../../docs/core'); +const SRC = path.resolve(__dirname, '../..', frameworkInstance('react').path); const MD = /\.mdx?$/; /** `foo.vue.md` replaces `foo.md` for Vue */ const VUE_OVERRIDE = /\.vue(\.mdx?)$/; @@ -115,7 +115,14 @@ function generate(framework) { if (!sources.has(file)) fs.rmSync(path.join(outDir, file)); } } - return { outDir, sources }; + /** Source of a mirror file (itself if not mirrored), for its git history */ + const sourceOf = file => { + const src = sources.get( + path.relative(outDir, file).split(path.sep).join('/'), + ); + return src ? path.join(SRC, src) : file; + }; + return { outDir, sources, sourceOf }; } /** Keep the mirror in sync during `docusaurus start` */ diff --git a/website/framework-docs/remarkFramework.js b/website/framework-docs/remarkFramework.js index 8087eadbcd38..be21cd9542a0 100644 --- a/website/framework-docs/remarkFramework.js +++ b/website/framework-docs/remarkFramework.js @@ -1,4 +1,4 @@ -/* global module */ +/* global module, require */ /** * Remark plugin that resolves framework-specific content in shared docs. * @@ -16,7 +16,8 @@ * instance when the target doc exists in it (`docIds`), so Vue pages link to * Vue pages; links to React-only docs keep going to /docs. */ -const FRAMEWORKS = ['react', 'vue']; +const { FRAMEWORKS } = require('./docsInstances.js'); + const DIRECTIVES = ['containerDirective', 'leafDirective', 'textDirective']; /** Heading left with no text (at most a `{#id}`) once the other framework's content is removed */ diff --git a/website/framework-docs/skillReferences.mjs b/website/framework-docs/skillReferences.mjs index 14a0b7fd2274..034175d7abd5 100644 --- a/website/framework-docs/skillReferences.mjs +++ b/website/framework-docs/skillReferences.mjs @@ -14,6 +14,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { frameworkInstance } from './docsInstances.js'; import { docToMarkdown, routeOf } from './docsToMarkdown.mjs'; import { ROOT, SITE, rel } from './site.mjs'; @@ -21,6 +22,8 @@ const SKILLS = path.join(ROOT, '.agents/skills'); const MANIFEST = 'references.json'; const HEADER = '