diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index ea717d99..00000000 --- a/.eslintignore +++ /dev/null @@ -1,9 +0,0 @@ -public -.next -package-lock.json -yarn.lock -bundler/dist -dist -build -_docs.page -og \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index d07bc392..00000000 --- a/.eslintrc +++ /dev/null @@ -1,40 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "extends": [ - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - "plugin:react/recommended", - "prettier" - ], - "parserOptions": { - "ecmaVersion": 2020, - // Allows for the parsing of modern ECMAScript features - "sourceType": "module" - // Allows for the use of imports - }, - "settings": { - "react": { - "version": "17.0.0" - } - }, - "env": { - "es6": true, - "node": true - }, - "rules": { - "react/no-unescaped-entities": 0, - "react/display-name": 0, - // All images are from remote sources - "@next/next/no-img-element": 0, - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/camelcase": 0, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-non-null-assertion": 0, - // suppress errors for missing 'import React' in files - "react/react-in-jsx-scope": "off", - "react/prop-types": "off", - // allow jsx syntax in js files (for next.js project) - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }] - } -} diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml new file mode 100644 index 00000000..f0067e8b --- /dev/null +++ b/.github/workflows/pull_request.yaml @@ -0,0 +1,17 @@ +name: Code quality + +on: + pull_request: + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + - name: Run Biome + run: biome ci . \ No newline at end of file diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml deleted file mode 100644 index 6884e635..00000000 --- a/.github/workflows/website.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: website - -on: - pull_request: - push: - branches: - - main - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2-beta - with: - node-version: '14' - - name: NPM Install - run: yarn - - name: Check Linting - run: yarn run check:linting - - name: Check Formatting - run: yarn run check:formatting diff --git a/.gitignore b/.gitignore index e2aebea7..04871a10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# We ignore this since we're using Bun for workspace management, and some environments +# will detect this file and use npm instead of bun. +package-lock.json + # dependencies node_modules /.pnp diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index c56984ad..00000000 --- a/.prettierignore +++ /dev/null @@ -1,8 +0,0 @@ -.next -node_modules -build -dist -domains.json -spelling.json -_docs.page -docs/components/tabs.mdx \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 58c3742b..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "arrowParens": "avoid", - "trailingComma": "all", - "useTabs": false, - "semi": true, - "singleQuote": true, - "bracketSpacing": true, - "tabWidth": 2, - "printWidth": 100 -} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1c93ae8d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +This repository uses [Bun](https://bun.sh/) for workspace and dependency management. To get started, run the following commands: + +```bash +bun install +``` + +The repository is structured as follows: + +- `api`: The API server which is served via `https://api.docs.page`. This is an express application which handles tasks such as fetching content from GitHub and markdown parsing. +- `og`: A Next.js application which serves the Open Graph images for documentation pages. +- `website`: A Remix application which serves the main `https://docs.page` website, and the documentation rendering for each repository. +- `packages/cli`: A CLI for running various commands and scripts for initialization, checking etc. Used locally and on CI environments. + +## Running docs.page + +Generally, you'll want to interface with the website and api. To run these concurrently, you can use the following command: + +```bash +bun dev +``` + +This will start the website on `http://localhost:5173` and the api on `http://localhost:8080`. + +> The API requires a `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` to be set in your environment. These are used to authenticate with the GitHub API. You can create a GitHub App in your GitHub account settings. \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index e984ef7e..11640129 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -13,14 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM oven/bun:1.0.29 +FROM oven/bun:1.1.22 ARG BUILD_SHA=default_value ENV __BUILD_SHA=$BUILD_SHA WORKDIR /opt/app -COPY . /opt/app -RUN cd /opt/app && bun install --frozen-lockfile --production +COPY . . +RUN bun install --frozen-lockfile --production + +WORKDIR /opt/app/api -WORKDIR /opt/app CMD ["bun","run","start"] diff --git a/api/bun.lockb b/api/bun.lockb deleted file mode 100755 index b7a1f90b..00000000 Binary files a/api/bun.lockb and /dev/null differ diff --git a/api/package.json b/api/package.json index b67ae62f..86c8c276 100644 --- a/api/package.json +++ b/api/package.json @@ -8,15 +8,17 @@ "license": "MIT", "dependencies": { "@code-hike/mdx": "0.7.2", + "@docs.page/cli": "workspace:*", "@mdx-js/mdx": "2.2.1", "@octokit/graphql": "5.0.0", - "@types/cors": "^2.8.12", + "@octokit/webhooks": "^13.2.7", + "@shikijs/transformers": "1.12.1", + "@shikijs/twoslash": "^1.12.1", "@types/express": "^4.17.13", "@types/morgan": "^1.9.3", "@types/node": "^18.0.0", "a2a": "^0.2.0", "camelcase": "7.0.0", - "cors": "2.8.5", "dotenv": "16.0.1", "esbuild": "0.14.47", "express": "4.18.1", @@ -26,11 +28,12 @@ "hast-util-parse-selector": "3.1.0", "is-badge": "^2.1.0", "js-yaml": "4.1.0", + "jszip": "^3.10.1", + "lodash.get": "^4.4.2", "mdx-bundler": "9.0.1", "morgan": "1.10.0", "node-fetch": "3.2.6", - "pm2": "5.3.0", - "probot": "12.2.8", + "octokit": "^4.0.2", "rehype-accessible-emojis": "0.3.2", "rehype-katex": "6.0.2", "rehype-slug": "5.0.1", @@ -38,9 +41,10 @@ "remark-gfm": "3.0.1", "remark-math": "5.1.1", "remark-parse": "10.0.1", - "shiki": "1.1.7", + "shiki": "1.12.1", "unist-util-visit": "4.1.0", "zod": "3.22.4", + "zod-to-json-schema": "^3.23.1", "zod-validation-error": "0.2.2" }, "scripts": { @@ -49,9 +53,11 @@ }, "devDependencies": { "@octokit/webhooks-types": "^6.2.4", + "@types/bun": "^1.1.6", "@types/js-yaml": "^4.0.5", + "@types/lodash.get": "^4.4.9", "rollup": "3.9.1", "ts-node": "^10.8.1", - "typescript": "4.7.4" + "typescript": "5.5.3" } } diff --git a/api/src/app.ts b/api/src/app.ts index bc922732..6db633dd 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,35 +1,32 @@ -import express, { Router, text } from 'express'; -import morgan from 'morgan'; -import cors from 'cors'; -import { config } from 'dotenv'; +import express, { Router, text } from "express"; +import morgan from "morgan"; -import bundle from './routes/bundle'; -import preview from './routes/preview'; -import mdx from './routes/mdx'; -import probot from './probot'; -import { notFound } from './res'; +import { notFound } from "./res"; +import bundle from "./routes/bundle"; +import preview from "./routes/preview"; +import schema from "./routes/schema"; +import githubWebhook from "./routes/webhooks.github"; -config(); +import { ENV } from "./env"; -const PORT = process.env.PORT || 8080; +const PORT = ENV.PORT; const app = express(); app.use(text()); -app.use(cors()); -app.use(morgan('dev')); +app.use(morgan("dev")); app.use(express.json()); app.use( express.urlencoded({ extended: true, }), ); -app.use(probot); const router = Router(); -router.get('/status', (_, res) => res.status(200).send('OK')); -router.post('/preview', preview); -router.get('/bundle', bundle); -router.post('/mdx', mdx); -router.all('*', (_, res) => notFound(res)); +router.get("/status", (_, res) => res.status(200).send("OK")); +router.get("/schema.json", schema); +router.post("/preview", preview); +router.get("/bundle", bundle); +router.post("/webhooks/github", githubWebhook); +router.all("*", (_, res) => notFound(res)); app.use(router); app.listen(PORT, () => { diff --git a/api/src/bundler/error.ts b/api/src/bundler/error.ts new file mode 100644 index 00000000..98423ec8 --- /dev/null +++ b/api/src/bundler/error.ts @@ -0,0 +1,23 @@ +export class BundlerError extends Error { + code: number; + name: string; + source?: string; + + constructor({ + code, + name, + message, + source, + }: { + code: number; + name: string; + message: string; + source?: string; + }) { + super(message); + this.code = code; + this.name = name; + this.message = message; + this.source = source; + } +} diff --git a/api/src/bundler/index.ts b/api/src/bundler/index.ts index 02b14c6e..a428c4e3 100644 --- a/api/src/bundler/index.ts +++ b/api/src/bundler/index.ts @@ -1,52 +1,60 @@ -import parseConfig, { Config, defaultConfig } from '../utils/config'; -import { getGitHubContents, getPullRequestMetadata } from '../utils/github'; -import { bundle } from './mdx'; -import { escapeHtml } from '../utils/sanitize'; - -export class BundlerError extends Error { - code: number; - links?: { title: string; url: string }[]; - - constructor({ - code, - name, - message, - cause, - links, - }: { - code: number; - name: string; - message: string; - cause?: string; - links?: { title: string; url: string }[]; - }) { - super(message); - this.code = code; - this.name = name; - this.message = message; - this.cause = cause; - this.links = links; - } -} +import { type Config, defaultConfig, parseConfig } from "../config"; +import { getGitHubContents, getPullRequestMetadata } from "../utils/github"; +import { escapeHtml } from "../utils/sanitize"; +import { replaceMoustacheVariables } from "../utils/variables"; +import { BundlerError } from "./error"; +import { parseMdx } from "./mdx"; +import type { HeadingNode } from "./plugins/rehype-headings"; export const ERROR_CODES = { - REPO_NOT_FOUND: 'REPO_NOT_FOUND', - FILE_NOT_FOUND: 'FILE_NOT_FOUND', - BUNDLE_ERROR: 'BUNDLE_ERROR', + CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND", + REPO_NOT_FOUND: "REPO_NOT_FOUND", + FILE_NOT_FOUND: "FILE_NOT_FOUND", + BUNDLE_ERROR: "BUNDLE_ERROR", } as const; +export type ErrorCodes = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; + type Source = { - type: 'PR' | 'commit' | 'branch'; + type: "PR" | "commit" | "branch"; owner: string; repository: string; ref?: string; }; -class Bundler { +export type BundlerOutput = { + source: Source; + ref: string; + stars: number; + forks: number; + private: boolean; + baseBranch: string; + path: string; + config: Config; + markdown: string; + headings: HeadingNode[]; + frontmatter: Record; + code: string; +}; + +type CreateBundlerParams = { + // The owner of the repository to bundle. + owner: string; + // The repository to bundle. + repository: string; + // The path to file in the repository to bundle. + path: string; + // An optional ref to use for the content. + ref?: string; + // A list of components which are supported for bundling. + components?: Array; +}; + +export class Bundler { readonly #owner: string; readonly #repository: string; readonly #path: string; - #notices: Array = []; + readonly #components: Array; #ref: string | undefined; #source?: Source; #config?: Config; @@ -57,6 +65,7 @@ class Bundler { this.#repository = params.repository; this.#path = params.path; this.#ref = params.ref; + this.#components = params.components || []; } /** @@ -68,10 +77,14 @@ class Bundler { if (this.#ref) { // If the ref is a PR if (/^[0-9]*$/.test(this.#ref)) { - const pullRequest = await getPullRequestMetadata(this.#owner, this.#repository, this.#ref); + const pullRequest = await getPullRequestMetadata( + this.#owner, + this.#repository, + this.#ref, + ); if (pullRequest) { return { - type: 'PR', + type: "PR", ...pullRequest, }; } @@ -80,7 +93,7 @@ class Bundler { // If the ref is a commit hash if (/^[a-fA-F0-9]{40}$/.test(this.#ref)) { return { - type: 'commit', + type: "commit", owner: this.#owner, repository: this.#repository, ref: this.#ref, @@ -89,7 +102,7 @@ class Bundler { } return { - type: 'branch', + type: "branch", owner: this.#owner, repository: this.#repository, ref: this.#ref, @@ -99,7 +112,7 @@ class Bundler { /** * Builds the payload with the MDX bundle. */ - build = async () => { + async build(): Promise { // Get the real source of the request this.#source = await this.getSource(); @@ -117,7 +130,21 @@ class Bundler { throw new BundlerError({ code: 404, name: ERROR_CODES.REPO_NOT_FOUND, - message: `The repository ${this.#source.owner}/${this.#source.repository} was not found.`, + message: `The repository ${this.#source.owner}/${ + this.#source.repository + } was not found.`, + }); + } + + if (!metadata.config.configJson && !metadata.config.configYaml) { + throw new BundlerError({ + code: 404, + name: ERROR_CODES.CONFIG_NOT_FOUND, + message: + "No configuration file was found in the repository. To get started, create a docs.json file at the root of your repository.", + source: `https://github.com/${this.#source.owner}/${ + this.#source.repository + }`, }); } @@ -125,17 +152,12 @@ class Bundler { throw new BundlerError({ code: 404, name: ERROR_CODES.FILE_NOT_FOUND, - message: `The file "/docs/${this.#path}.mdx" or "/docs/${ + message: `No file was found in the repository matching this path. Ensure a file exists at /docs/${ this.#path - }/index.mdx" in repository /${ - this.#source.owner + '/' + this.#source.repository - } was not found.`, - links: [ - { - title: 'Repository link', - url: `https://github.com/${this.#source.owner}/${this.#source.repository}`, - }, - ], + }.mdx or /docs/${this.#path}/index.mdx.`, + source: `https://github.com/${this.#source.owner}/${ + this.#source.repository + }`, }); } @@ -154,59 +176,41 @@ class Bundler { yaml: metadata.config.configYaml, }); } catch { - this.#notices.push( - 'The configuration file is invalid, falling back to the default configuration.', - ); this.#config = defaultConfig; } try { // Bundle the markdown file via MDX. - const mdx = await bundle(this.#markdown, { - headerDepth: this.#config.headerDepth, + const mdx = await parseMdx(this.#markdown, { + headerDepth: this.#config.content?.headerDepth ?? 3, + components: this.#components, }); return { source: this.#source, ref: this.#ref, + stars: metadata.stars, + forks: metadata.forks, + private: metadata.isPrivate, baseBranch: metadata.baseBranch, - notices: this.#notices, path: this.#path, config: this.#config, markdown: this.#markdown, headings: mdx.headings, frontmatter: mdx.frontmatter, - code: mdx.code, + code: replaceMoustacheVariables(this.#config.variables ?? {}, mdx.code), }; } catch (e) { console.error(e); // @ts-ignore - const message = escapeHtml(e?.message || ''); throw new BundlerError({ code: 500, name: ERROR_CODES.BUNDLE_ERROR, message: `Something went wrong while bundling the file /${metadata.path}.mdx. Are you sure the MDX is valid?`, - cause: message, - links: [ - { - title: `/${metadata.path}.mdx on GitHub`, - url: `https://github.com/${this.#source.owner}/${this.#source.repository}/blob/${ - this.#ref - }/${metadata.path}.mdx`, - }, - ], + source: `https://github.com/${this.#source.owner}/${ + this.#source.repository + }`, }); } - }; -} - -type CreateBundlerParams = { - owner: string; - repository: string; - path: string; - ref?: string; -}; - -export default function bundler(params: CreateBundlerParams) { - return new Bundler(params).build(); + } } diff --git a/api/src/bundler/mdx.ts b/api/src/bundler/mdx.ts index fa5c6de6..4f898d49 100644 --- a/api/src/bundler/mdx.ts +++ b/api/src/bundler/mdx.ts @@ -1,10 +1,10 @@ -import { compile } from '@mdx-js/mdx'; -import { Message } from 'esbuild'; -import frontmatter from 'gray-matter'; -import { getRehypePlugins, getRemarkPlugins } from './plugins/index'; -import rehypeHeadings, { HeadingNode } from './plugins/rehype-headings'; +import { compile } from "@mdx-js/mdx"; +import type { Message } from "esbuild"; +import frontmatter from "gray-matter"; +import { getRehypePlugins, getRemarkPlugins } from "./plugins/index"; +import rehypeHeadings, { type HeadingNode } from "./plugins/rehype-headings"; -type MdxBundlerResponse = { +type MdxResponse = { code: string; frontmatter: Record; errors: Message[]; @@ -22,12 +22,13 @@ export function headerDepthToHeaderList(depth: number): string[] { return list; } -export async function bundle( +export async function parseMdx( rawText: string, options: { headerDepth: number; + components: Array; }, -): Promise { +): Promise { const output = { headings: [] as HeadingNode[], frontmatter: {} as { [key: string]: string }, @@ -40,9 +41,11 @@ export async function bundle( // prevent this error `_jsxDEV is not a function` // enable next line // development: process.env.NODE_ENV === 'production', - format: 'mdx', - outputFormat: 'function-body', - remarkPlugins: getRemarkPlugins(), + format: "mdx", + outputFormat: "function-body", + remarkPlugins: getRemarkPlugins({ + components: options.components, + }), rehypePlugins: [ ...getRehypePlugins(), [ diff --git a/api/src/bundler/plugins/index.ts b/api/src/bundler/plugins/index.ts index 979c69e0..0ade873f 100644 --- a/api/src/bundler/plugins/index.ts +++ b/api/src/bundler/plugins/index.ts @@ -1,26 +1,32 @@ -import type { PluggableList } from '@mdx-js/mdx/lib/core'; +import type { PluggableList } from "@mdx-js/mdx/lib/core"; +import remarkComment from "remark-comment"; // Remark Plugins -import remarkGfm from 'remark-gfm'; -import remarkComment from 'remark-comment'; -import remarkComponentCheck from './remark-component-check'; -import remarkUndeclaredVariables from './remark-undeclared-variables'; +import remarkGfm from "remark-gfm"; +import remarkComponentCheck from "./remark-component-check"; +import remarkUndeclaredVariables from "./remark-undeclared-variables"; // import { remarkCodeHike } from '@code-hike/mdx'; // import { theme as codeHikeTheme } from './codeHikeTheme'; +import { rehypeAccessibleEmojis } from "rehype-accessible-emojis"; // Rehype Plugins -import rehypeSlug from 'rehype-slug'; -import { rehypeAccessibleEmojis } from 'rehype-accessible-emojis'; -import rehypeCodeBlocks from './rehype-code-blocks'; -import rehypeInlineBadges from './rehype-inline-badges'; +import rehypeSlug from "rehype-slug"; +import rehypeCodeBlocks from "./rehype-code-blocks"; +import rehypeInlineBadges from "./rehype-inline-badges"; type PluginOptions = { + components?: Array; codeHike?: boolean; math?: boolean; }; export function getRemarkPlugins(options?: PluginOptions): PluggableList { - const plugins = [remarkComponentCheck, remarkUndeclaredVariables, remarkGfm, remarkComment]; + const plugins = [ + remarkComponentCheck(options?.components ?? []), + remarkUndeclaredVariables, + remarkGfm, + remarkComment, + ]; if (options?.codeHike) { // plugins.push([remarkCodeHike, { theme: codeHikeTheme, lineNumbers: true }]); @@ -30,7 +36,12 @@ export function getRemarkPlugins(options?: PluginOptions): PluggableList { } export function getRehypePlugins(options?: PluginOptions): PluggableList { - const plugins = [rehypeCodeBlocks, rehypeSlug, rehypeInlineBadges, rehypeAccessibleEmojis]; + const plugins = [ + rehypeCodeBlocks, + rehypeSlug, + rehypeInlineBadges, + rehypeAccessibleEmojis, + ]; if (options?.codeHike) { // plugins.push([]); diff --git a/api/src/bundler/plugins/rehype-code-blocks.ts b/api/src/bundler/plugins/rehype-code-blocks.ts index 55648e44..5c2001ce 100644 --- a/api/src/bundler/plugins/rehype-code-blocks.ts +++ b/api/src/bundler/plugins/rehype-code-blocks.ts @@ -1,48 +1,72 @@ -import { visit } from 'unist-util-visit'; -import { Node } from 'unist'; -import { toString } from 'mdast-util-to-string'; -import * as shiki from 'shiki'; -import { Element } from 'hast'; +import { + transformerMetaHighlight, + transformerNotationDiff, + transformerNotationFocus, + transformerNotationHighlight, + transformerRemoveNotationEscape, +} from "@shikijs/transformers"; +import { transformerTwoslash } from "@shikijs/twoslash"; +import type { Element } from "hast"; +import { toString } from "mdast-util-to-string"; +import * as shiki from "shiki"; +import type { Node } from "unist"; +import { visit } from "unist-util-visit"; let highlighter: shiki.Highlighter; const languages: Record = { - '': 'text', - gradle: 'groovy', + "": "text", + gradle: "groovy", }; -shiki.bundledLanguagesInfo.forEach(lang => { + +for (const lang of shiki.bundledLanguagesInfo) { languages[lang.id] = lang.id; for (const alias of lang.aliases || []) { languages[alias] = lang.id; } -}); +} const cssVariablesTheme = shiki.createCssVariablesTheme({ - name: 'css-variables', - variablePrefix: '--shiki-', + name: "css-variables", + variablePrefix: "--shiki-", variableDefaults: {}, fontStyle: true, }); export default function rehypeCodeBlocks(): (ast: Node) => void { function visitor(node: Element, _i: number, parent: Element) { - if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') { + if (!parent || parent.tagName !== "pre" || node.tagName !== "code") { return; } const raw = toString(node); - const blockLanguage = getLanguage(node) || ''; - const languageActual: string = languages[blockLanguage] || 'text'; + const blockLanguage = getLanguage(node) || ""; + const languageActual: string = languages[blockLanguage] || "text"; if (!parent.properties) parent.properties = {}; - parent.properties['raw'] = raw; // Used to support copy/paste functionality, - parent.properties['language'] = languageActual; - parent.properties['html'] = highlighter.codeToHtml(raw, { + parent.properties.raw = raw; // Used to support copy/paste functionality, + parent.properties.language = languageActual; + + const transformers = [ + transformerNotationDiff(), + transformerNotationHighlight(), + transformerRemoveNotationEscape(), + transformerNotationFocus(), + transformerMetaHighlight(), + ]; + + if (languageActual === "typescript") { + transformers.push(transformerTwoslash()); + } + + parent.properties.html = highlighter.codeToHtml(raw, { lang: languageActual, - theme: 'css-variables', + theme: "css-variables", + transformers, }); - const meta = (node.data?.meta as string) ?? ''; + + const meta = (node.data?.meta as string) ?? ""; const title = extractTitle(meta); - if (title) parent.properties['title'] = title; + if (title) parent.properties.title = title; } return async (ast: Node): Promise => { if (!highlighter) { @@ -51,7 +75,7 @@ export default function rehypeCodeBlocks(): (ast: Node) => void { themes: [cssVariablesTheme], }); } - visit(ast, 'element', visitor); + visit(ast, "element", visitor); }; } @@ -66,7 +90,9 @@ function extractTitle(meta: string): string | null { return null; } - const title = Object.values(match.groups ?? []).find(value => value !== undefined); + const title = Object.values(match.groups ?? []).find( + (value) => value !== undefined, + ); return title || null; } @@ -78,15 +104,15 @@ function getLanguage(node: Element): string | undefined { while (++index < className.length) { value = className[index]; - if (value === 'no-highlight' || value === 'nohighlight') { + if (value === "no-highlight" || value === "nohighlight") { return undefined; } - if (value.slice(0, 5) === 'lang-') { + if (value.slice(0, 5) === "lang-") { return value.slice(5); } - if (value.slice(0, 9) === 'language-') { + if (value.slice(0, 9) === "language-") { return value.slice(9); } } diff --git a/api/src/bundler/plugins/rehype-headings.ts b/api/src/bundler/plugins/rehype-headings.ts index e02e0e08..c12b860c 100644 --- a/api/src/bundler/plugins/rehype-headings.ts +++ b/api/src/bundler/plugins/rehype-headings.ts @@ -1,11 +1,11 @@ -import { visit } from 'unist-util-visit'; -import { hasProperty } from 'hast-util-has-property'; -import { headingRank } from 'hast-util-heading-rank'; -import type { Node } from 'hast-util-heading-rank/lib'; -import { toString } from 'mdast-util-to-string'; -import { parseSelector } from 'hast-util-parse-selector'; -import { Data as UnistData, Node as UnistNode } from 'unist'; -import { Element as HastElement, Content as HastContent } from 'hast'; +import type { Content as HastContent, Element as HastElement } from "hast"; +import { hasProperty } from "hast-util-has-property"; +import { headingRank } from "hast-util-heading-rank"; +import type { Node } from "hast-util-heading-rank/lib"; +import { parseSelector } from "hast-util-parse-selector"; +import { toString } from "mdast-util-to-string"; +import type { Data as UnistData, Node as UnistNode } from "unist"; +import { visit } from "unist-util-visit"; export type HeadingNode = { id: string; @@ -19,7 +19,7 @@ type RehypeHeadingsOptions = { }; const defaultOptions: RehypeHeadingsOptions = { - headings: ['h2', 'h3', 'h4', 'h5', 'h6'], + headings: ["h2", "h3", "h4", "h5", "h6"], callback: () => { return; }, @@ -44,7 +44,7 @@ export default function rehypeHeadings( const nodes: HeadingNode[] = []; function visitor(node: HastElement): void { - if (headingRank(node) && hasProperty(node, 'id')) { + if (headingRank(node) && hasProperty(node, "id")) { if (options.headings.includes(node.tagName as string)) { nodes.push({ id: (node.properties as Record).id, @@ -56,32 +56,38 @@ export default function rehypeHeadings( } function newVisitor(node: Node & { children: HastElement[] }) { - const newChildren = partition(node.children, headingTest).map(part => { - const id = - ( - part.filter((child: Node) => headingTest(child))[0] as HastElement - )?.properties?.id?.toString() || ''; + const newChildren = partition(node.children, headingTest).map( + (part) => { + const id = + ( + part.filter((child: Node) => headingTest(child))[0] as HastElement + )?.properties?.id?.toString() || ""; - return wrapSection(part as HastElement[], id); - }); + return wrapSection(part as HastElement[], id); + }, + ); node.children = newChildren; } return (ast: UnistNode): void => { - visit(ast, 'element', visitor); - visit(ast, 'root', newVisitor); + visit(ast, "element", visitor); + visit(ast, "root", newVisitor); options.callback(nodes); }; } -const wrapSection: (children: HastElement[], id: string) => HastElement = (children, id) => { - const wrap = parseSelector(`section${id ? `#${id}` : ''}`); +const wrapSection: (children: HastElement[], id: string) => HastElement = ( + children, + id, +) => { + const wrap = parseSelector(`section${id ? `#${id}` : ""}`); wrap.children = children; return wrap; }; -const headingTest: (node: Node) => boolean = node => !!headingRank(node) && hasProperty(node, 'id'); +const headingTest: (node: Node) => boolean = (node) => + !!headingRank(node) && hasProperty(node, "id"); // partition an array based on a test function, e.g [a,b,b,b,a,b,b,a,b] should become [[a,b,b,b],[a,b,b],[a,b]] function partition(array: T[], test: (input: T) => boolean): T[][] { @@ -90,6 +96,7 @@ function partition(array: T[], test: (input: T) => boolean): T[][] { return [[current]]; } if (test(current)) { + // biome-ignore lint/performance/noAccumulatingSpread: TODO: Fix this lint return [...prev, [current]]; } diff --git a/api/src/bundler/plugins/rehype-inline-badges.ts b/api/src/bundler/plugins/rehype-inline-badges.ts index 11f910a2..892f15a6 100644 --- a/api/src/bundler/plugins/rehype-inline-badges.ts +++ b/api/src/bundler/plugins/rehype-inline-badges.ts @@ -3,10 +3,10 @@ * @typedef {import('mdast').Content} Content */ -import { isBadge } from 'is-badge'; +import { isBadge } from "is-badge"; -import { visit } from 'unist-util-visit'; -import type { Node } from 'hast-util-heading-rank/lib'; +import type { Node } from "hast-util-heading-rank/lib"; +import { visit } from "unist-util-visit"; /** * Provides a list of heading elements in the AST. @@ -16,8 +16,8 @@ import type { Node } from 'hast-util-heading-rank/lib'; export default function rehypeInlineBadges(): (ast: Node) => void { //@ts-ignore function visitor(node: NodeWithChildren) { - node.visited === 'true'; - node.children[0].properties.style = 'display: inline;'; + node.visited === "true"; + node.children[0].properties.style = "display: inline;"; } return (ast: Node): void => { //@ts-ignore @@ -25,8 +25,8 @@ export default function rehypeInlineBadges(): (ast: Node) => void { }; } //@ts-ignore -const containsBadge = node => - node.tagName === 'a' && - node.children[0].tagName === 'img' && +const containsBadge = (node) => + node.tagName === "a" && + node.children[0].tagName === "img" && isBadge(node?.children[0]?.properties.src) && - node.visited !== 'true'; + node.visited !== "true"; diff --git a/api/src/bundler/plugins/remark-component-check.ts b/api/src/bundler/plugins/remark-component-check.ts index 0dfba114..3e778585 100644 --- a/api/src/bundler/plugins/remark-component-check.ts +++ b/api/src/bundler/plugins/remark-component-check.ts @@ -1,25 +1,6 @@ +import type { Data, Node } from "unist"; /* eslint-disable @typescript-eslint/no-explicit-any */ -import { visit } from 'unist-util-visit'; -import { Node } from 'unist'; - -const components = [ - 'Accordion', - 'CodeGroup', - 'Icon', - 'Info', - 'Warning', - 'Error', - 'Success', - 'Heading', - 'Tweet', - 'Tabs', - 'TabItem', - 'Image', - 'YouTube', - 'Vimeo', - 'Video', - 'Zapp', -]; +import { visit } from "unist-util-visit"; /** * Converts undefined JSX components into plain text nodes @@ -31,47 +12,67 @@ interface DeclaredNode extends Node { } interface UnDeclaredNode extends Node { name: string; - data: any; - value: any; + data?: Data; + value?: string; + attributes?: { + type: string; + name: string; + value: string; + }[]; } -export default function remarkComponentCheck(): (ast: Node) => void { - const keywords = ['var', 'let', 'const', 'function']; - const withExport = keywords.map(k => new RegExp(`(export)[ \t]+${k}[ \t]`)); +export default function remarkComponentCheck( + components: Array, +): () => (ast: Node) => void { + return () => { + const keywords = ["var", "let", "const", "function"]; + const withExport = keywords.map( + (k) => new RegExp(`(export)[ \t]+${k}[ \t]`), + ); - const declared: string[] = []; + const declared: string[] = []; - function visitorForDeclared(node: DeclaredNode) { - // Get the kind of export. This is actually stored in the Node, but the following was quicker for typescript: - const exportKeyword = withExport.filter(re => re.test(node.value))[0]; + function visitorForDeclared(node: DeclaredNode) { + // Get the kind of export. This is actually stored in the Node, but the following was quicker for typescript: + const exportKeyword = withExport.filter((re) => re.test(node.value))[0]; - if (exportKeyword) { - declared.push( - node.value - .replace(exportKeyword, '') - .replace(/^[a-z0-9-_A-Z]*[ \t][a-z0-9-_A-Z]*[ \t]/, '') - .split(' ')[0], - ); + if (exportKeyword) { + declared.push( + node.value + .replace(exportKeyword, "") + .replace(/^[a-z0-9-_A-Z]*[ \t][a-z0-9-_A-Z]*[ \t]/, "") + .split(" ")[0], + ); + } } - } - function visitorForUndeclared(node: UnDeclaredNode) { - // HTML elements are not components (e.g.
) - if (!isUppercase(node.name.charAt(0))) { - return; - } + function visitorForUndeclared(node: UnDeclaredNode) { + // HTML elements are not components (e.g.
) + if (!isUppercase(node.name.charAt(0))) { + return; + } + + if (!declared.includes(node.name) && !components.includes(node.name)) { + // Override all props of the component with our own. + node.attributes = [ + { + type: "mdxJsxAttribute", + name: "name", + value: node.name, + }, + ]; - if (!declared.includes(node.name) && !components.includes(node.name)) { - node.type = 'text'; - node.data = undefined; - node.value = `\<${node.name}\ />`; + // Update the name of the component to the internal docs page component + // which renders a warning message. + node.name = "__InvalidComponent__"; + } } - } - return async (ast: Node): Promise => { - visit(ast, 'mdxjsEsm', visitorForDeclared); - visit(ast, 'mdxJsxFlowElement', visitorForUndeclared); - visit(ast, 'mdxJsxTextElement', visitorForUndeclared); + return async (ast: Node): Promise => { + visit(ast, "mdxjsEsm", visitorForDeclared); + visit(ast, "mdxJsxFlowElement", visitorForUndeclared); + visit(ast, "mdxJsxTextElement", visitorForUndeclared); + }; }; } diff --git a/api/src/bundler/plugins/remark-undeclared-variables.ts b/api/src/bundler/plugins/remark-undeclared-variables.ts index bef5eb25..6755d56c 100644 --- a/api/src/bundler/plugins/remark-undeclared-variables.ts +++ b/api/src/bundler/plugins/remark-undeclared-variables.ts @@ -1,7 +1,6 @@ +import type { Data, Node } from "unist"; /* eslint-disable @typescript-eslint/no-explicit-any */ -// @ts-ignore -import { visit } from 'unist-util-visit'; -import { Node } from 'unist'; +import { visit } from "unist-util-visit"; /** * Converts undeclared variables into plain text nodes @@ -13,40 +12,40 @@ interface DeclaredNode extends Node { } interface UnDeclaredNode extends Node { value: string; - data: any; + data?: Data; } export default function remarkUndeclaredVariables(): (ast: Node) => void { - const keywords = ['var', 'let', 'const', 'function']; - const withExport = keywords.map(k => new RegExp(`(export)[ \t]+${k}[ \t]`)); + const keywords = ["var", "let", "const", "function"]; + const withExport = keywords.map((k) => new RegExp(`(export)[ \t]+${k}[ \t]`)); const declared: string[] = []; function visitorForDeclared(node: DeclaredNode) { // Get the kind of export. This is actually stored in the Node, but the following was quicker for typescript: - const exportKeyword = withExport.filter(re => re.test(node.value))[0]; + const exportKeyword = withExport.filter((re) => re.test(node.value))[0]; if (exportKeyword) { declared.push( node.value - .replace(exportKeyword, '') - .replace(/^[a-z0-9-_A-Z]*[ \t][a-z0-9-_A-Z]*[ \t]/, '') - .split(' ')[0], + .replace(exportKeyword, "") + .replace(/^[a-z0-9-_A-Z]*[ \t][a-z0-9-_A-Z]*[ \t]/, "") + .split(" ")[0], ); } } function visitorForUndeclared(node: UnDeclaredNode) { if (node.value && !declared.includes(node.value)) { - node.type = 'text'; + node.type = "text"; node.data = undefined; node.value = `\{${node.value}\}`; } } return async (ast: Node): Promise => { - visit(ast, 'mdxjsEsm', visitorForDeclared); - visit(ast, 'mdxFlowExpression', visitorForUndeclared); - visit(ast, 'mdxJsxTextElement', visitorForUndeclared); + visit(ast, "mdxjsEsm", visitorForDeclared); + visit(ast, "mdxFlowExpression", visitorForUndeclared); + visit(ast, "mdxJsxTextElement", visitorForUndeclared); }; } diff --git a/api/src/config/index.ts b/api/src/config/index.ts new file mode 100644 index 00000000..3fd86b3d --- /dev/null +++ b/api/src/config/index.ts @@ -0,0 +1,28 @@ +import yaml from "js-yaml"; +import { type Config, ConfigSchema } from "./schema"; +import { V1ConfigSchema } from "./v1.schema"; + +export type { Config } from "./schema"; + +// Given a user config, merges the config with the default config. +export function parseConfig(configs: { json?: string; yaml?: string }): Config { + let parsedConfig: object = {}; + + if (configs.json) { + parsedConfig = JSON.parse(configs.json); + } else if (configs.yaml) { + parsedConfig = yaml.load(configs.yaml) as object; + } + + const isV1Schema = + ("logo" in parsedConfig && typeof parsedConfig.logo === "string") || + ("theme" in parsedConfig && typeof parsedConfig.theme === "string"); + + if (isV1Schema) { + return V1ConfigSchema.parse(parsedConfig); + } + + return ConfigSchema.parse(parsedConfig); +} + +export const defaultConfig = ConfigSchema.parse({}); diff --git a/api/src/config/models/anchors.ts b/api/src/config/models/anchors.ts new file mode 100644 index 00000000..c5ef67aa --- /dev/null +++ b/api/src/config/models/anchors.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +const anchor = z.object({ + icon: z.string().min(1), + title: z.string().min(1), + href: z.string().min(1), + locale: z.string().optional().catch(undefined), + tab: z.string().optional().catch(undefined), +}); + +export default z + .array(anchor.optional().catch(undefined)) + .catch([]) + .transform((val) => { + // Remove any empty (invalid) objects + return val.filter((anchor) => Boolean(anchor)) as Array< + z.infer + >; + }); diff --git a/api/src/config/models/content.ts b/api/src/config/models/content.ts new file mode 100644 index 00000000..14f3975a --- /dev/null +++ b/api/src/config/models/content.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export default z + .object({ + headerDepth: z.number().catch(3), + zoomImages: z.boolean().catch(false), + automaticallyInferNextPrevious: z.boolean().catch(true), + showPageTitle: z.boolean().catch(true), + showPageImage: z.boolean().catch(true), + }) + .catch({ + headerDepth: 3, + zoomImages: false, + automaticallyInferNextPrevious: true, + showPageTitle: true, + showPageImage: true, + }); diff --git a/api/src/config/models/header.ts b/api/src/config/models/header.ts new file mode 100644 index 00000000..777a21ef --- /dev/null +++ b/api/src/config/models/header.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export default z + .object({ + showName: z.boolean().optional().catch(true), + showThemeToggle: z.boolean().optional().catch(true), + showGitHubCard: z.boolean().optional().catch(true), + links: z + .array( + z.object({ + title: z.string().min(1), + href: z.string().min(1), + cta: z.boolean().optional().catch(false), + locale: z.string().optional().catch(undefined), + }), + ) + .optional() + .catch([]), + }) + .catch({ + showName: true, + showThemeToggle: true, + showGitHubCard: true, + links: [], + }); diff --git a/api/src/config/models/logo.ts b/api/src/config/models/logo.ts new file mode 100644 index 00000000..578d5705 --- /dev/null +++ b/api/src/config/models/logo.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export default z + .object({ + href: z.string().optional(), + light: z.string().optional(), + dark: z.string().optional(), + }) + .catch({ + href: undefined, + light: undefined, + dark: undefined, + }); diff --git a/api/src/config/models/scripts.ts b/api/src/config/models/scripts.ts new file mode 100644 index 00000000..4746713c --- /dev/null +++ b/api/src/config/models/scripts.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export default z + .object({ + googleTagManager: z.string().min(1).optional().catch(undefined), + googleAnalytics: z.string().min(1).optional().catch(undefined), + plausible: z + .union([z.string().min(1), z.boolean()]) + .optional() + .catch(undefined), + }) + .catch({ + googleTagManager: undefined, + googleAnalytics: undefined, + plausible: undefined, + }); diff --git a/api/src/config/models/search.ts b/api/src/config/models/search.ts new file mode 100644 index 00000000..c02a5f19 --- /dev/null +++ b/api/src/config/models/search.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +export default z + .object({ + docsearch: z + .object({ + appId: z.string().catch(""), + apiKey: z.string().catch(""), + indexName: z.string().catch(""), + }) + .optional() + .catch(undefined), + }) + .catch({ + docsearch: undefined, + }); diff --git a/api/src/config/models/seo.ts b/api/src/config/models/seo.ts new file mode 100644 index 00000000..b98cebb4 --- /dev/null +++ b/api/src/config/models/seo.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export default z + .object({ + noindex: z.boolean().catch(false), + }) + .catch({ + noindex: false, + }); diff --git a/api/src/config/models/sidebar.ts b/api/src/config/models/sidebar.ts new file mode 100644 index 00000000..b22e18db --- /dev/null +++ b/api/src/config/models/sidebar.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +const optionalString = z + .string() + .optional() + .transform((val) => val || undefined); + +// Represents a single page in the sidebar +const SidebarPageItemSchema = z.object({ + title: z.string().min(1), + href: z.string().min(1), + icon: z + .string() + .min(1) + .optional() + .transform((val) => val || undefined), +}); + +// Represents a group of pages in the sidebar +export type SidebarGroup = { + group?: string; + tab?: string; + href?: string; + icon?: string; + pages: (z.infer | SidebarGroup)[]; +}; + +// The overall schema for the sidebar +const SidebarSchema: z.ZodType = z.lazy(() => + z.object({ + group: optionalString, + tab: optionalString, + href: optionalString, + icon: optionalString, + pages: z.array(z.union([SidebarPageItemSchema, SidebarSchema])), + }), +); + +export type Sidebar = z.infer; + +export default z + .union([z.record(z.array(SidebarSchema)), z.array(SidebarSchema)]) + .catch({}); diff --git a/api/src/config/models/social.ts b/api/src/config/models/social.ts new file mode 100644 index 00000000..ef38c44e --- /dev/null +++ b/api/src/config/models/social.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +export default z + .object({ + preview: z.string().optional().catch(undefined), + website: z.string().optional().catch(undefined), + x: z.string().optional().catch(undefined), + youtube: z.string().optional().catch(undefined), + facebook: z.string().optional().catch(undefined), + instagram: z.string().optional().catch(undefined), + linkedin: z.string().optional().catch(undefined), + github: z.string().optional().catch(undefined), + slack: z.string().optional().catch(undefined), + discord: z.string().optional().catch(undefined), + }) + .catch({ + preview: undefined, + website: undefined, + x: undefined, + youtube: undefined, + facebook: undefined, + instagram: undefined, + linkedin: undefined, + github: undefined, + slack: undefined, + discord: undefined, + }); diff --git a/api/src/config/models/tabs.ts b/api/src/config/models/tabs.ts new file mode 100644 index 00000000..729aa68e --- /dev/null +++ b/api/src/config/models/tabs.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +const tab = z.object({ + id: z.string().min(1), + title: z.string().min(1), + href: z.string().min(1), + locale: z.string().optional().catch(undefined), +}); + +export default z + .array(tab.optional().catch(undefined)) + .catch([]) + .transform((val) => { + // Remove any empty (invalid) objects + return val.filter((anchor) => Boolean(anchor)) as Array< + z.infer + >; + }); diff --git a/api/src/config/models/theme.ts b/api/src/config/models/theme.ts new file mode 100644 index 00000000..2b7d1b82 --- /dev/null +++ b/api/src/config/models/theme.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; + +const hexColor = z + .string() + .optional() + .transform((val) => { + if (!val || !/^#?[0-9A-Fa-f]{6}$/.test(val)) { + return undefined; + } + + return val; + }); + +export default z + .object({ + defaultTheme: z + .union([z.literal("light"), z.literal("dark")]) + .optional() + .catch(undefined), + primary: hexColor, + primaryLight: hexColor, + primaryDark: hexColor, + backgroundLight: hexColor, + backgroundDark: hexColor, + }) + .catch({}); diff --git a/api/src/config/schema.ts b/api/src/config/schema.ts new file mode 100644 index 00000000..a3de169e --- /dev/null +++ b/api/src/config/schema.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +import anchors from "./models/anchors"; +import content from "./models/content"; +import header from "./models/header"; +import logo from "./models/logo"; +import scripts from "./models/scripts"; +import search from "./models/search"; +import seo from "./models/seo"; +import sidebar from "./models/sidebar"; +import social from "./models/social"; +import tabs from "./models/tabs"; +import theme from "./models/theme"; + +export type { Sidebar } from "./models/sidebar"; + +export const ConfigSchema = z + .object({ + // The name of the project + name: z.string().min(1).optional().catch(undefined), + // The description of the project + description: z.string().min(1).optional().catch(undefined), + // The favicon of the project + favicon: z.string().min(1).optional().catch(undefined), + // The preview image used in social media - either a path or a URL, or false to disable + socialPreview: z + .union([z.string().min(1), z.literal(false)]) + .optional() + .catch(undefined), + // The logo of the project, used in the header + logo, + // Theme settings + theme, + // Configuration for the header + header, + // Anchors to display in the sidebar + anchors, + // Social links to display in the footer + social, + // SEO settings + seo, + // Variables to override in mustache templates + variables: z.record(z.any()).catch({}), + // Search settings + search, + // Scripts to include in the documentation + scripts, + // Settings to control content rendering + content, + // Tabs to display in the header + tabs, + // Sidebar configuration + sidebar, + }) + .transform((config) => { + return { + ...config, + // Extract locales from the sidebar configuration + locales: Array.isArray(config.sidebar) + ? [] + : Object.keys(config.sidebar).filter((key) => key !== "default"), + }; + }); + +export type Config = z.infer; diff --git a/api/src/config/v1.schema.ts b/api/src/config/v1.schema.ts new file mode 100644 index 00000000..f2cd52a9 --- /dev/null +++ b/api/src/config/v1.schema.ts @@ -0,0 +1,182 @@ +import { z } from "zod"; +import { ConfigSchema } from "./schema"; +import type { Config, Sidebar } from "./schema"; + +const V1SidebarItem = z.tuple([ + z.coerce.string(), + z + .union([ + // URL + z.string(), + // Nested children + z + .array( + z + .tuple([z.coerce.string(), z.coerce.string()]) + .optional() + .catch(undefined), + ) + // Remove any undefined items from the array. + .transform((items) => { + return items.filter(Boolean); + }), + ]) + // Fallback to empty array if something is wrong, so the entire sidebar doesn't break + .catch([]), +]); + +export const V1ConfigSchema = z + .object({ + name: z.string().catch(""), + description: z.string().catch(""), + logo: z.string().catch(""), + logoDark: z.string().catch(""), + favicon: z.string().catch(""), + socialPreview: z.string().catch(""), + twitter: z.string().catch(""), + noindex: z.boolean().catch(false), + theme: z.string().catch(""), + headerDepth: z.number().catch(3), + variables: z.record(z.any()).catch({}), + googleTagManager: z.string().catch(""), + googleAnalytics: z.string().catch(""), + zoomImages: z.boolean().catch(false), + experimentalCodehike: z.boolean().catch(false), + experimentalMath: z.boolean().catch(false), + automaticallyDisplayName: z.boolean().catch(true), + automaticallyInferNextPrevious: z.boolean().catch(true), + plausibleAnalytics: z.boolean().catch(false), + plausibleAnalyticsScript: z + .string() + .catch("https://plausible.io/js/script.js"), + anchors: z + .array( + z + .object({ + icon: z.string(), + title: z.string(), + link: z.string(), + }) + .optional() + .catch(undefined), + ) + .transform((items) => items.filter(Boolean)) + .catch([]), + docsearch: z + .object({ + appId: z.string().catch(""), + apiKey: z.string().catch(""), + indexName: z.string().catch(""), + }) + .optional() + .catch(undefined), + sidebar: z + .union([z.record(z.array(V1SidebarItem)), z.array(V1SidebarItem)]) + .catch([]), + }) + .transform((v1) => { + const config: Config = { + name: v1.name, + description: v1.description, + favicon: v1.favicon, + socialPreview: v1.socialPreview, + logo: { + href: "/", + light: v1.logo, + dark: v1.logoDark, + }, + header: { + showName: true, + showThemeToggle: true, + showGitHubCard: true, + }, + theme: { + primary: v1.theme, + }, + social: { + x: v1.twitter, + }, + anchors: v1.anchors.filter(Boolean).map((anchor) => ({ + title: anchor!.title, + href: anchor!.link, + icon: anchor!.icon, + })), + seo: { + noindex: v1.noindex, + }, + search: { + docsearch: v1.docsearch, + }, + variables: v1.variables, + scripts: { + googleTagManager: v1.googleTagManager, + googleAnalytics: v1.googleAnalytics, + plausible: + // If they have `true` for plausible + v1.plausibleAnalytics + ? // Check if they have a custom script + v1.plausibleAnalyticsScript + ? v1.plausibleAnalyticsScript + : true + : undefined, + }, + content: { + headerDepth: v1.headerDepth, + zoomImages: v1.zoomImages, + automaticallyInferNextPrevious: v1.automaticallyInferNextPrevious, + showPageTitle: false, + showPageImage: false, + }, + tabs: [], // V1 doesn't have tabs + sidebar: [], // This is transformed below + locales: [], // This is overridden in the v2 schema + }; + + // Utility function to transform a sidebar item from v1 to latest + function transformSidebarItem( + item: z.infer, + ): Sidebar { + const [title, hrefOrChildren] = item; + + if (typeof hrefOrChildren === "string") { + return { + group: "", + pages: [ + { + title, + href: hrefOrChildren, + }, + ], + }; + } + + if (Array.isArray(hrefOrChildren)) { + return { + group: title, + pages: hrefOrChildren.map((child) => { + const [childTitle, childHref] = child || ["", ""]; + return { + title: childTitle, + href: childHref, + }; + }), + }; + } + + return { group: "", pages: [] }; + } + + if (Array.isArray(v1.sidebar)) { + config.sidebar = v1.sidebar.map(transformSidebarItem); + } else { + const sidebar: Record = {}; + Object.entries(v1.sidebar).map((entry) => { + const locale = entry[0]; + const sidebarItems = entry[1]; + sidebar[locale] = sidebarItems.map(transformSidebarItem); + }); + config.sidebar = sidebar; + } + + return ConfigSchema.parse(config); + }); diff --git a/api/src/env.ts b/api/src/env.ts new file mode 100644 index 00000000..83ee79e3 --- /dev/null +++ b/api/src/env.ts @@ -0,0 +1,17 @@ +import { config } from "dotenv"; +import { z } from "zod"; + +config(); + +export const ENV = z + .object({ + PORT: z + .string() + .optional() + .transform((v) => Number.parseInt(v || "8080", 10)), + GITHUB_PAT: z.string().min(1), + GITHUB_APP_ID: z.string().min(1), + GITHUB_APP_PRIVATE_KEY: z.string().min(1), + GITHUB_APP_WEBHOOK_SECRET: z.string().min(1), + }) + .parse(process.env); diff --git a/api/src/events/installation.ts b/api/src/events/installation.ts new file mode 100644 index 00000000..04f34677 --- /dev/null +++ b/api/src/events/installation.ts @@ -0,0 +1,8 @@ +import type { EmitterWebhookEvent } from "@octokit/webhooks"; + +export async function onInstallation( + event: EmitterWebhookEvent<"installation">, +) { + // TODO: Send a new installation event to analytics + console.log("Installation event received", event.payload); +} diff --git a/api/src/events/pull_request.opened.ts b/api/src/events/pull_request.opened.ts new file mode 100644 index 00000000..a0173711 --- /dev/null +++ b/api/src/events/pull_request.opened.ts @@ -0,0 +1,53 @@ +import type { EmitterWebhookEvent } from "@octokit/webhooks"; +import { getDomains, getOctokitForInstallation } from "../octokit"; +import { createGitHubCheckRun } from "../utils/github"; + +export async function onPullRequestOpened( + event: EmitterWebhookEvent<"pull_request.opened">, +) { + const pull_request = event.payload.pull_request; + const { repository } = event.payload; + + if (!event.payload.installation) { + throw new Error("Installation not found."); + } + + // Get an Octokit instance for the installation event. + const octokit = await getOctokitForInstallation( + event.payload.installation.id, + ); + + // org/repo + const name = repository.full_name.toLowerCase(); + + // Fetch the domains file from the main repository + const domains = await getDomains(octokit); + + // Find a custom domain for the repository, if it exists + const domain = domains.find(([, repository]) => repository === name)?.[0]; + + // Build a domain URL for the comment + const url = domain + ? `${domain}/~${pull_request.number}` + : `docs.page/${name}~${pull_request.number}`; + + const comment = `To view this pull requests documentation preview, visit the following URL: + \n\n\ + [${url}](https://${url}) + \n\n\ + Documentation is deployed and generated using [docs.page](https://docs.page).`; + + await octokit.rest.issues.createComment({ + owner: repository.owner.login, + repo: repository.name, + issue_number: pull_request.number, + body: comment, + }); + + // await createGitHubCheckRun( + // octokit, + // repository.owner.login, + // repository.name, + // pull_request.head.sha + // ); +} diff --git a/api/src/events/pull_request.synchronize.ts b/api/src/events/pull_request.synchronize.ts new file mode 100644 index 00000000..37e182d0 --- /dev/null +++ b/api/src/events/pull_request.synchronize.ts @@ -0,0 +1,26 @@ +import type { EmitterWebhookEvent } from "@octokit/webhooks"; +import { getDomains, getOctokitForInstallation } from "../octokit"; +import { createGitHubCheckRun } from "../utils/github"; + +export async function onPullRequestSynchronize( + event: EmitterWebhookEvent<"pull_request.synchronize">, +) { + const pull_request = event.payload.pull_request; + const { repository } = event.payload; + + if (!event.payload.installation) { + throw new Error("Installation not found."); + } + + // Get an Octokit instance for the installation event. + const octokit = await getOctokitForInstallation( + event.payload.installation.id, + ); + + await createGitHubCheckRun( + octokit, + repository.owner.login, + repository.name, + pull_request.head.sha, + ); +} diff --git a/api/src/octokit.ts b/api/src/octokit.ts new file mode 100644 index 00000000..e0311303 --- /dev/null +++ b/api/src/octokit.ts @@ -0,0 +1,51 @@ +import { App, type Octokit } from "octokit"; +import { ENV } from "./env"; + +export const app = new App({ + appId: ENV.GITHUB_APP_ID, + privateKey: ENV.GITHUB_APP_PRIVATE_KEY, +}); + +export async function getOctokitForInstallation(installationId: number) { + return await app.getInstallationOctokit(installationId); +} + +export type OctokitInstallation = Awaited< + ReturnType +>; + +// Type for a getFile response - assumes the repository is available +type GetFileResponse = { + repository: { + file?: { + text: string; + }; + }; +}; + +// Queries a repository and extracts a file +export async function getDomains( + octokit: Octokit, +): Promise> { + const response = await octokit.graphql( + ` + query GetDomains($owner: String!, $repo: String!, $file: String!) { + repository(owner: $owner, name: $repo) { + file: object(expression: $file) { + ... on Blob { + text + } + } + } + } + `, + { + owner: "invertase", + repo: "docs.page", + file: "main:domains.json", + }, + ); + + const file = response.repository.file?.text || "[]"; + return JSON.parse(file) as Array<[string, string]>; +} diff --git a/api/src/probot.ts b/api/src/probot.ts deleted file mode 100644 index 1476251b..00000000 --- a/api/src/probot.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { createNodeMiddleware, Probot, createProbot } from 'probot'; - -// Create a probot instance for the docs.page app -const probot = createProbot({ - overrides: { - appId: process.env.GITHUB_APP_ID, - privateKey: process.env.GITHUB_APP_PRIVATE_KEY, - }, -}); - -// Queries a repository and extracts a file -const getFile = ` - query GetDomains($owner: String!, $repo: String!, $file: String!) { - repository(owner: $owner, name: $repo) { - file: object(expression: $file) { - ... on Blob { - text - } - } - } - } -`; - -// Type for a getFile response - assumes the repository is available -type GetFileResponse = { - repository: { - file?: { - text: string; - }; - }; -}; - -const app = (app: Probot) => { - app.on('pull_request.opened', async context => { - app.log.info(context); - - const pull_request = context.payload.pull_request; - - const { repository } = context.payload; - - // e.g. org/repo - const name = repository.full_name.toLowerCase(); - - // Fetch the domains file from the main repository - const response = await context.octokit.graphql(getFile, { - owner: 'invertase', - repo: 'docs.page', - file: 'main:domains.json', - }); - - const file = response.repository.file?.text || '[]'; - - // Find and set a custom domain, if it exists - const domains = JSON.parse(file) as Array<[string, string]>; - const domain = domains.find(([, repository]) => repository === name)?.[0]; - - const url = domain - ? `${domain}/~${pull_request.number}` - : `docs.page/${name}~${pull_request.number}`; - - const comment = context.issue({ - body: `To view this pull requests documentation preview, visit the following URL:\n\n[${url}](https://${url})\n\nDocumentation is deployed and generated using [docs.page](https://docs.page).`, - }); - - await context.octokit.issues.createComment(comment); - }); -}; - -export default createNodeMiddleware(app, { - probot, - webhooksPath: '/webhooks/github', -}); diff --git a/api/src/res.ts b/api/src/res.ts index e16f52f5..85d87406 100644 --- a/api/src/res.ts +++ b/api/src/res.ts @@ -1,37 +1,15 @@ -import type { Response } from 'express'; -import { ZodError } from 'zod'; -import { fromZodError } from 'zod-validation-error'; +import type { Response } from "express"; +import type { ZodError } from "zod"; +import { fromZodError } from "zod-validation-error"; +import type { BundlerError } from "./bundler/error"; const status = { - 200: 'OK', - 400: 'BAD_REQUEST', - 404: 'NOT_FOUND', - 500: 'INTERNAL_SERVER_ERROR', + 200: "OK", + 400: "BAD_REQUEST", + 404: "NOT_FOUND", + 500: "INTERNAL_SERVER_ERROR", } as const; -export function response( - res: Response, - status: number, - code: string, - other: - | { - error: { - message: string; - cause?: string | unknown; - links?: { title: string; url: string }[]; - }; - } - | { - data: T; - }, -): Response { - res.status(status); - return res.json({ - code, - ...other, - }); -} - export function ok(res: Response, data: T): Response { res.status(200); return res.json({ @@ -40,13 +18,24 @@ export function ok(res: Response, data: T): Response { }); } +export function bundleError(res: Response, error: BundlerError): Response { + res.status(error.code); + return res.json({ + code: error.name, + error: { + message: error.message, + source: error.source, + }, + }); +} + export function badRequest(res: Response, message: string): Response; export function badRequest(res: Response, error: ZodError): Response; export function badRequest(res: Response, input: string | ZodError): Response { // Set the HTTP Status code res.status(400); - if (typeof input === 'string') { + if (typeof input === "string") { return res.json({ code: status[400], error: input, @@ -63,7 +52,7 @@ export function notFound(res: Response): Response { res.status(404); return res.json({ code: status[404], - error: 'Resource not found.', + error: "Resource not found.", }); } @@ -72,6 +61,6 @@ export function serverError(res: Response, error: unknown): Response { res.status(500); return res.json({ code: status[500], - error: 'Something went wrong.', + error: "Something went wrong.", }); } diff --git a/api/src/routes/bundle.ts b/api/src/routes/bundle.ts index 75d2538f..991d8b66 100644 --- a/api/src/routes/bundle.ts +++ b/api/src/routes/bundle.ts @@ -1,43 +1,60 @@ -import { Request, Response } from 'express'; -import { z } from 'zod'; -import { ok, badRequest, serverError, response } from '../res'; -import bundler, { BundlerError } from '../bundler/index'; +import type { Request, Response } from "express"; +import { z } from "zod"; +import { BundlerError } from "../bundler/error"; +import { Bundler, type BundlerOutput, type ErrorCodes } from "../bundler/index"; +import { badRequest, bundleError, ok, serverError } from "../res"; -const $input = z.object({ +const QuerySchema = z.object({ owner: z .string({ - required_error: 'Missing owner parameter.', - invalid_type_error: 'Owner parameter must be a string.', + required_error: "Missing owner parameter.", + invalid_type_error: "Owner parameter must be a string.", }) .min(1), repository: z .string({ - required_error: 'Missing repository parameter.', - invalid_type_error: 'Repository parameter must be a string.', + required_error: "Missing repository parameter.", + invalid_type_error: "Repository parameter must be a string.", }) .min(1), ref: z.string().optional(), - path: z.string().optional().default('index'), + path: z.string().optional().default("index"), + components: z.array(z.string()).optional(), }); -export default async function bundle(req: Request, res: Response): Promise { - const input = $input.safeParse(req.query); +export type BundleResponse = + | { + code: "OK"; + data: BundlerOutput; + } + | BundleErrorResponse; + +export type BundleErrorResponse = { + code: ErrorCodes | "NOT_FOUND" | "BAD_REQUEST" | "INTERNAL_SERVER_ERROR"; + error: + | string + | { + message: string; + source?: string; + }; +}; + +export default async function bundle( + req: Request, + res: Response, +): Promise { + const input = QuerySchema.safeParse(req.query); if (!input.success) { return badRequest(res, input.error); } try { - return ok(res, await bundler(input.data)); + const bundler = new Bundler(input.data); + return ok(res, await bundler.build()); } catch (e: unknown) { if (e instanceof BundlerError) { - return response(res, e.code, e.name, { - error: { - message: e.message, - cause: e.cause, - links: e.links, - }, - }); + return bundleError(res, e); } return serverError(res, e); } diff --git a/api/src/routes/mdx.ts b/api/src/routes/mdx.ts deleted file mode 100644 index 24971f70..00000000 --- a/api/src/routes/mdx.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Request, Response } from 'express'; -import { z } from 'zod'; -import { ok, badRequest, serverError } from '../res'; -import { bundle } from '../bundler/mdx'; - -const $input = z.object({ - markdown: z.string(), -}); - -export default async function mdx(req: Request, res: Response): Promise { - const input = $input.safeParse(req.body); - - if (!input.success) { - return badRequest(res, input.error); - } - - try { - const response = await bundle(input.data.markdown, { - headerDepth: 3, - }); - return ok(res, response); - } catch (e: unknown) { - return serverError(res, e); - } -} diff --git a/api/src/routes/preview.ts b/api/src/routes/preview.ts index 20afb629..99ae5dd6 100644 --- a/api/src/routes/preview.ts +++ b/api/src/routes/preview.ts @@ -1,41 +1,64 @@ -import { Request, Response } from 'express'; -import { z } from 'zod'; -import { ok, badRequest, serverError } from '../res'; -import { bundle } from '../bundler/mdx'; -import parseConfig from '../utils/config'; +import type { Request, Response } from "express"; +import { z } from "zod"; +import type { BundlerOutput } from "../bundler"; +import { parseMdx } from "../bundler/mdx"; +import { parseConfig } from "../config"; +import { badRequest, ok, serverError } from "../res"; -const $input = z.object({ - markdown: z.string(), +const PreviewSchema = z.object({ + markdown: z.string().nullable(), config: z.object({ - json: z.string().optional(), - yaml: z.string().optional(), + json: z.string().nullable(), + yaml: z.string().nullable(), }), + components: z.array(z.string()).optional(), }); -export default async function preview(req: Request, res: Response): Promise { - const input = $input.safeParse(JSON.parse(req.body)); +export default async function preview( + req: Request, + res: Response, +): Promise { + const input = PreviewSchema.safeParse(JSON.parse(req.body)); if (!input.success) { + console.error(input.error); return badRequest(res, input.error); } try { const config = parseConfig({ - json: input.data.config.json, - yaml: input.data.config.yaml, + json: input.data.config.json ?? undefined, + yaml: input.data.config.yaml ?? undefined, }); - const mdx = await bundle(input.data.markdown, { - headerDepth: config.headerDepth, + const mdx = await parseMdx(input.data.markdown ?? "", { + headerDepth: config.content?.headerDepth ?? 3, + components: input.data.components ?? [], }); - return ok(res, { + const output: BundlerOutput = { + source: { + type: "branch", + owner: "owner", + repository: "repository", + ref: "preview", + }, + private: false, + ref: "preview", + stars: 0, + forks: 0, + baseBranch: "preview", + path: "preview", config, + markdown: input.data.markdown ?? "", headings: mdx.headings, frontmatter: mdx.frontmatter, code: mdx.code, - }); + }; + + return ok(res, output); } catch (e: unknown) { + console.error(e); return serverError(res, e); } } diff --git a/api/src/routes/schema.ts b/api/src/routes/schema.ts new file mode 100644 index 00000000..6f22c8bd --- /dev/null +++ b/api/src/routes/schema.ts @@ -0,0 +1,11 @@ +import type { Request, Response } from "express"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { ConfigSchema } from "../config/schema"; + +export default async function schema( + req: Request, + res: Response, +): Promise { + res.status(200); + return res.json(zodToJsonSchema(ConfigSchema)); +} diff --git a/api/src/routes/webhooks.github.ts b/api/src/routes/webhooks.github.ts new file mode 100644 index 00000000..f697c790 --- /dev/null +++ b/api/src/routes/webhooks.github.ts @@ -0,0 +1,57 @@ +import { Webhooks } from "@octokit/webhooks"; +import type { Request, Response } from "express"; +import { badRequest, ok } from "../res"; + +import { onInstallation } from "../events/installation"; +import { onPullRequestOpened } from "../events/pull_request.opened"; +// import { onPullRequestSynchronize } from "../events/pull_request.synchronize"; + +export default async function githubWebhook( + req: Request, + res: Response, +): Promise { + // Webhooks are POST requests from GitHub + if (req.method.toUpperCase() !== "POST") { + return badRequest(res, "Invalid method."); + } + + // Get the body of the request (stringify since express converts it to JSON). + const body = req.body; + + // Create a new instance of the Webhooks class with the GitHub App secret. + const webhook = new Webhooks({ + secret: process.env.GITHUB_APP_WEBHOOK_SECRET!, + }); + + // Verify the signature of the request. + const verified = await webhook.verify( + JSON.stringify(body), + String(req.headers["x-hub-signature-256"]), + ); + + if (!verified) { + return badRequest(res, "Invalid signature."); + } + + webhook.on("installation", onInstallation); + webhook.on("pull_request.opened", onPullRequestOpened); + // webhook.on("pull_request.synchronize", onPullRequestSynchronize); + + try { + const id = String(req.headers["x-github-hook-id"]); + + // biome-ignore lint/suspicious/noExplicitAny: This will be a valid event name from GitHub. + const name = String(req.headers["x-github-event"]) as any; + + await webhook.receive({ + id, + name, + payload: body, + }); + + return ok(res, { message: "OK" }); + } catch (e) { + console.error(e); + return badRequest(res, "Webhook request failed."); + } +} diff --git a/api/src/types.ts b/api/src/types.ts new file mode 100644 index 00000000..3d78df42 --- /dev/null +++ b/api/src/types.ts @@ -0,0 +1,3 @@ +export type { BundlerOutput } from "./bundler/index"; +export type { BundleResponse, BundleErrorResponse } from "./routes/bundle"; +export type { SidebarGroup } from "./config/models/sidebar"; diff --git a/api/src/utils/config.ts b/api/src/utils/config.ts deleted file mode 100644 index cfb93c54..00000000 --- a/api/src/utils/config.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { z } from 'zod'; -import yaml from 'js-yaml'; - -const $SidebarItem = z.tuple([ - z.coerce.string(), - z - .union([ - // URL - z.string(), - // Nested children - z - .array(z.tuple([z.coerce.string(), z.coerce.string()]).optional().catch(undefined)) - // Remove any undefined items from the array. - .transform(items => { - return items.filter(Boolean); - }), - ]) - // Fallback to empty array if something is wrong, so the entire sidebar doesn't break - .catch([]), -]); - -const $Config = z - .object({ - name: z.string().catch(''), - description: z.string().catch(''), - logo: z.string().catch(''), - logoDark: z.string().catch(''), - favicon: z.string().catch(''), - socialPreview: z.string().catch(''), - twitter: z.string().catch(''), - noindex: z.boolean().catch(false), - theme: z.string().catch(''), - headerDepth: z.number().catch(3), - variables: z.record(z.any()).catch({}), - googleTagManager: z.string().catch(''), - googleAnalytics: z.string().catch(''), - zoomImages: z.boolean().catch(false), - experimentalCodehike: z.boolean().catch(false), - experimentalMath: z.boolean().catch(false), - automaticallyDisplayName: z.boolean().catch(true), - automaticallyInferNextPrevious: z.boolean().catch(true), - plausibleAnalytics: z.boolean().catch(false), - plausibleAnalyticsScript: z.string().catch('https://plausible.io/js/script.js'), - anchors: z - .array( - z - .object({ - icon: z.string(), - title: z.string(), - link: z.string(), - }) - .optional() - .catch(undefined), - ) - .transform(items => items.filter(Boolean)) - .catch([]), - docsearch: z - .object({ - appId: z.string().catch(''), - apiKey: z.string().catch(''), - indexName: z.string().catch(''), - }) - .optional() - .catch(undefined), - sidebar: z.union([z.record(z.array($SidebarItem)), z.array($SidebarItem)]).catch([]), - }) - .transform(config => { - return { - ...config, - locales: Array.isArray(config.sidebar) - ? [] - : Object.keys(config.sidebar).filter(key => key !== 'default'), - }; - }); - -export type Config = z.infer; - -// The default config is used to fill in missing values. -export const defaultConfig = $Config.parse({}); - -// Given a user config, merges the config with the default config. -export default function parseConfig(configs: { json?: string; yaml?: string }): Config { - let parsedConfig: unknown; - - if (configs.json) { - parsedConfig = JSON.parse(configs.json); - } else if (configs.yaml) { - parsedConfig = yaml.load(configs.yaml); - } - - // return $Config.parse(JSON.parse(test)); - return $Config.parse(parsedConfig); -} diff --git a/api/src/utils/github.ts b/api/src/utils/github.ts index 70061575..f67aa27e 100644 --- a/api/src/utils/github.ts +++ b/api/src/utils/github.ts @@ -1,19 +1,21 @@ -import A2A from 'a2a'; -import { graphql } from '@octokit/graphql'; -import dotenv from 'dotenv'; -dotenv.config(); +import { type CheckResult, check } from "@docs.page/cli"; +import { graphql } from "@octokit/graphql"; +import A2A from "a2a"; +import JSZip from "jszip"; +import { ENV } from "../env"; +import type { OctokitInstallation } from "../octokit"; const getGitHubToken = (() => { let index = 0; - const tokens = process.env.GITHUB_PAT ? process.env.GITHUB_PAT.split(',') : []; + const tokens = ENV.GITHUB_PAT ? ENV.GITHUB_PAT.split(",") : []; if (!tokens.length) { throw new Error( - 'Environment variable GITHUB_PAT is not defined or has no tokens or an invalid token.', + "Environment variable GITHUB_PAT is not defined or has no tokens or an invalid token.", ); } - return function () { + return () => { if (index >= tokens.length) index = 0; return tokens[index++]; }; @@ -23,7 +25,7 @@ export function getGithubGQLClient(): typeof graphql { const token = getGitHubToken(); if (!token) { throw new Error( - 'Environment variable GITHUB_PAT is not defined or has no tokens or an invalid token.', + "Environment variable GITHUB_PAT is not defined or has no tokens or an invalid token.", ); } return graphql.defaults({ @@ -42,10 +44,13 @@ type MetaData = { type PageContentsQuery = { repository: { + stars: number; + forks: number; baseBranch: { name: string; }; isFork: boolean; + isPrivate: boolean; configJson?: { text: string; }; @@ -65,7 +70,10 @@ type PageContentsQuery = { }; export type Contents = { + stars: number; + forks: number; isFork: boolean; + isPrivate: boolean; baseBranch: string; config: { configJson?: string; @@ -81,21 +89,24 @@ export async function getGitHubContents( metadata: MetaData, noDir?: boolean, ): Promise { - const base = noDir ? '' : 'docs/'; + const base = noDir ? "" : "docs/"; const absolutePath = `${base}${metadata.path}`; const indexPath = `${base}${metadata.path}/index`; - const ref = metadata.ref || 'HEAD'; + const ref = metadata.ref || "HEAD"; const [error, response] = await A2A( getGithubGQLClient()({ query: ` query RepositoryConfig($owner: String!, $repository: String!, $configJson: String!, $configYaml: String!, $mdx: String!, $mdxIndex: String!) { repository(owner: $owner, name: $repository) { + stars: stargazerCount + forks: forkCount baseBranch: defaultBranchRef { name } isFork + isPrivate configJson: object(expression: $configJson) { ... on Blob { text @@ -134,9 +145,12 @@ export async function getGitHubContents( } return { + stars: response?.repository?.stars ?? 0, + forks: response?.repository?.forks ?? 0, repositoryFound: true, isFork: response?.repository?.isFork ?? false, - baseBranch: response?.repository.baseBranch.name ?? 'main', + isPrivate: response?.repository?.isPrivate ?? false, + baseBranch: response?.repository.baseBranch.name ?? "main", config: { configJson: response?.repository.configJson?.text, configYaml: response?.repository.configYaml?.text, @@ -194,7 +208,7 @@ export async function getPullRequestMetadata( `, owner: owner, repository: repository, - pullRequest: parseInt(pullRequest), + pullRequest: Number.parseInt(pullRequest), }), ); if (error || !response) { @@ -207,3 +221,102 @@ export async function getPullRequestMetadata( ref: response?.repository?.pullRequest?.ref?.name, }; } + +export async function createGitHubCheckRun( + octokit: OctokitInstallation, + owner: string, + repository: string, + sha: string, +) { + const checkRunResult = await octokit.rest.checks.create({ + owner, + repo: repository, + name: "docs.page check", + head_sha: sha, + status: "in_progress", + }); + + // Download the repository as a zip + const archive = await octokit.rest.repos + .downloadZipballArchive({ + owner: "invertase", + repo: "docs.page", + ref: "main", + }) + .then((response) => response.data as ArrayBuffer); + + // Extract the files from the tar + const zip = await new JSZip().loadAsync(archive); + + // The zip file contains a directory with the repository name, so we need to remove that + // and store the files in a map with the path as the key. + const files = Object.keys(zip.files).reduce>( + (acc, path) => { + acc[path.split("/").slice(1).join("/")] = path; + return acc; + }, + {}, + ); + + // Function to get a file from the zip by relative path. + async function getFileFn(relativePath: string) { + const file = zip.file(files[relativePath]); + return file ? await file.async("string") : ""; + } + + const results: CheckResult[] = []; + + let hasErrors = false; + + const ms = new Date().getTime(); + for await (const result of check(new Set(Object.keys(files)), getFileFn)) { + if (result.type === "error") { + hasErrors = true; + } + + results.push(result); + } + const timer = new Date().getTime() - ms; + + const errors = results.map((result) => { + const tag = result.type === "error" ? "[ERROR]" : "[WARN]"; + const path = result.filePath + ? ` - ${result.filePath}:${result.line}:${result.column}` + : ""; + + return ` - ${tag}: ${path ? `${path} ` : ""} ${result.message}`; + }); + + let text = "## Results\n\n"; + text += errors.join("\n\n"); + text += `\n\n
View raw output\n\n\`\`\`json\n${JSON.stringify( + results, + null, + 2, + )}\n\`\`\`\n\n
`; + + await octokit.rest.checks.update({ + owner, + repo: repository, + check_run_id: checkRunResult.data.id, + status: "completed", + conclusion: hasErrors ? "failure" : "success", + output: { + title: "Check Run Results", + summary: `Checked ${results.length} files in ${timer}ms`, + text, + annotations: results + .filter((result) => !!result.filePath) + .map((result) => ({ + path: result.filePath!, + annotation_level: result.type === "warning" ? "warning" : "failure", + message: result.message, + start_line: result.line || 0, + end_line: result.line || 0, + start_column: result.column || 0, + end_column: result.column || 0, + title: result.message, + })), + }, + }); +} diff --git a/api/src/utils/sanitize.ts b/api/src/utils/sanitize.ts index a5591b01..bcad84bb 100644 --- a/api/src/utils/sanitize.ts +++ b/api/src/utils/sanitize.ts @@ -1,8 +1,8 @@ export const escapeHtml = (text: string): string => { return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); }; diff --git a/api/src/utils/variables.ts b/api/src/utils/variables.ts new file mode 100644 index 00000000..451623eb --- /dev/null +++ b/api/src/utils/variables.ts @@ -0,0 +1,22 @@ +import get from "lodash.get"; +const VARIABLE_REGEX = /{{\s([a-zA-Z0-9_.]*)\s}}/gm; + +// Replaces an object of variables with their moustache values in a string +export function replaceMoustacheVariables( + variables: Record, + value: string, +) { + let output = value; + let m: RegExpExecArray | null; + + // biome-ignore lint/suspicious/noAssignInExpressions: This is a false positive. + while ((m = VARIABLE_REGEX.exec(value)) !== null) { + // This is necessary to avoid infinite loops with zero-width matches + if (m.index === VARIABLE_REGEX.lastIndex) { + VARIABLE_REGEX.lastIndex++; + } + output = output.replace(m[0], get(variables, m[1], m[0])); + } + + return output; +} diff --git a/api/tsconfig.json b/api/tsconfig.json index f8000d1a..0b622c68 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,15 +1,19 @@ { "compilerOptions": { "resolveJsonModule": true, - "target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, - "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - "outDir": "./dist" /* Redirect output structure to the directory. */, - "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, - "strict": true /* Enable all strict type-checking options. */, - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, - "typeRoots": ["node_modules/@types"] + "target": "ESNEXT", + "module": "ESNext", + "outDir": "./dist", + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "typeRoots": ["node_modules/@types"], + "verbatimModuleSyntax": true, + "baseUrl": ".", + "paths": { + "@docs.page/cli": ["../packages/cli/src/index.ts"] + } } } diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..00de628f --- /dev/null +++ b/biome.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "files": { + "ignore": [ + "node_modules", + ".git", + ".vercel", + "dist/**", + "website/build/**", + "website/public/**" + ] + }, + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off" + }, + "suspicious": { + "noShadowRestrictedNames": "off", + "noArrayIndexKey": "off" + }, + "security": { + "noDangerouslySetInnerHtml": "off" + }, + "complexity": { + "noUselessFragments": "off" + } + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 00000000..e78480a6 Binary files /dev/null and b/bun.lockb differ diff --git a/docs.json b/docs.json index a5c2dfc6..498d950a 100644 --- a/docs.json +++ b/docs.json @@ -1,56 +1,248 @@ { "name": "docs.page", - "logo": "https://static.invertase.io/assets/docs.page/docs-page-logo.png", - "theme": "#36B9B9", - "twitter": "invertaseio", - "anchors": [ - { "title": "Homepage", "icon": "house", "link": "https://docs.page" }, - { "title": "Discord", "icon": "discord", "link": "https://invertase.link/discord" } + "description": "Ship documentation, like you ship code", + "logo": { + "href": "https://docs.page", + "light": "https://static.invertase.io/assets/docs.page/docs-page-logo.png" + }, + "favicon": "https://static.invertase.io/assets/docs.page/docs-page-logo.png", + "theme": { + "primary": "#ECC918", + "primaryLight": "#BFA213" + }, + "content": { + "showPageTitle": true, + "showPageImage": true + }, + "social": { + "x": "invertaseio", + "github": "invertase/docs.page", + "discord": "invertase" + }, + "tabs": [ + { + "id": "root", + "title": "Documentation", + "href": "/" + }, + { + "id": "components", + "title": "Components", + "href": "/components" + }, + { + "id": "cli", + "title": "CLI", + "href": "/cli" + } ], "sidebar": [ - [ - "Getting Started", - [ - ["Overview", "/"], - ["Getting Started", "/getting-started"], - ["Configuration", "/configuration"], - ["Writing Content", "/writing-content"] + { + "group": "Getting Started", + "tab": "root", + "pages": [ + { "title": "Overview", "href": "/", "icon": "book" }, + { + "title": "Getting Started", + "href": "/getting-started", + "icon": "rocket" + }, + { "title": "Configuration", "href": "/configuration", "icon": "gear" }, + { "title": "Navigation", "href": "/navigation", "icon": "map" }, + { + "title": "Writing Content", + "href": "/writing-content", + "icon": "pencil" + }, + { + "title": "Frontmatter", + "href": "/frontmatter", + "icon": "circle-info" + }, + { + "title": "Local Previewing", + "href": "/local-previewing", + "icon": "eye" + }, + { + "title": "Live Previews", + "href": "/live-previews", + "icon": "code-branch" + }, + { + "title": "Publishing", + "href": "/publishing", + "icon": "globe" + } + ] + }, + { + "group": "Components", + "tab": "components", + "pages": [ + { + "title": "Accordion", + "href": "/components/accordion", + "icon": "square-caret-down" + }, + { + "title": "Callouts", + "href": "/components/callouts", + "icon": "bullhorn" + }, + { + "title": "Cards", + "href": "/components/cards", + "icon": "square-full" + }, + { + "title": "Code Blocks", + "href": "/components/code-blocks", + "icon": "code" + }, + { + "title": "Code Group", + "href": "/components/code-group", + "icon": "laptop-code" + }, + { + "title": "Headings", + "href": "/components/headings", + "icon": "heading" + }, + { + "title": "Tweet", + "href": "/components/tweet", + "icon": "twitter" + }, + { + "title": "Tabs", + "href": "/components/tabs", + "icon": "table-columns" + }, + { + "title": "Icons", + "href": "/components/icons", + "icon": "icons" + }, + { + "title": "Images", + "href": "/components/images", + "icon": "image" + }, + { + "title": "Property", + "href": "/components/property", + "icon": "signature" + }, + { + "title": "Steps", + "href": "/components/steps", + "icon": "list-ol" + }, + { + "title": "YouTube", + "href": "/components/youtube", + "icon": "youtube" + }, + { + "title": "Vimeo", + "href": "/components/vimeo", + "icon": "vimeo" + }, + { + "title": "Video", + "href": "/components/video", + "icon": "video" + }, + { + "title": "Zapp!", + "href": "/components/zapp", + "icon": "bolt" + }, + { + "title": "Custom Components", + "href": "/components/custom", + "icon": "square-pen" + } + ] + }, + { + "group": "Advanced", + "tab": "root", + "pages": [ + { + "title": "Assets", + "href": "/assets", + "icon": "image" + }, + { + "title": "Components", + "href": "/components", + "icon": "shapes" + }, + { + "title": "Custom Domains", + "href": "/custom-domains", + "icon": "globe" + }, + { + "title": "Search", + "href": "/search", + "icon": "magnifying-glass" + }, + + { + "title": "Locales", + "href": "/locales", + "icon": "language" + }, + { + "title": "GitHub Bot", + "href": "/github-bot", + "icon": "github" + }, + { + "title": "Command Line Interface", + "href": "/cli", + "icon": "terminal" + } ] - ], - [ - "Components", - [ - ["Accordion", "/components/accordion"], - ["Code Blocks", "/components/code-blocks"], - ["Callouts", "/components/callouts"], - ["Headings", "/components/headings"], - ["Tweet", "/components/tweet"], - ["Tabs", "/components/tabs"], - ["Images", "/components/images"], - ["YouTube", "/components/youtube"], - ["Vimeo", "/components/vimeo"], - ["Video", "/components/video"], - ["Zapp!", "/components/zapp"], - ["Custom Components", "/components/custom"] + }, + { + "tab": "cli", + "group": "CLI", + "pages": [ + { "title": "Overview", "href": "/cli" }, + { + "group": "Commands", + "pages": [ + { "title": "init", "href": "/cli/init" }, + { "title": "check", "href": "/cli/check" } + ] + } ] - ], - [ - "Advanced", - [ - ["Frontmatter", "/frontmatter"], - ["Previews", "/previews"], - ["Search", "/search"], - ["Custom Domains", "/custom-domains"], - ["Locales", "/locales"], - ["GitHub Bot", "/github-bot"] + }, + { + "tab": "root", + "group": "Misc.", + "pages": [ + { + "title": "Contributing", + "href": "/contributing", + "icon": "heart" + } ] - ], - ["Misc.", [["Contributing", "/contributing"]]] + } ], - "docsearch": { - "apiKey": "9b58d13ee3195094105d528fc6161a01", - "appId": "BH4D9OD16A", - "indexName": "use_docs_page" + "search": { + "docsearch": { + "apiKey": "9b58d13ee3195094105d528fc6161a01", + "appId": "BH4D9OD16A", + "indexName": "use_docs_page" + } }, - "googleTagManager": "GTM-W89J6BX" + "scripts": { + "googleTagManager": "GTM-W89J6BX" + } } diff --git a/docs/assets.mdx b/docs/assets.mdx new file mode 100644 index 00000000..1ca64742 --- /dev/null +++ b/docs/assets.mdx @@ -0,0 +1,39 @@ +--- +title: Assets +description: Learn how to add assets to your project. +--- + +Assets such as images and videos are a common requirement for documentation. + +## Remote Assets + +To use remote assets in your documentation, you can reference them directly in your markdown files. + +For example, to include an image from a URL: + +```md +![Description](https://example.com/image.png) +``` + +## Local Assets + +To use assets with your documentation, you can add them to your repository within the `docs/` directory and reference them in your markdown files. + +For example, consider the following directory structure: + +``` +docs/ + assets/ + image.png + index.mdx +``` + +Within your markdown file, you can reference the image using a relative path: + +```md +![Description](/assets/image.png) +``` + + + You can place assets where you wish - whether in a root `docs/assets` directory or co-located next to your markdown. + \ No newline at end of file diff --git a/docs/assets/github-bot-comment.png b/docs/assets/github-bot-comment.png new file mode 100644 index 00000000..679fe6ca Binary files /dev/null and b/docs/assets/github-bot-comment.png differ diff --git a/docs/assets/local-preview/step-1.png b/docs/assets/local-preview/step-1.png new file mode 100644 index 00000000..3e20bb28 Binary files /dev/null and b/docs/assets/local-preview/step-1.png differ diff --git a/docs/assets/local-preview/step-3.png b/docs/assets/local-preview/step-3.png new file mode 100644 index 00000000..6521762c Binary files /dev/null and b/docs/assets/local-preview/step-3.png differ diff --git a/docs/cli/check.mdx b/docs/cli/check.mdx new file mode 100644 index 00000000..c1dce257 --- /dev/null +++ b/docs/cli/check.mdx @@ -0,0 +1,27 @@ +--- +title: check +description: The `check` command scans the current project for errors and warnings which need fixing. +--- + +## Usage + +```bash +@docs.page/cli check [path] +``` + +The command current performs the following checks: + +- Broken Links: Checks markdown content for any broken relative links. +- Missing Assets: Checks markdown content for any local [assets](/assets) that are missing. + +## Arguments + + + The path to the directory to check for issues. This should be the path to the root of the project. + + Defaults to the current directory. + + ``` + @docs.page/cli check my-project + ``` + \ No newline at end of file diff --git a/docs/cli/index.mdx b/docs/cli/index.mdx new file mode 100644 index 00000000..2c40304d --- /dev/null +++ b/docs/cli/index.mdx @@ -0,0 +1,24 @@ +--- +title: CLI +description: The CLI is a command-line interface for interacting with docs.page. +--- + +The docs.page CLI is a command-line interface for interacting with docs.page. It allows you to initialize a new project and check for issues with your documentation, +such as broken links. + +The CLI is published as an NPM package and can be installed globally using the following command: + +```bash +npm install -g @docs.page/cli +``` + +Alternatively, you can use `npx` to run the CLI without installing it globally: + +```bash +npx @docs.page/cli +``` + +## Commands + +- [`init`](/cli/init): Initialize a new project. +- [`check`](/cli/check): Check for issues with your documentation. \ No newline at end of file diff --git a/docs/cli/init.mdx b/docs/cli/init.mdx new file mode 100644 index 00000000..071ce3b8 --- /dev/null +++ b/docs/cli/init.mdx @@ -0,0 +1,25 @@ +--- +title: init +description: The `init` command initializes a new project. It creates a new directory with a basic project structure and configuration file. +--- + + +## Usage + +```bash +@docs.page/cli init [path] +``` + +The init command will fail if a `docs.json` file already exists in the specified directory. + +If the `docs/` directory exists, the command will not output example `.mdx` files. + +## Arguments + + + The path to the directory to initialize the project in. Defaults to the current directory. + + ``` + @docs.page/cli init my-project + ``` + \ No newline at end of file diff --git a/docs/components/accordion.mdx b/docs/components/accordion.mdx index e8be3bd6..fd321f7c 100644 --- a/docs/components/accordion.mdx +++ b/docs/components/accordion.mdx @@ -3,31 +3,56 @@ title: Accordion description: Display an accordion component with collapsible content panels. --- -# Accordion - -An accordion can be used to render collapsible content, useful for information which might not +Use the `` component to collapsible content, useful for information which might not be relevant to all users. -## Usage +## Example -Use the `` component to render an accordion, providing a `title` property. +This is collapsible content! ```jsx This is collapsible content! ``` -This is collapsible content! +## Props + + + The title of the accordion. + + +--- -### Default value + + Whether or not the accordion should be open by default. Defaults to `false`. + -Pass the `defaultOpen` prop to have the accordion open by default. +--- + + + The icon to display next to the accordion title. + + +## Accordion Groups + +To group accordions into a single display unit, use the `` component, with each +`` component as a child: + + + + This is collapsible content! + + + This is collapsible content! + + ```jsx - - This is collapsible content! - + + + This is collapsible content! + + + This is collapsible content! + + ``` - - - This is collapsible content! - diff --git a/docs/components/callouts.mdx b/docs/components/callouts.mdx index bcc34454..ffa9ccc3 100644 --- a/docs/components/callouts.mdx +++ b/docs/components/callouts.mdx @@ -3,11 +3,11 @@ title: Callouts description: Add callout boxes to your pages to display important information. --- -# Callouts +Use one of the callout components to draw attention to important information on your page, such as a success, warning, information or error message. -Callouts help break up the page flow by providing colorful boxes with text information to notifiy the user about something important on the page. +## Examples -## Info Callout +### Info Callout Displays a soft blue box with an info icon, useful for information based information. @@ -17,7 +17,7 @@ Displays a soft blue box with an info icon, useful for information based informa This draws attention to useful page information. ``` -## Warning Callout +### Warning Callout Displays a yellow box with a warning icon, useful for warning the user about something. @@ -27,7 +27,7 @@ Displays a yellow box with a warning icon, useful for warning the user about som This draws attention to a warning they should take notice of. ``` -## Error Callout +### Error Callout Displays a red box with an error icon, useful for loud pieces of information. @@ -37,7 +37,7 @@ Displays a red box with an error icon, useful for loud pieces of information. This draws attention to an error the user might run into if they don't pay attenion. ``` -## Success Callout +### Success Callout Displays a green box with a check icon, useful for showing the user they did something right. @@ -46,3 +46,4 @@ Displays a green box with a check icon, useful for showing the user they did som ```jsx This draws attention to a successful action the user has taken. ``` + diff --git a/docs/components/cards.mdx b/docs/components/cards.mdx new file mode 100644 index 00000000..8767aab7 --- /dev/null +++ b/docs/components/cards.mdx @@ -0,0 +1,94 @@ +--- +title: Cards +description: Display important content in a card, with an optional link. +--- + +Use the `` component to display data within a card. You can also use the `CardGroup` component to display multiple cards in a row. + +## Example + + + Install this package using npm or yarn. + + ```bash + npm install @myorg/my-package + ``` + + +````jsx + + Install this package using npm or yarn. + + ```bash + npm install @myorg/my-package + ``` + +```` + +Optionally, add a `href` prop to the `Card` component to make the card clickable. + + + Click here to learn more about this package. + + +````jsx + + Click here to learn more about this package. + +```` + +## Props + + + The title of the card. + + +--- + + + Whether this card should be clickable, and where it should link to. + + +--- + + + The icon to display next to the card title. + + + +## Card Groups + +To group cards into columns, use the `` component. + + + + Install this package using npm or yarn. + + + Read the documentation. + + + + +````jsx + + + Install this package using npm or yarn. + + ```bash + npm install @myorg/my-package + ``` + + + Click here to learn more about this package. + + +```` + +### Props + + + The number of columns to display the cards in. Defaults to `2`. + + + diff --git a/docs/components/code-blocks.mdx b/docs/components/code-blocks.mdx index 563888fc..dec3453c 100644 --- a/docs/components/code-blocks.mdx +++ b/docs/components/code-blocks.mdx @@ -3,36 +3,14 @@ title: Code Blocks description: Display code blocks with syntax highlighting. --- -# Code Blocks +Code Blocks are a great way to display code snippets in your documentation. Use the standard markdown syntax of three backticks +(```) to create a code block, or use the `` component. -Render code blocks inline or as a code snippet with syntax highlighting by enclosing them in backticks (`). +You can also specify the language of the code block to enable syntax highlighting, and other features such as a title. -## Inline Code +## Example -Code can be rendereed inline by enclosing it in single backticks (`). - -For example: `print("Hello World")`. - -This can be created by adding the following to your markdown: - -``` -For example: `print("Hello World")`. -``` - -## Code Snippets - -A fully syntax highlighted code block can be generated by using fenced code blocks, which are blocks -of code surrounded by three backticks on the lines above and below the code block. - -All code blocks provide a copy button in the top right corner of the code block when hovering, for example: - -```bash -echo "Hello World" -``` - -Following the backticks, you can optionally specify a language identifier to enable syntax highlighting: - -```javascript +```js export async function apiRequest(options) { const response = await fetch(options.url); const data = await response.json(); @@ -40,81 +18,122 @@ export async function apiRequest(options) { } ``` -The above code block was created with the following snippet: - -````text -```javascript -export async function apiRequest(options) { - // ... ```` - -Syntax highlighting is generated using [Shiki](https://github.com/shikijs/shiki). You can find a list of -[supported languages here](https://github.com/shikijs/shiki/blob/main/docs/languages.md#all-languages). - -### Adding a Title - -You can add a title to code blocks by adding an attribute to the opening backticks: - -```javascript title="Sending a request" +```js export async function apiRequest(options) { const response = await fetch(options.url); const data = await response.json(); return data; } ``` - -The above code block was created with the following snippet: - -````text -```javascript title="Sending a request" -export async function apiRequest(options) { - // ... ```` -## Code Groups +## Props -You can group multiple code blocks together by using the `` component. This is useful for showing -variations of the "same" code in a different language + + The title of the code block. - - ```javascript + ```` + ```js title="Logging: Hello World" console.log("Hello World"); ``` + ```` + + +## Advanced + +Code Blocks also support a number of other useful features, such as diffing, line highlighting and more. -```python -print('Hello World!') +### Diffing + +Diffing codeblocks enables you to visually compare the difference between code in a single code block. For example: + +```js +console.log('hewwo') // [!code --] +console.log('hello') // [!code ++] +console.log('goodbye') ``` -```dart -void main() { - print('Hello World!'); -} +To enable diffing, add the `// [!code --]` and `// [!code ++]` annotations to the end of each line: + +```` +```js +console.log('hewwo') // [\!code --] +console.log('hello') // [\!code ++] +console.log('goodbye') ``` +```` + +### Line Highlighting - +You can highlight specific lines in a code block, useful for calling out important lines of code. For example: -Specify each of the code blocks and their language as children of the component. +```js +console.log('hello') // [!code highlight] +console.log('world') +``` + +To highlight a line, add the `// [!code highlight]` annotation to the end of the line: ```` - - ```javascript - console.log("Hello World"); - ``` +```ts +console.log('hello') // [\!code highlight] +console.log('world') +``` +```` - ```python - print('Hello World!') - ``` +### Focused Lines - ```dart - void main() { - print('Hello World!'); - } - ``` - +You can focus on specific lines in a code block, blurring out the rest of the code. For example: + +```js +console.log('Not focused'); +console.log('Focused') // [!code focus] +console.log('Not focused'); +``` + + +To focus on a line, add the `// [!code focus]` annotation to the end of the line: + +```` +```js +console.log('Not focused'); +console.log('Focused') // [\!code focus] +console.log('Not focused'); +``` ```` -You can also specify the default language using the `defaultLanguage` property: +### Typescript Support -```jsx - +Out of the box, all code blocks will automatically be enhanced with rich type information when the code area is hovered. + +```ts +type APIResponse = { + data: T; + status: number; +}; + +export async function apiRequest(options: { url: string }) { + const response = await fetch(options.url); + const data = await response.json() as APIResponse<{ hello: 'world' }>; + return data; +} ``` + +To enable this, ensure that the code block is tagged with the `typescript` (or `ts`) language: + +```` +```ts +type APIResponse = { + data: T; + status: number; +}; + +export async function apiRequest(options: { url: string }) { + const response = await fetch(options.url); + const data = await response.json() as APIResponse<{ hello: 'world' }>; + return data; +} +``` +```` + diff --git a/docs/components/code-group.mdx b/docs/components/code-group.mdx new file mode 100644 index 00000000..a9323fec --- /dev/null +++ b/docs/components/code-group.mdx @@ -0,0 +1,85 @@ +--- +title: Code Groups +description: Display groups of code blocks with syntax highlighting. +--- + +Use the `` component to display multiple code blocks together, useful for showing variations of the "same" code in a different language. + +## Example + + + ```javascript + console.log("Hello World"); + ``` + + ```python + print('Hello World!') + ``` + + ```dart + void main() { + print('Hello World!'); + } + ``` + + +````jsx + + ```javascript + console.log("Hello World"); + ``` + + ```python + print('Hello World!') + ``` + + ```dart + void main() { + print('Hello World!'); + } + ``` + +```` + +## Synchronizing languages + +In some scenarios, you may wish to synchronize the language selection across multiple code groups. You can do this by setting the `synchronize` prop on each `` component. For example, +the following code groups will synchronize their language selection: + + + ```javascript + console.log("Hello World"); + ``` + + ```dart + print('Hello World!'); + ``` + + + + ```javascript + process.exit(0); + ``` + + ```dart + exit(0); + ``` + + +## Props + + + The title of the code block. + + +--- + + + The default language to display code blocks in. Defaults to the first language in the group. + + +--- + + + If `true`, synchronizes the language selection across multiple code groups with the same language defined. + \ No newline at end of file diff --git a/docs/components/custom.mdx b/docs/components/custom.mdx index d43390b0..0ca30a90 100644 --- a/docs/components/custom.mdx +++ b/docs/components/custom.mdx @@ -3,8 +3,6 @@ title: Custom Components description: Use page level custom components in your documentation --- -# Custom Components - Those familar with [MDX](https://mdxjs.com/) may be familiar with the concept of custom components. With MDX, you can create your own components and use them in your markdown files. diff --git a/docs/components/headings.mdx b/docs/components/headings.mdx index 2b785fc6..6b3887c8 100644 --- a/docs/components/headings.mdx +++ b/docs/components/headings.mdx @@ -3,11 +3,11 @@ title: Headings description: Use headings in your documentation --- -# Headings - All markdown headings will be rendered in a size ordered format. You can render headings using the `#` symbol, with the number of hashes indicating the heading type. +## Example + ``` # Heading 1 ## Heading 2 diff --git a/docs/components/icons.mdx b/docs/components/icons.mdx new file mode 100644 index 00000000..f54b26d5 --- /dev/null +++ b/docs/components/icons.mdx @@ -0,0 +1,23 @@ +--- +title: Icons +description: Display icons from Font Awesome. +--- + +You can use icons from [Font Awesome](https://fontawesome.com/icons) using the `` component. + +The component takes a `name` property which represents the name of the icon on the Font Awesome website. + +## Example + +```jsx + +``` + + + +## Props + +- `name` (string): The name of the icon on the Font Awesome website. +- `size` (number): The optional size of the icon in pixels. Defaults to the parent's font size. + +The other props are passed to the underlying `` element. \ No newline at end of file diff --git a/docs/components/images.mdx b/docs/components/images.mdx index a0172c90..bf88da1a 100644 --- a/docs/components/images.mdx +++ b/docs/components/images.mdx @@ -3,12 +3,12 @@ title: Images description: Add images to your documentation --- -# Images - Images can be displayed from remote, or local sources using either standard Markdown syntax, or the `` component. -To display images using Markdown syntax, use the following syntax: `![alt text](image url)` +## Example + +To display images using Markdown syntax, use the following syntax: `![alt text](image url)`: ![Orange Car](https://images.unsplash.com/photo-1671483579112-0872cb05978f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1469&q=80) @@ -24,7 +24,7 @@ that reference. For example, if your repository contains a file at `/docs/assets/my-image.png`, pass `/assets/my-image.png` as the source of the image. -## Image Component +## Image Attributes The `` component can be used to enhance images within your documentation. The component accepts all of the standard `HTMLImageElement` attributes: @@ -33,7 +33,7 @@ all of the standard `HTMLImageElement` attributes: Pretty picture ``` -### Zooming +## Zooming The `` component also supports zooming of images. To enable zooming, pass the `zoom` prop to the image: @@ -64,3 +64,14 @@ To add a caption below the image, use the `caption` prop: caption="An abstract image" src="https://images.unsplash.com/photo-1671519821564-ced7e41ee7ae?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1364&q=80" /> + +## Theme + +By default, images will be displayed on both the light and dark theme. To display an image only a specific +theme, use the `theme` with either `light` or `dark`: + +```jsx + + + +``` \ No newline at end of file diff --git a/docs/components/index.mdx b/docs/components/index.mdx new file mode 100644 index 00000000..20652a83 --- /dev/null +++ b/docs/components/index.mdx @@ -0,0 +1,3 @@ +--- +redirect: /components/accordion +--- \ No newline at end of file diff --git a/docs/components/property.mdx b/docs/components/property.mdx new file mode 100644 index 00000000..a38297b1 --- /dev/null +++ b/docs/components/property.mdx @@ -0,0 +1,94 @@ +--- +title: Property +description: Display the signature details of a property. +--- + +Use the `` component to display the signature of a property, such as the name, type and description. + +The description can be provided as children to the component, allowing for rich formatting using Markdown syntax. + +## Example + + + Defines the theming properties for your application. + + ```json + { + "color": "#ff0000", + "font": "Arial" + } + ``` + + The `Theme` object should contain the following properties: + + + + The theme colour of your brand. + + + --- + + + The font family to use for your application. Defaults to `Comic Sans`. + + + + +## Usage + +````jsx + + Defines the theming properties for your application. + + ```json + { + "color": "#ff0000", + "font": "Arial" + } + ``` + + The `Theme` object should contain the following properties: + + + + The theme colour of your brand. + + + --- + + + The font family to use for your application. Defaults to `Comic Sans`. + + + +```` + +## Props + + + The name of the property + + +--- + + + The type of the property, such as `string`, `boolean` etc. + + +--- + + + Whether to mark this property as required. + + +--- + + + Whether to mark this property as optional. + + +--- + + + The children to render as the properties description. + \ No newline at end of file diff --git a/docs/components/steps.mdx b/docs/components/steps.mdx new file mode 100644 index 00000000..7bbfc6a9 --- /dev/null +++ b/docs/components/steps.mdx @@ -0,0 +1,80 @@ +--- +title: Steps +description: Steps allow users to navigate through a series of steps in a sequenced order. +--- + +Use the `` & `` component to display data in a sequential order to your users, useful for onboarding or guiding users through a process. + +## Example + + + + Install via npm: + + ```js + npm install newpackage + ``` + + The `newpackage` executable will be available in your terminal. + + + Configure the package + + ```js + newpackage configure --init + ``` + + + Start your development server. + + ```js + newpackage dev --port 3000 + ``` + + + + +````jsx + + + Install via npm: + + ```js + npm install newpackage + ``` + + The `newpackage` executable will be available in your terminal. + + + Configure the package + + ```js + newpackage configure --init + ``` + + + Start your development server. + + ```js + newpackage dev + ``` + + +```` + +This example will render the following: + + + +## Props + + + The title of the Step. + + +--- + + + An optional [Font Awesome Icon](/components/icons) to display instead of the current step index. + + diff --git a/docs/components/tabs.mdx b/docs/components/tabs.mdx index ece546ba..e1114ccb 100644 --- a/docs/components/tabs.mdx +++ b/docs/components/tabs.mdx @@ -1,55 +1,45 @@ --- -title: Display content within different tabs +title: Tabs description: Learn how to display content within different tabs --- -# Tabs +Use the `` component to display content within different tabs. -Tabs are a great way to display content within different tabs. Tabs can be displayed using the `` component, -providing a label and value. +## Example -## Basic Usage + + 👋 This is the content for the first tab. + ...and this is the content for the second tab! + -To display tabs, provide the `` component with a list of values: +To display tabs, provide the `` component with child `` components: ```jsx - - 👋 This is the content for the first tab. - ...and this is the content for the second tab! + + 👋 This is the content for the first tab. + ...and this is the content for the second tab! ``` - - 👋 This is the content for the first tab. - ...and this is the content for the second tab! - +## `Tabs` Props -## Default Value + + The default tab to display. Needs to match a `TabItem`'s `value` prop. + + Defaults to the first tab. + -By default, the first tab will be displayed. To display the second tab, provide a `defaultValue` prop. +## `TabItem` Props -```jsx - - 👋 This is the content for the first tab. - ...and this is the content for the second tab! - -``` + + The label to display for the tab. + + +--- + + + The value of the tab. Use this to identify the tab for the `defaultValue` property. + ## Synchronizing Tabs @@ -64,10 +54,6 @@ don't have to keep selecting it everywhere. ```jsx ``` @@ -77,14 +63,8 @@ The below examples shows how to perform a network request to our API. First, import the package: - - + + Import the `axios` package: ```bash @@ -98,7 +78,7 @@ First, import the package: ```` - + Add the `dio` package: ```bash @@ -116,20 +96,14 @@ First, import the package: Next, perform a network request to 'https://example.com': - - + + ```js const response = await axios.get('https://example.com'); console.log(response.data); ``` - + ```dart var dio = Dio(); final response = await dio.get('https://example.com'); diff --git a/docs/components/tweet.mdx b/docs/components/tweet.mdx index e783e21a..c23f353f 100644 --- a/docs/components/tweet.mdx +++ b/docs/components/tweet.mdx @@ -1,19 +1,19 @@ --- -title: Embed Tweets +title: Tweets description: Embed Tweets in your documentation. --- -# Tweets +Use the `` component to embed tweets in your documentation, using the Tweet ID. -To imbed a tweet into the documentation, use the `` component and provide the ID of the tweet. +## Example -```jsx -``` +```jsx +``` -## Cards +### Cards To display tweet preview cards alongside the tweet, provide the `cards` prop. @@ -23,7 +23,7 @@ To display tweet preview cards alongside the tweet, provide the `cards` prop. -## Conversation +### Conversation To display conversation information about the tweet, provide the `conversation` prop. @@ -33,7 +33,7 @@ To display conversation information about the tweet, provide the `conversation` -## Cards & Conversation +### Cards & Conversation Both card and conversational data can be displayed simultaneously. @@ -42,3 +42,21 @@ Both card and conversational data can be displayed simultaneously. ``` + +## Props + + + The ID of the tweet to display. + + +--- + + + Whether or not to display tweet preview cards. Defaults to `false`. + + +--- + + + Whether or not to display conversation information. Defaults to `false`. + diff --git a/docs/components/video.mdx b/docs/components/video.mdx index b1c3e441..b9e1a120 100644 --- a/docs/components/video.mdx +++ b/docs/components/video.mdx @@ -1,14 +1,15 @@ --- -title: Add videos to your documentation +title: Videos description: Learn how to add videos to your documentation. --- -# Video +Use the `
- ), +
+ )} +
Powered by docs.page
+ , { width: 1200, height: 630, fonts: [ { - name: 'Inter', + name: "Inter", data: fontData[0], weight: 400, - style: 'normal', + style: "normal", }, { - name: 'Inter', + name: "Inter", data: fontData[1], weight: 700, - style: 'normal', + style: "normal", }, ], }, ); - } catch (e: any) { - console.log(`${e.message}`); - return new Response(`Failed to generate the image`, { + } catch (e: unknown) { + console.log(e); + return new Response("Failed to generate the image", { status: 500, }); } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index e979c6a8..00000000 --- a/package-lock.json +++ /dev/null @@ -1,3171 +0,0 @@ -{ - "name": "docs.page", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "typescript": "^4.5.4" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.9.1", - "@typescript-eslint/parser": "^5.9.1", - "concurrently": "^7.0.0", - "eslint": "^8.6.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.28.0", - "prettier": "2.7.1", - "prettier-plugin-astro": "^0.7.0", - "prettier-plugin-tailwindcss": "^0.1.3" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@astrojs/compiler": { - "version": "0.31.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/runtime": { - "version": "7.24.0", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.11.3", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", - "es-shim-unscopables": "^1.0.2" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "7.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "date-fns": "^2.29.1", - "lodash": "^4.17.21", - "rxjs": "^7.0.0", - "shell-quote": "^1.7.3", - "spawn-command": "^0.0.2-1", - "supports-color": "^8.1.0", - "tree-kill": "^1.2.2", - "yargs": "^17.3.1" - }, - "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/date-fns": { - "version": "2.30.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/es-abstract": { - "version": "1.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.1", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.0", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.0.17", - "dev": true, - "license": "MIT", - "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.4", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.2", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" - }, - "peerDependenciesMeta": { - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "5.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.7.1", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/prettier-plugin-astro": { - "version": "0.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@astrojs/compiler": "^0.31.3", - "prettier": "^2.7.1", - "sass-formatter": "^0.7.5", - "synckit": "^0.8.4" - }, - "engines": { - "node": "^14.15.0 || >=16.0.0", - "pnpm": ">=7.14.0" - } - }, - "node_modules/prettier-plugin-astro/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-tailwindcss": { - "version": "0.1.13", - "dev": true, - "engines": { - "node": ">=12.17.0" - }, - "peerDependencies": { - "prettier": ">=2.2.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react-is": { - "version": "16.13.1", - "dev": true, - "license": "MIT" - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.0.0", - "get-intrinsic": "^1.2.3", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "dev": true, - "license": "MIT" - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "2.0.0-next.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/s.color": { - "version": "0.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-array-concat": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sass-formatter": { - "version": "0.7.9", - "dev": true, - "license": "MIT", - "dependencies": { - "suf-log": "^2.5.3" - } - }, - "node_modules/semver": { - "version": "7.6.0", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.2", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/spawn-command": { - "version": "0.0.2-1", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/suf-log": { - "version": "2.5.3", - "dev": true, - "license": "MIT", - "dependencies": { - "s.color": "0.0.15" - } - }, - "node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.8.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "dev": true, - "license": "0BSD" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "4.9.5", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.14", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.6", - "call-bind": "^1.0.5", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json index 2218d8ea..f7895291 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,22 @@ { "private": true, "scripts": { - "dev": "concurrently \"yarn dev:api\" \"yarn dev:website\"", - "dev:website": "cd website && yarn dev", - "dev:api": "cd api && yarn dev", - "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,mdx}\"", - "check:linting": "eslint . --max-warnings=0", - "check:formatting": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,mdx}\"" + "dev": "concurrently \"npm run dev:api\" \"npm run dev:website\"", + "dev:api": "cd api && bun dev", + "dev:website": "cd website && npm run dev", + "check": "npx @biomejs/biome check --write ." }, "dependencies": { - "typescript": "^4.5.4" + "typescript": "^5.5.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.9.1", - "@typescript-eslint/parser": "^5.9.1", - "concurrently": "^7.0.0", - "eslint": "^8.6.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.28.0", - "prettier": "2.7.1", - "prettier-plugin-astro": "^0.7.0", - "prettier-plugin-tailwindcss": "^0.1.3" - } + "@biomejs/biome": "1.8.3", + "concurrently": "^7.0.0" + }, + "workspaces": [ + "api", + "website", + "og", + "packages/*" + ] } diff --git a/packages/client/.gitignore b/packages/cli/.gitignore similarity index 100% rename from packages/client/.gitignore rename to packages/cli/.gitignore diff --git a/packages/client/.npmignore b/packages/cli/.npmignore similarity index 100% rename from packages/client/.npmignore rename to packages/cli/.npmignore diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 00000000..50be2096 --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,9 @@ +## 1.0.4 + +- Fixed issues with the `init` command failing to run. +- Added prompts to confirm installation directory. + +## 1.0.0 + +- `init` command now creates a `docs.json` and `docs` folder in the root of the project. +- `check` command outputs issues with the documentation, such as invalid links. \ No newline at end of file diff --git a/packages/client/LICENSE b/packages/cli/LICENSE similarity index 100% rename from packages/client/LICENSE rename to packages/cli/LICENSE diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..655734e4 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,23 @@ +# @docs.page/cli + +The docs.page CLI provides useful commands for managing your documentation via [https://docs.page](https://docs.page). + +To learn more, view the [documentation](https://use.docs.page/cli). + +## Commands + +### `init` + +Initializes a new documentation project in the current directory. This command creates a `docs.json` file and a `docs` folder in the root of the project. + +```sh +npx @docs.page/cli init new-project +``` + +### `check` + +Checks the documentation for issues, such as invalid links. + +```sh +npx @docs.page/cli check +``` \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..eb355497 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,34 @@ +{ + "name": "@docs.page/cli", + "version": "1.0.4", + "author": "Invertase (http://invertase.io)", + "license": "Apache-2.0", + "type": "module", + "bin": { + "@docs.page/cli": "dist/cli.js" + }, + "keywords": ["docs.page", "documentation", "docs"], + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "rimraf dist && npx tsup src/index.ts src/cli.ts --format esm --dts", + "watch": "npx tsup src/index.ts src/cli.ts --format esm --dts --watch", + "prepublishOnly": "npm run build" + }, + "files": ["dist", "LICENSE", "README.md"], + "dependencies": { + "@inquirer/prompts": "^5.3.8", + "chalk": "^5.3.0", + "commander": "^12.1.0", + "rimraf": "^6.0.1" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.2.4", + "typescript": "^5.5.4" + } +} diff --git a/packages/cli/src/check/configuration.ts b/packages/cli/src/check/configuration.ts new file mode 100644 index 00000000..42466ecb --- /dev/null +++ b/packages/cli/src/check/configuration.ts @@ -0,0 +1,8 @@ +import type { CheckResult, Routes } from "./types"; + +export function* checkConfiguration( + routes: Routes, + configuration: unknown, +): Generator { + // +} diff --git a/packages/cli/src/check/index.ts b/packages/cli/src/check/index.ts new file mode 100644 index 00000000..f411f672 --- /dev/null +++ b/packages/cli/src/check/index.ts @@ -0,0 +1,84 @@ +import { checkConfiguration } from "./configuration"; +import { checkRelativeLinks } from "./relative-links"; +import type { CheckResult, Route } from "./types"; +export type * from "./types"; + +export async function* check( + // A set of files in the directory to check. + files: Set, + // Callback to get the content of a file (e.g. from fs, or a virtual file system). + getFile: (path: string) => Promise, +): AsyncGenerator { + // There must be a docs.json (or deprecated docs.yaml) file in the directory. + if (!files.has("docs.json") && !files.has("docs.yaml")) { + yield { + type: "error", + message: + "Directory is missing a configuration file. Expected a `docs.json` file in the root of the directory.", + }; + } + + const routes = new Map(); + + // Extract the routes from the files + for (const filePath of files) { + // We only care about files within the /docs/** directory + if (!filePath.startsWith("docs/")) { + continue; + } + + // Remove the docs/ prefix from the file path + let normalizedFilePath = filePath.replace(/^docs\//, ""); + + // File paths can either be /hello/world.mdx, so convert it to /hello/world + normalizedFilePath = normalizedFilePath.replace(/\.(mdx)$/i, ""); + + // If ending with index + normalizedFilePath = normalizedFilePath.replace(/\/index$/i, ""); + + // If the normalized path is index, it means the file is the index file. + if (normalizedFilePath === "index") { + normalizedFilePath = ""; + } + + // If the normalized path is empty, it means the file is the index file. + normalizedFilePath = normalizedFilePath ? `/${normalizedFilePath}` : "/"; + + // If there is a clash of routes (e.g. /hello/world.mdx and /hello/world/index.mdx), + // throw an error. + if (routes.has(normalizedFilePath)) { + yield { + type: "error", + message: `There are multiple files which resolve to the same route "${normalizedFilePath}".`, + filePath, + line: 0, + column: 0, + }; + } + + // Store the file as a route. + routes.set(normalizedFilePath, { + filePath, + // Don't bother reading none .mdx files, since we don't care about their content. + content: filePath.endsWith(".mdx") ? await getFile(filePath) : "", + }); + } + + try { + const configFile = await getFile( + files.has("docs.json") ? "docs.json" : "docs.yaml", + ); + + yield* checkConfiguration(routes, JSON.parse(configFile!)); + } catch { + yield { + type: "error", + message: "Failed to parse the configuration file.", + filePath: files.has("docs.json") ? "docs.json" : "docs.yaml", + line: 0, + column: 0, + }; + } + + yield* checkRelativeLinks(routes); +} diff --git a/packages/cli/src/check/relative-links.ts b/packages/cli/src/check/relative-links.ts new file mode 100644 index 00000000..43ae7d8c --- /dev/null +++ b/packages/cli/src/check/relative-links.ts @@ -0,0 +1,93 @@ +import type { CheckResult, Route, Routes } from "./types"; + +const CODE_BLOCK = /```[\s\S]*?```/g; + +const IDENTIFIERS = { + // Matches all markdown links (e.g. [text](href)) + MD_LINK: /\[.*?\]\((.*?)\)/g, + // Matches all markdown images (e.g. ![alt](src)) + MDX_IMAGE: /!\[.*?\]\((.*?)\)/g, + // Matches all anchor html tags (e.g. ) + ANCHOR_HREF: /]*?\s+)?href="(.*?)"/g, + // Matches all image html tags (e.g. ) + IMAGE_SRC: /]*?\s+)?src="(.*?)"/g, + // Matches all MDX Image components (e.g. ) + MDX_IMAGE_SRC: /]*?\s+)?src="(.*?)"/g, + // Matches all video html tags (e.g. + ); +} diff --git a/website/app/components/Button.tsx b/website/app/components/Button.tsx new file mode 100644 index 00000000..ee507f1f --- /dev/null +++ b/website/app/components/Button.tsx @@ -0,0 +1,55 @@ +import { ChevronRightIcon } from "lucide-react"; +import { type ComponentProps, cloneElement, createElement } from "react"; +import { cn } from "~/utils"; + +type Props = + | ({ + as: "a"; + href: string; + children: string; + cta?: boolean; + } & ComponentProps<"a">) + | ({ + as: "button"; + children: string; + cta?: boolean; + } & ComponentProps<"button">); + +export function Button({ className, cta, ...props }: Props) { + let el: React.ReactNode; + + if (props.as === "button") { + el =