import { Rule, SchematicsException, Tree } from '@angular-devkit/schematics'; import { DeployPlatform, Schema } from './schema'; type PackageManager = 'npm' | 'pnpm' | 'yarn'; interface DeployFile { path: string; content: string; } interface ProjectInfo { name: string; outputPath: string; } export function deploy(options: Schema): Rule { return (tree: Tree) => { const workspace = readJson(tree, '/angular.json'); const project = resolveProjectInfo(workspace, options.project); const packageManager = detectPackageManager(tree); const files = buildDeployFiles(options.platform, project, packageManager); const existingFiles = files.map((file) => file.path).filter((path) => tree.exists(path)); if (existingFiles.length > 0 && !options.force) { throw new SchematicsException(`${existingFiles.join(', ')} already exist. Re-run with --force to overwrite them.`); } for (const file of files) { writeFile(tree, file.path, file.content); } }; } function buildDeployFiles(platform: DeployPlatform, project: ProjectInfo, packageManager: PackageManager): DeployFile[] { if (platform === 'vercel') { return [ { path: '/vercel.json', content: `${JSON.stringify( { buildCommand: getBuildCommand(packageManager), outputDirectory: project.outputPath, rewrites: [{ source: '/(.*)', destination: '/index.html' }], }, null, 2 )}\n`, }, ]; } if (platform === 'netlify') { return [ { path: '/netlify.toml', content: `[build] command = "${getBuildCommand(packageManager)}" publish = "${project.outputPath}" [[redirects]] from = "/*" to = "/index.html" status = 200 `, }, ]; } if (platform === 'firebase') { return [ { path: '/firebase.json', content: `${JSON.stringify( { hosting: { public: project.outputPath, ignore: ['firebase.json', '**/.*', '**/node_modules/**'], rewrites: [{ source: '**', destination: '/index.html' }], }, }, null, 2 )}\n`, }, { path: '/.firebaserc', content: `${JSON.stringify({ projects: { default: project.name } }, null, 2)}\n`, }, ]; } if (platform === 'github-pages') { return [ { path: '/.github/workflows/deploy.yml', content: buildGitHubPagesWorkflow(project.outputPath, packageManager), }, ]; } return [ { path: '/compose.yml', content: `services: web: build: . ports: - "8080:80" restart: unless-stopped `, }, { path: '/deploy/docker.README.md', content: `# Docker Deployment Build and run the generated Docker image: \`\`\`bash docker compose up --build \`\`\` The application will be available at http://localhost:8080. `, }, ]; } function buildGitHubPagesWorkflow(outputPath: string, packageManager: PackageManager): string { const setupPackageManagerStep = packageManager === 'pnpm' ? ` - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 ` : packageManager === 'yarn' ? ` - name: Enable Corepack run: corepack enable ` : ''; return `name: Deploy to GitHub Pages on: push: branches: [main] workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 ${setupPackageManagerStep} - name: Setup Node uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: ${getInstallCommand(packageManager)} - name: Build run: ${getBuildCommand(packageManager)} - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ${outputPath} deploy: environment: name: github-pages url: \${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy id: deployment uses: actions/deploy-pages@v4 `; } function resolveProjectInfo(workspace: Record, projectName?: string): ProjectInfo { const name = resolveProjectName(workspace, projectName); return { name, outputPath: resolveBuildOutputPath(workspace, name), }; } function resolveProjectName(workspace: Record, projectName?: string): string { if (projectName) { if (!workspace.projects?.[projectName]) { throw new SchematicsException(`Project "${projectName}" was not found in angular.json.`); } return projectName; } if (workspace.defaultProject && workspace.projects?.[workspace.defaultProject]) { return workspace.defaultProject as string; } const projects = Object.keys(workspace.projects ?? {}); if (projects.length === 0) { throw new SchematicsException('No Angular project was found in angular.json.'); } return projects[0]; } function resolveBuildOutputPath(workspace: Record, projectName: string): string { const project = workspace.projects?.[projectName] ?? {}; const buildTarget = project.architect?.build ?? project.targets?.build; const outputPath = buildTarget?.options?.outputPath; if (typeof outputPath === 'string') { return trimSlashes(outputPath); } if (outputPath?.base) { return trimSlashes([outputPath.base, outputPath.browser].filter(Boolean).join('/')); } return `dist/${projectName}/browser`; } function detectPackageManager(tree: Tree): PackageManager { if (tree.exists('/pnpm-lock.yaml')) { return 'pnpm'; } if (tree.exists('/yarn.lock')) { return 'yarn'; } return 'npm'; } function getInstallCommand(packageManager: PackageManager): string { if (packageManager === 'pnpm') { return 'pnpm install --frozen-lockfile'; } if (packageManager === 'yarn') { return 'yarn install --frozen-lockfile'; } return 'npm ci'; } function getBuildCommand(packageManager: PackageManager): string { if (packageManager === 'pnpm') { return 'pnpm run build'; } if (packageManager === 'yarn') { return 'yarn build'; } return 'npm run build'; } function writeFile(tree: Tree, path: string, content: string): void { if (tree.exists(path)) { tree.overwrite(path, content); } else { tree.create(path, content); } } function trimSlashes(value: string): string { return value.replace(/^\/+|\/+$/g, ''); } function readJson(tree: Tree, path: string): Record { const content = tree.readText(path); return JSON.parse(content) as Record; }