snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import { checkStoryEmbeds } from '../check-story-embeds.mjs';
const storyIds = new Set(['ui-input-input--default']);
describe('checkStoryEmbeds', () => {
it('accepts an existing story inside a nested preview', () => {
expect(
checkStoryEmbeds({
content: `<Tabs>
<Tab title="Preview">
<StoryEmbed storyId="ui-input-input--default" />
</Tab>
</Tabs>`,
storyIds,
}),
).toEqual([]);
});
it('reports a renamed or deleted story with its line number', () => {
expect(
checkStoryEmbeds({
content: '\n<StoryEmbed storyId="ui-input-input--removed" />',
storyIds,
}),
).toEqual([
'Line 2: Storybook story "ui-input-input--removed" does not exist.',
]);
});
it.each([
'<StoryEmbed />',
'<StoryEmbed storyId={selectedStory} />',
'<StoryEmbed storyId="" />',
'<StoryEmbed storyId="ui-input-input--default" {...props} />',
])('rejects an unverifiable story ID in %s', (content) => {
expect(checkStoryEmbeds({ content, storyIds })).toEqual([
'Line 1: StoryEmbed needs a literal storyId.',
]);
});
it('checks a renamed snippet import', () => {
expect(
checkStoryEmbeds({
content: `import { StoryEmbed as Preview } from '/snippets/ui/StoryEmbed.mdx';
<Preview storyId="removed-story" />`,
storyIds,
}),
).toEqual(['Line 3: Storybook story "removed-story" does not exist.']);
});
it('ignores examples and comments while parsing page metadata', () => {
expect(
checkStoryEmbeds({
content: `---
title: Preview
description: "A {component} preview"
---
\`\`\`mdx
<StoryEmbed storyId="not-a-live-embed" />
\`\`\`
\`<StoryEmbed storyId="inline-code" />\`
{/* <StoryEmbed storyId="commented-out" /> */}`,
storyIds,
}),
).toEqual([]);
});
});

View File

