From b4ed23eed21f2cb488ecd8369d6f124434474b8a Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Fri, 7 Apr 2023 14:26:39 +0800 Subject: [PATCH 1/6] chore: move annocemnent bar to packages. announcement bar now in docs site as well. --- apps/docs/layouts/SiteLayout.tsx | 6 ++++++ apps/www/components/Nav/index.tsx | 6 ++---- packages/ui/index.tsx | 5 +++++ .../ui/src/layout/banners/Announcement.Countdown.tsx | 4 ++-- .../Nav => packages/ui/src/layout/banners}/Announcement.tsx | 2 +- .../ui/src/layout/banners}/data/Announcement.json | 0 packages/ui/src/layout/banners/index.tsx | 2 ++ 7 files changed, 18 insertions(+), 7 deletions(-) rename apps/www/components/LaunchWeek/Banners/CountdownBanner.tsx => packages/ui/src/layout/banners/Announcement.Countdown.tsx (96%) rename {apps/www/components/Nav => packages/ui/src/layout/banners}/Announcement.tsx (97%) rename {apps/www => packages/ui/src/layout/banners}/data/Announcement.json (100%) create mode 100644 packages/ui/src/layout/banners/index.tsx diff --git a/apps/docs/layouts/SiteLayout.tsx b/apps/docs/layouts/SiteLayout.tsx index 68c0b88d87b..6b516e64e54 100644 --- a/apps/docs/layouts/SiteLayout.tsx +++ b/apps/docs/layouts/SiteLayout.tsx @@ -8,6 +8,7 @@ import { memo, useEffect } from 'react' import Footer from '~/components/Navigation/Footer' import { menuState, useMenuLevelId, useMenuMobileOpen } from '~/hooks/useMenuState' import Head from 'next/head' +import { Announcement, AnnouncementCountdown } from 'ui' const levelsData = { home: { @@ -334,6 +335,11 @@ const SiteLayout = ({ children }) => { Supabase Docs
+
+ + + +
diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 7baf842c5f5..ce576e04b80 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' -import { Button, Badge, IconStar, IconChevronDown } from 'ui' +import { Button, Badge, Announcement, AnnouncementCountdown, IconStar, IconChevronDown } from 'ui' import FlyOut from '~/components/UI/FlyOut' import Transition from 'lib/Transition' @@ -10,14 +10,12 @@ import SolutionsData from 'data/Solutions.json' import Solutions from '~/components/Nav/Product' import Developers from '~/components/Nav/Developers' -import Announcement from '~/components/Nav/Announcement' import { useIsLoggedIn, useTheme } from 'common' import TextLink from '../TextLink' import Image from 'next/image' import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png' import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png' -import CountdownBanner from '../LaunchWeek/Banners/CountdownBanner' const Nav = () => { const { isDarkMode } = useTheme() @@ -196,7 +194,7 @@ const Nav = () => { return ( <> - +
Date: Thu, 20 Apr 2023 17:29:01 -0600 Subject: [PATCH 2/6] refactor(search): split sources into their own files --- apps/docs/scripts/generate-embeddings.ts | 452 +----------------- apps/docs/scripts/search/sources/base.ts | 20 + apps/docs/scripts/search/sources/index.ts | 85 ++++ apps/docs/scripts/search/sources/markdown.ts | 191 ++++++++ .../scripts/search/sources/reference-doc.ts | 139 ++++++ apps/docs/scripts/search/sources/util.ts | 43 ++ 6 files changed, 487 insertions(+), 443 deletions(-) create mode 100644 apps/docs/scripts/search/sources/base.ts create mode 100644 apps/docs/scripts/search/sources/index.ts create mode 100644 apps/docs/scripts/search/sources/markdown.ts create mode 100644 apps/docs/scripts/search/sources/reference-doc.ts create mode 100644 apps/docs/scripts/search/sources/util.ts diff --git a/apps/docs/scripts/generate-embeddings.ts b/apps/docs/scripts/generate-embeddings.ts index 520450083e3..23158f7f5bc 100644 --- a/apps/docs/scripts/generate-embeddings.ts +++ b/apps/docs/scripts/generate-embeddings.ts @@ -1,411 +1,24 @@ import { createClient } from '@supabase/supabase-js' -import { createHash } from 'crypto' import dotenv from 'dotenv' -import { ObjectExpression } from 'estree' -import { readdir, readFile, stat } from 'fs/promises' -import GithubSlugger from 'github-slugger' -import yaml from 'js-yaml' -import { Content, Root } from 'mdast' -import { fromMarkdown } from 'mdast-util-from-markdown' -import { mdxFromMarkdown, MdxjsEsm } from 'mdast-util-mdx' -import { toMarkdown } from 'mdast-util-to-markdown' -import { toString } from 'mdast-util-to-string' -import { mdxjs } from 'micromark-extension-mdxjs' import 'openai' import { Configuration, OpenAIApi } from 'openai' -import { OpenAPIV3 } from 'openapi-types' -import { basename, dirname, join } from 'path' -import { u } from 'unist-builder' -import { filter } from 'unist-util-filter' import { inspect } from 'util' -import { ICommonFunc, IFunctionDefinition, ISpec } from '../components/reference/Reference.types' -import { CliCommand, CliSpec } from '../generator/types/CliSpec' -import { flattenSections } from '../lib/helpers' -import { enrichedOperation, gen_v3 } from '../lib/refGenerator/helpers' +import { fetchSources } from './search/sources' dotenv.config() -const ignoredFiles = ['pages/404.mdx'] - -/** - * Extracts ES literals from an `estree` `ObjectExpression` - * into a plain JavaScript object. - */ -function getObjectFromExpression(node: ObjectExpression) { - return node.properties.reduce< - Record - >((object, property) => { - if (property.type !== 'Property') { - return object - } - - const key = (property.key.type === 'Identifier' && property.key.name) || undefined - const value = (property.value.type === 'Literal' && property.value.value) || undefined - - if (!key) { - return object - } - - return { - ...object, - [key]: value, - } - }, {}) -} - -/** - * Extracts the `meta` ESM export from the MDX file. - * - * This info is akin to frontmatter. - */ -function extractMetaExport(mdxTree: Root) { - const metaExportNode = mdxTree.children.find((node): node is MdxjsEsm => { - return ( - node.type === 'mdxjsEsm' && - node.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' && - node.data.estree.body[0].declaration?.type === 'VariableDeclaration' && - node.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' && - node.data.estree.body[0].declaration.declarations[0].id.name === 'meta' - ) - }) - - if (!metaExportNode) { - return undefined - } - - const objectExpression = - (metaExportNode.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' && - metaExportNode.data.estree.body[0].declaration?.type === 'VariableDeclaration' && - metaExportNode.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' && - metaExportNode.data.estree.body[0].declaration.declarations[0].id.name === 'meta' && - metaExportNode.data.estree.body[0].declaration.declarations[0].init?.type === - 'ObjectExpression' && - metaExportNode.data.estree.body[0].declaration.declarations[0].init) || - undefined - - if (!objectExpression) { - return undefined - } - - return getObjectFromExpression(objectExpression) -} - -/** - * Splits a `mdast` tree into multiple trees based on - * a predicate function. Will include the splitting node - * at the beginning of each tree. - * - * Useful to split a markdown file into smaller sections. - */ -function splitTreeBy(tree: Root, predicate: (node: Content) => boolean) { - return tree.children.reduce((trees, node) => { - const [lastTree] = trees.slice(-1) - - if (!lastTree || predicate(node)) { - const tree: Root = u('root', [node]) - return trees.concat(tree) - } - - lastTree.children.push(node) - return trees - }, []) -} - -type Meta = ReturnType - -type Section = { - content: string - heading?: string - slug?: string -} - -type ProcessedMdx = { - checksum: string - meta: Meta - sections: Section[] -} - -/** - * Processes MDX content for search indexing. - * It extracts metadata, strips it of all JSX, - * and splits it into sub-sections based on criteria. - */ -function processMdxForSearch(content: string): ProcessedMdx { - const checksum = createHash('sha256').update(content).digest('base64') - - const mdxTree = fromMarkdown(content, { - extensions: [mdxjs()], - mdastExtensions: [mdxFromMarkdown()], - }) - - const meta = extractMetaExport(mdxTree) - - // Remove all MDX elements from markdown - const mdTree = filter( - mdxTree, - (node) => - ![ - 'mdxjsEsm', - 'mdxJsxFlowElement', - 'mdxJsxTextElement', - 'mdxFlowExpression', - 'mdxTextExpression', - ].includes(node.type) - ) - - if (!mdTree) { - return { - checksum, - meta, - sections: [], - } - } - - const sectionTrees = splitTreeBy(mdTree, (node) => node.type === 'heading') - - const slugger = new GithubSlugger() - - const sections = sectionTrees.map((tree) => { - const [firstNode] = tree.children - - const heading = firstNode.type === 'heading' ? toString(firstNode) : undefined - const slug = heading ? slugger.slug(heading) : undefined - - return { - content: toMarkdown(tree), - heading, - slug, - } - }) - - return { - checksum, - meta, - sections, - } -} - -type WalkEntry = { - path: string - parentPath?: string -} - -async function walk(dir: string, parentPath?: string): Promise { - const immediateFiles = await readdir(dir) - - const recursiveFiles = await Promise.all( - immediateFiles.map(async (file) => { - const path = join(dir, file) - const stats = await stat(path) - if (stats.isDirectory()) { - // Keep track of document hierarchy (if this dir has corresponding doc file) - const docPath = `${basename(path)}.mdx` - - return walk( - path, - immediateFiles.includes(docPath) ? join(dirname(path), docPath) : parentPath - ) - } else if (stats.isFile()) { - return [ - { - path: path, - parentPath, - }, - ] - } else { - return [] - } - }) - ) - - const flattenedFiles = recursiveFiles.reduce( - (all, folderContents) => all.concat(folderContents), - [] - ) - - return flattenedFiles.sort((a, b) => a.path.localeCompare(b.path)) -} - -abstract class BaseEmbeddingSource { - checksum?: string - meta?: Meta - sections?: Section[] - - constructor(public source: string, public path: string, public parentPath?: string) {} - - abstract load(): Promise<{ checksum: string; meta?: Meta; sections: Section[] }> -} - -class MarkdownEmbeddingSource extends BaseEmbeddingSource { - type: 'markdown' = 'markdown' - - constructor(source: string, public filePath: string, public parentFilePath?: string) { - const path = filePath.replace(/^pages/, '').replace(/\.mdx?$/, '') - const parentPath = parentFilePath?.replace(/^pages/, '').replace(/\.mdx?$/, '') - - super(source, path, parentPath) - } - - async load() { - const contents = await readFile(this.filePath, 'utf8') - - const { checksum, meta, sections } = processMdxForSearch(contents) - - this.checksum = checksum - this.meta = meta - this.sections = sections - - return { - checksum, - meta, - sections, - } - } -} - -abstract class ReferenceEmbeddingSource extends BaseEmbeddingSource { - type: 'reference' = 'reference' - - constructor( - source: string, - path: string, - public meta: Meta, - public specFilePath: string, - public sectionsFilePath: string - ) { - super(source, path) - } - - async load() { - const specContents = await readFile(this.specFilePath, 'utf8') - const refSectionsContents = await readFile(this.sectionsFilePath, 'utf8') - - const refSections: ICommonFunc[] = JSON.parse(refSectionsContents) - const flattenedRefSections = flattenSections(refSections) - - const checksum = createHash('sha256') - .update(specContents + refSectionsContents) - .digest('base64') - - const specSections = this.getSpecSections(specContents) - - const sections = flattenedRefSections - .map((refSection) => { - const specSection = this.matchSpecSection(specSections, refSection.id) - - if (!specSection) { - return - } - - return { - heading: refSection.title, - slug: refSection.slug, - content: `${this.meta.title} for ${refSection.title}:\n${this.formatSection( - specSection, - refSection - )}`, - } - }) - .filter((section) => !!section) - - this.checksum = checksum - this.sections = sections - - return { - checksum, - sections, - meta: this.meta, - } - } - - abstract getSpecSections(specContents: string): SpecSection[] - abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection - abstract formatSection(specSection: SpecSection, refSection: ICommonFunc): string -} - -class OpenApiEmbeddingSource extends ReferenceEmbeddingSource { - getSpecSections(specContents: string): enrichedOperation[] { - const spec: OpenAPIV3.Document<{}> = JSON.parse(specContents) - - const generatedSpec = gen_v3(spec, '', { - apiUrl: 'apiv0', - }) - - return generatedSpec.operations - } - matchSpecSection(operations: enrichedOperation[], id: string): enrichedOperation { - return operations.find((operation) => operation.operationId === id) - } - formatSection(specOperation: enrichedOperation) { - const { summary, description, operation, path, tags } = specOperation - return JSON.stringify({ - summary, - description, - operation, - path, - tags, - }) - } -} - -class ClientLibEmbeddingSource extends ReferenceEmbeddingSource { - getSpecSections(specContents: string): IFunctionDefinition[] { - const spec = yaml.load(specContents) as ISpec - - return spec.functions - } - matchSpecSection(functionDefinitions: IFunctionDefinition[], id: string): IFunctionDefinition { - return functionDefinitions.find((functionDefinition) => functionDefinition.id === id) - } - formatSection(functionDefinition: IFunctionDefinition, refSection: ICommonFunc): string { - const { title } = refSection - const { description, title: functionName } = functionDefinition - - return JSON.stringify({ - title, - description, - functionName, - }) - } -} - -class CliEmbeddingSource extends ReferenceEmbeddingSource { - getSpecSections(specContents: string): CliCommand[] { - const spec = yaml.load(specContents) as CliSpec - - return spec.commands - } - matchSpecSection(cliCommands: CliCommand[], id: string): CliCommand { - return cliCommands.find((cliCommand) => cliCommand.id === id) - } - formatSection(cliCommand: CliCommand): string { - const { summary, description, usage } = cliCommand - return JSON.stringify({ - summary, - description, - usage, - }) - } -} - -type EmbeddingSource = - | MarkdownEmbeddingSource - | OpenApiEmbeddingSource - | ClientLibEmbeddingSource - | CliEmbeddingSource - async function generateEmbeddings() { // TODO: use better CLI lib like yargs const args = process.argv.slice(2) const shouldRefresh = args.includes('--refresh') - if ( - !process.env.NEXT_PUBLIC_SUPABASE_URL || - !process.env.SUPABASE_SERVICE_ROLE_KEY || - !process.env.OPENAI_KEY - ) { - return console.log( - 'Environment variables NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and OPENAI_KEY are required: skipping embeddings generation' + const requiredEnvVars = ['NEXT_PUBLIC_SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'OPENAI_KEY'] + + if (requiredEnvVars.some((name) => !process.env[name])) { + throw new Error( + `Environment variables ${requiredEnvVars.join( + ', ' + )} are required: skipping embeddings generation` ) } @@ -420,54 +33,7 @@ async function generateEmbeddings() { } ) - const embeddingSources: EmbeddingSource[] = [ - new OpenApiEmbeddingSource( - 'api', - '/reference/api', - { title: 'Management API Reference' }, - '../../spec/transforms/api_v0_openapi_deparsed.json', - '../../spec/common-api-sections.json' - ), - new ClientLibEmbeddingSource( - 'js-lib', - '/reference/javascript', - { title: 'JavaScript Reference' }, - '../../spec/supabase_js_v2.yml', - '../../spec/common-client-libs-sections.json' - ), - new ClientLibEmbeddingSource( - 'dart-lib', - '/reference/dart', - { title: 'Dart Reference' }, - '../../spec/supabase_dart_v1.yml', - '../../spec/common-client-libs-sections.json' - ), - new ClientLibEmbeddingSource( - 'python-lib', - '/reference/python', - { title: 'Python Reference' }, - '../../spec/supabase_py_v2.yml', - '../../spec/common-client-libs-sections.json' - ), - new ClientLibEmbeddingSource( - 'csharp-lib', - '/reference/csharp', - { title: 'C# Reference' }, - '../../spec/supabase_csharp_v0.yml', - '../../spec/common-client-libs-sections.json' - ), - new CliEmbeddingSource( - 'cli', - '/reference/cli', - { title: 'CLI Reference' }, - '../../spec/cli_v1_commands.yaml', - '../../spec/common-cli-sections.json' - ), - ...(await walk('pages')) - .filter(({ path }) => /\.mdx?$/.test(path)) - .filter(({ path }) => !ignoredFiles.includes(path)) - .map((entry) => new MarkdownEmbeddingSource('guide', entry.path)), - ] + const embeddingSources = await fetchSources() console.log(`Discovered ${embeddingSources.length} pages`) diff --git a/apps/docs/scripts/search/sources/base.ts b/apps/docs/scripts/search/sources/base.ts new file mode 100644 index 00000000000..3a12513ed96 --- /dev/null +++ b/apps/docs/scripts/search/sources/base.ts @@ -0,0 +1,20 @@ +export type Json = Record< + string, + string | number | boolean | null | Json[] | { [key: string]: Json } +> + +export type Section = { + content: string + heading?: string + slug?: string +} + +export abstract class BaseSource { + checksum?: string + meta?: Json + sections?: Section[] + + constructor(public source: string, public path: string, public parentPath?: string) {} + + abstract load(): Promise<{ checksum: string; meta?: Json; sections: Section[] }> +} diff --git a/apps/docs/scripts/search/sources/index.ts b/apps/docs/scripts/search/sources/index.ts new file mode 100644 index 00000000000..66ff45b2932 --- /dev/null +++ b/apps/docs/scripts/search/sources/index.ts @@ -0,0 +1,85 @@ +import { MarkdownSource } from './markdown' +import { + CliReferenceSource, + ClientLibReferenceSource, + OpenApiReferenceSource, +} from './reference-doc' +import { walk } from './util' + +const ignoredFiles = ['pages/404.mdx'] + +export type SearchSource = + | MarkdownSource + | OpenApiReferenceSource + | ClientLibReferenceSource + | CliReferenceSource + +/** + * Fetches all the sources we want to index for search + */ +export async function fetchSources() { + const openApiReferenceSource = new OpenApiReferenceSource( + 'api', + '/reference/api', + { title: 'Management API Reference' }, + '../../spec/transforms/api_v0_openapi_deparsed.json', + '../../spec/common-api-sections.json' + ) + + const jsLibReferenceSource = new ClientLibReferenceSource( + 'js-lib', + '/reference/javascript', + { title: 'JavaScript Reference' }, + '../../spec/supabase_js_v2.yml', + '../../spec/common-client-libs-sections.json' + ) + + const dartLibReferenceSource = new ClientLibReferenceSource( + 'dart-lib', + '/reference/dart', + { title: 'Dart Reference' }, + '../../spec/supabase_dart_v1.yml', + '../../spec/common-client-libs-sections.json' + ) + + const pythonLibReferenceSource = new ClientLibReferenceSource( + 'python-lib', + '/reference/python', + { title: 'Python Reference' }, + '../../spec/supabase_py_v2.yml', + '../../spec/common-client-libs-sections.json' + ) + + const cSharpLibReferenceSource = new ClientLibReferenceSource( + 'csharp-lib', + '/reference/csharp', + { title: 'C# Reference' }, + '../../spec/supabase_csharp_v0.yml', + '../../spec/common-client-libs-sections.json' + ) + + const cliReferenceSource = new CliReferenceSource( + 'cli', + '/reference/cli', + { title: 'CLI Reference' }, + '../../spec/cli_v1_commands.yaml', + '../../spec/common-cli-sections.json' + ) + + const guideSources = (await walk('pages')) + .filter(({ path }) => /\.mdx?$/.test(path)) + .filter(({ path }) => !ignoredFiles.includes(path)) + .map((entry) => new MarkdownSource('guide', entry.path)) + + const sources: SearchSource[] = [ + openApiReferenceSource, + jsLibReferenceSource, + dartLibReferenceSource, + pythonLibReferenceSource, + cSharpLibReferenceSource, + cliReferenceSource, + ...guideSources, + ] + + return sources +} diff --git a/apps/docs/scripts/search/sources/markdown.ts b/apps/docs/scripts/search/sources/markdown.ts new file mode 100644 index 00000000000..1c7b29640c8 --- /dev/null +++ b/apps/docs/scripts/search/sources/markdown.ts @@ -0,0 +1,191 @@ +import { createHash } from 'crypto' +import { ObjectExpression } from 'estree' +import { readFile } from 'fs/promises' +import GithubSlugger from 'github-slugger' +import { Content, Root } from 'mdast' +import { fromMarkdown } from 'mdast-util-from-markdown' +import { MdxjsEsm, mdxFromMarkdown } from 'mdast-util-mdx' +import { toMarkdown } from 'mdast-util-to-markdown' +import { toString } from 'mdast-util-to-string' +import { mdxjs } from 'micromark-extension-mdxjs' +import { u } from 'unist-builder' +import { filter } from 'unist-util-filter' +import { BaseSource, Json, Section } from './base' + +/** + * Extracts ES literals from an `estree` `ObjectExpression` + * into a plain JavaScript object. + */ +export function getObjectFromExpression(node: ObjectExpression) { + return node.properties.reduce< + Record + >((object, property) => { + if (property.type !== 'Property') { + return object + } + + const key = (property.key.type === 'Identifier' && property.key.name) || undefined + const value = (property.value.type === 'Literal' && property.value.value) || undefined + + if (!key) { + return object + } + + return { + ...object, + [key]: value, + } + }, {}) +} + +/** + * Extracts the `meta` ESM export from the MDX file. + * + * This info is akin to frontmatter. + */ +export function extractMetaExport(mdxTree: Root) { + const metaExportNode = mdxTree.children.find((node): node is MdxjsEsm => { + return ( + node.type === 'mdxjsEsm' && + node.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' && + node.data.estree.body[0].declaration?.type === 'VariableDeclaration' && + node.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' && + node.data.estree.body[0].declaration.declarations[0].id.name === 'meta' + ) + }) + + if (!metaExportNode) { + return undefined + } + + const objectExpression = + (metaExportNode.data?.estree?.body[0]?.type === 'ExportNamedDeclaration' && + metaExportNode.data.estree.body[0].declaration?.type === 'VariableDeclaration' && + metaExportNode.data.estree.body[0].declaration.declarations[0]?.id.type === 'Identifier' && + metaExportNode.data.estree.body[0].declaration.declarations[0].id.name === 'meta' && + metaExportNode.data.estree.body[0].declaration.declarations[0].init?.type === + 'ObjectExpression' && + metaExportNode.data.estree.body[0].declaration.declarations[0].init) || + undefined + + if (!objectExpression) { + return undefined + } + + return getObjectFromExpression(objectExpression) +} + +/** + * Splits a `mdast` tree into multiple trees based on + * a predicate function. Will include the splitting node + * at the beginning of each tree. + * + * Useful to split a markdown file into smaller sections. + */ +export function splitTreeBy(tree: Root, predicate: (node: Content) => boolean) { + return tree.children.reduce((trees, node) => { + const [lastTree] = trees.slice(-1) + + if (!lastTree || predicate(node)) { + const tree: Root = u('root', [node]) + return trees.concat(tree) + } + + lastTree.children.push(node) + return trees + }, []) +} + +/** + * Processes MDX content for search indexing. + * It extracts metadata, strips it of all JSX, + * and splits it into sub-sections based on criteria. + */ +export function processMdxForSearch(content: string): ProcessedMdx { + const checksum = createHash('sha256').update(content).digest('base64') + + const mdxTree = fromMarkdown(content, { + extensions: [mdxjs()], + mdastExtensions: [mdxFromMarkdown()], + }) + + const meta = extractMetaExport(mdxTree) + const serializableMeta: Json = JSON.parse(JSON.stringify(meta)) + + // Remove all MDX elements from markdown + const mdTree = filter( + mdxTree, + (node) => + ![ + 'mdxjsEsm', + 'mdxJsxFlowElement', + 'mdxJsxTextElement', + 'mdxFlowExpression', + 'mdxTextExpression', + ].includes(node.type) + ) + + if (!mdTree) { + return { + checksum, + meta: serializableMeta, + sections: [], + } + } + + const sectionTrees = splitTreeBy(mdTree, (node) => node.type === 'heading') + + const slugger = new GithubSlugger() + + const sections = sectionTrees.map((tree) => { + const [firstNode] = tree.children + + const heading = firstNode.type === 'heading' ? toString(firstNode) : undefined + const slug = heading ? slugger.slug(heading) : undefined + + return { + content: toMarkdown(tree), + heading, + slug, + } + }) + + return { + checksum, + meta: serializableMeta, + sections, + } +} + +export type ProcessedMdx = { + checksum: string + meta: Json + sections: Section[] +} + +export class MarkdownSource extends BaseSource { + type = 'markdown' as const + + constructor(source: string, public filePath: string, public parentFilePath?: string) { + const path = filePath.replace(/^pages/, '').replace(/\.mdx?$/, '') + const parentPath = parentFilePath?.replace(/^pages/, '').replace(/\.mdx?$/, '') + + super(source, path, parentPath) + } + + async load() { + const contents = await readFile(this.filePath, 'utf8') + + const { checksum, meta, sections } = processMdxForSearch(contents) + + this.checksum = checksum + this.meta = meta + this.sections = sections + + return { + checksum, + meta, + sections, + } + } +} diff --git a/apps/docs/scripts/search/sources/reference-doc.ts b/apps/docs/scripts/search/sources/reference-doc.ts new file mode 100644 index 00000000000..9f2304700bc --- /dev/null +++ b/apps/docs/scripts/search/sources/reference-doc.ts @@ -0,0 +1,139 @@ +import { createHash } from 'crypto' +import { readFile } from 'fs/promises' +import yaml from 'js-yaml' +import { OpenAPIV3 } from 'openapi-types' +import { Meta } from '..' +import { + ICommonFunc, + IFunctionDefinition, + ISpec, +} from '../../../components/reference/Reference.types' +import { CliCommand, CliSpec } from '../../../generator/types/CliSpec' +import { flattenSections } from '../../../lib/helpers' +import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers' +import { BaseSource } from './base' + +export abstract class ReferenceSource extends BaseSource { + type = 'reference' as const + + constructor( + source: string, + path: string, + public meta: Meta, + public specFilePath: string, + public sectionsFilePath: string + ) { + super(source, path) + } + + async load() { + const specContents = await readFile(this.specFilePath, 'utf8') + const refSectionsContents = await readFile(this.sectionsFilePath, 'utf8') + + const refSections: ICommonFunc[] = JSON.parse(refSectionsContents) + const flattenedRefSections = flattenSections(refSections) + + const checksum = createHash('sha256') + .update(specContents + refSectionsContents) + .digest('base64') + + const specSections = this.getSpecSections(specContents) + + const sections = flattenedRefSections + .map((refSection) => { + const specSection = this.matchSpecSection(specSections, refSection.id) + + if (!specSection) { + return + } + + return { + heading: refSection.title, + slug: refSection.slug, + content: `${this.meta.title} for ${refSection.title}:\n${this.formatSection( + specSection, + refSection + )}`, + } + }) + .filter((section) => !!section) + + this.checksum = checksum + this.sections = sections + + return { + checksum, + sections, + meta: this.meta, + } + } + + abstract getSpecSections(specContents: string): SpecSection[] + abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection + abstract formatSection(specSection: SpecSection, refSection: ICommonFunc): string +} + +export class OpenApiReferenceSource extends ReferenceSource { + getSpecSections(specContents: string): enrichedOperation[] { + const spec: OpenAPIV3.Document<{}> = JSON.parse(specContents) + + const generatedSpec = gen_v3(spec, '', { + apiUrl: 'apiv0', + }) + + return generatedSpec.operations + } + matchSpecSection(operations: enrichedOperation[], id: string): enrichedOperation { + return operations.find((operation) => operation.operationId === id) + } + formatSection(specOperation: enrichedOperation) { + const { summary, description, operation, path, tags } = specOperation + return JSON.stringify({ + summary, + description, + operation, + path, + tags, + }) + } +} + +export class ClientLibReferenceSource extends ReferenceSource { + getSpecSections(specContents: string): IFunctionDefinition[] { + const spec = yaml.load(specContents) as ISpec + + return spec.functions + } + matchSpecSection(functionDefinitions: IFunctionDefinition[], id: string): IFunctionDefinition { + return functionDefinitions.find((functionDefinition) => functionDefinition.id === id) + } + formatSection(functionDefinition: IFunctionDefinition, refSection: ICommonFunc): string { + const { title } = refSection + const { description, title: functionName } = functionDefinition + + return JSON.stringify({ + title, + description, + functionName, + }) + } +} + +export class CliReferenceSource extends ReferenceSource { + getSpecSections(specContents: string): CliCommand[] { + const spec = yaml.load(specContents) as CliSpec + + return spec.commands + } + matchSpecSection(cliCommands: CliCommand[], id: string): CliCommand { + return cliCommands.find((cliCommand) => cliCommand.id === id) + } + formatSection(cliCommand: CliCommand): string { + const { summary, description, usage } = cliCommand + return JSON.stringify({ + summary, + description, + usage, + }) + } +} diff --git a/apps/docs/scripts/search/sources/util.ts b/apps/docs/scripts/search/sources/util.ts new file mode 100644 index 00000000000..b43bf98cdd1 --- /dev/null +++ b/apps/docs/scripts/search/sources/util.ts @@ -0,0 +1,43 @@ +import { readdir, stat } from 'fs/promises' +import { basename, dirname, join } from 'path' + +export type WalkEntry = { + path: string + parentPath?: string +} + +export async function walk(dir: string, parentPath?: string): Promise { + const immediateFiles = await readdir(dir) + + const recursiveFiles = await Promise.all( + immediateFiles.map(async (file) => { + const path = join(dir, file) + const stats = await stat(path) + if (stats.isDirectory()) { + // Keep track of document hierarchy (if this dir has corresponding doc file) + const docPath = `${basename(path)}.mdx` + + return walk( + path, + immediateFiles.includes(docPath) ? join(dirname(path), docPath) : parentPath + ) + } else if (stats.isFile()) { + return [ + { + path: path, + parentPath, + }, + ] + } else { + return [] + } + }) + ) + + const flattenedFiles = recursiveFiles.reduce( + (all, folderContents) => all.concat(folderContents), + [] + ) + + return flattenedFiles.sort((a, b) => a.path.localeCompare(b.path)) +} From f617dc06f6c06a623943aa11b65fd8af4ecfb92d Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 20 Apr 2023 17:29:57 -0600 Subject: [PATCH 3/6] docs: update search explanation in developers.md --- apps/docs/DEVELOPERS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/docs/DEVELOPERS.md b/apps/docs/DEVELOPERS.md index 5c5b74e39de..2f132d69801 100644 --- a/apps/docs/DEVELOPERS.md +++ b/apps/docs/DEVELOPERS.md @@ -96,4 +96,6 @@ On the Next.JS side of things, these work almost exactly the same as the client #### Search -Search is handled through Algolia. When the site is built, a [search script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/build-search.ts) runs through all of the types of content, generating search objects that are sent to Algolia to index. +Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/generate-embeddings.ts) aggregates all content sources (eg. guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database. + +At runtime, an [Edge Function](https://github.com/supabase/supabase/blob/master/supabase/functions) is executed that performs a similarity search between the user's query and the above content sources using [`pgvector`](https://github.com/pgvector/pgvector) embeddings. From 1ff754b8792c1d44067acf2a5033b533aab18b2d Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 20 Apr 2023 17:49:20 -0600 Subject: [PATCH 4/6] chore: remove algolia build-search script --- apps/docs/DEVELOPERS.md | 2 +- apps/docs/package.json | 4 +- apps/docs/scripts/build-search.ts | 155 ------------------ apps/docs/scripts/files/api.ts | 43 ----- apps/docs/scripts/files/cli.ts | 43 ----- apps/docs/scripts/files/client-libs.ts | 57 ------- apps/docs/scripts/helpers.ts | 9 - .../{ => search}/generate-embeddings.ts | 2 +- 8 files changed, 4 insertions(+), 311 deletions(-) delete mode 100644 apps/docs/scripts/build-search.ts delete mode 100644 apps/docs/scripts/files/api.ts delete mode 100644 apps/docs/scripts/files/cli.ts delete mode 100644 apps/docs/scripts/files/client-libs.ts delete mode 100644 apps/docs/scripts/helpers.ts rename apps/docs/scripts/{ => search}/generate-embeddings.ts (99%) diff --git a/apps/docs/DEVELOPERS.md b/apps/docs/DEVELOPERS.md index 2f132d69801..a998eaba7d2 100644 --- a/apps/docs/DEVELOPERS.md +++ b/apps/docs/DEVELOPERS.md @@ -96,6 +96,6 @@ On the Next.JS side of things, these work almost exactly the same as the client #### Search -Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/generate-embeddings.ts) aggregates all content sources (eg. guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database. +Search is handled using a Supabase instance. During CI, [a script](https://github.com/supabase/supabase/blob/master/apps/docs/scripts/search/generate-embeddings.ts) aggregates all content sources (eg. guides, reference docs, etc), indexes them using OpenAI embeddings, and stores them in a Supabase database. At runtime, an [Edge Function](https://github.com/supabase/supabase/blob/master/supabase/functions) is executed that performs a similarity search between the user's query and the above content sources using [`pgvector`](https://github.com/pgvector/pgvector) embeddings. diff --git a/apps/docs/package.json b/apps/docs/package.json index cfda86a3831..ca12c92170b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -9,9 +9,9 @@ "start": "next start", "lint": "next lint", "build:sitemap": "node ./internals/generate-sitemap.mjs", - "embeddings": "tsx scripts/generate-embeddings.ts", + "embeddings": "tsx scripts/search/generate-embeddings.ts", "embeddings:refresh": "npm run embeddings -- --refresh", - "postbuild": "ts-node ./scripts/build-search.ts && node ./internals/generate-sitemap.mjs", + "postbuild": "node ./internals/generate-sitemap.mjs", "generate:all": "npm-run-all --parallel gen:api gen:cli gen:gotrue gen:storage gen:supabase-dart:v0 gen:supabase-dart:v1 gen:supabase-csharp:v0 gen:supabase-js:v1 gen:supabase-js:v2 gen:realtime", "gen:api": "npm-run-all gen:api:usage", "gen:api:usage": "ts-node ./generator/index.ts gen --type api --url https://api.supabase.com --input ../../spec/transforms/api_v0_openapi_deparsed.json --output ./docs/reference/api/generated/usage.mdx", diff --git a/apps/docs/scripts/build-search.ts b/apps/docs/scripts/build-search.ts deleted file mode 100644 index 1fdc4e18a88..00000000000 --- a/apps/docs/scripts/build-search.ts +++ /dev/null @@ -1,155 +0,0 @@ -import fs from 'fs' -import crypto from 'crypto' -import path from 'path' -import matter from 'gray-matter' -import dotenv from 'dotenv' -import algoliasearch from 'algoliasearch/lite' -import { isEmpty } from 'lodash' - -// search objects -import { generateClientLibSearchObjects } from './files/client-libs' -import { generateAPISearchObjects } from './files/api' -import { generateCLISearchObjects } from './files/cli' - -const cliObjects = generateCLISearchObjects() -const apiObjects = generateAPISearchObjects() -const clientLibSearchObjects = generateClientLibSearchObjects() - -// @ts-ignore -// The properties of the searchObject that are specifically read -// by DocSearch are "type" and "hierarchy". The rest, though saved into Algolia (which we -// can potentially use to craft more nuanced search experiences) are not used by DocSearch -const ignoredFiles = [ - 'pages/404.mdx', - 'pages/faq.mdx', - 'pages/support.mdx', - 'pages/oss.tsx', - 'pages/_app.tsx', - 'pages/_document.tsx', - 'pages/[...slug].tsx', - 'pages/handbook/contributing.mdx', - 'pages/handbook/introduction.mdx', - 'pages/handbook/supasquad.mdx', -] - -async function walk(dir) { - let files = await fs.promises.readdir(dir) - //@ts-ignore - files = await Promise.all( - files.map(async (file) => { - const filePath = path.join(dir, file) - const stats = await fs.promises.stat(filePath) - if (stats.isDirectory()) return walk(filePath) - else if (stats.isFile()) return filePath - }) - ) - - return files.reduce((all, folderContents) => all.concat(folderContents), []) -} - -;(async function () { - // initialize environment variables - dotenv.config() - - if (!process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || !process.env.ALGOLIA_SEARCH_ADMIN_KEY) { - return console.log( - 'Missing Algolia app ID / admin Key: skipping saving of Algolia search index' - ) - } - - console.log('Preparing docs indexing for Algolia') - - try { - const indexName = process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME - const client = algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, - process.env.ALGOLIA_SEARCH_ADMIN_KEY - ) - const index = client.initIndex(indexName) - - const guidePages = (await walk('pages')).filter((slug) => !ignoredFiles.includes(slug)) - - // generate search objects for mdx guide pages - const guidePagesearchObjects = guidePages - .map((slug) => { - let id, title, description - const fileContents = fs.readFileSync(slug, 'utf8') - const { data, content } = matter(fileContents) - - if (isEmpty(data)) { - // Guide pages do not have front-matter meta, unlike reference pages, have to manually extract - const metaIndex = fileContents.indexOf('export const meta = {') - if (metaIndex !== -1) { - const metaString = - fileContents - .slice(metaIndex + 20, fileContents.indexOf('}', metaIndex + 1) + 1) - .replace(/\n/g, '') - .slice(0, -2) + '}' - const meta = eval(`(${metaString})`) - id = meta.id - title = meta.title - description = meta.description - } - } else { - id = data.id - title = data.title - description = data.description - } - - const url = (slug.includes('/generated') ? slug.replace('/generated', '') : slug) - .replace('docs', '') - .replace('pages', '') - .replace(/\.mdx$/, '') - const source = slug.includes('/reference') ? 'reference' : 'guide' - - const object = { - // For Algolia - objectID: crypto.randomUUID(), - id, - title, - description, - url, - source, - //pageContent: content, - pageContent: '', - category: undefined, - version: undefined, - - // Docsearch specific - type: 'lvl1', - hierarchy: { - lvl0: 'Guides', - lvl1: title, - lvl2: null, - lvl3: null, - lvl4: null, - lvl5: null, - lvl6: null, - }, - } - - return object - }) - // Some of the reference generated files come with an 'index' page that we can ignore - .filter((object) => !object.url.endsWith('/index')) - .filter((object) => !object.url.endsWith('/.gitkeep')) - - const combinedSearchObjects = guidePagesearchObjects.concat( - clientLibSearchObjects, - apiObjects, - cliObjects - ) - - //@ts-ignore - await index.clearObjects() - console.log(`Successfully cleared records from ${indexName}`) - - //@ts-ignore - const algoliaResponse = await index.saveObjects(combinedSearchObjects) - - //@ts-ignore - console.log(`Successfully saved ${algoliaResponse.objectIDs.length} records into ${indexName}.`) - } catch (error) { - console.log('Error:', error) - } -})() diff --git a/apps/docs/scripts/files/api.ts b/apps/docs/scripts/files/api.ts deleted file mode 100644 index ed98a9c4921..00000000000 --- a/apps/docs/scripts/files/api.ts +++ /dev/null @@ -1,43 +0,0 @@ -import crypto from 'crypto' -import { flattenSections } from '../../lib/helpers' - -import apiCommonSections from '~/../../spec/common-api-sections.json' -import specFile from '~/../../spec/transforms/api_v0_openapi_deparsed.json' -import { gen_v3 } from '../../lib/refGenerator/helpers' - -// @ts-ignore -const generatedSpec = gen_v3(specFile, 'wat', { apiUrl: 'apiv0' }) -const sections = flattenSections(apiCommonSections) - -export function generateAPISearchObjects() { - let searchObjects = [] - - //@ts-ignore - sections.map((section) => { - const object = searchObjects.push({ - objectID: crypto.randomUUID(), - id: section.id, - title: section.title, - // @ts-ignore - description: generatedSpec.operations.find((item) => item.operationId === section.id) - ?.summary, - url: `/reference/api/${section.slug}`, - source: 'reference', - pageContent: '', - category: section.product, - version: '', - type: 'lvl2', - hierarchy: { - lvl0: 'References', - lvl1: 'Management API', - lvl2: section.title, - lvl3: null, - lvl4: null, - lvl5: null, - lvl6: null, - }, - }) - return object - }) - return searchObjects -} diff --git a/apps/docs/scripts/files/cli.ts b/apps/docs/scripts/files/cli.ts deleted file mode 100644 index 84caae225c6..00000000000 --- a/apps/docs/scripts/files/cli.ts +++ /dev/null @@ -1,43 +0,0 @@ -import crypto from 'crypto' -import fs from 'fs' -import { flattenSections } from '../../lib/helpers' -import cliCommonSections from '~/../../spec/common-cli-sections.json' -import yaml from 'js-yaml' - -// @ts-ignore - -const spec = yaml.load(fs.readFileSync(`../../spec/cli_v1_commands.yaml`, 'utf8')) - -const commonSections = flattenSections(cliCommonSections) - -export function generateCLISearchObjects() { - let searchObjects = [] - - //@ts-ignore - spec.commands.map((section) => { - const object = searchObjects.push({ - objectID: crypto.randomUUID(), - id: section.id, - title: section.title, - // @ts-ignore - description: section.description?.substr(0, section.description.indexOf('\n')), - url: `/reference/cli/${commonSections.find((item) => item.id === section.id)?.slug}`, - source: 'reference', - pageContent: '', - category: section.product, - version: '', - type: 'lvl2', - hierarchy: { - lvl0: 'References', - lvl1: 'Supabase CLI', - lvl2: section.title, - lvl3: null, - lvl4: null, - lvl5: null, - lvl6: null, - }, - }) - return object - }) - return searchObjects -} diff --git a/apps/docs/scripts/files/client-libs.ts b/apps/docs/scripts/files/client-libs.ts deleted file mode 100644 index 714769ebcc7..00000000000 --- a/apps/docs/scripts/files/client-libs.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fs from 'fs' -import crypto from 'crypto' -import yaml from 'js-yaml' -import { flattenSections } from '../../lib/helpers' -import { nameMap } from '../helpers' - -import commonLibSections from '~/../../spec/common-client-libs-sections.json' - -const clientLibFiles = [ - { fileName: 'supabase_js_v2', label: 'javascript', version: 'v2', versionSlug: false }, - { fileName: 'supabase_js_v1', label: 'javascript', version: 'v1', versionSlug: true }, - { fileName: 'supabase_dart_v1', label: 'dart', version: 'v1', versionSlug: false }, - { fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true }, - { fileName: 'supabase_dart_v0', label: 'dart', version: 'v0', versionSlug: true }, -] - -const flatCommonLibSections = flattenSections(commonLibSections) - -export function generateClientLibSearchObjects() { - // loop through each spec file, find the correspending entry in the common file and grab the title / description / slug - let clientLibSearchObjects = [] - - clientLibFiles.map((file) => { - const specs = yaml.load(fs.readFileSync(`../../spec/${file.fileName}.yml`, 'utf8')) - - //take each function id, find it in the commonLibSections file and return { id, title, slug, description, } - //@ts-ignore - specs.functions.map((fn) => { - const item = flatCommonLibSections.find((section) => section.id === fn.id) - if (item) { - const object = clientLibSearchObjects.push({ - objectID: crypto.randomUUID(), - id: item.id, - title: item.title, - description: item.title, - url: `/reference/${file.label}/${file.versionSlug ? file.version + '/' : ''}${item.slug}`, - source: 'reference', - pageContent: '', - category: item.product, - version: file.version, - type: 'lvl2', - hierarchy: { - lvl0: 'References', - lvl1: `${nameMap[file.label]} ${file.version}`, - lvl2: item.title, - lvl3: file.version, - lvl4: null, - lvl5: null, - lvl6: null, - }, - }) - return object - } - }) - }) - return clientLibSearchObjects -} diff --git a/apps/docs/scripts/helpers.ts b/apps/docs/scripts/helpers.ts deleted file mode 100644 index 97ae8ddb86e..00000000000 --- a/apps/docs/scripts/helpers.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const nameMap = { - api: 'Management API', - cli: 'Supabase CLI', - auth: 'Auth Server', - storage: 'Storage Server', - postgres: 'Postgres', - dart: 'Supabase Flutter Library', - javascript: 'Supabase JavaScript Library', -} diff --git a/apps/docs/scripts/generate-embeddings.ts b/apps/docs/scripts/search/generate-embeddings.ts similarity index 99% rename from apps/docs/scripts/generate-embeddings.ts rename to apps/docs/scripts/search/generate-embeddings.ts index 23158f7f5bc..d04765afc67 100644 --- a/apps/docs/scripts/generate-embeddings.ts +++ b/apps/docs/scripts/search/generate-embeddings.ts @@ -3,7 +3,7 @@ import dotenv from 'dotenv' import 'openai' import { Configuration, OpenAIApi } from 'openai' import { inspect } from 'util' -import { fetchSources } from './search/sources' +import { fetchSources } from './sources' dotenv.config() From 67c041877d27204eb65320d6415e68f453f4450f Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 20 Apr 2023 19:40:55 -0600 Subject: [PATCH 5/6] fix: type error --- apps/docs/scripts/search/sources/reference-doc.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/docs/scripts/search/sources/reference-doc.ts b/apps/docs/scripts/search/sources/reference-doc.ts index 9f2304700bc..61743a7748f 100644 --- a/apps/docs/scripts/search/sources/reference-doc.ts +++ b/apps/docs/scripts/search/sources/reference-doc.ts @@ -2,7 +2,6 @@ import { createHash } from 'crypto' import { readFile } from 'fs/promises' import yaml from 'js-yaml' import { OpenAPIV3 } from 'openapi-types' -import { Meta } from '..' import { ICommonFunc, IFunctionDefinition, @@ -11,7 +10,7 @@ import { import { CliCommand, CliSpec } from '../../../generator/types/CliSpec' import { flattenSections } from '../../../lib/helpers' import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers' -import { BaseSource } from './base' +import { BaseSource, Json } from './base' export abstract class ReferenceSource extends BaseSource { type = 'reference' as const @@ -19,7 +18,7 @@ export abstract class ReferenceSource extends BaseSource { constructor( source: string, path: string, - public meta: Meta, + public meta: Json, public specFilePath: string, public sectionsFilePath: string ) { From a6d13cd7d4d2adebbbcb45a7e7a08135a29e1285 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Fri, 21 Apr 2023 12:49:01 +0800 Subject: [PATCH 6/6] fix duplicate issue --- apps/www/components/Nav/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/www/components/Nav/index.tsx b/apps/www/components/Nav/index.tsx index 2faef55350d..4f6e5699b1e 100644 --- a/apps/www/components/Nav/index.tsx +++ b/apps/www/components/Nav/index.tsx @@ -11,8 +11,6 @@ import SolutionsData from 'data/Solutions.json' import Solutions from '~/components/Nav/Product' import Developers from '~/components/Nav/Developers' -import Announcement from '~/components/Nav/Announcement' -import CountdownBanner from '~/components/LaunchWeek/Banners/CountdownBanner' import ScrollProgress from '~/components/ScrollProgress' import { useIsLoggedIn, useTheme } from 'common' @@ -23,7 +21,6 @@ import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-w import * as supabaseLogoWordmarkWhite from 'common/assets/images/supabase-logo-wordmark--white.png' - const Nav = () => { const { isDarkMode } = useTheme() const { pathname } = useRouter()