Files
shadcn-ui/.github/workflows/templates.yml
2026-05-29 11:15:40 +04:00

315 lines
10 KiB
YAML

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"