@@ -0,0 +1,48 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { checkUiReferenceImports } from '../check-ui-reference-imports';
describe('UI reference imports', () => {
let pagesDirectory: string;
beforeEach(() => {
pagesDirectory = mkdtempSync(join(tmpdir(), 'twenty-ui-pages-test-'));
mkdirSync(join(pagesDirectory, 'components/input'), { recursive: true });
writeFileSync(
join(pagesDirectory, 'components/input/checkbox.mdx'),
"---\ntitle: Checkbox\n---\n\nimport CheckboxReference from '/snippets/ui/generated/input/checkbox.mdx';\n\n<CheckboxReference />\n",
);
writeFileSync(
join(pagesDirectory, 'tokens.mdx'),
"import TokenReference from '/snippets/ui/generated/tokens.mdx'\n",
);
});
afterEach(() => {
rmSync(pagesDirectory, { recursive: true, force: true });
});
it('passes when every generated reference is imported by a page', () => {
expect(
checkUiReferenceImports({
pagesDirectory,
outputs: [{ name: 'input/checkbox.mdx' }, { name: 'tokens.mdx' }],
}),
).toEqual([]);
});
it('reports pages importing missing references and references no page imports', () => {
expect(
checkUiReferenceImports({
pagesDirectory,
outputs: [{ name: 'input/switch.mdx' }, { name: 'tokens.mdx' }],
}),
).toEqual([
'Missing UI reference: components/input/checkbox.mdx imports input/checkbox.mdx, which is not generated.',
'Unused UI reference: input/switch.mdx is not imported by any page.',
]);
});
});

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { normalizeDocumentationDefaultValue } from '../../../../twenty-ui/docs/normalizeDocumentationDefaultValue';
import { renderComponentReference } from '../render-component-reference';
describe('documentation defaults', () => {
it.each([
["'onSubmit'", 'onSubmit'],
['"horizontal"', 'horizontal'],
["'it\\'s ready'", "it's ready"],
['"undefined"', 'undefined'],
["''", ''],
['accent', 'accent'],
[false, 'false'],
[0, '0'],
[null, 'null'],
[undefined, null],
['undefined', null],
])('normalizes %j to %j', (value, expected) => {
expect(normalizeDocumentationDefaultValue({ value })).toBe(expected);
});
it('omits absent defaults and renders string defaults without display quotes', () => {
const reference = renderComponentReference({
name: 'Example',
entryPoint: 'twenty-ui/primitives/input',
slug: 'input/example',
props: [
{
name: 'mode',
type: 'string',
required: false,
defaultValue: normalizeDocumentationDefaultValue({
value: "'onSubmit'",
}),
description: '',
},
{
name: 'checked',
type: 'boolean',
required: false,
defaultValue: normalizeDocumentationDefaultValue({
value: 'undefined',
}),
description: '',
},
],
});
expect(normalizeDocumentationDefaultValue(null)).toBeNull();
expect(reference).toContain('default="onSubmit"');
expect(reference.match(/default=/g)).toHaveLength(1);
});
});

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { extractDocumentationExamples } from '../../../../twenty-ui/docs/extractDocumentationExamples';
describe('documentation examples', () => {
it('extracts fences with info strings, indentation, and other fence markers', () => {
const examples = extractDocumentationExamples(
[
'```tsx App.tsx',
'const app = 1;',
'```',
'<Tabs>',
' ```TSX title="Indented"',
' const indented = 2;',
' ```',
'</Tabs>',
'~~~ts',
'const tilde = 3;',
'~~~',
'````tsx',
'```',
'const nested = 4;',
'````',
'```bash',
'yarn add twenty-ui',
'```',
'```css',
'.summary {}',
'```',
].join('\n'),
);
expect(examples).toEqual([
{ language: 'tsx', code: 'const app = 1;\n' },
{ language: 'tsx', code: 'const indented = 2;\n' },
{ language: 'ts', code: 'const tilde = 3;\n' },
{ language: 'tsx', code: '```\nconst nested = 4;\n' },
]);
});
it('handles CRLF line endings and ignores an unterminated fence', () => {
expect(
extractDocumentationExamples(
'```tsx\r\nconst app = 1;\r\n```\r\n```tsx\r\nconst open = 2;\r\n',
),
).toEqual([{ language: 'tsx', code: 'const app = 1;\n' }]);
});
});

View File

@@ -0,0 +1,66 @@
import ts from 'typescript';
import { describe, expect, it } from 'vitest';
import { getDocumentationImportDiagnostics } from '../../../../twenty-ui/docs/getDocumentationImportDiagnostics';
const allowedModules = new Set([
'twenty-ui',
'twenty-ui/primitives/input',
'twenty-ui/style.css',
'react',
]);
const checkImports = (content: string) =>
getDocumentationImportDiagnostics({
source: ts.createSourceFile(
'example.tsx',
content,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
),
allowedModules,
});
describe('documentation imports', () => {
it('allows public entry points, exported CSS, and peer dependencies', () => {
expect(
checkImports(`
import { Input } from 'twenty-ui/primitives/input';
import 'twenty-ui/style.css';
export { Text } from 'twenty-ui';
import { useState } from 'react';
`),
).toEqual([]);
});
it.each([
"import { Internal } from 'twenty-ui/internal';",
"import 'twenty-ui/private.css';",
"export { Internal } from 'twenty-ui/internal';",
"const component = import('twenty-ui/internal');",
"type Internal = import('twenty-ui/internal').Internal;",
"const component = require('twenty-ui/internal');",
])('rejects an unpublished subpath in %s', (content) => {
const diagnostics = checkImports(content);
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].category).toBe(ts.DiagnosticCategory.Error);
expect(diagnostics[0].messageText).toContain(
'is not exported by twenty-ui/package.json',
);
});
it.each([
"import { Checkbox } from '@ui/primitives/input/Checkbox/Checkbox';",
"import { Input } from '../../twenty-ui/src/input';",
"import { vi } from 'vitest';",
])('rejects a module outside the public surface in %s', (content) => {
const diagnostics = checkImports(content);
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].messageText).toContain(
'is not a twenty-ui entry point or peer dependency',
);
});
});

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest';
import { normalizeDocumentationPropType } from '../../../../twenty-ui/docs/normalizeDocumentationPropType';
describe('documentation prop types', () => {
it.each([
[
{
type: {
name: 'string | ((state: State) => string | undefined) | undefined',
},
required: false,
},
'string | ((state: State) => string | undefined)',
],
[
{
type: {
name: '((details: { trigger: Element | undefined; }) => void) | undefined',
},
required: false,
},
'((details: { trigger: Element | undefined; }) => void)',
],
[
{ type: { name: 'Element | undefined' }, required: true },
'Element | undefined',
],
[{ type: { name: 'boolean' }, required: false }, 'boolean'],
[
{
type: {
name: 'enum',
value: [{ value: '"sm"' }, { value: '"md"' }, { value: 'undefined' }],
},
required: false,
},
'"sm" | "md"',
],
[
{
type: {
name: 'enum',
value: [{ value: '"sm"' }, { value: 'undefined' }],
},
required: true,
},
'"sm" | undefined',
],
])('normalizes %j to %j', (input, expected) => {
expect(normalizeDocumentationPropType(input)).toBe(expected);
});
});

