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
32 changes: 27 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
on:
push:
tags:
- 'v*.*.*' # Matches v1.0.0, v1.2.3, etc.
- 'v*.*.*-beta.*' # Matches v1.0.0-beta.1, etc.
- 'v*.*.*' # Matches v1.0.0, v1.2.3, etc.
- 'v*.*.*-beta.*' # Matches v1.0.0-beta.1, etc.

permissions:
contents: write
Expand Down Expand Up @@ -40,27 +40,49 @@

- name: Extract version from tag
id: get_version
run: |

Check warning on line 43 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Lint GitHub Workflows

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:2:40: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/release.yml:43:9: shellcheck reported issue in this script: SC2086:info:2:40: Double quote to prevent globbing and word splitting [shellcheck]

Check warning on line 43 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Lint GitHub Workflows

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:1:45: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/release.yml:43:9: shellcheck reported issue in this script: SC2086:info:1:45: Double quote to prevent globbing and word splitting [shellcheck]
echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

- name: Update package.json version
run: npm version ${{ steps.get_version.outputs.VERSION }} --no-git-tag-version --allow-same-version

- name: Publish to NPM
- name: Pack release artifact
id: pack
run: |
ASSET="$(npm pack --silent | tail -n1)"
echo "asset=${ASSET}" >> "$GITHUB_OUTPUT"

- name: Install Cosign
uses: sigstore/cosign-installer@v3

- name: Sign release artifact
run: |
cosign sign-blob --yes \
--output-signature "${{ steps.pack.outputs.asset }}.sig" \
--output-certificate "${{ steps.pack.outputs.asset }}.pem" \
--bundle "${{ steps.pack.outputs.asset }}.sigstore.json" \
"${{ steps.pack.outputs.asset }}"

- name: Publish to NPM (with provenance)
run: |

Check warning on line 68 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Lint GitHub Workflows

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2193:warning:1:50: The arguments to this comparison can never be equal. Make sure your syntax is correct [shellcheck] Raw Output: w:.github/workflows/release.yml:68:9: shellcheck reported issue in this script: SC2193:warning:1:50: The arguments to this comparison can never be equal. Make sure your syntax is correct [shellcheck]
if [[ "${{ steps.get_version.outputs.VERSION }}" == *"beta"* ]]; then
npm publish --tag beta
npm publish "${{ steps.pack.outputs.asset }}" --provenance --tag beta
else
npm publish --tag latest
npm publish "${{ steps.pack.outputs.asset }}" --provenance --tag latest
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v1

Check failure on line 78 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / Lint GitHub Workflows

[actionlint] reported by reviewdog 🐶 the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue [action] Raw Output: e:.github/workflows/release.yml:78:15: the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue [action]
with:
generate_release_notes: true
prerelease: ${{ contains(steps.get_version.outputs.VERSION, 'beta') }}
files: |
${{ steps.pack.outputs.asset }}
${{ steps.pack.outputs.asset }}.sig
${{ steps.pack.outputs.asset }}.pem
${{ steps.pack.outputs.asset }}.sigstore.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"execa": "^9.5.2",
"fs-extra": "^11.2.0",
"ora": "^8.1.1",
"typescript": "^5.7.2",
"zod": "^3.24.1"
},
"devDependencies": {
Expand All @@ -79,7 +80,6 @@
"lint-staged": "^16.2.7",
"prettier": "^3.4.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.54.0",
"vitest": "^4.0.18"
},
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/architecture-generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { generateArchitectureDoc } from '../docs/architecture-generator';
import { DEFAULT_CONFIG, type ProjectConfig } from '../config/schema';

function createConfig(overrides: Partial<ProjectConfig> = {}): ProjectConfig {
return {
...DEFAULT_CONFIG,
name: 'test-app',
path: './test-app',
...overrides,
};
}

