From 4ed3428314dc876ee0aed94b8d883671dce945c8 Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Thu, 29 May 2025 19:10:56 +0800 Subject: [PATCH 1/2] Make git setup optional --- scripts/build-docs.test.ts | 107 +++++++++++++---------- scripts/build-docs.ts | 18 ++-- scripts/lib/config.ts | 11 +-- scripts/lib/getLastCommitDate.ts | 5 ++ scripts/lib/plugins/insertFrontmatter.ts | 25 +++--- 5 files changed, 95 insertions(+), 71 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 80f0ff5df7..81f83023dd 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -19,20 +19,20 @@ const tempConfig = { // Whether to preserve temp directories after tests // (helpful for debugging, but requires manual cleanup) preserveTemp: false, + + // Whether to setup a git repository in each test + setupGit: false, } async function createTempFiles( files: { path: string; content: string }[], - options?: { - prefix?: string // Prefix for the temp directory name - preserveTemp?: boolean // Override global preserveTemp setting - useLocalTemp?: boolean // Override global useLocalTemp setting - }, + { + tempDirectoryPrefix = 'clerk-docs-test-', + preserveTemp = tempConfig.preserveTemp, + useLocalTemp = tempConfig.useLocalTemp, + setupGit = tempConfig.setupGit, + } = {}, ) { - const prefix = options?.prefix || 'clerk-docs-test-' - const preserve = options?.preserveTemp ?? tempConfig.preserveTemp - const useLocalTemp = options?.useLocalTemp ?? tempConfig.useLocalTemp - // Determine base directory for temp files let baseDir: string @@ -46,7 +46,7 @@ async function createTempFiles( } // Create temp folder with unique name - const tempDir = await fs.mkdtemp(path.join(baseDir, prefix)) + const tempDir = await fs.mkdtemp(path.join(baseDir, tempDirectoryPrefix)) // Create all files for (const file of files) { @@ -60,27 +60,30 @@ async function createTempFiles( await fs.writeFile(filePath, file.content) } - // Initialize git repository - const git = simpleGit(tempDir) + const initialCommitDate = new Date() - await git.init() + if (setupGit) { + // Initialize git repository + const git = simpleGit(tempDir) - // Locally set the git user config - await git.addConfig('user.name', 'Test User') - await git.addConfig('user.email', 'test@example.com') + await git.init() - // Add all files to git - await git.add('.') + // Locally set the git user config + await git.addConfig('user.name', 'Test User') + await git.addConfig('user.email', 'test@example.com') - // Use a fixed date for the initial commit - const initialCommitDate = new Date() - initialCommitDate.setMilliseconds(0) // git will drop off the milliseconds so if we don't do that then the times will miss-match - await git.commit('Initial commit', undefined, { - '--date': initialCommitDate.toISOString(), - }) + // Add all files to git + await git.add('.') + + // Use a fixed date for the initial commit + initialCommitDate.setMilliseconds(0) // git will drop off the milliseconds so if we don't do that then the times will miss-match + await git.commit('Initial commit', undefined, { + '--date': initialCommitDate.toISOString(), + }) + } // Register cleanup unless preserveTemp is true - if (!preserve) { + if (!preserveTemp) { onTestFinished(async () => { try { await fs.rm(tempDir, { recursive: true, force: true }) @@ -112,7 +115,7 @@ async function createTempFiles( }, // Pass through the git instance incase we need to use it for something - git, + git: setupGit ? simpleGit(tempDir) : undefined, // Return the initial commit date initialCommitDate, @@ -162,23 +165,27 @@ const baseConfig = { collapseDefault: false, hideTitleDefault: false, }, - skipApiErrors: true, - cleanDist: false, -} + flags: { + skipGit: true, + clean: true, + skipApiErrors: true, + }, +} satisfies Partial[0]> describe('Basic Functionality', () => { test('Basic build test with simple files', async () => { // Create temp environment with minimal files array - const { tempDir, pathJoin, initialCommitDate } = await createTempFiles([ - { - path: './docs/manifest.json', - content: JSON.stringify({ - navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], - }), - }, - { - path: './docs/simple-test.mdx', - content: `--- + const { tempDir, pathJoin, initialCommitDate } = await createTempFiles( + [ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'Simple Test', href: '/docs/simple-test' }]], + }), + }, + { + path: './docs/simple-test.mdx', + content: `--- title: Simple Test description: This is a simple test page --- @@ -186,14 +193,21 @@ description: This is a simple test page # Simple Test Page Testing with a simple page.`, - }, - ]) + }, + ], + { setupGit: true }, + ) const output = await build( await createConfig({ ...baseConfig, basePath: tempDir, validSdks: ['nextjs', 'react'], + flags: { + skipGit: false, + clean: true, + skipApiErrors: true, + }, }), ) @@ -1038,7 +1052,7 @@ title: Quickstart }) test('sdk in frontmatter filters the docs', async () => { - const { tempDir, pathJoin, initialCommitDate } = await createTempFiles([ + const { tempDir, pathJoin } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ @@ -1079,7 +1093,6 @@ Testing with a simple page.`, title: Simple Test sdk: react canonical: /docs/:sdk:/simple-test -lastUpdated: ${initialCommitDate.toISOString()} --- # Simple Test Page @@ -4264,7 +4277,7 @@ sdk: react, nextjs describe('API Errors Generation', () => { test('should generate api errors', async () => { - const { tempDir, readFile, listFiles } = await createTempFiles([ + const { tempDir, readFile } = await createTempFiles([ { path: './docs/manifest.json', content: JSON.stringify({ @@ -4292,8 +4305,12 @@ describe('API Errors Generation', () => { await createConfig({ ...baseConfig, basePath: tempDir, - skipApiErrors: false, validSdks: ['react'], + flags: { + skipApiErrors: false, + skipGit: true, + clean: true, + }, }), ) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index acb207b336..6fe23dba18 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -144,11 +144,12 @@ async function main() { collapseDefault: false, hideTitleDefault: false, }, - skipApiErrors: false, - cleanDist: false, flags: { watch: args.includes('--watch'), controlled: args.includes('--controlled'), + skipApiErrors: args.includes('--skip-api-errors'), + clean: args.includes('--clean'), + skipGit: args.includes('--skip-git'), }, }) @@ -167,7 +168,7 @@ async function main() { if (config.flags.watch) { console.info(`Watching for changes...`) - watchAndRebuild(store, { ...config, cleanDist: true }, build) + watchAndRebuild(store, { ...config, flags: { ...config.flags, clean: true } }, build) } else if (output !== '') { process.exit(1) } @@ -205,7 +206,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore console.info('✓ Read, optimized and transformed redirects') } - if (!config.skipApiErrors) { + if (!config.flags.skipApiErrors) { await generateApiErrorDocs(config) console.info('✓ Generated API Error MDX files') } @@ -533,9 +534,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore .use(checkTypedoc(config, validatedTypedocs, filePath, { reportWarnings: false, embed: true })) .use( insertFrontmatter({ - lastUpdated: ( - (await getCommitDate(path.join(config.docsPath, '..', filePath))) ?? new Date() - ).toISOString(), + lastUpdated: (await getCommitDate(path.join(config.docsPath, '..', filePath)))?.toISOString() ?? undefined, }), ) .process(doc.vfile) @@ -589,9 +588,8 @@ template: wide .use( insertFrontmatter({ canonical: doc.sdk ? scopeHrefToSDK(config)(doc.href, ':sdk:') : doc.href, - lastUpdated: ( - (await getCommitDate(path.join(config.docsPath, '..', filePath))) ?? new Date() - ).toISOString(), + lastUpdated: + (await getCommitDate(path.join(config.docsPath, '..', filePath)))?.toISOString() ?? undefined, }), ) .process({ diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index b867f2b6b3..82d458929e 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -38,11 +38,12 @@ type BuildConfigOptions = { outputPath: string } } - skipApiErrors?: boolean - cleanDist: boolean flags?: { watch?: boolean controlled?: boolean + skipGit?: boolean + clean?: boolean + skipApiErrors?: boolean } } @@ -111,12 +112,12 @@ export async function createConfig(config: BuildConfigOptions) { } : null, - skipApiErrors: config.skipApiErrors ?? false, - cleanDist: config.cleanDist, - flags: { watch: config.flags?.watch ?? false, controlled: config.flags?.controlled ?? false, + skipGit: config.flags?.skipGit ?? false, + clean: config.flags?.clean ?? false, + skipApiErrors: config.flags?.skipApiErrors ?? false, }, } } diff --git a/scripts/lib/getLastCommitDate.ts b/scripts/lib/getLastCommitDate.ts index 52f15cf79a..9b03b3ed9f 100644 --- a/scripts/lib/getLastCommitDate.ts +++ b/scripts/lib/getLastCommitDate.ts @@ -2,7 +2,12 @@ import simpleGit from 'simple-git' import type { BuildConfig } from './config' export const getLastCommitDate = (config: BuildConfig) => { + if (config.flags.skipGit) { + return async () => null + } + const git = simpleGit(config.docsPath) + return async (filePath: string) => { const log = await git.log({ file: filePath, n: 1 }) return log.latest?.date ? new Date(log.latest.date) : null diff --git a/scripts/lib/plugins/insertFrontmatter.ts b/scripts/lib/plugins/insertFrontmatter.ts index d7fd82ec43..b84a7c6718 100644 --- a/scripts/lib/plugins/insertFrontmatter.ts +++ b/scripts/lib/plugins/insertFrontmatter.ts @@ -1,18 +1,21 @@ import { map as mdastMap } from 'unist-util-map' +import { VFile } from 'vfile' import yaml from 'yaml' +import type { Node } from 'unist' -export const insertFrontmatter = (newFrontmatter: Record) => () => (tree, vfile) => { - return mdastMap(tree, (node) => { - if (node.type !== 'yaml') return node - if (!('value' in node)) return node - if (typeof node.value !== 'string') return node +export const insertFrontmatter = + (newFrontmatter: Record) => () => (tree: Node, vfile: VFile) => { + return mdastMap(tree, (node) => { + if (node.type !== 'yaml') return node + if (!('value' in node)) return node + if (typeof node.value !== 'string') return node - const frontmatter = yaml.parse(node.value) + const frontmatter = yaml.parse(node.value) - const transformedFrontmatter = { ...frontmatter, ...newFrontmatter } + const transformedFrontmatter = { ...frontmatter, ...newFrontmatter } - node.value = yaml.stringify(transformedFrontmatter).split('\n').slice(0, -1).join('\n') + node.value = yaml.stringify(transformedFrontmatter).split('\n').slice(0, -1).join('\n') - return node - }) -} + return node + }) + } From 733fdbc3337be244c8cfe452a9deffcfce263fef Mon Sep 17 00:00:00 2001 From: Nick Wylynko Date: Fri, 30 May 2025 00:11:23 +0800 Subject: [PATCH 2/2] Extract 'deprecated' as a key from the frontmatter and include it in the manifest --- scripts/build-docs.test.ts | 158 ++++++++++++++++++++++ scripts/build-docs.ts | 20 ++- scripts/lib/config.ts | 2 + scripts/lib/manifest.ts | 4 + scripts/lib/plugins/extractFrontmatter.ts | 4 +- 5 files changed, 183 insertions(+), 5 deletions(-) diff --git a/scripts/build-docs.test.ts b/scripts/build-docs.test.ts index 81f83023dd..e0c7a158c6 100644 --- a/scripts/build-docs.test.ts +++ b/scripts/build-docs.test.ts @@ -164,6 +164,7 @@ const baseConfig = { wrapDefault: true, collapseDefault: false, hideTitleDefault: false, + deprecatedDefault: false, }, flags: { skipGit: true, @@ -459,6 +460,7 @@ title: Simple Test wrapDefault: false, collapseDefault: false, hideTitleDefault: false, + deprecatedDefault: false, }, }), ) @@ -4346,3 +4348,159 @@ describe('API Errors Generation', () => { expect(fapi).toContain('Status Code: 400') }) }) + +describe('File deprecation', () => { + test('Should include the deprecation frontmatter in the dist file', async () => { + const { tempDir, readFile } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'API Doc', href: '/docs/api-doc' }]], + }), + }, + { + path: './docs/api-doc.mdx', + content: `--- +title: API Documentation +description: Generated API docs +deprecated: true +--- + +# API Documentation`, + }, + ]) + + await build( + await createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(await readFile('dist/api-doc.mdx')).toContain(`--- +title: API Documentation +description: Generated API docs +deprecated: true +--- + +# API Documentation`) + }) + + test('Should include the deprecation field in the manifest', async () => { + const { tempDir, readFile } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [[{ title: 'API Doc', href: '/docs/api-doc' }]], + }), + }, + { + path: './docs/api-doc.mdx', + content: `--- +title: API Documentation +description: Generated API docs +deprecated: true +--- + +# API Documentation`, + }, + ]) + + await build( + await createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(JSON.parse(await readFile('dist/manifest.json'))).toEqual({ + navigation: [ + [ + { + href: '/docs/api-doc', + title: 'API Doc', + deprecated: true, + }, + ], + ], + }) + }) + + test('Should include the deprecation field in the group above', async () => { + const { tempDir, readFile } = await createTempFiles([ + { + path: './docs/manifest.json', + content: JSON.stringify({ + navigation: [ + [ + { + title: 'API', + items: [ + [ + { title: 'API Doc', href: '/docs/api-doc' }, + { title: 'API Doc 2', href: '/docs/api-doc-2' }, + ], + ], + }, + ], + ], + }), + }, + { + path: './docs/api-doc.mdx', + content: `--- +title: API Documentation +description: Generated API docs +deprecated: true +--- + +# API Documentation`, + }, + { + path: './docs/api-doc-2.mdx', + content: `--- +title: API Documentation 2 +description: Generated API docs 2 +deprecated: true +--- + +# API Documentation 2`, + }, + ]) + + await build( + await createConfig({ + ...baseConfig, + basePath: tempDir, + validSdks: ['react'], + }), + ) + + expect(JSON.parse(await readFile('dist/manifest.json'))).toEqual({ + navigation: [ + [ + { + title: 'API', + deprecated: true, + items: [ + [ + { + title: 'API Doc', + href: '/docs/api-doc', + deprecated: true, + }, + { + title: 'API Doc 2', + href: '/docs/api-doc-2', + deprecated: true, + }, + ], + ], + }, + ], + ], + }) + }) +}) diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts index 6fe23dba18..ceb49c3a02 100644 --- a/scripts/build-docs.ts +++ b/scripts/build-docs.ts @@ -143,6 +143,7 @@ async function main() { wrapDefault: true, collapseDefault: false, hideTitleDefault: false, + deprecatedDefault: false, }, flags: { watch: args.includes('--watch'), @@ -261,7 +262,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore // Goes through and grabs the sdk scoping out of the manifest const sdkScopedManifestFirstPass = await traverseTree( - { items: userManifest, sdk: undefined as undefined | SDK[] }, + { items: userManifest, sdk: undefined as undefined | SDK[], deprecated: undefined as undefined | true }, async (item, tree) => { if (!item.href?.startsWith(config.baseDocsLink)) { return { @@ -305,6 +306,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore return { ...item, sdk, + deprecated: doc.frontmatter.deprecated, } }, async ({ items, ...details }, tree) => { @@ -350,7 +352,11 @@ export async function build(config: BuildConfig, store: Store = createBlankStore ) const sdkScopedManifest = await traverseTreeItemsFirst( - { items: sdkScopedManifestFirstPass, sdk: undefined as undefined | SDK[] }, + { + items: sdkScopedManifestFirstPass, + sdk: undefined as undefined | SDK[], + deprecated: undefined as undefined | true, + }, async (item, tree) => item, async ({ items, ...details }, tree) => { // This takes all the children items, grabs the sdks out of them, and combines that in to a list @@ -363,6 +369,8 @@ export async function build(config: BuildConfig, store: Store = createBlankStore return uniqueSDKs })() + const deprecated = items?.every((item) => item.every((item) => item.deprecated)) + // This is the sdk of the group const groupSDK = details.sdk @@ -371,12 +379,12 @@ export async function build(config: BuildConfig, store: Store = createBlankStore // If there are no children items, then we either use the group we are looking at sdks if its defined, or its parent group if (groupsItemsCombinedSDKs.length === 0) { - return { ...details, sdk: groupSDK ?? parentSDK, items } as ManifestGroup + return { ...details, sdk: groupSDK ?? parentSDK, items, deprecated } as ManifestGroup } // If all the children items have the same sdk as the group, then we don't need to set the sdk on the group if (groupsItemsCombinedSDKs.length === config.validSdks.length) { - return { ...details, sdk: undefined, items } as ManifestGroup + return { ...details, sdk: undefined, items, deprecated } as ManifestGroup } if (groupSDK !== undefined && groupSDK.length > 0) { @@ -384,6 +392,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore ...details, sdk: groupSDK, items, + deprecated, } as ManifestGroup } @@ -394,6 +403,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore // If there are children items, then we combine the sdks of the group and the children items sdks sdk: combinedSDKs, items, + deprecated, } as ManifestGroup }, (item, error) => { @@ -505,6 +515,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore icon: item.icon, target: item.target, sdk: item.sdk, + deprecated: item.deprecated === config.manifestOptions.deprecatedDefault ? undefined : item.deprecated, }), // @ts-expect-error - This traverseTree function might just be the death of me async (group) => ({ @@ -516,6 +527,7 @@ export async function build(config: BuildConfig, store: Store = createBlankStore hideTitle: group.hideTitle === config.manifestOptions.hideTitleDefault ? undefined : group.hideTitle, sdk: group.sdk, items: group.items, + deprecated: group.deprecated === config.manifestOptions.deprecatedDefault ? undefined : group.deprecated, }), ), }), diff --git a/scripts/lib/config.ts b/scripts/lib/config.ts index 82d458929e..559d7300fa 100644 --- a/scripts/lib/config.ts +++ b/scripts/lib/config.ts @@ -27,6 +27,7 @@ type BuildConfigOptions = { wrapDefault: boolean collapseDefault: boolean hideTitleDefault: boolean + deprecatedDefault: boolean } redirects?: { static: { @@ -97,6 +98,7 @@ export async function createConfig(config: BuildConfigOptions) { wrapDefault: true, collapseDefault: false, hideTitleDefault: false, + deprecatedDefault: false, }, redirects: config.redirects diff --git a/scripts/lib/manifest.ts b/scripts/lib/manifest.ts index fa2d3f27f3..ea09c23ed2 100644 --- a/scripts/lib/manifest.ts +++ b/scripts/lib/manifest.ts @@ -40,6 +40,7 @@ export type ManifestItem = { icon?: Icon target?: '_blank' sdk?: SDK[] + deprecated?: boolean } export type ManifestGroup = { @@ -51,6 +52,7 @@ export type ManifestGroup = { icon?: Icon hideTitle?: boolean sdk?: SDK[] + deprecated?: boolean } type Manifest = (ManifestItem | ManifestGroup)[][] @@ -66,6 +68,7 @@ const createManifestSchema = (config: BuildConfig) => { icon: icon.optional(), target: z.enum(['_blank']).optional(), sdk: z.array(sdk).optional(), + deprecated: z.boolean().default(config.manifestOptions.deprecatedDefault), }) .strict() @@ -79,6 +82,7 @@ const createManifestSchema = (config: BuildConfig) => { icon: icon.optional(), hideTitle: z.boolean().default(config.manifestOptions.hideTitleDefault), sdk: z.array(sdk).optional(), + deprecated: z.boolean().default(config.manifestOptions.deprecatedDefault), }) .strict() diff --git a/scripts/lib/plugins/extractFrontmatter.ts b/scripts/lib/plugins/extractFrontmatter.ts index bdbda330a6..4bb0423142 100644 --- a/scripts/lib/plugins/extractFrontmatter.ts +++ b/scripts/lib/plugins/extractFrontmatter.ts @@ -10,6 +10,7 @@ export type Frontmatter = { title: string description?: string sdk?: SDK[] + deprecated?: boolean } export const extractFrontmatter = @@ -33,7 +34,7 @@ export const extractFrontmatter = if (!('value' in node)) return if (typeof node.value !== 'string') return - const frontmatterYaml: Record<'title' | 'description' | 'sdk', string | undefined> = yaml.parse(node.value) + const frontmatterYaml = yaml.parse(node.value) const frontmatterSDKs = frontmatterYaml.sdk?.split(', ') @@ -64,6 +65,7 @@ export const extractFrontmatter = title: frontmatterYaml.title, description: frontmatterYaml.description, sdk: frontmatterSDKs, + deprecated: frontmatterYaml.deprecated === true, } }, )