name: Templates on: pull_request: branches: ["*"] paths: - ".github/workflows/templates.yml" - "apps/v4/registry/**" - "package.json" - "packages/shadcn/src/commands/add.ts" - "packages/shadcn/src/commands/init.ts" - "packages/shadcn/src/templates/**" - "packages/shadcn/src/utils/create-project.ts" - "packages/shadcn/src/utils/get-monorepo-info.ts" - "packages/shadcn/src/utils/get-package-manager.ts" - "pnpm-lock.yaml" - "templates/**" jobs: validate: runs-on: ubuntu-latest name: ${{ matrix.package-manager == 'pnpm' && format('pnpm {0}', matrix.pnpm-version) || matrix.package-manager }} ${{ matrix.template }} permissions: contents: read timeout-minutes: 45 strategy: fail-fast: false matrix: template: [next, vite, astro, start, react-router] package-manager: [pnpm, bun, npm, yarn] pnpm-version: [10.33.4, 11] exclude: - package-manager: bun pnpm-version: 11 - package-manager: npm pnpm-version: 11 - package-manager: yarn pnpm-version: 11 env: NEXT_PUBLIC_APP_URL: http://localhost:4000 NEXT_PUBLIC_V0_URL: https://v0.dev REGISTRY_URL: http://localhost:4000/r ROOT_PNPM_VERSION: 10.33.4 TEMPLATE_PNPM_VERSION: ${{ matrix.pnpm-version }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v4 name: Install pnpm id: pnpm-install with: version: ${{ env.ROOT_PNPM_VERSION }} run_install: false - name: Install Bun uses: oven-sh/setup-bun@v2 - name: Install Yarn if: matrix.package-manager == 'yarn' run: | corepack enable COREPACK_ENABLE_PROJECT_SPEC=0 corepack prepare yarn@4.12.0 --activate - name: Get pnpm store directory id: pnpm-cache run: | echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Build packages run: | pnpm --filter=shadcn build pnpm --filter=v4 registry:build - name: Validate templates env: TEMPLATE: ${{ matrix.template }} TEMPLATE_PACKAGE_MANAGER: ${{ matrix.package-manager }} SHADCN_TEMPLATE_DIR: ${{ github.workspace }}/templates run: | set -euo pipefail root_pnpm="$(command -v pnpm)" validation_script="$RUNNER_TEMP/validate-templates.sh" cat > "$validation_script" <<'BASH' set -euo pipefail bin_dir="$RUNNER_TEMP/template-pnpm-bin" mkdir -p "$bin_dir" cat > "$bin_dir/pnpm" <<'PNPM' #!/usr/bin/env bash exec npx -y "pnpm@${TEMPLATE_PNPM_VERSION}" "$@" PNPM chmod +x "$bin_dir/pnpm" export PATH="$bin_dir:$PATH" echo "Using template pnpm $(pnpm --version)" cli="$GITHUB_WORKSPACE/packages/shadcn/dist/index.js" template_root="$RUNNER_TEMP/generated-template-${TEMPLATE_PACKAGE_MANAGER}-${TEMPLATE}" rm -rf "$template_root" mkdir -p "$template_root" modes=(app monorepo) has_script() { node -e "const pkg = require('./package.json'); process.exit(pkg.scripts && pkg.scripts[process.argv[1]] ? 0 : 1)" "$1" } run_script_if_present() { local script="$1" if has_script "$script"; then pnpm run "$script" else echo "No $script script found; skipping." fi } validate_non_pnpm_project() { local package_manager="$1" local project_path="$2" local check_workspace_protocol="$3" local is_monorepo="$4" cd "$project_path" test ! -f pnpm-workspace.yaml test ! -f pnpm-lock.yaml EXPECTED_PACKAGE_MANAGER="$package_manager" \ CHECK_WORKSPACE_PROTOCOL="$check_workspace_protocol" \ IS_MONOREPO="$is_monorepo" \ node <<'NODE' const fs = require("node:fs") const path = require("node:path") const expectedPackageManager = process.env.EXPECTED_PACKAGE_MANAGER const checkWorkspaceProtocol = process.env.CHECK_WORKSPACE_PROTOCOL === "true" const isMonorepo = process.env.IS_MONOREPO === "true" const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")) if (isMonorepo) { const workspaces = pkg.workspaces ?? [] if (!Array.isArray(workspaces)) { throw new Error("Expected package.json workspaces to be an array.") } if (workspaces.length === 0) { throw new Error("Expected package.json workspaces to have entries.") } for (const workspace of ["sharp", "unrs-resolver", "esbuild"]) { if (workspaces.includes(workspace)) { throw new Error(`Unexpected workspace entry: ${workspace}`) } } if (!pkg.packageManager?.startsWith(`${expectedPackageManager}@`)) { throw new Error( `Expected packageManager to use ${expectedPackageManager}, got ${pkg.packageManager}` ) } } else { if (pkg.workspaces !== undefined) { throw new Error("Did not expect package.json workspaces for app template.") } if (pkg.packageManager !== undefined) { throw new Error( `Did not expect packageManager for app template, got ${pkg.packageManager}` ) } } if (checkWorkspaceProtocol) { const packageJsonFiles = [] function collectPackageJsonFiles(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === "node_modules") { continue } const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { collectPackageJsonFiles(fullPath) } else if (entry.name === "package.json") { packageJsonFiles.push(fullPath) } } } collectPackageJsonFiles(process.cwd()) for (const file of packageJsonFiles) { const json = fs.readFileSync(file, "utf8") if (json.includes("workspace:")) { throw new Error(`Unexpected workspace: protocol in ${file}`) } } } NODE } for mode in "${modes[@]}"; do project="test-${TEMPLATE}-${mode}-${TEMPLATE_PACKAGE_MANAGER}" project_path="$template_root/$project" echo "::group::${TEMPLATE} ${mode} ${TEMPLATE_PACKAGE_MANAGER}" args=( init --defaults --name "$project" --template "$TEMPLATE" --cwd "$template_root" --silent ) if [ "$mode" = "monorepo" ]; then args+=(--monorepo) is_monorepo="true" else args+=(--no-monorepo) is_monorepo="false" fi case "$TEMPLATE_PACKAGE_MANAGER" in pnpm) SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \ REGISTRY_URL="$REGISTRY_URL" \ npm_config_user_agent="pnpm/${TEMPLATE_PNPM_VERSION}" \ node "$cli" "${args[@]}" cd "$project_path" pnpm install --frozen-lockfile run_script_if_present typecheck run_script_if_present build ;; bun) ( cd "$template_root" SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \ REGISTRY_URL="$REGISTRY_URL" \ npm_config_user_agent="bun/$(bun --version)" \ bunx --bun --package "$GITHUB_WORKSPACE/packages/shadcn" \ shadcn "${args[@]}" ) validate_non_pnpm_project \ "bun" \ "$project_path" \ "false" \ "$is_monorepo" ;; npm) ( cd "$template_root" SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \ REGISTRY_URL="$REGISTRY_URL" \ npm_config_user_agent="npm/$(npm --version)" \ npx --yes --package "$GITHUB_WORKSPACE/packages/shadcn" \ shadcn "${args[@]}" ) validate_non_pnpm_project \ "npm" \ "$project_path" \ "true" \ "$is_monorepo" ;; yarn) ( cd "$template_root" SHADCN_TEMPLATE_DIR="$SHADCN_TEMPLATE_DIR" \ REGISTRY_URL="$REGISTRY_URL" \ COREPACK_ENABLE_PROJECT_SPEC=0 \ npm_config_user_agent="yarn/$(COREPACK_ENABLE_PROJECT_SPEC=0 yarn --version)" \ yarn dlx --package "$GITHUB_WORKSPACE/packages/shadcn" \ shadcn "${args[@]}" ) validate_non_pnpm_project \ "yarn" \ "$project_path" \ "false" \ "$is_monorepo" ;; esac echo "::endgroup::" done BASH "$root_pnpm" exec start-server-and-test \ "$root_pnpm v4:dev" \ http://localhost:4000 \ "bash $validation_script"