View File

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { formatDocumentationTokenValue } from '../../../../twenty-ui/docs/formatDocumentationTokenValue';
describe('documentation token values', () => {
it.each([
[
'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIYAAABQCA), url(data:image/jpeg;base64,/9j/4AAQ)',
'url(data:image/png;base64,...), url(data:image/jpeg;base64,...)',
],
[
'color(display-p3 0.992 0.992 0.996)',
'color(display-p3 0.992 0.992 0.996)',
],
['url(/noise.png)', 'url(/noise.png)'],
['16', '16'],
])('formats %s as %s', (value, expected) => {
expect(formatDocumentationTokenValue(value)).toBe(expected);
});
});

View File

@@ -0,0 +1,156 @@
import { describe, expect, it } from 'vitest';
import { renderComponentReference } from '../render-component-reference';
import { renderTokenReference } from '../render-ui-reference';
describe('renderComponentReference', () => {
it('escapes attributes and prose while keeping markdown and code intact', () => {
const reference = renderComponentReference({
name: 'Example',
entryPoint: 'twenty-ui/primitives/input',
slug: 'input/example',
props: [
{
name: 'render',
type: 'Ref<custom> | "quoted"',
required: true,
defaultValue: null,
description:
'Render `<custom>` with {value} & more.\n\n- `unmount`: Manually unmounts.\n\n```tsx\n<Example render={<span />} />\n```',
},
],
});
expect(reference).toBe(
[
'<ParamField body="render" type="Ref&lt;custom&gt; | &quot;quoted&quot;" required>',
' Render `<custom>` with &#123;value&#125; &amp; more.',
'',
' - `unmount`: Manually unmounts.',
'',
' ```tsx',
' <Example render={<span />} />',
' ```',
'</ParamField>',
'',
].join('\n'),
);
});
});
describe('compound component references', () => {
it('includes convenience props before compound parts', () => {
const reference = renderComponentReference({
name: 'Tooltip',
entryPoint: 'twenty-ui/primitives/surfaces',
slug: 'surfaces/tooltip',
props: [
{
name: 'content',
type: 'ReactNode',
required: true,
defaultValue: null,
description: 'Tooltip content.',
},
],
parts: [
{
name: 'Popup',
props: [
{
name: 'arrow',
type: 'boolean',
required: false,
defaultValue: 'false',
description: 'Show an arrow.',
},
],
},
],
});
expect(reference).toContain('<ParamField body="content"');
expect(reference).toContain('### Tooltip.Popup');
expect(reference).toContain('<ParamField body="Popup.arrow"');
expect(reference.indexOf('body="content"')).toBeLessThan(
reference.indexOf('### Tooltip.Popup'),
);
});
it('distinguishes identically named props on different parts and retains defaults', () => {
const reference = renderComponentReference({
name: 'Menu',
entryPoint: 'twenty-ui/primitives/surfaces',
slug: 'surfaces/menu',
props: [],
parts: [
{
name: 'Item',
props: [
{
name: 'closeOnClick',
type: 'boolean',
required: false,
defaultValue: 'true',
description: 'Close after selection.',
},
],
},
{
name: 'CheckboxItem',
props: [
{
name: 'closeOnClick',
type: 'boolean',
required: false,
defaultValue: 'false',
description: 'Keep open for multiple choices.',
},
],
},
],
});
expect(reference).toContain('### Menu.Item');
expect(reference).toContain('### Menu.CheckboxItem');
expect(reference).toContain(
'<ParamField body="Item.closeOnClick" type="boolean" default="true">',
);
expect(reference).toContain(
'<ParamField body="CheckboxItem.closeOnClick" type="boolean" default="false">',
);
});
});
describe('renderTokenReference', () => {
it('keeps token expressions inside one table cell and distinguishes numeric values', () => {
const reference = renderTokenReference([
{
path: 'spacing.1',
cssVariable: '--t-spacing-1',
light: 'calc(1px | <custom> & {value})\nnext',
dark: '4px',
isNumber: false,
},
{
path: 'icon.size.md',
cssVariable: '--t-icon-size-md',
light: '16',
dark: '16',
isNumber: true,
},
]);
const rows = reference
.split('\n')
.filter(
(line) => line.startsWith('| spacing.') || line.startsWith('| icon.'),
);
expect(rows).toHaveLength(2);
expect(rows.every((row) => row.split('|').length === 7)).toBe(true);
expect(reference).not.toContain('<custom>');
expect(rows[1]).toContain('| Yes |');
expect(reference).toContain('## spacing');
expect(reference).toContain('## icon');
});
});

