Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:48:52 +08:00
commit 6ec3196ecc
434 changed files with 125248 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
pytest>=7.0.0
pytest-cov>=4.0.0
pytest-mock>=3.10.0

View File

@@ -0,0 +1,319 @@
"""Tests for nextjs-init.py script."""
import json
import sys
from pathlib import Path
import pytest
# Add parent directory to path to import the script
sys.path.insert(0, str(Path(__file__).parent.parent))
from nextjs_init import NextJSInitializer
class TestNextJSInitializer:
"""Test suite for NextJSInitializer."""
def test_init_with_defaults(self, tmp_path):
"""Test initialization with default parameters."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app"
)
assert initializer.name == "test-app"
assert initializer.typescript is True
assert initializer.app_router is True
assert initializer.src_dir is False
assert initializer.tailwind is False
assert initializer.eslint is True
def test_validate_name_valid(self, tmp_path):
"""Test name validation with valid names."""
valid_names = ["my-app", "my_app", "myapp123", "test-app-1"]
for name in valid_names:
initializer = NextJSInitializer(
name=name,
directory=tmp_path / name
)
initializer.validate_name() # Should not raise
def test_validate_name_invalid(self, tmp_path):
"""Test name validation with invalid names."""
invalid_cases = [
("", ValueError, "empty"),
("123app", ValueError, "starts with number"),
("my app", ValueError, "contains space"),
("my@app", ValueError, "contains special char"),
]
for name, expected_error, reason in invalid_cases:
initializer = NextJSInitializer(
name=name,
directory=tmp_path / (name or "empty")
)
with pytest.raises(expected_error):
initializer.validate_name()
def test_check_directory_exists(self, tmp_path):
"""Test directory existence check."""
existing_dir = tmp_path / "existing"
existing_dir.mkdir()
initializer = NextJSInitializer(
name="test-app",
directory=existing_dir
)
with pytest.raises(FileExistsError):
initializer.check_directory()
def test_create_directory_structure_app_router(self, tmp_path):
"""Test directory structure creation with App Router."""
project_dir = tmp_path / "test-app"
initializer = NextJSInitializer(
name="test-app",
directory=project_dir,
app_router=True
)
initializer.create_directory_structure()
# Check directories
assert (project_dir / "app").exists()
assert (project_dir / "public").exists()
assert (project_dir / "components").exists()
assert (project_dir / "lib").exists()
# Check App Router files
assert (project_dir / "app" / "layout.tsx").exists()
assert (project_dir / "app" / "page.tsx").exists()
assert (project_dir / "app" / "globals.css").exists()
def test_create_directory_structure_with_src(self, tmp_path):
"""Test directory structure with src/ directory."""
project_dir = tmp_path / "test-app"
initializer = NextJSInitializer(
name="test-app",
directory=project_dir,
src_dir=True
)
initializer.create_directory_structure()
# Check src structure
assert (project_dir / "src" / "app").exists()
assert (project_dir / "src" / "components").exists()
assert (project_dir / "src" / "lib").exists()
def test_package_json_generation(self, tmp_path):
"""Test package.json generation."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app",
typescript=True,
tailwind=True,
eslint=True
)
package_json = initializer._get_package_json()
assert package_json["name"] == "test-app"
assert package_json["version"] == "0.1.0"
assert package_json["private"] is True
# Check scripts
assert "dev" in package_json["scripts"]
assert "build" in package_json["scripts"]
assert "start" in package_json["scripts"]
assert "lint" in package_json["scripts"]
# Check dependencies
assert "next" in package_json["dependencies"]
assert "react" in package_json["dependencies"]
assert "react-dom" in package_json["dependencies"]
# Check TypeScript dependencies
assert "typescript" in package_json["devDependencies"]
assert "@types/node" in package_json["devDependencies"]
assert "@types/react" in package_json["devDependencies"]
# Check Tailwind dependencies
assert "tailwindcss" in package_json["dependencies"]
# Check ESLint dependencies
assert "eslint" in package_json["devDependencies"]
def test_tsconfig_generation(self, tmp_path):
"""Test tsconfig.json generation."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app",
typescript=True,
import_alias="@/*"
)
tsconfig = initializer._get_tsconfig()
assert "compilerOptions" in tsconfig
assert tsconfig["compilerOptions"]["strict"] is True
assert tsconfig["compilerOptions"]["jsx"] == "preserve"
assert "@/*" in tsconfig["compilerOptions"]["paths"]
assert "next-env.d.ts" in tsconfig["include"]
def test_layout_content_typescript(self, tmp_path):
"""Test layout.tsx content generation."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app",
typescript=True
)
content = initializer._get_layout_content()
assert "import './globals.css'" in content
assert "export const metadata" in content
assert "children: React.ReactNode" in content
assert "<html lang=\"en\">" in content
def test_layout_content_javascript(self, tmp_path):
"""Test layout.jsx content generation."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app",
typescript=False
)
content = initializer._get_layout_content()
assert "import './globals.css'" in content
assert "export const metadata" in content
assert "React.ReactNode" not in content # No TypeScript types
def test_tailwind_config_typescript(self, tmp_path):
"""Test Tailwind config generation with TypeScript."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app",
typescript=True,
tailwind=True
)
config = initializer._get_tailwind_config()
assert "import type { Config }" in config
assert "const config: Config" in config
assert "content:" in config
def test_tailwind_config_javascript(self, tmp_path):
"""Test Tailwind config generation with JavaScript."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app",
typescript=False,
tailwind=True
)
config = initializer._get_tailwind_config()
assert "module.exports" in config
assert "content:" in config
def test_gitignore_generation(self, tmp_path):
"""Test .gitignore generation."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app"
)
gitignore = initializer._get_gitignore()
assert "/node_modules" in gitignore
assert "/.next/" in gitignore
assert ".env*.local" in gitignore
assert ".DS_Store" in gitignore
def test_readme_generation(self, tmp_path):
"""Test README.md generation."""
initializer = NextJSInitializer(
name="test-app",
directory=tmp_path / "test-app"
)
readme = initializer._get_readme()
assert "# test-app" in readme
assert "Next.js" in readme
assert "npm run dev" in readme
def test_create_config_files(self, tmp_path):
"""Test configuration files creation."""
project_dir = tmp_path / "test-app"
initializer = NextJSInitializer(
name="test-app",
directory=project_dir,
typescript=True,
tailwind=True,
eslint=True
)
initializer.create_directory_structure()
initializer.create_config_files()
# Check all config files exist
assert (project_dir / "package.json").exists()
assert (project_dir / "next.config.js").exists()
assert (project_dir / "tsconfig.json").exists()
assert (project_dir / ".eslintrc.json").exists()
assert (project_dir / "tailwind.config.ts").exists()
assert (project_dir / "postcss.config.js").exists()
assert (project_dir / ".gitignore").exists()
assert (project_dir / "README.md").exists()
# Verify package.json is valid JSON
with open(project_dir / "package.json") as f:
package_json = json.load(f)
assert package_json["name"] == "test-app"
def test_full_initialization(self, tmp_path):
"""Test full initialization process."""
project_dir = tmp_path / "test-app"
initializer = NextJSInitializer(
name="test-app",
directory=project_dir,
typescript=True,
app_router=True,
tailwind=True
)
initializer.initialize()
# Verify directory exists
assert project_dir.exists()
# Verify structure
assert (project_dir / "app").exists()
assert (project_dir / "public").exists()
# Verify config files
assert (project_dir / "package.json").exists()
assert (project_dir / "tsconfig.json").exists()
assert (project_dir / "next.config.js").exists()
def test_pages_router_structure(self, tmp_path):
"""Test Pages Router directory structure."""
project_dir = tmp_path / "test-app"
initializer = NextJSInitializer(
name="test-app",
directory=project_dir,
app_router=False # Use Pages Router
)
initializer.create_directory_structure()
# Check Pages Router files
assert (project_dir / "pages" / "_app.tsx").exists()
assert (project_dir / "pages" / "index.tsx").exists()

