Files
supabase/apps/docs/features/docs/Reference.generated.script.ts
Charis fc164b5d07 Refactor/app router refs (#28095)
Migrates client SDK References to App Router. (Management and CLI API references aren't migrated yet, nor are self-hosting config references.)

Some notes:

Big changes to the way crawler pages are built and individual section URLs (e.g., javascript/select) are served. All of these used to be SSG-generated pages, but the number of heavy pages was just too much to handle -- slow as molasses and my laptop sounded like it was taking off, and CI sometimes refuses to build it all at all.

Tried various tricks with caching and pre-generating data but no dice.

So I changed to only building one copy of each SDK+version page, then serving the sub-URLs through a response rewrite. That's for the actual user-visible pages.

For the bot pages, each sub-URL needs to be its own page, but prebuilding it doesn't work, and rendering on demand from React components is too slow (looking for super-fast response here for SEO). Instead I changed to using an API route that serves very minimal, hand-crafted HTML. It looks ugly, but it's purely for the search bots.

You can test what bots see by running curl --user-agent "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" <URL_OF_PAGE>

Also added some smoke tests to run against prod for the crawler routes, since we don't keep an eye on those regularly, and Vercel config changes could surprise-break them. Tested the meta images on Open Graph and all seems to work fine.

With this approach, full production builds are really fast: ~5 minutes

Starts using the new type spec handling, which is better at finding params automatically, so I could remove some of the manually written ones from the spec files.
2024-08-13 16:12:59 -04:00

137 lines
4.4 KiB
TypeScript

import { keyBy, isPlainObject } from 'lodash'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse } from 'yaml'
import { REFERENCES, clientSdkIds } from '~/content/navigation.references'
import { parseTypeSpec } from '~/features/docs/Reference.typeSpec'
import type { AbbrevCommonClientLibSection } from '~/features/docs/Reference.utils'
import { deepFilterRec } from '~/features/helpers.fn'
import type { Json } from '~/features/helpers.types'
import commonClientLibSections from '~/spec/common-client-libs-sections.json' assert { type: 'json' }
const DOCS_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), '../..')
const SPEC_DIRECTORY = join(DOCS_DIRECTORY, 'spec')
const GENERATED_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), 'generated')
async function getSpec(specFile: string, { ext = 'yml' }: { ext?: string } = {}) {
const specFullPath = join(SPEC_DIRECTORY, `${specFile}.${ext}`)
const rawSpec = await readFile(specFullPath, 'utf-8')
return ext === 'yml' ? parse(rawSpec) : rawSpec
}
async function parseFnsList(rawSpec: Json): Promise<Array<{ id: unknown }>> {
if (isPlainObject(rawSpec) && 'functions' in (rawSpec as object)) {
const _rawSpec = rawSpec as { functions: unknown }
if (Array.isArray(_rawSpec.functions)) {
return _rawSpec.functions.filter(({ id }) => !!id)
}
}
return []
}
function genClientSdkSectionTree(fns: Array<{ id: unknown }>, excludeName: string) {
const validSections = deepFilterRec(
commonClientLibSections as Array<AbbrevCommonClientLibSection>,
'items',
(section) =>
section.type === 'markdown' || section.type === 'category'
? !('excludes' in section && section.excludes.includes(excludeName))
: section.type === 'function'
? fns.some(({ id }) => section.id === id)
: true
)
return validSections
}
export function flattenCommonClientLibSections(tree: Array<AbbrevCommonClientLibSection>) {
return tree.reduce((acc, elem) => {
if ('items' in elem) {
const prunedElem = { ...elem }
delete prunedElem.items
acc.push(prunedElem)
acc.push(...flattenCommonClientLibSections(elem.items))
} else {
acc.push(elem)
}
return acc
}, [] as Array<AbbrevCommonClientLibSection>)
}
async function writeTypes() {
const types = await parseTypeSpec()
await writeFile(
join(GENERATED_DIRECTORY, 'typeSpec.json'),
JSON.stringify(types, (key, value) => {
if (key === 'methods') {
return Object.fromEntries(value.entries())
} else {
return value
}
})
)
}
async function writeReferenceSections() {
return Promise.all(
clientSdkIds
.flatMap((sdkId) => {
const versions = REFERENCES[sdkId].versions
return versions.map((version) => ({
sdkId,
version,
}))
})
.flatMap(async ({ sdkId, version }) => {
const spec = await getSpec(REFERENCES[sdkId].meta[version].specFile)
const fnsList = await parseFnsList(spec)
const pendingFnListWrite = writeFile(
join(GENERATED_DIRECTORY, `${sdkId}.${version}.functions.json`),
JSON.stringify(fnsList)
)
const sectionTree = genClientSdkSectionTree(fnsList, REFERENCES[sdkId].meta[version].libId)
const pendingSectionTreeWrite = writeFile(
join(GENERATED_DIRECTORY, `${sdkId}.${version}.sections.json`),
JSON.stringify(sectionTree)
)
const flattened = flattenCommonClientLibSections(sectionTree)
const pendingFlattenedWrite = writeFile(
join(GENERATED_DIRECTORY, `${sdkId}.${version}.flat.json`),
JSON.stringify(flattened)
)
const sectionsBySlug = keyBy(flattened, (section) => section.slug)
const pendingSlugDictionaryWrite = writeFile(
join(GENERATED_DIRECTORY, `${sdkId}.${version}.bySlug.json`),
JSON.stringify(sectionsBySlug)
)
return [
pendingFnListWrite,
pendingSectionTreeWrite,
pendingFlattenedWrite,
pendingSlugDictionaryWrite,
]
})
)
}
async function run() {
try {
await mkdir(GENERATED_DIRECTORY, { recursive: true })
await Promise.all([writeTypes(), writeReferenceSections()])
} catch (err) {
console.error(err)
}
}
run()