From c12385b1e9d3c02c821f31b5026961a9bdabba03 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 1 Dec 2024 14:28:51 +0100 Subject: [PATCH 01/20] docs(api): add refresh button to examples --- .gitignore | 5 + docs/.vitepress/components/api-docs/method.ts | 1 + .../.vitepress/components/api-docs/method.vue | 117 +++++++++++++++++- .../components/api-docs/refresh-button.vue | 48 +++++++ docs/.vitepress/config.ts | 1 + docs/api/.gitignore | 10 -- scripts/apidocs/output/page.ts | 79 +++++++++++- tsconfig.json | 2 + 8 files changed, 246 insertions(+), 17 deletions(-) create mode 100644 docs/.vitepress/components/api-docs/refresh-button.vue delete mode 100644 docs/api/.gitignore diff --git a/.gitignore b/.gitignore index a26870e4325..df1219d9d09 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,11 @@ versions.json /dist /docs/.vitepress/cache /docs/.vitepress/dist +/docs/api/*.ts +!/docs/api/api-types.ts +/docs/api/*.md +!/docs/api/index.md +/docs/api/api-search-index.json /docs/public/api-diff-index.json # Faker diff --git a/docs/.vitepress/components/api-docs/method.ts b/docs/.vitepress/components/api-docs/method.ts index 4da480b806e..91f99d4ee8f 100644 --- a/docs/.vitepress/components/api-docs/method.ts +++ b/docs/.vitepress/components/api-docs/method.ts @@ -8,6 +8,7 @@ export interface ApiDocsMethod { readonly throws: string | undefined; // HTML readonly signature: string; // HTML readonly examples: string; // HTML + readonly refresh: (() => Promise) | undefined; readonly seeAlsos: string[]; readonly sourcePath: string; // URL-Suffix } diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 83a4100cfde..5f6274cbd6f 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -1,8 +1,10 @@ + + + + diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 746803ad594..4fcc469eb92 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -133,6 +133,7 @@ async function enableFaker() { e.g. 'faker.food.description()' or 'fakerZH_CN.person.firstName()' For other languages please refer to https://fakerjs.dev/guide/localization.html#available-locales For a full list of all methods please refer to https://fakerjs.dev/api/\`, logStyle); + enableFaker = () => imported; // Init only once return imported; } `, diff --git a/docs/api/.gitignore b/docs/api/.gitignore deleted file mode 100644 index 47b11a83ed7..00000000000 --- a/docs/api/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Markdown -*.md -!index.md - -# TypeScript -*.ts -!api-types.ts - -# JSON -*.json diff --git a/scripts/apidocs/output/page.ts b/scripts/apidocs/output/page.ts index c00ba4d23ad..0590898ca88 100644 --- a/scripts/apidocs/output/page.ts +++ b/scripts/apidocs/output/page.ts @@ -33,7 +33,7 @@ export async function writePages(pages: RawApiDocsPage[]): Promise { async function writePage(page: RawApiDocsPage): Promise { try { await writePageMarkdown(page); - await writePageJsonData(page); + await writePageData(page); } catch (error) { throw new Error(`Error writing page ${page.title}`, { cause: error }); } @@ -51,7 +51,7 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise { let content = ` @@ -98,16 +98,42 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise { * * @param page The page to write. */ -async function writePageJsonData(page: RawApiDocsPage): Promise { +async function writePageData(page: RawApiDocsPage): Promise { const { camelTitle, methods } = page; const pageData: Record = Object.fromEntries( await Promise.all( methods.map(async (method) => [method.name, await toMethodData(method)]) ) ); - const content = JSON.stringify(pageData, null, 2); - writeFileSync(resolve(FILE_PATH_API_DOCS, `${camelTitle}.json`), content); + const refreshFunctions: Record = Object.fromEntries( + await Promise.all( + methods.map(async (method) => [ + method.name, + await toRefreshFunction(method), + ]) + ) + ); + + const content = `export default ${JSON.stringify( + pageData, + (_, value: unknown) => { + if (typeof value === 'function') { + return value.toString(); // Insert placeholder + } + + return value; + }, + 2 + )}`.replaceAll( + /"refresh-([^"-]+)-placeholder"/g, + (_, name) => refreshFunctions[name] + ); + + writeFileSync( + resolve(FILE_PATH_API_DOCS, `${camelTitle}.ts`), + await formatTypescript(content) + ); } const defaultCommentRegex = /\s+Defaults to `([^`]+)`\..*/; @@ -130,6 +156,10 @@ async function toMethodData(method: RawApiDocsMethod): Promise { let formattedSignature = await formatTypescript(signature); formattedSignature = formattedSignature.trim(); + // eslint-disable-next-line @typescript-eslint/require-await + const refresh = async () => ['refresh', name, 'placeholder']; + refresh.toString = () => `refresh-${name}-placeholder`; + /* Target order, omitted to improve diff to old files return { name, @@ -167,6 +197,7 @@ async function toMethodData(method: RawApiDocsMethod): Promise { returns: returns.text, signature: codeToHtml(formattedSignature), examples: codeToHtml(examples.join('\n')), + refresh, deprecated: mdToHtml(deprecated), seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)), }; @@ -175,3 +206,41 @@ async function toMethodData(method: RawApiDocsMethod): Promise { export function extractSummaryDefault(description: string): string | undefined { return defaultCommentRegex.exec(description)?.[1]; } + +async function toRefreshFunction(method: RawApiDocsMethod): Promise { + const { name, signatures } = method; + const signatureData = required(signatures.at(-1), 'method signature'); + const { examples } = signatureData; + + const exampleLines = examples + .join('\n') + .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments + .replaceAll(/^import .*$/gm, '') // Remove imports + .replaceAll( + /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\));?$/gim, + `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` + ); // collect results of faker calls + + const fullMethod = `async (): Promise => { +await enableFaker(); +faker.seed(); +faker.setDefaultRefDate(); +const result: unknown[] = []; + +${exampleLines} + +return result; +}`; + try { + const formattedMethod = await formatTypescript(fullMethod); + return formattedMethod.replace(/;\s+$/, ''); // Remove trailing semicolon + } catch (error: unknown) { + console.error( + 'Failed to format refresh function for', + name, + fullMethod, + error + ); + return 'undefined'; + } +} diff --git a/tsconfig.json b/tsconfig.json index c8ffb862324..388a8f79c31 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,8 @@ "exclude": [ "node_modules", "dist", + // Ignore the generated API documentation + "docs/api", // required for the signature related tests on macOS #2280 "test/scripts/apidocs/temp" ] From caa3928477d90f1e3500b591b4c5bfc29f84efc3 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 1 Dec 2024 20:35:56 +0100 Subject: [PATCH 02/20] chore: improve button behavior slightly --- .../components/api-docs/refresh-button.vue | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/components/api-docs/refresh-button.vue b/docs/.vitepress/components/api-docs/refresh-button.vue index f097d27af5b..1547be5a391 100644 --- a/docs/.vitepress/components/api-docs/refresh-button.vue +++ b/docs/.vitepress/components/api-docs/refresh-button.vue @@ -7,13 +7,24 @@ const spinning = ref(false); async function onRefresh() { spinning.value = true; - await refresh(); + await Promise.all([refresh(), delay(100)]); spinning.value = false; } + +// Extra delay to make the spinning effect more visible +// Some examples barely/don't change, so the spinning is the only visible effect +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} From aa90d2d3c77324b727ea08c4f3ba95eb305fc64a Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 1 Dec 2024 21:25:01 +0100 Subject: [PATCH 03/20] chore: improve output format --- .../.vitepress/components/api-docs/method.vue | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 5f6274cbd6f..38cb3df53cd 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -95,13 +95,7 @@ async function onRefresh() { for (let i = 0; i < results.length; i++) { const result = results[i]; const domLine = codeLines[i]; - const resultLines = - result === undefined - ? ['undefined'] - : JSON.stringify(result) - .replaceAll(/\\r/g, '') - .replaceAll(/ + `${pre}'${main.replaceAll('\\"', '"')}'${post}` + ) + .replaceAll(/\n */g, ' '); +} + function newCommentLine(content: string): string { return `\n${newCommentSpan(content)}`; } From dd8ff0a4e3e67ea5101eae67ac68bb67e74a1d0d Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 1 Dec 2024 22:31:06 +0100 Subject: [PATCH 04/20] chore: ignore examples without recordable results --- scripts/apidocs/output/page.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/apidocs/output/page.ts b/scripts/apidocs/output/page.ts index 0590898ca88..095bb20bf83 100644 --- a/scripts/apidocs/output/page.ts +++ b/scripts/apidocs/output/page.ts @@ -212,14 +212,20 @@ async function toRefreshFunction(method: RawApiDocsMethod): Promise { const signatureData = required(signatures.at(-1), 'method signature'); const { examples } = signatureData; - const exampleLines = examples - .join('\n') + const exampleCode = examples.join('\n'); + if (!/^\w*faker\w*\./im.test(exampleCode)) { + // No recordable faker calls in examples + return 'undefined'; + } + + const exampleLines = exampleCode .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments .replaceAll(/^import .*$/gm, '') // Remove imports .replaceAll( + // record results of faker calls /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\));?$/gim, `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` - ); // collect results of faker calls + ); const fullMethod = `async (): Promise => { await enableFaker(); From d83341e20969de775117771e7c38d8fea5570afb Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Mon, 2 Dec 2024 10:55:24 +0100 Subject: [PATCH 05/20] temp --- docs/.vitepress/components/api-docs/format.ts | 25 +++++++++++++++++++ .../.vitepress/components/api-docs/method.vue | 25 ++++++------------- eslint.config.ts | 1 + 3 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 docs/.vitepress/components/api-docs/format.ts diff --git a/docs/.vitepress/components/api-docs/format.ts b/docs/.vitepress/components/api-docs/format.ts new file mode 100644 index 00000000000..d37c35232fd --- /dev/null +++ b/docs/.vitepress/components/api-docs/format.ts @@ -0,0 +1,25 @@ +import type { Options } from 'prettier'; +import pluginBabel from 'prettier/plugins/babel'; +import pluginEstree from 'prettier/plugins/estree'; +import { format } from 'prettier/standalone'; + +export async function formatResult(result: unknown): Promise { + return result === undefined + ? 'undefined' + : ( + await format( + `export default ${JSON.stringify(result).replaceAll('\\r', '').replaceAll('<', '<')}`, + options + ) + ) + .replace(/^export default /, '') + .replace(/;\s*$/, '') + .replaceAll(/\n */g, ' '); +} + +const options: Options = { + singleQuote: true, + trailingComma: 'none', + parser: 'babel', + plugins: [pluginBabel, pluginEstree], // TODO try json5/jsonc +}; diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 38cb3df53cd..42dccdd6e03 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -84,18 +84,22 @@ async function onRefresh() { if (refresh != null && codeBlock.value != null) { codeLines ??= initRefresh(); + const [results, { formatResult }] = await Promise.all([ + refresh(), + import('./format.ts'), + ]); + // Remove old comments codeBlock.value .querySelectorAll('.comment-delete-marker') .forEach((el) => el.remove()); - const results = await refresh(); - // Insert new comments for (let i = 0; i < results.length; i++) { const result = results[i]; const domLine = codeLines[i]; - const resultLines = stringify(result).split('\\n'); + const prettyResult = await formatResult(result); + const resultLines = prettyResult.split('\\n'); if (resultLines.length === 1) { domLine.insertAdjacentHTML('beforeend', newCommentSpan(resultLines[0])); @@ -108,21 +112,6 @@ async function onRefresh() { } } -function stringify(result: unknown): string { - return result === undefined - ? 'undefined' - : JSON.stringify(result, null, 1) - .replaceAll('\\r', '') - .replaceAll('<', '<') - .replaceAll( - // Change quotes if possible - /^( *)"([^'\n]*)"(,?)$/gm, - (_, pre, main, post) => - `${pre}'${main.replaceAll('\\"', '"')}'${post}` - ) - .replaceAll(/\n */g, ' '); -} - function newCommentLine(content: string): string { return `\n${newCommentSpan(content)}`; } diff --git a/eslint.config.ts b/eslint.config.ts index 0ae63ccc3d8..b23bdd45c93 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -24,6 +24,7 @@ const config: ReturnType = tseslint.config( '.github/workflows/commentCodeGeneration.ts', '.prettierrc.js', 'docs/.vitepress/components/shims.d.ts', + 'docs/.vitepress/components/api-docs/format.ts', 'docs/.vitepress/shared/utils/slugify.ts', 'docs/.vitepress/theme/index.ts', 'eslint.config.js', From 18cd35c0f7c9613ef51ef20d37f08b54976ced0e Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Mon, 2 Dec 2024 21:36:54 +0100 Subject: [PATCH 06/20] chore: use svg button --- .../components/api-docs/refresh-button.vue | 13 +++++++++++-- docs/.vitepress/components/api-docs/refresh.svg | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 docs/.vitepress/components/api-docs/refresh.svg diff --git a/docs/.vitepress/components/api-docs/refresh-button.vue b/docs/.vitepress/components/api-docs/refresh-button.vue index 1547be5a391..b864632624c 100644 --- a/docs/.vitepress/components/api-docs/refresh-button.vue +++ b/docs/.vitepress/components/api-docs/refresh-button.vue @@ -25,7 +25,7 @@ function delay(ms: number) { :disabled="spinning" @click="onRefresh" > -
+
@@ -39,12 +39,21 @@ button.refresh { vertical-align: middle; } +button.refresh div { + background-image: url('refresh.svg'); + background-position: 50%; + background-size: 20px; + background-repeat: no-repeat; + width: 100%; + height: 100%; +} + button.refresh:hover { background-color: var(--vp-code-copy-code-bg); opacity: 1; } -.spinning { +div.spinning { animation: spin 1s linear infinite; } diff --git a/docs/.vitepress/components/api-docs/refresh.svg b/docs/.vitepress/components/api-docs/refresh.svg new file mode 100644 index 00000000000..8320a2b2ba6 --- /dev/null +++ b/docs/.vitepress/components/api-docs/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file From a7d7177d34d98cb11dd2e22eda99f7b5be3e67a0 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Mon, 2 Dec 2024 21:47:36 +0100 Subject: [PATCH 07/20] chore: use json5 format for test --- docs/.vitepress/components/api-docs/format.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/.vitepress/components/api-docs/format.ts b/docs/.vitepress/components/api-docs/format.ts index d37c35232fd..df0419929e6 100644 --- a/docs/.vitepress/components/api-docs/format.ts +++ b/docs/.vitepress/components/api-docs/format.ts @@ -8,18 +8,15 @@ export async function formatResult(result: unknown): Promise { ? 'undefined' : ( await format( - `export default ${JSON.stringify(result).replaceAll('\\r', '').replaceAll('<', '<')}`, + JSON.stringify(result).replaceAll('\\r', '').replaceAll('<', '<'), options ) - ) - .replace(/^export default /, '') - .replace(/;\s*$/, '') - .replaceAll(/\n */g, ' '); + ).replaceAll(/\n */g, ' '); } const options: Options = { singleQuote: true, trailingComma: 'none', - parser: 'babel', - plugins: [pluginBabel, pluginEstree], // TODO try json5/jsonc + parser: 'json5', + plugins: [pluginBabel, pluginEstree], }; From 075e71631c1adbc258e72ec283bef31a9309c896 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 14 Dec 2024 11:30:22 +0100 Subject: [PATCH 08/20] chore: simplify result formatting --- docs/.vitepress/components/api-docs/format.ts | 30 +++++++------------ .../.vitepress/components/api-docs/method.vue | 6 ++-- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/docs/.vitepress/components/api-docs/format.ts b/docs/.vitepress/components/api-docs/format.ts index df0419929e6..041b04fbd3f 100644 --- a/docs/.vitepress/components/api-docs/format.ts +++ b/docs/.vitepress/components/api-docs/format.ts @@ -1,22 +1,14 @@ -import type { Options } from 'prettier'; -import pluginBabel from 'prettier/plugins/babel'; -import pluginEstree from 'prettier/plugins/estree'; -import { format } from 'prettier/standalone'; - -export async function formatResult(result: unknown): Promise { +export function formatResult(result: unknown): string { return result === undefined ? 'undefined' - : ( - await format( - JSON.stringify(result).replaceAll('\\r', '').replaceAll('<', '<'), - options - ) - ).replaceAll(/\n */g, ' '); + : typeof result === 'bigint' + ? `${result}n` + : JSON.stringify(result, undefined, 2) + .replaceAll('\\r', '') + .replaceAll('<', '<') + .replaceAll( + /"([^'\n]*?)"$/gm, + (_, p1) => `'${p1.replace(/\\"/g, '"')}'` + ) + .replaceAll(/\n */g, ' '); } - -const options: Options = { - singleQuote: true, - trailingComma: 'none', - parser: 'json5', - plugins: [pluginBabel, pluginEstree], -}; diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 42dccdd6e03..6d84157e5bd 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue'; import { sourceBaseUrl } from '../../../api/source-base-url'; import { slugify } from '../../shared/utils/slugify'; +import { formatResult } from './format'; import type { ApiDocsMethod } from './method'; import MethodParameters from './method-parameters.vue'; import RefreshButton from './refresh-button.vue'; @@ -84,10 +85,7 @@ async function onRefresh() { if (refresh != null && codeBlock.value != null) { codeLines ??= initRefresh(); - const [results, { formatResult }] = await Promise.all([ - refresh(), - import('./format.ts'), - ]); + const results = await refresh(); // Remove old comments codeBlock.value From 9139886e3d87f8ba99f6ad87bce83604bce86f76 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 14 Dec 2024 12:33:58 +0100 Subject: [PATCH 09/20] test: add formatting tests --- docs/.vitepress/components/api-docs/format.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/components/api-docs/format.ts b/docs/.vitepress/components/api-docs/format.ts index 041b04fbd3f..e34c65a616e 100644 --- a/docs/.vitepress/components/api-docs/format.ts +++ b/docs/.vitepress/components/api-docs/format.ts @@ -7,8 +7,8 @@ export function formatResult(result: unknown): string { .replaceAll('\\r', '') .replaceAll('<', '<') .replaceAll( - /"([^'\n]*?)"$/gm, - (_, p1) => `'${p1.replace(/\\"/g, '"')}'` + /(^ *|: )"([^'\n]*?)"(,?$|: )/gm, + (_, p1, p2, p3) => `${p1}'${p2.replace(/\\"/g, '"')}'${p3}` ) .replaceAll(/\n */g, ' '); } From a7b7b2bbcc6a038150ff1f9deb48ea161488ac8f Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 14 Dec 2024 15:55:32 +0100 Subject: [PATCH 10/20] test: add e2e refresh test --- cypress/e2e/example-refresh.cy.ts | 32 +++++++++ .../.vitepress/components/api-docs/method.vue | 1 + test/docs/__snapshots__/format.spec.ts.snap | 19 +++++ test/docs/format.spec.ts | 69 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 cypress/e2e/example-refresh.cy.ts create mode 100644 test/docs/__snapshots__/format.spec.ts.snap create mode 100644 test/docs/format.spec.ts diff --git a/cypress/e2e/example-refresh.cy.ts b/cypress/e2e/example-refresh.cy.ts new file mode 100644 index 00000000000..a7fbcb08d24 --- /dev/null +++ b/cypress/e2e/example-refresh.cy.ts @@ -0,0 +1,32 @@ +describe('example-refresh', () => { + it('should refresh the page', () => { + // given + cy.visit('/api/faker.html#constructor'); + cy.get('.refresh').first().as('refresh'); + cy.get('@refresh').next().find('code').as('codeBlock'); + cy.get('@codeBlock').then(($el) => { + const originalCodeText = $el.text(); + + cy.get('@refresh') + .click() + .should('not.be.disabled') // stays disabled on error + .then(() => { + cy.get('@codeBlock').then(($el) => { + const newCodeText = $el.text(); + expect(newCodeText).not.to.equal(originalCodeText); + + cy.get('@refresh') + .click() + .should('not.be.disabled') // stays disabled on error + .then(() => { + cy.get('@codeBlock').then(($el) => { + const newCodeText2 = $el.text(); + expect(newCodeText2).not.to.equal(originalCodeText); + expect(newCodeText2).not.to.equal(newCodeText); + }); + }); + }); + }); + }); + }); +}); diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 6d84157e5bd..f853c2f9c0f 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -153,6 +153,7 @@ function seeAlsoToUrl(see: string): string {

Examples

should format Date 1`] = `"'2024-12-14T11:32:25.255Z'"`; + +exports[`formatResult > should format array 1`] = `"[ 1, '2' ]"`; + +exports[`formatResult > should format bigint 1`] = `"135464154865415n"`; + +exports[`formatResult > should format number 1`] = `"123"`; + +exports[`formatResult > should format object 1`] = `"{ 'a': 1, 'b': "2" }"`; + +exports[`formatResult > should format string 1`] = `"'a simple string'"`; + +exports[`formatResult > should format string with new lines 1`] = `"'string\\nwith\\nnew\\nlines'"`; + +exports[`formatResult > should format string with special characters 1`] = `"'string with "special" characters'"`; + +exports[`formatResult > should format undefined 1`] = `"undefined"`; diff --git a/test/docs/format.spec.ts b/test/docs/format.spec.ts new file mode 100644 index 00000000000..54cf33f8ddf --- /dev/null +++ b/test/docs/format.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; +import { formatResult } from '../../docs/.vitepress/components/api-docs/format'; + +describe('formatResult', () => { + it('should format undefined', () => { + const value = undefined; + const actual = formatResult(value); + + expect(actual).toBeTypeOf('string'); + expect(actual).toBe('undefined'); + expect(actual).toMatchSnapshot(); + }); + + it('should format bigint', () => { + const actual = formatResult(135464154865415n); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format object', () => { + const actual = formatResult({ a: 1, b: '2' }); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format array', () => { + const actual = formatResult([1, '2']); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format string', () => { + const actual = formatResult('a simple string'); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format string with special characters', () => { + const actual = formatResult('string with "special" characters'); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format string with new lines', () => { + const actual = formatResult('string\nwith\nnew\nlines'); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format number', () => { + const actual = formatResult(123); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); + + it('should format Date', () => { + const actual = formatResult(new Date()); + + expect(actual).toBeTypeOf('string'); + expect(actual).toMatchSnapshot(); + }); +}); From 8c5e0a579f5f5ac631a989c987406634117ad445 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 14 Dec 2024 16:02:14 +0100 Subject: [PATCH 11/20] test: use static test values --- test/docs/__snapshots__/format.spec.ts.snap | 2 +- test/docs/format.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/docs/__snapshots__/format.spec.ts.snap b/test/docs/__snapshots__/format.spec.ts.snap index d020ca013ba..4cc36652581 100644 --- a/test/docs/__snapshots__/format.spec.ts.snap +++ b/test/docs/__snapshots__/format.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`formatResult > should format Date 1`] = `"'2024-12-14T11:32:25.255Z'"`; +exports[`formatResult > should format Date 1`] = `"'2025-01-01T00:00:00.000Z'"`; exports[`formatResult > should format array 1`] = `"[ 1, '2' ]"`; diff --git a/test/docs/format.spec.ts b/test/docs/format.spec.ts index 54cf33f8ddf..bc4a0d6638e 100644 --- a/test/docs/format.spec.ts +++ b/test/docs/format.spec.ts @@ -61,7 +61,7 @@ describe('formatResult', () => { }); it('should format Date', () => { - const actual = formatResult(new Date()); + const actual = formatResult(new Date(Date.UTC(2025, 0, 1))); expect(actual).toBeTypeOf('string'); expect(actual).toMatchSnapshot(); From a7f52a9b7f49f642b0d05a8de82f0ff165f7a0bc Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 15 Dec 2024 00:04:01 +0100 Subject: [PATCH 12/20] chore: fix regex --- docs/.vitepress/components/api-docs/format.ts | 4 ++-- test/docs/__snapshots__/format.spec.ts.snap | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/.vitepress/components/api-docs/format.ts b/docs/.vitepress/components/api-docs/format.ts index e34c65a616e..34de1e0c494 100644 --- a/docs/.vitepress/components/api-docs/format.ts +++ b/docs/.vitepress/components/api-docs/format.ts @@ -7,8 +7,8 @@ export function formatResult(result: unknown): string { .replaceAll('\\r', '') .replaceAll('<', '<') .replaceAll( - /(^ *|: )"([^'\n]*?)"(,?$|: )/gm, - (_, p1, p2, p3) => `${p1}'${p2.replace(/\\"/g, '"')}'${p3}` + /(^ *|: )"([^'\n]*?)"(?=,?$|: )/gm, + (_, p1, p2) => `${p1}'${p2.replace(/\\"/g, '"')}'` ) .replaceAll(/\n */g, ' '); } diff --git a/test/docs/__snapshots__/format.spec.ts.snap b/test/docs/__snapshots__/format.spec.ts.snap index 4cc36652581..36679523c33 100644 --- a/test/docs/__snapshots__/format.spec.ts.snap +++ b/test/docs/__snapshots__/format.spec.ts.snap @@ -8,7 +8,7 @@ exports[`formatResult > should format bigint 1`] = `"135464154865415n"`; exports[`formatResult > should format number 1`] = `"123"`; -exports[`formatResult > should format object 1`] = `"{ 'a': 1, 'b': "2" }"`; +exports[`formatResult > should format object 1`] = `"{ 'a': 1, 'b': '2' }"`; exports[`formatResult > should format string 1`] = `"'a simple string'"`; From e6e1ff01ca976e8895c7bd7d62fce9c94efe6768 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 15 Dec 2024 00:13:29 +0100 Subject: [PATCH 13/20] chore: simplify refresh placeholder --- scripts/apidocs/output/page.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/scripts/apidocs/output/page.ts b/scripts/apidocs/output/page.ts index 095bb20bf83..cfd51edf64d 100644 --- a/scripts/apidocs/output/page.ts +++ b/scripts/apidocs/output/page.ts @@ -115,20 +115,11 @@ async function writePageData(page: RawApiDocsPage): Promise { ) ); - const content = `export default ${JSON.stringify( - pageData, - (_, value: unknown) => { - if (typeof value === 'function') { - return value.toString(); // Insert placeholder - } - - return value; - }, - 2 - )}`.replaceAll( - /"refresh-([^"-]+)-placeholder"/g, - (_, name) => refreshFunctions[name] - ); + const content = + `export default ${JSON.stringify(pageData, undefined, 2)}`.replaceAll( + /"refresh-([^"-]+)-placeholder"/g, + (_, name) => refreshFunctions[name] + ); writeFileSync( resolve(FILE_PATH_API_DOCS, `${camelTitle}.ts`), @@ -158,7 +149,9 @@ async function toMethodData(method: RawApiDocsMethod): Promise { // eslint-disable-next-line @typescript-eslint/require-await const refresh = async () => ['refresh', name, 'placeholder']; - refresh.toString = () => `refresh-${name}-placeholder`; + // This is a placeholder to be replaced by the actual refresh function code + // If we put the actual code here, it would be a string and not executable + refresh.toJSON = () => `refresh-${name}-placeholder`; /* Target order, omitted to improve diff to old files return { From 866b702bf92d937e876400a888be0f960995feb7 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sun, 15 Dec 2024 00:15:05 +0100 Subject: [PATCH 14/20] Update cypress/e2e/example-refresh.cy.ts --- cypress/e2e/example-refresh.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/example-refresh.cy.ts b/cypress/e2e/example-refresh.cy.ts index a7fbcb08d24..df792eba5b2 100644 --- a/cypress/e2e/example-refresh.cy.ts +++ b/cypress/e2e/example-refresh.cy.ts @@ -1,5 +1,5 @@ describe('example-refresh', () => { - it('should refresh the page', () => { + it('should refresh the example', () => { // given cy.visit('/api/faker.html#constructor'); cy.get('.refresh').first().as('refresh'); From 22039a9df2d8052158361712ba38388c53803e40 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Thu, 19 Dec 2024 01:03:47 +0100 Subject: [PATCH 15/20] fix: handle property after function call --- .../.vitepress/components/api-docs/method.vue | 8 +- scripts/apidocs/output/page.ts | 6 +- .../apidocs/__snapshots__/page.spec.ts.snap | 97 +++++++++++++++ test/scripts/apidocs/page.spec.ts | 114 ++++++++++++++++++ 4 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 test/scripts/apidocs/__snapshots__/page.spec.ts.snap create mode 100644 test/scripts/apidocs/page.spec.ts diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index f853c2f9c0f..e4a86d3daeb 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -46,11 +46,17 @@ function initRefresh(): Element[] { // Skip to end of the invocation (if multiline) while ( domLines[lineIndex] != null && - !/^([^ ].*)?\);? ?(\/\/|$)/.test(domLines[lineIndex]?.textContent ?? '') + !/^([^ ].*)?\)(\.\w+)?;? ?(\/\/|$)/.test( + domLines[lineIndex]?.textContent ?? '' + ) ) { lineIndex++; } + if (lineIndex >= domLines.length) { + break; + } + const domLine = domLines[lineIndex]; result.push(domLine); lineIndex++; diff --git a/scripts/apidocs/output/page.ts b/scripts/apidocs/output/page.ts index cfd51edf64d..a95703099c4 100644 --- a/scripts/apidocs/output/page.ts +++ b/scripts/apidocs/output/page.ts @@ -200,7 +200,9 @@ export function extractSummaryDefault(description: string): string | undefined { return defaultCommentRegex.exec(description)?.[1]; } -async function toRefreshFunction(method: RawApiDocsMethod): Promise { +export async function toRefreshFunction( + method: RawApiDocsMethod +): Promise { const { name, signatures } = method; const signatureData = required(signatures.at(-1), 'method signature'); const { examples } = signatureData; @@ -216,7 +218,7 @@ async function toRefreshFunction(method: RawApiDocsMethod): Promise { .replaceAll(/^import .*$/gm, '') // Remove imports .replaceAll( // record results of faker calls - /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\));?$/gim, + /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim, `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` ); diff --git a/test/scripts/apidocs/__snapshots__/page.spec.ts.snap b/test/scripts/apidocs/__snapshots__/page.spec.ts.snap new file mode 100644 index 00000000000..4824c101ab5 --- /dev/null +++ b/test/scripts/apidocs/__snapshots__/page.spec.ts.snap @@ -0,0 +1,97 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`toRefreshFunction > should handle multiline calls 1`] = ` +"async (): Promise => { + await enableFaker(); + faker.seed(); + faker.setDefaultRefDate(); + const result: unknown[] = []; + + try { + result.push( + faker.number.int({ + min: 1, + max: 10, + }) + ); + } catch (error: unknown) { + result.push(error instanceof Error ? error.name : 'Error'); + } + + return result; +}" +`; + +exports[`toRefreshFunction > should handle multiple calls 1`] = ` +"async (): Promise => { + await enableFaker(); + faker.seed(); + faker.setDefaultRefDate(); + const result: unknown[] = []; + + try { + result.push(faker.number.int()); + } catch (error: unknown) { + result.push(error instanceof Error ? error.name : 'Error'); + } + + try { + result.push(faker.number.int()); + } catch (error: unknown) { + result.push(error instanceof Error ? error.name : 'Error'); + } + + return result; +}" +`; + +exports[`toRefreshFunction > should handle properties after calls 1`] = ` +"async (): Promise => { + await enableFaker(); + faker.seed(); + faker.setDefaultRefDate(); + const result: unknown[] = []; + + try { + result.push(faker.airline.airport().name); + } catch (error: unknown) { + result.push(error instanceof Error ? error.name : 'Error'); + } + + return result; +}" +`; + +exports[`toRefreshFunction > should handle single line calls with semicolon 1`] = ` +"async (): Promise => { + await enableFaker(); + faker.seed(); + faker.setDefaultRefDate(); + const result: unknown[] = []; + + try { + result.push(faker.number.int()); + } catch (error: unknown) { + result.push(error instanceof Error ? error.name : 'Error'); + } + + return result; +}" +`; + +exports[`toRefreshFunction > should handle single line calls without semicolon 1`] = ` +"async (): Promise => { + await enableFaker(); + faker.seed(); + faker.setDefaultRefDate(); + const result: unknown[] = []; + + try { + result.push(faker.number.int()); + } catch (error: unknown) { + result.push(error instanceof Error ? error.name : 'Error'); + } + + return result; +}" +`; diff --git a/test/scripts/apidocs/page.spec.ts b/test/scripts/apidocs/page.spec.ts new file mode 100644 index 00000000000..00fd4da2277 --- /dev/null +++ b/test/scripts/apidocs/page.spec.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from 'vitest'; +import { toRefreshFunction } from '../../../scripts/apidocs/output/page'; +import type { RawApiDocsMethod } from '../../../scripts/apidocs/processing/method'; +import type { RawApiDocsSignature } from '../../../scripts/apidocs/processing/signature'; + +function newTestMethod( + signature: Partial +): RawApiDocsMethod { + return { + name: 'test', + signatures: [ + { + deprecated: 'deprecated', + description: 'description', + since: 'since', + parameters: [], + returns: { + type: 'simple', + text: 'returns', + }, + throws: [], + signature: 'signature', + examples: [], + seeAlsos: [], + ...signature, + }, + ], + source: { + filePath: 'test/page.spec.ts', + line: 1, + column: 1, + }, + }; +} + +describe('toRefreshFunction', () => { + it("should return 'undefined' when there are no faker calls", async () => { + // given + const method = newTestMethod({ + examples: ['const a = 1;'], + }); + + // when + const result = await toRefreshFunction(method); + + // then + expect(result).toBe('undefined'); + }); + + it('should handle single line calls with semicolon', async () => { + // given + const method = newTestMethod({ + examples: ['faker.number.int(); // 834135'], + }); + + // when + const result = await toRefreshFunction(method); + + // then + expect(result).toMatchSnapshot(); + }); + + it('should handle single line calls without semicolon', async () => { + // given + const method = newTestMethod({ + examples: ['faker.number.int() // 834135'], + }); + + // when + const result = await toRefreshFunction(method); + + // then + expect(result).toMatchSnapshot(); + }); + + it('should handle multiple calls', async () => { + // given + const method = newTestMethod({ + examples: ['faker.number.int()', 'faker.number.int()'], + }); + + // when + const result = await toRefreshFunction(method); + + // then + expect(result).toMatchSnapshot(); + }); + + it('should handle multiline calls', async () => { + // given + const method = newTestMethod({ + examples: 'faker.number.int({\n min: 1,\n max: 10\n})'.split('\n'), + }); + + // when + const result = await toRefreshFunction(method); + + // then + expect(result).toMatchSnapshot(); + }); + + it('should handle properties after calls', async () => { + // given + const method = newTestMethod({ + examples: ['faker.airline.airport().name'], + }); + + // when + const result = await toRefreshFunction(method); + + // then + expect(result).toMatchSnapshot(); + }); +}); From c23f91f5278a2cc581d6d94586ee5386985adff5 Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 21 Dec 2024 10:02:26 +0100 Subject: [PATCH 16/20] Apply suggestions from code review Co-authored-by: Shinigami --- docs/.vitepress/components/api-docs/method.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index e4a86d3daeb..8938118bde1 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -87,7 +87,7 @@ function initRefresh(): Element[] { return result; } -async function onRefresh() { +async function onRefresh(): Promise { if (refresh != null && codeBlock.value != null) { codeLines ??= initRefresh(); @@ -102,7 +102,7 @@ async function onRefresh() { for (let i = 0; i < results.length; i++) { const result = results[i]; const domLine = codeLines[i]; - const prettyResult = await formatResult(result); + const prettyResult = formatResult(result); const resultLines = prettyResult.split('\\n'); if (resultLines.length === 1) { From 2247965c26f71a5aab00b605cc01ac232fcc8ecc Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 21 Dec 2024 11:59:02 +0100 Subject: [PATCH 17/20] Apply suggestions from code review Co-authored-by: Shinigami --- docs/.vitepress/components/api-docs/method.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 8938118bde1..ce5fda13dc7 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -1,5 +1,6 @@