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
15 changes: 14 additions & 1 deletion .github/workflows/renovate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,24 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Validate Renovate token
run: |
TOKEN="${{ secrets.RENOVATE_TOKEN }}"
if [ -z "${TOKEN}" ]; then
echo "RENOVATE_TOKEN secret is required to let Renovate-created PRs trigger CI workflows."
exit 1
fi
if [[ "${TOKEN}" == ghs_* ]]; then
echo "RENOVATE_TOKEN must be a PAT (ghp_* or github_pat_*), not GITHUB_TOKEN."
exit 1
fi

- name: Run Renovate
uses: renovatebot/github-action@v44.0.3
with:
configurationFile: renovate.json
token: ${{ secrets.RENOVATE_TOKEN || github.token }}
token: ${{ secrets.RENOVATE_TOKEN }}
env:
LOG_LEVEL: debug
RENOVATE_REPOSITORIES: ${{ github.repository }}
RENOVATE_PLATFORM_AUTOMERGE: 'false'
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
[![Dependency Review](https://github.com/chiragmak10/create-react-forge/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/chiragmak10/create-react-forge/actions/workflows/dependency-review.yml)
[![CI](https://github.com/chiragmak10/create-react-forge/actions/workflows/ci.yml/badge.svg)](https://github.com/chiragmak10/create-react-forge/actions/workflows/ci.yml)


# create-react-forge

Production-ready React scaffolding CLI with first-class testing, flexible runtimes (Vite/Next.js), and a composable template system inspired by [bulletproof-react](https://github.com/alan2207/bulletproof-react).
Expand Down Expand Up @@ -117,11 +116,11 @@ The CLI uses pinned, tested versions for all dependencies:

## Automated dependency updates

This repo now uses Renovate to auto-update dependencies (including template manifests under `src/templates/overlays/**/manifest.json`, `src/dependencies/resolver.ts`, and the dependency versions table in this README). The workflow runs weekly and can also be run manually.
This repo now uses Renovate to auto-update dependencies (including template manifests under `src/templates/overlays/**/manifest.json`, `src/dependencies/resolver.ts`, annotated test fixture constants, and the dependency versions table in this README). The workflow runs weekly and can also be run manually.

### One-time setup

1. No additional token is required. Renovate uses the default `secrets.GITHUB_TOKEN`.
1. Add a repository secret named `RENOVATE_TOKEN` (fine-grained PAT with `Contents: Read and write` and `Pull requests: Read and write`) so Renovate PRs can trigger CI workflows.
2. Enable repository auto-merge in GitHub settings.
3. Protect `master` and require CI checks before merge.

Expand All @@ -131,7 +130,7 @@ Config file: `renovate.json`
Behavior:

- All dependency updates (major, minor, patch) auto-merge after checks pass.
- Custom regex managers keep template manifests, the resolver registry, and README dependency rows in sync.
- Custom regex managers keep template manifests, the resolver registry, annotated test fixtures, and README dependency rows in sync.

## Screenshot

Expand Down
12 changes: 11 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"branchConcurrentLimit": 0,
"recreateWhen": "always",
"recreateWhen": "auto",
"lockFileMaintenance": {
"enabled": true,
"automerge": true,
Expand Down Expand Up @@ -37,6 +37,16 @@
"datasourceTemplate": "npm",
"versioningTemplate": "npm"
},
{
"customType": "regex",
"description": "Keep test fixture dependency versions in sync",
"managerFilePatterns": ["/^src\\/__tests__\\/assembler\\.test\\.ts$/"],
"matchStrings": [
"const\\s+[A-Z0-9_]+_VERSION\\s*=\\s*'(?<currentValue>[~^]?\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)';\\s*\\/\\/\\s*renovate:\\s*depName=(?<depName>[^\\s]+)"
],
"datasourceTemplate": "npm",
"versioningTemplate": "npm"
},
{
"customType": "regex",
"description": "Keep README React version row in sync",
Expand Down
18 changes: 12 additions & 6 deletions src/__tests__/assembler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { describe, expect, it } from 'vitest';
import type { ProjectConfig } from '../config/schema';
import { ProjectAssembler } from '../assembler';

const TSX_VERSION = '^4.19.2'; // renovate: depName=tsx
const REACT_VERSION = '^19.0.0'; // renovate: depName=react
const TYPESCRIPT_VERSION = '^5.7.2'; // renovate: depName=typescript

function createConfig(overrides: Partial<ProjectConfig> = {}): ProjectConfig {
return {
name: 'test-app',
Expand Down Expand Up @@ -40,7 +44,9 @@ describe('ProjectAssembler', () => {
);
const files = assembler.getFiles();

expect(files.get('README.md')).toBe('awesome-app | A production-ready React application | | MIT');
expect(files.get('README.md')).toBe(
'awesome-app | A production-ready React application | | MIT'
);

files.set('another.md', 'changed');
expect(assembler.getFiles().has('another.md')).toBe(false);
Expand All @@ -64,11 +70,11 @@ describe('ProjectAssembler', () => {
);

assembler.addDependencies({ zeta: '^1.0.0', alpha: '^1.0.0' });
assembler.addDevDependencies({ tsx: '^4.0.0' });
assembler.addDevDependencies({ tsx: TSX_VERSION });
assembler.addScripts({ dev: 'vite', build: 'vite build' });
assembler.mergeTemplateDeps({
dependencies: { react: '^19.0.0' },
devDependencies: { typescript: '^5.0.0' },
dependencies: { react: REACT_VERSION },
devDependencies: { typescript: TYPESCRIPT_VERSION },
scripts: { test: 'vitest' },
});

Expand All @@ -91,8 +97,8 @@ describe('ProjectAssembler', () => {

expect(Object.keys(pkg.dependencies)).toEqual(['alpha', 'react', 'zeta']);
expect(pkg.devDependencies).toMatchObject({
tsx: '^4.0.0',
typescript: '^5.0.0',
tsx: TSX_VERSION,
typescript: TYPESCRIPT_VERSION,
});
expect(pkg.scripts).toMatchObject({
dev: 'vite',
Expand Down
82 changes: 82 additions & 0 deletions src/__tests__/dependency-version-consistency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { readdirSync, readFileSync } from 'fs';
import { join } from 'path';
import { describe, expect, it } from 'vitest';
import { VERSION_REGISTRY } from '../dependencies/resolver';

const README_VERSION_ROWS: Array<{ label: string; depName: string }> = [
{ label: 'React', depName: 'react' },
{ label: 'Vite', depName: 'vite' },
{ label: 'Next.js', depName: 'next' },
{ label: 'Tailwind CSS', depName: 'tailwindcss' },
{ label: 'TanStack Query', depName: '@tanstack/react-query' },
{ label: 'Vitest', depName: 'vitest' },
{ label: 'Playwright', depName: '@playwright/test' },
{ label: 'TypeScript', depName: 'typescript' },
];

function collectManifestPaths(dirPath: string): string[] {
const manifestPaths: string[] = [];
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
manifestPaths.push(...collectManifestPaths(fullPath));
continue;
}
if (entry.name === 'manifest.json') {
manifestPaths.push(fullPath);
}
}
return manifestPaths;
}

function readReadmeRowVersion(content: string, label: string): string | null {
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const rowRegex = new RegExp(`^\\|\\s*${escapedLabel}\\s*\\|\\s*([^|\\s]+)\\s*\\|\\s*$`, 'm');
const match = content.match(rowRegex);
return match?.[1] ?? null;
}

describe('Dependency Version Consistency', () => {
it('should keep tracked README dependency rows in sync with VERSION_REGISTRY', () => {
const readmeContent = readFileSync('README.md', 'utf-8');

for (const { label, depName } of README_VERSION_ROWS) {
const actual = readReadmeRowVersion(readmeContent, label);
expect(actual, `README row missing or malformed for "${label}"`).not.toBeNull();
expect(actual).toBe(VERSION_REGISTRY[depName]);
}
});

it('should keep all template manifest dependency versions in sync with VERSION_REGISTRY', () => {
const manifestPaths = collectManifestPaths('src/templates/overlays');
const missingInRegistry: string[] = [];
const mismatches: string[] = [];

for (const manifestPath of manifestPaths) {
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const depEntries = Object.entries({
...(manifest.dependencies || {}),
...(manifest.devDependencies || {}),
});

for (const [depName, currentValue] of depEntries) {
const expectedValue = VERSION_REGISTRY[depName];
if (!expectedValue) {
missingInRegistry.push(`${depName} (${manifestPath})`);
continue;
}
if (currentValue !== expectedValue) {
mismatches.push(
`${depName} (${manifestPath}) expected ${expectedValue} but found ${currentValue}`
);
}
}
}

expect(missingInRegistry).toEqual([]);
expect(mismatches).toEqual([]);
});
});
42 changes: 24 additions & 18 deletions src/dependencies/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,60 @@
*/
export const VERSION_REGISTRY: Record<string, string> = {
// Runtime
'vite': '^6.0.7',
vite: '^6.0.7',
'@vitejs/plugin-react': '^5.0.0',
'next': '^16.1.6',
'react': '^19.0.0',
next: '^16.1.6',
react: '^19.0.0',
'react-dom': '^19.0.0',

// Language
'typescript': '^5.7.2',
typescript: '^5.7.2',

// Styling
'tailwindcss': '^4.0.0',
tailwindcss: '^4.0.0',
'@tailwindcss/postcss': '^4.0.0',
'postcss': '^8.4.49',
'autoprefixer': '^10.4.20',
postcss: '^8.4.49',
autoprefixer: '^10.4.20',
'styled-components': '^6.1.14',
'@types/styled-components': '^5.1.34',
'babel-plugin-styled-components': '^2.1.4',

// State Management
'@reduxjs/toolkit': '^2.5.0',
'react-redux': '^9.2.0',
'zustand': '^5.0.3',
zustand: '^5.0.3',
jotai: '^2.10.0',

// Data Fetching
'@tanstack/react-query': '^5.62.10',
'@tanstack/react-query-devtools': '^5.62.10',

// Testing - Unit
'vitest': '^2.1.8',
vitest: '^2.1.8',
'@vitest/ui': '^2.1.8',
'jest': '^29.7.0',
'@vitest/coverage-v8': '^2.1.8',
jest: '^29.7.0',
'jest-environment-jsdom': '^29.7.0',
'@types/jest': '^29.5.14',
'ts-jest': '^29.2.6',

// Testing - Component
'@testing-library/react': '^16.1.0',
'@testing-library/jest-dom': '^6.6.3',
'@testing-library/user-event': '^14.5.2',
'jsdom': '^25.0.1',
jsdom: '^25.0.1',
msw: '^2.7.0',

// Testing - E2E
'playwright': '^1.49.1',
playwright: '^1.49.1',
'@playwright/test': '^1.49.1',
'cypress': '^15.0.0',
cypress: '^15.0.0',

// Formatting
'prettier': '^3.4.2',
prettier: '^3.4.2',

// Build & Dev
'tsx': '^4.19.2',
tsx: '^4.19.2',

// Type definitions
'@types/react': '^19.0.6',
Expand Down Expand Up @@ -125,9 +133,7 @@ export class DependencyResolver {
*/
private pinVersion(version: string): string {
// Check if version is in registry
const pkg = Object.keys(VERSION_REGISTRY).find(
(key) => VERSION_REGISTRY[key] === version
);
const pkg = Object.keys(VERSION_REGISTRY).find((key) => VERSION_REGISTRY[key] === version);
if (pkg) {
return VERSION_REGISTRY[pkg];
}
Expand Down
Loading