View File

@@ -0,0 +1,71 @@
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { syncUiReferenceFiles } from '../sync-ui-reference-files';
describe('generated UI reference files', () => {
let directory: string;
const outputs = [
{ name: 'input/current.mdx', content: 'Current reference\n' },
];
beforeEach(() => {
directory = mkdtempSync(join(tmpdir(), 'twenty-ui-reference-test-'));
mkdirSync(join(directory, 'input'));
writeFileSync(join(directory, 'input/obsolete.mdx'), 'Old reference\n');
});
afterEach(() => {
rmSync(directory, { recursive: true, force: true });
});
it('rejects obsolete and missing references without changing files in check mode', () => {
expect(
syncUiReferenceFiles({ directory, outputs, isCheckMode: true }),
).toEqual([
'Obsolete UI reference: input/obsolete.mdx',
'Stale UI reference: input/current.mdx',
]);
expect(readFileSync(join(directory, 'input/obsolete.mdx'), 'utf8')).toBe(
'Old reference\n',
);
expect(existsSync(join(directory, 'input/current.mdx'))).toBe(false);
});
it('removes obsolete snippets, writes current outputs, and passes a subsequent check', () => {
writeFileSync(join(directory, 'metadata.json'), '{}');
expect(
syncUiReferenceFiles({ directory, outputs, isCheckMode: false }),
).toEqual([]);
expect(existsSync(join(directory, 'input/obsolete.mdx'))).toBe(false);
expect(readFileSync(join(directory, 'input/current.mdx'), 'utf8')).toBe(
outputs[0].content,
);
expect(existsSync(join(directory, 'metadata.json'))).toBe(true);
expect(
syncUiReferenceFiles({ directory, outputs, isCheckMode: true }),
).toEqual([]);
});
it('reports changed content without overwriting it in check mode', () => {
rmSync(join(directory, 'input/obsolete.mdx'));
writeFileSync(join(directory, 'input/current.mdx'), 'Outdated reference\n');
expect(
syncUiReferenceFiles({ directory, outputs, isCheckMode: true }),
).toEqual(['Stale UI reference: input/current.mdx']);
expect(readFileSync(join(directory, 'input/current.mdx'), 'utf8')).toBe(
'Outdated reference\n',
);
});
});

