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
76 changes: 74 additions & 2 deletions create-bestax/src/__tests__/project-creator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ describe('ProjectCreator', () => {
>
).mockResolvedValue(true);

await projectCreator.setupSkills('/target/path', 'my-app');
await projectCreator.setupSkills('/target/path', 'my-app', {
bulmaFlavor: 'complete',
iconLibrary: 'none',
});

expect(fs.default.copy).toHaveBeenCalledWith(
expect.stringContaining('skills'),
Expand All @@ -231,6 +234,72 @@ describe('ProjectCreator', () => {
expect.stringContaining('CLAUDE.md'),
expect.stringContaining('my-app')
);
const written = (
fs.default.writeFile as unknown as jest.Mock
).mock.calls.find(([p]) => String(p).includes('CLAUDE.md'))?.[1];
expect(written).toContain('Bulma flavor: **complete**');
expect(written).toContain('No icon library is installed');
expect(written).not.toContain('classPrefix');
});

it('documents the prefix caveat and icon library for a prefixed scaffold', async () => {
(
fileSystem.checkDirectoryExists as jest.MockedFunction<
typeof fileSystem.checkDirectoryExists
>
).mockResolvedValue(true);

await projectCreator.setupSkills('/target/path', 'my-app', {
bulmaFlavor: 'prefixed',
iconLibrary: 'fontawesome',
});

const written = (
fs.default.writeFile as unknown as jest.Mock
).mock.calls.find(([p]) => String(p).includes('CLAUDE.md'))?.[1];
expect(written).toContain('classPrefix="bestax-"');
expect(written).toContain('bestax-prefixed.css');
expect(written).toContain('Icon library: **Font Awesome**');
// fontawesome is the Icon default — no provider wrapper, no caveat
expect(written).not.toContain('iconLibrary=');
});

it('documents the ConfigProvider iconLibrary caveat for a non-default icon library', async () => {
(
fileSystem.checkDirectoryExists as jest.MockedFunction<
typeof fileSystem.checkDirectoryExists
>
).mockResolvedValue(true);

await projectCreator.setupSkills('/target/path', 'my-app', {
bulmaFlavor: 'complete',
iconLibrary: 'mdi',
});

const written = (
fs.default.writeFile as unknown as jest.Mock
).mock.calls.find(([p]) => String(p).includes('CLAUDE.md'))?.[1];
expect(written).toContain('iconLibrary="mdi"');
expect(written).toContain('extend that provider');
});

it('names the mapped provider value for ionicons in the caveat', async () => {
(
fileSystem.checkDirectoryExists as jest.MockedFunction<
typeof fileSystem.checkDirectoryExists
>
).mockResolvedValue(true);

await projectCreator.setupSkills('/target/path', 'my-app', {
bulmaFlavor: 'complete',
iconLibrary: 'ionicons',
});

const written = (
fs.default.writeFile as unknown as jest.Mock
).mock.calls.find(([p]) => String(p).includes('CLAUDE.md'))?.[1];
// the scaffolder writes iconLibrary="ion" (not "ionicons") into main.tsx
expect(written).toContain('iconLibrary="ion"');
});

it('skips when the skills bundle is missing', async () => {
Expand All @@ -240,7 +309,10 @@ describe('ProjectCreator', () => {
>
).mockResolvedValue(false);

await projectCreator.setupSkills('/target/path', 'my-app');
await projectCreator.setupSkills('/target/path', 'my-app', {
bulmaFlavor: 'complete',
iconLibrary: 'none',
});

expect(fs.default.copy).not.toHaveBeenCalled();
expect(fs.default.writeFile).not.toHaveBeenCalled();
Expand Down
65 changes: 63 additions & 2 deletions create-bestax/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,73 @@ export const PROMPTS = {
'Install the bestax AI skills (.claude/skills) for Claude Code and other agents?',
} as const;

// Icon libraries that require a `ConfigProvider iconLibrary` wrapper, mapped
// to the exact prop value the scaffolder writes into main.tsx/jsx ('none' and
// 'fontawesome' are the defaults and need no provider). Shared with
// project-creator.setupConfigProvider AND the CLAUDE_MD template below so the
// generated docs can never drift from the generated code.
export const CONFIG_PROVIDER_ICON_VALUES: Record<string, string> = {
mdi: 'mdi',
ionicons: 'ion',
'material-icons': 'material-icons',
'material-symbols': 'material-symbols',
};

// Minimal CLAUDE.md scaffolded alongside the skills so an AI agent knows the
// stack and where the skills live.
export const CLAUDE_MD = (projectName: string): string => `# ${projectName}
// stack, this app's scaffold choices, and where the skills live. Kept short:
// it loads into every agent session.
export interface ClaudeMdOptions {
bulmaFlavor: string;
iconLibrary: string;
}

export const CLAUDE_MD = (
projectName: string,
{ bulmaFlavor, iconLibrary }: ClaudeMdOptions
): string => {
const flavor = BULMA_FLAVORS.find(f => f.name === bulmaFlavor);
const icon = ICON_LIBRARIES.find(lib => lib.name === iconLibrary);
const setupLines = [
`- Bulma flavor: **${bulmaFlavor}** — the app imports **prebuilt** CSS` +
(flavor ? ` (\`${flavor.importStatement.trim()}\`)` : '') +
`; there is no Sass pipeline unless you add \`sass\`.`,
];
if (flavor?.needsPrefix) {
setupLines.push(
`- Every Bulma class carries the \`bestax-\` prefix and the app is wrapped in ` +
`\`<ConfigProvider classPrefix="bestax-">\` — custom CSS selectors must match the prefix.`
);
}
const providerIconValue = CONFIG_PROVIDER_ICON_VALUES[iconLibrary];
setupLines.push(
iconLibrary === 'none'
? `- No icon library is installed — add one before using \`<Icon>\` ` +
`(https://bestax.io/docs/api/elements/icon).`
: `- Icon library: **${icon?.display ?? iconLibrary}** — use \`<Icon name="..." />\`.` +
(providerIconValue
? ` The app is already wrapped in \`<ConfigProvider iconLibrary="${providerIconValue}">\` ` +
`— extend that provider rather than adding a second one.`
: '')
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return `# ${projectName}

This app is built with [\`@allxsmith/bestax-bulma\`](https://bestax.io) — React components for
Bulma 1.x.

## This app's setup

${setupLines.join('\n')}

## House style

- Never inline \`style={{}}\` — use the helper props every component accepts (\`m*\`/\`p*\`
spacing, \`textColor\`/\`bgColor\`, \`display="flex"\`, \`flexDirection\`, \`alignItems\`).
Flex layouts have no \`gap\` helper — space children with margins (\`Grid\` has a real
\`gap\` prop and \`Columns\` has \`gapSize\`, so prefer those there).
- Compose existing components before writing custom CSS; theme via \`Theme\` and \`--bulma-*\`
variables, never hardcoded colors.
- There is no test runner or Storybook in this app — don't assume one.

## AI skills

\`.claude/skills/\` contains Agent Skills that teach Claude how to build with this library. They load
Expand All @@ -68,6 +128,7 @@ Prefer the library's components and these skills over hand-written Bulma markup
- LLM-ready docs: https://bestax.io/llms.txt (curated index) and https://bestax.io/llms-full.txt
- Using bestax with AI tools: https://bestax.io/docs/guides/llms
`;
};

export interface IconLibrary {
name: string;
Expand Down
26 changes: 15 additions & 11 deletions create-bestax/src/project-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
ICON_LIBRARIES,
BULMA_FLAVORS,
CLAUDE_MD,
CONFIG_PROVIDER_ICON_VALUES,
type ClaudeMdOptions,
} from './constants.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -101,7 +103,11 @@ export class ProjectCreator {
await copyDirectory(templatePath, targetPath);
}

async setupSkills(targetPath: string, projectName: string): Promise<void> {
async setupSkills(
targetPath: string,
projectName: string,
options: ClaudeMdOptions
): Promise<void> {
const skillsSrc = path.join(this.templatesDir, 'skills');
if (!(await checkDirectoryExists(skillsSrc))) {
console.log(
Expand All @@ -113,7 +119,7 @@ export class ProjectCreator {
await fs.copy(skillsSrc, path.join(targetPath, '.claude', 'skills'));
await fs.writeFile(
path.join(targetPath, 'CLAUDE.md'),
CLAUDE_MD(projectName)
CLAUDE_MD(projectName, options)
);
console.log(chalk.green(MESSAGES.SKILLS_ADDED));
}
Expand Down Expand Up @@ -404,14 +410,9 @@ export class ProjectCreator {
configProps.push('classPrefix="bestax-"');
}
if (needsIconLibrary) {
// Map the icon library name to the correct value for ConfigProvider
const iconLibraryMap: Record<string, string> = {
mdi: 'mdi',
ionicons: 'ion',
'material-icons': 'material-icons',
'material-symbols': 'material-symbols',
};
const iconLibraryValue = iconLibraryMap[iconLibrary];
// Shared with the CLAUDE_MD template so the generated docs always
// name the same provider value the scaffolder writes here.
const iconLibraryValue = CONFIG_PROVIDER_ICON_VALUES[iconLibrary];
if (iconLibraryValue) {
configProps.push(`iconLibrary="${iconLibraryValue}"`);
}
Expand Down Expand Up @@ -557,7 +558,10 @@ export class ProjectCreator {
template
);
if (installSkills) {
await this.setupSkills(targetPath, projectName);
await this.setupSkills(targetPath, projectName, {
bulmaFlavor,
iconLibrary,
});
}
displaySuccess(targetDir);
} catch (error) {
Expand Down
Loading