Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@

## Project

React hooks/components library. Monorepo with the library in `packages/react-simplikit`; mobile web hooks live under `src/mobile` and share the same public API.
React hooks/components library. Monorepo with the library in `packages/react-simplikit`.

## Architecture

Unidirectional layers: `components → hooks → utils → _internal`
Nothing outside `src/mobile` imports from it; `src/mobile` may use `src/utils` and `_internal` (test infrastructure is exempt).

## Code Style

Expand Down
3 changes: 1 addition & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

## Quick Reference

- Monorepo: the library is `packages/react-simplikit`; mobile web hooks live under `src/mobile` and share the same public API
- Monorepo: the library is `packages/react-simplikit`
- Architecture: `components → hooks → utils → _internal` (unidirectional, no circular imports)
- Nothing outside `src/mobile` imports from it; `src/mobile` may use `src/utils` and `_internal` (test infrastructure is exempt)

## Code Style Rules

Expand Down
18 changes: 4 additions & 14 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,6 @@ jobs:
- 'packages/*/src/hooks/**/!(*.spec|*.test).tsx'
- 'packages/*/src/utils/**/!(index|*.spec|*.test).ts'
- 'packages/*/src/utils/**/!(*.spec|*.test).tsx'
- 'packages/*/src/mobile/hooks/**/!(index|*.spec|*.test).ts'
- 'packages/*/src/mobile/hooks/**/!(*.spec|*.test).tsx'
- 'packages/*/src/mobile/utils/**/!(index|*.spec|*.test).ts'
- 'packages/*/src/mobile/utils/**/!(*.spec|*.test).tsx'

verify-test:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -344,22 +340,17 @@ jobs:
HAS_ERROR=0
FOUND=0