describe('generateArchitectureDoc', () => {
it('should include TypeScript language and types folder for TypeScript projects', () => {
const doc = generateArchitectureDoc(createConfig({ language: 'typescript' }));

expect(doc).toContain('- **Language**: TypeScript');
expect(doc).toContain('├── types/ # TypeScript type definitions');
});

it('should include JavaScript language and omit types folder for JavaScript projects', () => {
const doc = generateArchitectureDoc(createConfig({ language: 'javascript' }));

expect(doc).toContain('- **Language**: JavaScript');
expect(doc).not.toContain('├── types/ # TypeScript type definitions');
});

it('should include state management folder when state management is enabled', () => {
const doc = generateArchitectureDoc(createConfig({ stateManagement: 'zustand' }));

expect(doc).toContain('├── stores/ # State management stores');
});

it('should include lib folder when data fetching is enabled', () => {
const doc = generateArchitectureDoc(
createConfig({ dataFetching: { enabled: true, library: 'tanstack-query' } })
);

expect(doc).toContain('├── lib/ # Third-party library configs');
});
});
13 changes: 13 additions & 0 deletions src/__tests__/cli-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ describe('CLI Index', () => {
expect(config.testing.e2e.runner).toBe('cypress');
});

it('should disable E2E when unit-component testing is selected', () => {
const config = promptAnswersToConfig(
createAnswers({
testing: 'unit-component',
e2eRunner: 'playwright',
})
);

expect(config.testing.enabled).toBe(true);
expect(config.testing.e2e.enabled).toBe(false);
expect(config.testing.e2e.runner).toBe('none');
});