View File

@@ -0,0 +1,73 @@
import { createProcessor } from '@mdx-js/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import { isNonEmptyString } from '@sniptt/guards';
import { visit } from 'unist-util-visit';
export const checkStoryEmbeds = ({
content,
storyIds,
}: {
content: string;
storyIds: Set<string>;
}): string[] => {
const errors: string[] = [];
const tree = createProcessor({
remarkPlugins: [remarkFrontmatter],
}).parse(content);
const embedNames = new Set(['StoryEmbed']);
visit(tree, 'mdxjsEsm', (node) => {
for (const statement of node.data?.estree?.body ?? []) {
if (
statement.type !== 'ImportDeclaration' ||
statement.source.value !== '/snippets/ui/StoryEmbed.mdx'
) {
continue;
}
for (const specifier of statement.specifiers) {
const isNamedEmbed =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === 'StoryEmbed';
if (specifier.type === 'ImportDefaultSpecifier' || isNamedEmbed) {
embedNames.add(specifier.local.name);
}
}
}
});
visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
if (
(node.type !== 'mdxJsxFlowElement' &&
node.type !== 'mdxJsxTextElement') ||
!embedNames.has(node.name ?? '')
) {
return;
}
const storyId = node.attributes.find(
(attribute) =>
attribute.type === 'mdxJsxAttribute' && attribute.name === 'storyId',
);
const value =
storyId?.type === 'mdxJsxAttribute' ? storyId.value : undefined;
const line = node.position?.start.line;
const hasSpreadAttributes = node.attributes.some(
(attribute) => attribute.type === 'mdxJsxExpressionAttribute',
);
if (!isNonEmptyString(value) || hasSpreadAttributes) {
errors.push(`Line ${line}: StoryEmbed needs a literal storyId.`);
return;
}
if (!storyIds.has(value)) {
errors.push(`Line ${line}: Storybook story "${value}" does not exist.`);
}
});
return errors;
};

View File

@@ -0,0 +1,39 @@
import { globSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const REFERENCE_IMPORT_PATTERN =
/^import\s+\w+\s+from\s+'\/snippets\/ui\/generated\/([^']+)';?$/gm;
export const checkUiReferenceImports = ({
pagesDirectory,
outputs,
}: {
pagesDirectory: string;
outputs: { name: string }[];
}): string[] => {
const errors: string[] = [];
const generatedNames = new Set(outputs.map((output) => output.name));
const importedNames = new Set<string>();
for (const page of globSync('**/*.mdx', { cwd: pagesDirectory }).sort()) {
const content = readFileSync(resolve(pagesDirectory, page), 'utf8');
for (const [, name] of content.matchAll(REFERENCE_IMPORT_PATTERN)) {
importedNames.add(name);
if (!generatedNames.has(name)) {
errors.push(
`Missing UI reference: ${page} imports ${name}, which is not generated.`,
);
}
}
}
for (const name of generatedNames) {
if (!importedNames.has(name)) {
errors.push(`Unused UI reference: ${name} is not imported by any page.`);
}
}
return errors;
};

View File