# Every implementation — the src/mobile tree included — must be
# exported from the package root barrel: it is the only public entry.
# Every implementation must be exported from the package root barrel:
# it is the only public entry.
for pkg in packages/*/; do
ROOT_INDEX="${pkg}src/index.ts"

if [ ! -f "$ROOT_INDEX" ]; then
continue
fi

for prefix in "" "mobile/"; do
if [ -n "$prefix" ] && [ ! -d "${pkg}src/$prefix" ]; then
continue
fi

for dir in "${IMPL_DIRS[@]}"; do
for impl_dir in ${pkg}src/$prefix$dir/*/; do
for impl_dir in ${pkg}src/$dir/*/; do
if [ ! -d "$impl_dir" ]; then
continue
fi
Expand Down Expand Up @@ -393,14 +384,13 @@ jobs:
HAS_ERROR=1
fi

relative_path="./$prefix$dir/$impl_name/index.ts"
relative_path="./$dir/$impl_name/index.ts"
if ! grep -q "from.*['\"]$relative_path['\"]" "$ROOT_INDEX"; then
ERROR_LOG+="❌ $impl_name is not exported in $ROOT_INDEX\n"
HAS_ERROR=1
fi
done
done
done
done

if [ "$FOUND" -eq 0 ]; then
Expand Down
38 changes: 16 additions & 22 deletions .scripts/commands/generateReferenceIndex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ import { extractDescription } from '../generateSkill/catalog.ts';

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

type Group = { labelKey: 'hooksLabel' | 'componentsLabel' | 'utilsLabel'; directories: string[] };
type Group = { labelKey: 'hooksLabel' | 'componentsLabel' | 'utilsLabel'; directory: string };
type IndexEntry = { name: string; url: string; description?: string; translated: boolean };
type IndexSection = { label: string; entries: IndexEntry[] };

// Mirrors the sidebar: the src/mobile trees share the flat hooks/utils URLs and the
// same three lists, sorted together.
const GROUPS: Group[] = [
{ labelKey: 'hooksLabel', directories: ['hooks', 'mobile/hooks'] },
{ labelKey: 'componentsLabel', directories: ['components'] },
{ labelKey: 'utilsLabel', directories: ['utils', 'mobile/utils'] },
{ labelKey: 'hooksLabel', directory: 'hooks' },
{ labelKey: 'componentsLabel', directory: 'components' },
{ labelKey: 'utilsLabel', directory: 'utils' },
];

/**
Expand All @@ -43,22 +41,18 @@ export function generateReferenceIndex(): void {
function collectEntries(root: string, locale: LocaleDefinition, group: Group): IndexEntry[] {
const isRoot = locale.path === '';
const urlPrefix = isRoot ? '' : `/${locale.path}`;

return group.directories
.flatMap(directory => {
const base = path.join(root, PACKAGE_SRC, directory);
const category = path.basename(directory);

return listDirectories(base).map(name => {
const localized = isRoot ? undefined : readDescription(path.join(base, name, locale.path, `${name}.md`), name);

return {
name,
url: `${urlPrefix}/${category}/${name}`,
description: localized ?? readDescription(path.join(base, name, `${name}.md`), name),
translated: isRoot || localized != null,
};
});
const base = path.join(root, PACKAGE_SRC, group.directory);

return listDirectories(base)
.map(name => {
const localized = isRoot ? undefined : readDescription(path.join(base, name, locale.path, `${name}.md`), name);

return {
name,
url: `${urlPrefix}/${group.directory}/${name}`,
description: localized ?? readDescription(path.join(base, name, `${name}.md`), name),
translated: isRoot || localized != null,
};
})
.sort((a, b) => a.name.localeCompare(b.name));
}
Expand Down
4 changes: 1 addition & 3 deletions .scripts/commands/generateSkill/catalog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import { describe, it } from 'vitest';
import { extractDescription, getCategory, renderSkill } from './catalog.ts';

describe('getCategory', () => {
it('derives the category from the export source path, ignoring the mobile directory', () => {
it('derives the category from the export source path', () => {
assert.equal(getCategory('./hooks/useToggle/index.ts'), 'hooks');
assert.equal(getCategory('./components/Separated/index.ts'), 'components');
assert.equal(getCategory('./utils/mergeRefs/index.ts'), 'utils');
assert.equal(getCategory('./mobile/hooks/useKeyboardHeight/index.ts'), 'hooks');
assert.equal(getCategory('./mobile/utils/isIOS/index.ts'), 'utils');
});

it('rejects a path outside the known categories', () => {
Expand Down
9 changes: 2 additions & 7 deletions .scripts/commands/generateSkill/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@ type RenderSkillOptions = {

const CATALOG_PLACEHOLDER = '<!-- CATALOG -->';

/**
* Catalog heading for an export, from where it lives: `./hooks/x/index.ts` and
* `./mobile/hooks/x/index.ts` both land under `hooks` — the directory split is a source
* layout detail, not something a consumer sees.
*/
/** Catalog heading for an export: the first segment of its source path, `./hooks/x/index.ts` → `hooks`. */
export function getCategory(sourcePath: string): Category {
const segments = sourcePath.replace(/^\.\//, '').split('/');
const category = segments[0] === 'mobile' ? segments[1] : segments[0];
const category = sourcePath.replace(/^\.\//, '').split('/')[0];
const known = CATEGORIES.find(candidate => candidate === category);

if (known === undefined) {
Expand Down
8 changes: 4 additions & 4 deletions .scripts/commands/generateSkill/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ describe('generateSkill', () => {
it('writes SKILL.md with a catalog row and a reference page per public export', async () => {
const root = await writeFixtureRoot({
index: `
export { isIOS } from './mobile/utils/isIOS/index.ts';
export { isIOS } from './utils/isIOS/index.ts';
export { useToggle } from './hooks/useToggle/index.ts';
`,
pages: {
'hooks/useToggle/useToggle.md': '# useToggle\n\n`useToggle` flips a boolean. More text.\n\n## Interface\n',
'mobile/utils/isIOS/isIOS.md': '# isIOS\n\n`isIOS` detects iOS.\n',
'utils/isIOS/isIOS.md': '# isIOS\n\n`isIOS` detects iOS.\n',
},
});
const outputDirectory = path.join(root, 'out');
Expand Down Expand Up @@ -52,12 +52,12 @@ export { useToggle } from './hooks/useToggle/index.ts';
it('produces identical output on a second run and drops pages of removed exports', async () => {
const root = await writeFixtureRoot({
index: `
export { isIOS } from './mobile/utils/isIOS/index.ts';
export { isIOS } from './utils/isIOS/index.ts';
export { useToggle } from './hooks/useToggle/index.ts';
`,
pages: {
'hooks/useToggle/useToggle.md': '# useToggle\n\n`useToggle` flips a boolean.\n',
'mobile/utils/isIOS/isIOS.md': '# isIOS\n\n`isIOS` detects iOS.\n',
'utils/isIOS/isIOS.md': '# isIOS\n\n`isIOS` detects iOS.\n',
},
});
const outputDirectory = path.join(root, 'out');
Expand Down
5 changes: 2 additions & 3 deletions .scripts/verify-pack/packages.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type SizeGate = {
// Names the gate in output: one package can carry several entry points now.
label: string;
entry: string;
// Per-module output means adding hooks never moves this number — only the
Expand All @@ -21,12 +20,12 @@ export const TARGET_PACKAGES: TargetPackage[] = [
dir: 'packages/react-simplikit',
sizeGates: [
{
label: 'root',
label: 'useToggle',
entry: `export { useToggle } from 'react-simplikit';`,
limitBytes: 256,
},
{
label: 'hook from src/mobile',
label: 'useNetworkStatus',
entry: `export { useNetworkStatus } from 'react-simplikit';`,
limitBytes: 768,
},
Expand Down
2 changes: 1 addition & 1 deletion .scripts/verifyDocsI18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ try {
const localeCount = localeDirectories.length + 1;
const referenceItemCount = (
await Promise.all(
['hooks', 'components', 'utils', 'mobile/hooks', 'mobile/utils'].map(
['hooks', 'components', 'utils'].map(
async directory =>
(
await fs.readdir(path.join(root, 'packages/react-simplikit/src', directory), { withFileTypes: true })
Expand Down
14 changes: 3 additions & 11 deletions .vitepress/libs/buildLocaleConfig.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DefaultTheme, LocaleSpecificConfig } from 'vitepress';

import { LocaleDefinition } from '../locales.mts';
import { mobileSourceRoot, packageSourceRoot } from '../shared.mts';
import { packageSourceRoot } from '../shared.mts';
import { getSidebarItems } from './getSidebarItems.mts';
import { sortByText } from './sortByText.mts';

Expand All @@ -16,17 +16,9 @@ export function buildLocaleConfig(
const sidebarLocale = definition.path === '' ? undefined : definition.path;
const strings = definition.themeStrings;

// Reference URLs are flat: category segment only. The src/mobile trees are folded into
// the same three lists (sortByText orders each list), so a hook is a hook wherever its source lives.
const hooks = [
...getSidebarItems(packageSourceRoot, 'hooks', '', sidebarLocale),
...getSidebarItems(mobileSourceRoot, 'hooks', '', sidebarLocale),
];
const hooks = getSidebarItems(packageSourceRoot, 'hooks', '', sidebarLocale);
const components = getSidebarItems(packageSourceRoot, 'components', '', sidebarLocale);
const utils = [
...getSidebarItems(packageSourceRoot, 'utils', '', sidebarLocale),
...getSidebarItems(mobileSourceRoot, 'utils', '', sidebarLocale),
];
const utils = getSidebarItems(packageSourceRoot, 'utils', '', sidebarLocale);

return {
lang: definition.lang,
Expand Down
54 changes: 31 additions & 23 deletions .vitepress/libs/legacyRedirects.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fs from 'node:fs';
import path from 'node:path';

import { legacyRoutePatterns, localeDirectories } from '../locales.mts';
import { listDirectories, projectRoot, SITE_ORIGIN } from '../shared.mts';
import { localeDirectories } from '../locales.mts';
import { listDirectories, packageSourceRoot, SITE_ORIGIN } from '../shared.mts';

type RedirectPair = { from: string; to: string };

Expand All @@ -21,34 +21,42 @@ const GUIDE_REDIRECTS: RedirectPair[] = [
{ from: 'mobile/contributing.html', to: 'contributing.html' },
];

export function collectLegacyRedirects(): RedirectPair[] {
return [...collectGuideRedirects(), ...collectReferenceRedirects()];
}
// Reference pages sat under core/ or mobile/ before the flattening. Their sources now share
// src/hooks and src/utils, so the mobile set is listed rather than read off the tree.
const RETIRED_MOBILE_PAGES = new Set([
'useAvoidKeyboard',
'useBodyScrollLock',
'useKeyboardHeight',
'useNetworkStatus',
'usePageVisibility',
'useSafeAreaInset',
'useScrollDirection',
'useVisualViewport',
'disableBodyScrollLock',
'enableBodyScrollLock',
'getKeyboardHeight',
'getSafeAreaInset',
'isAndroid',
'isIOS',
'isKeyboardVisible',
'isServer',
'subscribeKeyboardHeight',
]);

function collectGuideRedirects(): RedirectPair[] {
return GUIDE_REDIRECTS.flatMap(pair => [
export function collectLegacyRedirects(): RedirectPair[] {
return [...GUIDE_REDIRECTS, ...collectReferenceRedirects()].flatMap(pair => [
pair,
...localeDirectories.map(locale => ({ from: `${locale}/${pair.from}`, to: `${locale}/${pair.to}` })),
]);
}

/**
* A pattern like `packages/react-simplikit/src/hooks/:hook/:hook.md` with the
* legacy destination `core/hooks/:hook.md` yields one pair per hook directory.
*/
function collectReferenceRedirects(): RedirectPair[] {
return legacyRoutePatterns.flatMap(route => {
const itemsRoot = path.join(projectRoot, route.source.split(/\/:\w+\//)[0]);

return listDirectories(itemsRoot).map(name => ({
from: toHtmlPath(route.from, name),
to: toHtmlPath(route.to, name),
}));
});
}

function toHtmlPath(pattern: string, name: string): string {
return pattern.replace(/:\w+/, name).replace(/\.md$/, '.html');
return ['hooks', 'components', 'utils'].flatMap(category =>
listDirectories(path.join(packageSourceRoot, category)).map(name => ({
from: `${RETIRED_MOBILE_PAGES.has(name) ? 'mobile' : 'core'}/${category}/${name}.html`,
to: `${category}/${name}.html`,
}))
);
}

/** Writes one redirect stub per legacy URL into the build output. */
Expand Down
Loading
Loading