it('should validate project name', () => {
expect(validateProjectName('')).toEqual({
valid: false,
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/cli-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('CLI parser', () => {
const stylingOption = createCmd?.options.find((o) => o.long === '--styling');
const stateOption = createCmd?.options.find((o) => o.long === '--state');
const runtimeOption = createCmd?.options.find((o) => o.long === '--runtime');
const languageOption = createCmd?.options.find((o) => o.long === '--language');
const unitRunnerOption = createCmd?.options.find((o) => o.long === '--unit-runner');

expect(runtimeOption?.argChoices).toEqual(['vite', 'nextjs']);
Expand All @@ -31,6 +32,7 @@ describe('CLI parser', () => {
'css',
]);
expect(stateOption?.argChoices).toEqual(['none', 'redux', 'zustand', 'jotai']);
expect(languageOption?.argChoices).toEqual(['javascript', 'typescript']);
expect(unitRunnerOption?.argChoices).toEqual(['vitest', 'jest']);
});

Expand Down
16 changes: 16 additions & 0 deletions src/__tests__/config-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,20 @@ describe('ConfigBuilder', () => {

expect(validation.success).toBe(true);
});

it('should not mutate DEFAULT_CONFIG when toggling testing', () => {
const defaultTesting = structuredClone(DEFAULT_CONFIG.testing);

new ConfigBuilder().setTestingEnabled(false).build();

expect(DEFAULT_CONFIG.testing).toEqual(defaultTesting);
});

it('should not leak state across builder instances', () => {
const disabled = new ConfigBuilder().setTestingEnabled(false).build();
const fresh = new ConfigBuilder().build();

expect(disabled.testing.enabled).toBe(false);
expect(fresh.testing.enabled).toBe(true);
});
});
53 changes: 52 additions & 1 deletion src/__tests__/integration/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,58 @@ describe('ProjectGenerator Integration', () => {
expect(result.errors).toHaveLength(0);
expect(existsSync(join(config.path, 'package.json'))).toBe(true);
});

it('should write .gitignore before git initialization', async () => {
const config = createBaseConfig({
name: 'gitignore-write',
git: { init: true, initialCommit: false },
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
const result = await generator.generate();

expect(result.success).toBe(true);
expect(existsSync(join(config.path, '.gitignore'))).toBe(true);
});

it('should generate JavaScript projects without TypeScript-only files', async () => {
const config = createBaseConfig({
name: 'javascript-supported',
language: 'javascript',
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
const result = await generator.generate();
const pkg = readGeneratedPackageJson(config.path);
const devDeps = pkg.devDependencies as Record<string, string>;
const scripts = pkg.scripts as Record<string, string>;

expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
expect(existsSync(join(config.path, 'src/main.jsx'))).toBe(true);
expect(existsSync(join(config.path, 'tsconfig.json'))).toBe(false);
expect(devDeps).not.toHaveProperty('typescript');
expect(scripts.build).not.toContain('tsc -b');
});

it('should generate Next.js JavaScript projects with JavaScript entry files', async () => {
const config = createBaseConfig({
name: 'javascript-nextjs',
runtime: 'nextjs',
language: 'javascript',
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
const result = await generator.generate();

expect(result.success).toBe(true);
expect(existsSync(join(config.path, 'src/app/page.jsx'))).toBe(true);
expect(existsSync(join(config.path, 'next-env.d.ts'))).toBe(false);
expect(existsSync(join(config.path, 'tsconfig.json'))).toBe(false);
});
});

describe('Package.json Validation', () => {
Expand Down Expand Up @@ -380,4 +432,3 @@ describe('ProjectGenerator Integration', () => {
});
});
});

40 changes: 39 additions & 1 deletion src/__tests__/integration/package-json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,29 @@ describe('Package.json Generation', () => {

expect(devDeps).toHaveProperty('typescript');
});

it('should exclude TypeScript-only dependencies for javascript projects', async () => {
const config = createConfig('javascript-deps', {
language: 'javascript',
testing: {
enabled: true,
unit: { enabled: true, runner: 'jest' },
component: { enabled: true, library: 'testing-library' },
e2e: { enabled: false, runner: 'none' },
},
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
await generator.generate();

const pkg = readGeneratedPackageJson(config.path);
const devDeps = pkg.devDependencies as Record<string, string>;

expect(devDeps).not.toHaveProperty('typescript');
expect(devDeps).not.toHaveProperty('ts-jest');
expect(Object.keys(devDeps).some((dep) => dep.startsWith('@types/'))).toBe(false);
});
});

describe('Scripts', () => {
Expand All @@ -381,6 +404,22 @@ describe('Package.json Generation', () => {
expect(scripts.build).toContain('vite');
});

it('should not include TypeScript build step for javascript Vite projects', async () => {
const config = createConfig('vite-js-scripts', {
runtime: 'vite',
language: 'javascript',
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
await generator.generate();

const pkg = readGeneratedPackageJson(config.path);
const scripts = pkg.scripts as Record<string, string>;

expect(scripts.build).toBe('vite build');
});

it('should have dev script for Next.js', async () => {
const config = createConfig('nextjs-scripts', { runtime: 'nextjs' });
projectPaths.push(config.path);
Expand Down Expand Up @@ -468,4 +507,3 @@ describe('Package.json Generation', () => {
});
});
});

33 changes: 32 additions & 1 deletion src/__tests__/integration/scenarios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,38 @@ describe('Real-World Scenarios', () => {
});
});

describe('JavaScript Configuration', () => {
it('should not include tsconfig files for JavaScript projects', async () => {
const config = createConfig('js-config', {
language: 'javascript',
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
await generator.generate();

expect(existsSync(join(config.path, 'tsconfig.json'))).toBe(false);
expect(existsSync(join(config.path, 'tsconfig.node.json'))).toBe(false);
expect(existsSync(join(config.path, 'src/main.jsx'))).toBe(true);
});

it('should not include TypeScript as devDependency in JavaScript projects', async () => {
const config = createConfig('js-dep', {
language: 'javascript',
});
projectPaths.push(config.path);

const generator = new ProjectGenerator(config);
await generator.generate();

const pkg = readGeneratedPackageJson(config.path);
const devDeps = pkg.devDependencies as Record<string, string>;

expect(devDeps).not.toHaveProperty('typescript');
expect(Object.keys(devDeps).some((dep) => dep.startsWith('@types/'))).toBe(false);
});
});

describe('No Conflicts Between Templates', () => {
it('should not have conflicting scripts when multiple templates loaded', async () => {
const config = createConfig('no-conflicts', {
Expand Down Expand Up @@ -522,4 +554,3 @@ describe('Real-World Scenarios', () => {
});
});
});

2 changes: 1 addition & 1 deletion src/__tests__/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('CLI Prompts', () => {
stateManagement: 'none',
testing: 'none',
unitRunner: 'vitest',
e2eRunner: 'playwright',
e2eRunner: 'none',
dataFetching: false,
packageManager: 'npm',
git: true,
Expand Down
Loading
Loading