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
16 changes: 16 additions & 0 deletions create-bestax/src/__tests__/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
MESSAGES,
PROMPTS,
LAUNCH_JSON,
CLAUDE_MD,
} = await import('../constants.js');

describe('constants', () => {
Expand Down Expand Up @@ -227,6 +228,21 @@ describe('constants', () => {
});
});

describe('CLAUDE_MD', () => {
it('tells agents to customize the scaffolded title and README', () => {
const content = CLAUDE_MD('my-app', {
bulmaFlavor: 'complete',
iconLibrary: 'none',
});
expect(content).toContain('# my-app');
expect(content).toContain('`<title>`');
expect(content).toContain('README.md');
expect(content).toContain('starts as the project name');
expect(content).toContain('meta tags');
expect(content).toContain('rewrite the README');
});
Comment thread
allxsmith marked this conversation as resolved.
});

describe('DEFAULT_PROJECT_NAME', () => {
it('should be a valid project name', () => {
expect(DEFAULT_PROJECT_NAME).toBeDefined();
Expand Down
78 changes: 78 additions & 0 deletions create-bestax/src/__tests__/project-creator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,53 @@ describe('ProjectCreator', () => {
});
});

describe('updateIndexHtmlTitle', () => {
it('replaces the stock <title> with the project name', async () => {
const targetPath = '/test/project';
const indexHtmlPath = '/test/project/index.html';

(
fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync>
).mockImplementation(path => path === indexHtmlPath);
(
fs.default.readFile as jest.MockedFunction<typeof fs.readFile>
).mockResolvedValue(
'<head><title>Bestax + Vite + React</title></head>' as unknown
);

await projectCreator.updateIndexHtmlTitle(targetPath, 'my-app');

expect(fs.default.writeFile).toHaveBeenCalledWith(
indexHtmlPath,
'<head><title>my-app</title></head>'
);
});

it('does nothing when index.html is missing', async () => {
(
fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync>
).mockReturnValue(false);

await projectCreator.updateIndexHtmlTitle('/test/project', 'my-app');

expect(fs.default.readFile).not.toHaveBeenCalled();
expect(fs.default.writeFile).not.toHaveBeenCalled();
});

it('leaves index.html untouched when it has no <title> tag', async () => {
(
fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync>
).mockReturnValue(true);
(
fs.default.readFile as jest.MockedFunction<typeof fs.readFile>
).mockResolvedValue('<head></head>' as unknown);

await projectCreator.updateIndexHtmlTitle('/test/project', 'my-app');

expect(fs.default.writeFile).not.toHaveBeenCalled();
});
});

describe('setupIconLibrary', () => {
it('should skip setup for none', async () => {
await projectCreator.setupIconLibrary('/test/project', 'none', 'vite');
Expand Down Expand Up @@ -964,6 +1011,37 @@ describe('ProjectCreator', () => {
expect(fileSystem.updatePackageJson).toHaveBeenCalled();
});

it('sets the index.html title to the project name', async () => {
(
fileSystem.checkDirectoryExists as jest.MockedFunction<
typeof fileSystem.checkDirectoryExists
>
).mockResolvedValue(false);
(
fileSystem.isDirectoryEmpty as jest.MockedFunction<
typeof fileSystem.isDirectoryEmpty
>
).mockResolvedValue(true);
(
fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync>
).mockImplementation(path => String(path).endsWith('index.html'));
(
fs.default.readFile as jest.MockedFunction<typeof fs.readFile>
).mockResolvedValue(
'<head><title>Bestax + Vite + React</title></head>' as unknown
);

await projectCreator.create('test-project', { yes: true, skills: false });

expect(
(fs.default.writeFile as unknown as jest.Mock).mock.calls.some(
([p, content]) =>
String(p).endsWith('index.html') &&
String(content).includes('<title>test-project</title>')
)
).toBe(true);
});

it('writes launch.json when the skills opt-in is accepted', async () => {
// target dir does not exist; the bundled skills dir does
(
Expand Down
3 changes: 3 additions & 0 deletions create-bestax/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ ${setupLines.join('\n')}
- 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.
- \`index.html\`'s \`<title>\` starts as the project name and \`README.md\` is stock template
boilerplate — once this app has a real identity, set the title (and any meta tags) to match
it and rewrite the README to describe *this* app, not the template.

## AI skills

Expand Down
20 changes: 20 additions & 0 deletions create-bestax/src/project-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ export class ProjectCreator {
await copyDirectory(templatePath, targetPath);
}

async updateIndexHtmlTitle(
targetPath: string,
projectName: string
): Promise<void> {
const indexHtmlPath = path.join(targetPath, 'index.html');
if (!fs.existsSync(indexHtmlPath)) return;

const htmlContent = await fs.readFile(indexHtmlPath, 'utf8');
// projectName is validated against PROJECT_NAME_REGEX (letters, digits,
// dots, dashes, underscores only), so it needs no HTML escaping.
const updated = htmlContent.replace(
/<title>[^<]*<\/title>/,
`<title>${projectName}</title>`
);
if (updated !== htmlContent) {
await fs.writeFile(indexHtmlPath, updated);
}
}

async setupSkills(
targetPath: string,
projectName: string,
Expand Down Expand Up @@ -559,6 +578,7 @@ export class ProjectCreator {
try {
await this.copyTemplate(template, targetPath);
await updatePackageJson(targetPath, projectName);
await this.updateIndexHtmlTitle(targetPath, projectName);
await this.setupBulmaFlavor(targetPath, bulmaFlavor, template);
await this.setupIconLibrary(targetPath, iconLibrary, template);
await this.setupConfigProvider(
Expand Down
Loading