@@ -0,0 +1,72 @@
import { type ComponentDocumentation } from '../../../twenty-ui/docs/types';
const CODE_SEGMENT_PATTERN = /(`{3,})[\s\S]*?\1|`[^`\n]+`/g;
const escapeAttributeValue = (value: string): string =>
value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
const escapeProseText = (text: string): string =>
text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/\{/g, '&#123;')
.replace(/\}/g, '&#125;');
const escapeMarkdown = (markdown: string): string => {
let escaped = '';
let proseStart = 0;
for (const codeSegment of markdown.matchAll(CODE_SEGMENT_PATTERN)) {
escaped +=
escapeProseText(markdown.slice(proseStart, codeSegment.index)) +
codeSegment[0];
proseStart = codeSegment.index + codeSegment[0].length;
}
return escaped + escapeProseText(markdown.slice(proseStart));
};
const indent = (text: string): string =>
text
.split('\n')
.map((line) => (line.length > 0 ? ` ${line}` : line))
.join('\n');
const renderProps = ({
props,
partName,
}: {
props: ComponentDocumentation['props'];
partName?: string;
}): string =>
props
.map((prop) => {
const attributes = [
`body="${escapeAttributeValue(partName ? `${partName}.${prop.name}` : prop.name)}"`,
`type="${escapeAttributeValue(prop.type)}"`,
...(prop.required ? ['required'] : []),
...(prop.defaultValue === null
? []
: [`default="${escapeAttributeValue(prop.defaultValue)}"`]),
];
return `<ParamField ${attributes.join(' ')}>\n${indent(escapeMarkdown(prop.description))}\n</ParamField>`;
})
.join('\n\n') + '\n';
export const renderComponentReference = (
component: ComponentDocumentation,
): string =>
[
...(component.props.length > 0
? [renderProps({ props: component.props })]
: []),
...(component.parts ?? []).map(
(part) =>
`### ${component.name}.${part.name}\n\n${renderProps({ props: part.props, partName: part.name })}`,
),
].join('\n');

View File

@@ -0,0 +1,43 @@
import { type TokenDocumentation } from '../../../twenty-ui/docs/types';
const escapeTableCell = (value: string): string =>
value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\{/g, '&#123;')
.replace(/\}/g, '&#125;')
.replace(/\|/g, '&#124;')
.replace(/\r?\n/g, ' ');
export const renderTokenReference = (tokens: TokenDocumentation[]): string => {
const groups = new Map<string, TokenDocumentation[]>();
for (const token of tokens) {
const group = token.path.split('.')[0];
const entries = groups.get(group);
if (entries) {
entries.push(token);
} else {
groups.set(group, [token]);
}
}
return (
[...groups.entries()]
.map(([group, entries]) =>
[
`## ${group}`,
'',
'| Token path | CSS variable | Light | Dark | Numeric |',
'| --- | --- | --- | --- | --- |',
...entries.map(
(token) =>
`| ${[token.path, token.cssVariable, token.light, token.dark, token.isNumber ? 'Yes' : ''].map(escapeTableCell).join(' | ')} |`,
),
].join('\n'),
)
.join('\n\n') + '\n'
);
};

View File

@@ -0,0 +1,53 @@
import {
existsSync,
globSync,
mkdirSync,
readFileSync,
unlinkSync,
writeFileSync,
} from 'node:fs';
import { dirname, resolve } from 'node:path';
export const syncUiReferenceFiles = ({
directory,
outputs,
isCheckMode,
}: {
directory: string;
outputs: { name: string; content: string }[];
isCheckMode: boolean;
}): string[] => {
const errors: string[] = [];
const expectedPaths = new Set(
outputs.map((output) => resolve(directory, output.name)),
);
for (const name of globSync('**/*.mdx', { cwd: directory })) {
const filePath = resolve(directory, name);
if (expectedPaths.has(filePath)) {
continue;
}
if (isCheckMode) {
errors.push(`Obsolete UI reference: ${name}`);
} else {
unlinkSync(filePath);
}
}
for (const { name, content } of outputs) {
const filePath = resolve(directory, name);
if (isCheckMode) {
if (!existsSync(filePath) || readFileSync(filePath, 'utf8') !== content) {
errors.push(`Stale UI reference: ${name}`);
}
} else {
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, content);
}
}
return errors;
};