Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ Welcome! We appreciate your interest in contributing to react-simplikit. This do

Each contribution requires:

- **Implementation** — following our [Design Principles](https://react-simplikit.slash.page/core/design-principles.html)
- **Implementation** — following our [Design Principles](https://react-simplikit.slash.page/design-principles.html)
- **Test Code** — 100% coverage required
- **JSDoc** — documentation is auto-generated from JSDoc, so no separate docs needed

For detailed instructions, see the package-specific guides:

- [Core Package Contributing Guide](../docs/core/contributing.md)
- [Mobile Package Contributing Guide](../docs/mobile/contributing.md)
- [Contributing Guide](../docs/contributing.md)
- [Mobile Web](../docs/mobile-web.md)

## Scaffolding

Expand Down Expand Up @@ -41,5 +41,5 @@ Select the version bump type (`patch`, `minor`, or `major`).
## Useful Links

- [Documentation Site](https://react-simplikit.slash.page)
- [Design Principles](https://react-simplikit.slash.page/core/design-principles.html)
- [Design Principles](https://react-simplikit.slash.page/design-principles.html)
- [Discord](https://discord.gg/vGXbVjP2nY) — Community chat for questions and discussions
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ coverage
generated-locales/

# transient fixtures written by `yarn test:docs`, which a killed run cannot clean up
docs/core/untranslated-fallback-fixture.md
docs/untranslated-fallback-fixture.md
packages/react-simplikit/src/hooks/useUntranslatedFallbackFixture/

# next
Expand All @@ -53,3 +53,7 @@ packages/react-simplikit/src/hooks/useUntranslatedFallbackFixture/
context/
.omc/
.omx/

# generated per-locale reference index
docs/reference.md
docs/*/reference.md
124 changes: 124 additions & 0 deletions .scripts/commands/generateReferenceIndex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import fs from 'node:fs/promises';
import path from 'node:path';

import { localeDefinitions } from '../../../.vitepress/locales.mts';
import { getRootPath } from '../../utils/getRootPath.ts';

const PACKAGE_SRC = 'packages/react-simplikit/src';

type Group = { labelKey: 'hooksLabel' | 'componentsLabel' | 'utilsLabel' | 'mobileWebLabel'; directories: string[] };

// Mirrors the sidebar grouping: the mobile trees share the flat hooks/utils URLs
// but stay a separate group so mobile web stays discoverable as a category.
const GROUPS: Group[] = [
{ labelKey: 'hooksLabel', directories: ['hooks'] },
{ labelKey: 'componentsLabel', directories: ['components'] },
{ labelKey: 'utilsLabel', directories: ['utils'] },
{ labelKey: 'mobileWebLabel', directories: ['mobile/hooks', 'mobile/utils'] },
];

/**
* Extracts the first descriptive sentence from a co-located document: the first
* paragraph line after the title, cut at the end of its first sentence.
*/
async function firstSentence(markdownPath: string): Promise<string | undefined> {
let text: string;

try {
text = await fs.readFile(markdownPath, 'utf8');
} catch {
return undefined;
}

const lines = text.split('\n');
let inFrontmatter = false;

for (const [index, line] of lines.entries()) {
if (index === 0 && line.trim() === '---') {
inFrontmatter = true;
continue;
}

if (inFrontmatter) {
if (line.trim() === '---') {
inFrontmatter = false;
}
continue;
}

const trimmed = line.trim();

if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('<') || trimmed.startsWith(':::')) {
continue;
}

const sentenceEnd = trimmed.search(/(?<=[.!?。])\s|(?<=[.!?。])$/);
return sentenceEnd === -1 ? trimmed : trimmed.slice(0, sentenceEnd);
}

return undefined;
}

/**
* Generates one reference index page per locale: every export as
* "name - first sentence of its document", grouped the same way as the sidebar.
* The output is untracked; `docs:prepare` recreates it before every build.
*/
export async function generateReferenceIndex(): Promise<void> {
const root = getRootPath();

for (const definition of Object.values(localeDefinitions)) {
const localeSegment = definition.path === '' ? '' : `${definition.path}/`;
const urlPrefix = definition.path === '' ? '' : `/${definition.path}`;
const strings = definition.themeStrings;
let untranslatedCount = 0;
const sections: string[] = [`# ${strings.referenceLabel}`];

for (const group of GROUPS) {
const items: string[] = [];

for (const directory of group.directories) {
const category = directory.split('/').pop() as string;
const base = path.join(root, PACKAGE_SRC, directory);
const entries = await fs.readdir(base, { withFileTypes: true });

for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}

const name = entry.name;
const localized = path.join(base, name, definition.path, `${name}.md`);
const english = path.join(base, name, `${name}.md`);
const localizedDescription = definition.path === '' ? undefined : await firstSentence(localized);
const description = localizedDescription ?? (await firstSentence(english));

if (definition.path !== '' && localizedDescription === undefined) {
untranslatedCount += 1;
}

items.push(
`- [${name}](${urlPrefix}/${category}/${name})${description === undefined ? '' : ` — ${description}`}`
);
}
}

items.sort((a, b) => a.localeCompare(b));
sections.push(`## ${strings[group.labelKey]}`, items.join('\n'));
}

// Descriptions fall back to the English documents whenever a locale has none,
// so the page carries the same untranslated banner an individual fallback gets.
const frontmatter = ['---', 'editLink: false'];

if (definition.path !== '' && untranslatedCount > 0) {
frontmatter.push('untranslated: true', 'sourceLocale: en');
}

frontmatter.push('---');

const target = path.join(root, 'docs', localeSegment, 'reference.md');
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, `${frontmatter.join('\n')}\n\n${sections.join('\n\n')}\n`);
}
}
4 changes: 2 additions & 2 deletions .scripts/commands/prepareLocalizedFallbacks/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ afterEach(async () => {
describe('prepareLocalizedFallbacks', () => {
it('creates a marked English fallback when a localized document is missing', async () => {
const root = await createFixtureDirectory();
await writeFile(root, 'docs/core/intro.md', '# Introduction\n');
await writeFile(root, 'docs/intro.md', '# Introduction\n');

await prepareLocalizedFallbacks({ localeDirectories: ['ja'], root });

await expectFile(
root,
'generated-locales/docs/ja/core/intro.md',
'generated-locales/docs/ja/intro.md',
`---\nuntranslated: true\nsourceLocale: en\n---\n# Introduction\n`
);
});
Expand Down
8 changes: 8 additions & 0 deletions .scripts/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command } from 'commander';

import { generateDocs } from './commands/generateDocs/index.ts';
import { generateReferenceIndex } from './commands/generateReferenceIndex/index.ts';
import { generateSkill } from './commands/generateSkill/index.ts';
import { prepareLocalizedFallbacks } from './commands/prepareLocalizedFallbacks/index.ts';
import { scaffold } from './commands/scaffold/index.ts';
Expand All @@ -26,6 +27,13 @@ export function cli(args: string[]) {
await prepareLocalizedFallbacks();
});

program
.command('generate-reference-index')
.description('Generate the per-locale reference index page from the source tree')
.action(async () => {
await generateReferenceIndex();
});

program
.command('generate-skill')
.description('Generate the react-simplikit agent skill (SKILL.md + references) from the documentation pages')
Expand Down
12 changes: 8 additions & 4 deletions .scripts/utils/assertLlmsOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ type AssertLlmsOutputOptions = {
const PACKAGE_INDEX_FILE = 'packages/react-simplikit/src/index.ts';

// The generated links are absolute (the plugin's `domain` option), and every documentation page
// lives under core/ or mobile/. A ko/ or ja/ link means the localized copies leaked into the
// lives in a flat reference namespace (hooks/components/utils) or at the root as a guide.
// listing, which would make an agent read the same page several times in different languages.
const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(core|mobile)\//;
const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(?:(?:hooks|components|utils)\/)?[^/]+\.md$/;

/**
* Checks the llms outputs vitepress-plugin-llms wrote into a docs build:
Expand All @@ -28,7 +28,11 @@ export async function assertLlmsOutput({ buildOutputDirectory, root }: AssertLlm
assert.notEqual(links.length, 0, 'llms.txt must list the documentation pages');

for (const link of links) {
assert.match(link, ALLOWED_LINK, `llms.txt must only link English pages under core/ or mobile/: ${link}`);
assert.match(
link,
ALLOWED_LINK,
`llms.txt must only link English pages in the flat reference namespaces or the guides: ${link}`
);
}

for (const name of await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE))) {
Expand All @@ -42,7 +46,7 @@ export async function assertLlmsOutput({ buildOutputDirectory, root }: AssertLlm
const llmsFullTxt = await fs.readFile(path.join(buildOutputDirectory, 'llms-full.txt'), 'utf8');
assert.equal(llmsFullTxt.includes('# useDebounce'), true, 'llms-full.txt must inline the page contents');

const pageMarkdown = await fs.readFile(path.join(buildOutputDirectory, 'core/hooks/useDebounce.md'), 'utf8');
const pageMarkdown = await fs.readFile(path.join(buildOutputDirectory, 'hooks/useDebounce.md'), 'utf8');
assert.equal(pageMarkdown.includes('# useDebounce'), true, 'each page must be served as Markdown');
assert.equal(pageMarkdown.includes('<Interface'), false, 'each page must be served without the VitePress components');
}
Loading
Loading