View File

@@ -0,0 +1,374 @@
"""Tests for turborepo-migrate.py script."""
import json
import sys
from pathlib import Path
import pytest
# Add parent directory to path to import the script
sys.path.insert(0, str(Path(__file__).parent.parent))
from turborepo_migrate import TurborepoMigrator
@pytest.fixture
def mock_monorepo(tmp_path):
"""Create a mock monorepo structure."""
# Root package.json
root_pkg = {
"name": "test-monorepo",
"private": True,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test --workspaces"
}
}
(tmp_path / "package.json").write_text(json.dumps(root_pkg, indent=2))
# Create apps
apps_dir = tmp_path / "apps"
apps_dir.mkdir()
web_dir = apps_dir / "web"
web_dir.mkdir()
(web_dir / "package.json").write_text(json.dumps({
"name": "web",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"@repo/ui": "*",
"next": "latest"
}
}, indent=2))
# Create Next.js output directory
(web_dir / ".next").mkdir()
# Create packages
packages_dir = tmp_path / "packages"
packages_dir.mkdir()
ui_dir = packages_dir / "ui"
ui_dir.mkdir()
(ui_dir / "package.json").write_text(json.dumps({
"name": "@repo/ui",
"version": "0.0.0",
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"react": "latest"
}
}, indent=2))
# Create dist directory
(ui_dir / "dist").mkdir()
return tmp_path
class TestTurborepoMigrator:
"""Test suite for TurborepoMigrator."""
def test_init(self, tmp_path):
"""Test migrator initialization."""
migrator = TurborepoMigrator(
path=tmp_path,
dry_run=True,
package_manager="npm"
)
assert migrator.path == tmp_path.resolve()
assert migrator.dry_run is True
assert migrator.package_manager == "npm"
def test_validate_path_exists(self, mock_monorepo):
"""Test path validation with valid monorepo."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.validate_path() # Should not raise
def test_validate_path_not_exists(self, tmp_path):
"""Test path validation with non-existent path."""
migrator = TurborepoMigrator(path=tmp_path / "nonexistent")
with pytest.raises(FileNotFoundError):
migrator.validate_path()
def test_validate_path_not_directory(self, tmp_path):
"""Test path validation with file instead of directory."""
file_path = tmp_path / "file.txt"
file_path.touch()
migrator = TurborepoMigrator(path=file_path)
with pytest.raises(NotADirectoryError):
migrator.validate_path()
def test_validate_path_no_package_json(self, tmp_path):
"""Test path validation without package.json."""
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
migrator = TurborepoMigrator(path=empty_dir)
with pytest.raises(FileNotFoundError):
migrator.validate_path()
def test_analyze_workspace_npm(self, mock_monorepo):
"""Test workspace analysis for npm/yarn workspaces."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
assert migrator.workspace_config["type"] == "npm/yarn"
assert "apps/*" in migrator.workspace_config["patterns"]
assert "packages/*" in migrator.workspace_config["patterns"]
def test_analyze_workspace_pnpm(self, tmp_path):
"""Test workspace analysis for pnpm workspaces."""
# Create root package.json without workspaces
(tmp_path / "package.json").write_text(json.dumps({
"name": "test-monorepo",
"private": True
}))
# Create pnpm-workspace.yaml
(tmp_path / "pnpm-workspace.yaml").write_text("""packages:
- 'apps/*'
- 'packages/*'
""")
migrator = TurborepoMigrator(path=tmp_path)
migrator.analyze_workspace()
assert migrator.workspace_config["type"] == "pnpm"
assert migrator.workspace_config["file"] == "pnpm-workspace.yaml"
def test_discover_packages(self, mock_monorepo):
"""Test package discovery."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
migrator.discover_packages()
assert len(migrator.packages) == 2
package_names = {pkg["name"] for pkg in migrator.packages}
assert "web" in package_names
assert "@repo/ui" in package_names
def test_analyze_scripts(self, mock_monorepo):
"""Test script analysis."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
migrator.discover_packages()
common_scripts = migrator.analyze_scripts()
# All packages have build, test, lint
assert "build" in common_scripts
assert "test" in common_scripts
assert "lint" in common_scripts
# Check package counts
assert len(common_scripts["build"]) == 2
assert len(common_scripts["test"]) == 2
def test_infer_build_outputs(self, mock_monorepo):
"""Test build output inference."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
migrator.discover_packages()
outputs = migrator._infer_build_outputs()
# Should detect .next and dist directories
assert ".next/**" in outputs
assert "!.next/cache/**" in outputs
assert "dist/**" in outputs
def test_generate_turbo_config(self, mock_monorepo):
"""Test turbo.json generation."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
migrator.discover_packages()
common_scripts = migrator.analyze_scripts()
turbo_config = migrator.generate_turbo_config(common_scripts)
assert "$schema" in turbo_config
assert "pipeline" in turbo_config
# Check build task
assert "build" in turbo_config["pipeline"]
assert turbo_config["pipeline"]["build"]["dependsOn"] == ["^build"]
assert "outputs" in turbo_config["pipeline"]["build"]
# Check test task
assert "test" in turbo_config["pipeline"]
assert "coverage/**" in turbo_config["pipeline"]["test"]["outputs"]
# Check lint task
assert "lint" in turbo_config["pipeline"]
# Note: dev task won't be in pipeline because it's only in 1 package
# (needs to be in 2+ packages to be considered "common")
# This is correct behavior - only truly common scripts are included
def test_update_root_package_json(self, mock_monorepo):
"""Test root package.json update."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
migrator.discover_packages()
updated_package_json = migrator.update_root_package_json()
# Check turbo added to devDependencies
assert "turbo" in updated_package_json["devDependencies"]
assert updated_package_json["devDependencies"]["turbo"] == "latest"
# Check scripts updated (only common scripts are added)
assert updated_package_json["scripts"]["build"] == "turbo run build"
assert updated_package_json["scripts"]["test"] == "turbo run test"
assert updated_package_json["scripts"]["lint"] == "turbo run lint"
# dev is only in one package, so it won't be added
def test_generate_migration_report(self, mock_monorepo):
"""Test migration report generation."""
migrator = TurborepoMigrator(path=mock_monorepo)
migrator.analyze_workspace()
migrator.discover_packages()
common_scripts = migrator.analyze_scripts()
turbo_config = migrator.generate_turbo_config(common_scripts)
updated_package_json = migrator.update_root_package_json()
report = migrator.generate_migration_report(turbo_config, updated_package_json)
assert "TURBOREPO MIGRATION REPORT" in report
assert "PACKAGES:" in report
assert "TURBO.JSON PIPELINE:" in report
assert "ROOT PACKAGE.JSON SCRIPTS:" in report
assert "RECOMMENDATIONS:" in report
# Check package names appear
assert "web" in report
assert "@repo/ui" in report
def test_write_files_dry_run(self, mock_monorepo, capsys):
"""Test file writing in dry-run mode."""
migrator = TurborepoMigrator(path=mock_monorepo, dry_run=True)
migrator.analyze_workspace()
migrator.discover_packages()
common_scripts = migrator.analyze_scripts()
turbo_config = migrator.generate_turbo_config(common_scripts)
updated_package_json = migrator.update_root_package_json()
migrator.write_files(turbo_config, updated_package_json)
# Check files not created
assert not (mock_monorepo / "turbo.json").exists()
# Check output
captured = capsys.readouterr()
assert "DRY RUN" in captured.out
def test_write_files_actual(self, mock_monorepo):
"""Test actual file writing."""
migrator = TurborepoMigrator(path=mock_monorepo, dry_run=False)
migrator.analyze_workspace()
migrator.discover_packages()
common_scripts = migrator.analyze_scripts()
turbo_config = migrator.generate_turbo_config(common_scripts)
updated_package_json = migrator.update_root_package_json()
migrator.write_files(turbo_config, updated_package_json)
# Check turbo.json created
assert (mock_monorepo / "turbo.json").exists()
# Verify content
with open(mock_monorepo / "turbo.json") as f:
saved_config = json.load(f)
assert saved_config["$schema"] == turbo_config["$schema"]
assert "pipeline" in saved_config
# Check package.json updated
with open(mock_monorepo / "package.json") as f:
saved_package = json.load(f)
assert "turbo" in saved_package["devDependencies"]
def test_full_migration_dry_run(self, mock_monorepo):
"""Test full migration process in dry-run mode."""
migrator = TurborepoMigrator(path=mock_monorepo, dry_run=True)
migrator.migrate()
# Files should not be created in dry-run
assert not (mock_monorepo / "turbo.json").exists()
# Original package.json should be unchanged
with open(mock_monorepo / "package.json") as f:
package_json = json.load(f)
assert "turbo" not in package_json.get("devDependencies", {})
def test_full_migration_actual(self, mock_monorepo):
"""Test full migration process."""
migrator = TurborepoMigrator(path=mock_monorepo, dry_run=False)
migrator.migrate()
# Check turbo.json created
assert (mock_monorepo / "turbo.json").exists()
with open(mock_monorepo / "turbo.json") as f:
turbo_config = json.load(f)
assert "$schema" in turbo_config
assert "pipeline" in turbo_config
assert "build" in turbo_config["pipeline"]
# Check package.json updated
with open(mock_monorepo / "package.json") as f:
package_json = json.load(f)
assert "turbo" in package_json["devDependencies"]
assert package_json["scripts"]["build"] == "turbo run build"
def test_parse_pnpm_workspace(self, tmp_path):
"""Test pnpm-workspace.yaml parsing."""
yaml_content = """packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'
"""
yaml_file = tmp_path / "pnpm-workspace.yaml"
yaml_file.write_text(yaml_content)
migrator = TurborepoMigrator(path=tmp_path)
patterns = migrator._parse_pnpm_workspace(yaml_file)
assert len(patterns) == 3
assert "apps/*" in patterns
assert "packages/*" in patterns
assert "tools/*" in patterns
def test_monorepo_without_workspaces(self, tmp_path):
"""Test migration fails for non-workspace monorepo."""
# Create package.json without workspaces
(tmp_path / "package.json").write_text(json.dumps({
"name": "not-a-monorepo",
"version": "1.0.0"
}))
migrator = TurborepoMigrator(path=tmp_path)
# migrate() calls sys.exit(1) on error, so we catch SystemExit
with pytest.raises(SystemExit):
migrator.migrate()