mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
769 lines
25 KiB
Bash
Executable File
769 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Deploy script for the Next.js deploy test harness.
|
|
# Called by the Next.js test runner (run-tests.js) for each isolated test app.
|
|
#
|
|
# Contract (per https://nextjs.org/docs/app/api-reference/adapters/testing-adapters):
|
|
# - cwd is the isolated test app directory
|
|
# - Must print the deployment URL to stdout (nothing else on stdout)
|
|
# - Must exit non-zero on failure
|
|
# - Diagnostic output goes to stderr or files in the working directory
|
|
#
|
|
# This script injects vinext as a local file dependency into the test app,
|
|
# builds with `vinext build`, starts with `vinext start`, and prints the URL.
|
|
set -euo pipefail
|
|
|
|
# Accept ADAPTER_DIR (Next.js docs convention) or VINEXT_DIR
|
|
VINEXT_DIR="${VINEXT_DIR:-${ADAPTER_DIR:-}}"
|
|
if [ -z "${VINEXT_DIR}" ]; then
|
|
echo "Either VINEXT_DIR or ADAPTER_DIR must be set" >&2
|
|
exit 1
|
|
fi
|
|
VINEXT_DIR="$(cd "${VINEXT_DIR}" && pwd)"
|
|
VINEXT_PKG_DIR="${VINEXT_PKG_DIR:-${VINEXT_DIR}/packages/vinext}"
|
|
VINEXT_PKG_DIR="$(cd "${VINEXT_PKG_DIR}" && pwd)"
|
|
|
|
BUILD_LOG=".vinext-deploy-build.log"
|
|
SERVER_LOG=".vinext-deploy-server.log"
|
|
PID_FILE=".vinext-deploy-server.pid"
|
|
PORT_FILE=".vinext-deploy-server.port"
|
|
DEBUG_ROOT_DIR="${VINEXT_DEPLOY_DEBUG_DIR:-${VINEXT_DIR}/reports/nextjs-deploy-debug}"
|
|
DEBUG_RUN_DIR="${DEBUG_ROOT_DIR}/$(date +%s)-$$"
|
|
|
|
DEPLOYMENT_READY=0
|
|
|
|
persist_debug_artifacts() {
|
|
mkdir -p "${DEBUG_RUN_DIR}"
|
|
|
|
if [ -f "package.json" ]; then
|
|
cp "package.json" "${DEBUG_RUN_DIR}/package.json"
|
|
fi
|
|
|
|
if [ -f "${BUILD_LOG}" ]; then
|
|
cp "${BUILD_LOG}" "${DEBUG_RUN_DIR}/${BUILD_LOG}"
|
|
fi
|
|
|
|
if [ -f "${SERVER_LOG}" ]; then
|
|
cp "${SERVER_LOG}" "${DEBUG_RUN_DIR}/${SERVER_LOG}"
|
|
fi
|
|
|
|
if [ -f "dist/server/entry.js" ]; then
|
|
mkdir -p "${DEBUG_RUN_DIR}/dist/server"
|
|
cp "dist/server/entry.js" "${DEBUG_RUN_DIR}/dist/server/entry.js"
|
|
fi
|
|
|
|
if [ -f "dist/server/index.mjs" ]; then
|
|
mkdir -p "${DEBUG_RUN_DIR}/dist/server"
|
|
cp "dist/server/index.mjs" "${DEBUG_RUN_DIR}/dist/server/index.mjs"
|
|
fi
|
|
|
|
{
|
|
echo "cwd: $(pwd)"
|
|
echo "next_test_dir: ${NEXT_TEST_DIR:-unknown}"
|
|
echo "deploy_url: ${DEPLOYMENT_URL:-unknown}"
|
|
if [ -d "dist" ]; then
|
|
echo "--- dist files ---"
|
|
find "dist" -maxdepth 4 -type f | sort
|
|
fi
|
|
} > "${DEBUG_RUN_DIR}/context.txt"
|
|
}
|
|
|
|
run_pnpm() {
|
|
if command -v pnpm >/dev/null 2>&1; then
|
|
pnpm "$@"
|
|
return
|
|
fi
|
|
|
|
if command -v corepack >/dev/null 2>&1; then
|
|
corepack pnpm "$@"
|
|
return
|
|
fi
|
|
|
|
# Vite+ (vp) environments expose vp instead of pnpm. The subcommand
|
|
# names (install, run, exec) are the same, but pnpm-specific flags
|
|
# must be forwarded as pass-through args.
|
|
if command -v vp >/dev/null 2>&1; then
|
|
local subcmd="$1"
|
|
shift
|
|
|
|
if [ "${subcmd}" = "install" ]; then
|
|
local vp_args=()
|
|
local passthrough_args=()
|
|
|
|
for arg in "$@"; do
|
|
case "${arg}" in
|
|
--no-frozen-lockfile) vp_args+=("${arg}") ;;
|
|
--strict-peer-dependencies=*) passthrough_args+=("${arg}") ;;
|
|
*) vp_args+=("${arg}") ;;
|
|
esac
|
|
done
|
|
|
|
if [ ${#passthrough_args[@]} -gt 0 ]; then
|
|
vp install "${vp_args[@]}" -- "${passthrough_args[@]}"
|
|
else
|
|
vp install "${vp_args[@]}"
|
|
fi
|
|
else
|
|
vp "${subcmd}" "$@"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
echo "No package manager found (tried pnpm, corepack, vp)" >&2
|
|
exit 1
|
|
}
|
|
|
|
find_free_port() {
|
|
node <<'EOF'
|
|
const net = require('node:net')
|
|
|
|
const server = net.createServer()
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const address = server.address()
|
|
if (!address || typeof address !== 'object') {
|
|
console.error('Failed to allocate a free port')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(address.port)
|
|
server.close()
|
|
})
|
|
EOF
|
|
}
|
|
|
|
wait_for_http() {
|
|
local url="$1"
|
|
local attempts="${2:-120}"
|
|
|
|
for _ in $(seq 1 "${attempts}"); do
|
|
local status
|
|
status="$(curl -s -o /dev/null -w '%{http_code}' "${url}" || true)"
|
|
if [ -n "${status}" ] && [ "${status}" != "000" ]; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
ensure_python_command_for_native_builds() {
|
|
if command -v python >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
local python3_bin
|
|
python3_bin="$(command -v python3 || true)"
|
|
if [ -z "${python3_bin}" ]; then
|
|
return
|
|
fi
|
|
|
|
local shim_dir=".vinext-native-build-bin"
|
|
mkdir -p "${shim_dir}"
|
|
ln -sf "${python3_bin}" "${shim_dir}/python"
|
|
export PATH="$(pwd)/${shim_dir}:${PATH}"
|
|
echo "Added python -> ${python3_bin} shim for native addon builds" >> "${BUILD_LOG}"
|
|
}
|
|
|
|
detach_symlinked_node_modules() {
|
|
if [ ! -L "node_modules" ]; then
|
|
return
|
|
fi
|
|
|
|
local target
|
|
target="$(readlink "node_modules" || true)"
|
|
rm -f "node_modules"
|
|
echo "Detached symlinked node_modules${target:+ -> ${target}} before install; pnpm will create a local modules directory." >> "${BUILD_LOG}"
|
|
}
|
|
|
|
read_build_id() {
|
|
if [ -f "dist/server/BUILD_ID" ]; then
|
|
cat "dist/server/BUILD_ID"
|
|
return 0
|
|
fi
|
|
|
|
node <<'EOF'
|
|
const fs = require('node:fs')
|
|
|
|
const bundlePath = [
|
|
'dist/server/index.mjs',
|
|
'dist/server/index.js',
|
|
'dist/server/entry.mjs',
|
|
'dist/server/entry.js',
|
|
].find((candidate) => fs.existsSync(candidate))
|
|
if (!bundlePath) {
|
|
console.error('Missing dist/server/index.{js,mjs} and dist/server/entry.{js,mjs}')
|
|
process.exit(1)
|
|
}
|
|
|
|
const code = fs.readFileSync(bundlePath, 'utf8')
|
|
const match =
|
|
code.match(/get buildId\(\)\s*\{\s*return "([^"]+)"/) ||
|
|
code.match(/\bbuildId\s*=\s*"([^"]+)"/)
|
|
if (!match) {
|
|
console.error(`Failed to extract build ID from ${bundlePath}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(match[1])
|
|
EOF
|
|
}
|
|
|
|
cleanup_on_error() {
|
|
if [ "${DEPLOYMENT_READY}" = "1" ]; then
|
|
return
|
|
fi
|
|
|
|
persist_debug_artifacts
|
|
|
|
if [ -f "${PID_FILE}" ]; then
|
|
local pid
|
|
pid="$(cat "${PID_FILE}")"
|
|
kill -TERM "${pid}" >/dev/null 2>&1 || true
|
|
sleep 1
|
|
kill -KILL "${pid}" >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
# Kill any process still listening on the allocated port (handles orphaned children).
|
|
# Read from PORT_FILE rather than $PORT since the variable may not be set if
|
|
# the script failed before port allocation.
|
|
if [ -f "${PORT_FILE}" ]; then
|
|
local cleanup_port
|
|
cleanup_port="$(cat "${PORT_FILE}")"
|
|
local listener_pid
|
|
listener_pid="$(lsof -ti "tcp:${cleanup_port}" -sTCP:LISTEN 2>/dev/null || true)"
|
|
if [ -n "${listener_pid}" ]; then
|
|
kill -TERM ${listener_pid} >/dev/null 2>&1 || true
|
|
sleep 1
|
|
kill -KILL ${listener_pid} >/dev/null 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
{
|
|
echo
|
|
echo "=== vinext e2e deploy debug ==="
|
|
if [ -f "${BUILD_LOG}" ]; then
|
|
local build_log_tail
|
|
build_log_tail="$(tail -80 "${BUILD_LOG}" 2>/dev/null || true)"
|
|
echo "--- last 80 lines of ${BUILD_LOG} ---"
|
|
printf '%s\n' "${build_log_tail}"
|
|
echo "--- end ${BUILD_LOG} (persisted to ${DEBUG_RUN_DIR}/${BUILD_LOG}) ---"
|
|
fi
|
|
if [ -f "${SERVER_LOG}" ]; then
|
|
local server_log_tail
|
|
server_log_tail="$(tail -40 "${SERVER_LOG}" 2>/dev/null || true)"
|
|
echo "--- last 40 lines of ${SERVER_LOG} ---"
|
|
printf '%s\n' "${server_log_tail}"
|
|
echo "--- end ${SERVER_LOG} (persisted to ${DEBUG_RUN_DIR}/${SERVER_LOG}) ---"
|
|
fi
|
|
echo "=== end vinext e2e deploy debug ==="
|
|
echo
|
|
} >&2
|
|
}
|
|
|
|
trap cleanup_on_error EXIT
|
|
|
|
# Some Next.js tests check for .next/trace existence (telemetry trace file)
|
|
# during the test harness's destroy() cleanup. vinext doesn't produce one, so
|
|
# create an empty file to satisfy those checks. Do this BEFORE any step that
|
|
# can fail (dep install, vinext init, vinext build) so the file exists even
|
|
# on failure — otherwise the test harness logs ENOENT noise (303 lines per
|
|
# deploy-suite run before this fix).
|
|
mkdir -p ".next"
|
|
: > ".next/trace"
|
|
|
|
if [ ! -f "${VINEXT_PKG_DIR}/dist/cli.js" ]; then
|
|
echo "vinext dist/cli.js not found at ${VINEXT_PKG_DIR}/dist/cli.js" >&2
|
|
echo "Build vinext first: corepack pnpm build" >&2
|
|
exit 1
|
|
fi
|
|
|
|
PORT="$(find_free_port)"
|
|
DEPLOYMENT_URL="http://127.0.0.1:${PORT}"
|
|
DEPLOYMENT_ID="${NEXT_DEPLOYMENT_ID:-vinext-local-${PORT}}"
|
|
|
|
{
|
|
echo "vinext dir: ${VINEXT_DIR}"
|
|
echo "vinext package dir: ${VINEXT_PKG_DIR}"
|
|
echo "deploy url: ${DEPLOYMENT_URL}"
|
|
echo "deployment id: ${DEPLOYMENT_ID}"
|
|
echo "next test dir: ${NEXT_TEST_DIR:-unknown}"
|
|
} > "${BUILD_LOG}"
|
|
|
|
node <<'EOF' >> "${BUILD_LOG}" 2>&1
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const vinextDir = process.env.VINEXT_DIR
|
|
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
const rootPkg = JSON.parse(fs.readFileSync(path.join(vinextDir, 'package.json'), 'utf8'))
|
|
const vinextPkg = JSON.parse(
|
|
fs.readFileSync(path.join(vinextDir, 'packages', 'vinext', 'package.json'), 'utf8'),
|
|
)
|
|
const cloudflarePkg = JSON.parse(
|
|
fs.readFileSync(path.join(vinextDir, 'packages', 'cloudflare', 'package.json'), 'utf8'),
|
|
)
|
|
const typesPkg = JSON.parse(
|
|
fs.readFileSync(path.join(vinextDir, 'packages', 'types', 'package.json'), 'utf8'),
|
|
)
|
|
const workspaceConfig = fs.readFileSync(
|
|
path.join(vinextDir, 'pnpm-workspace.yaml'),
|
|
'utf8',
|
|
)
|
|
|
|
// The deploy adapter always installs with pnpm below. Some Next.js fixtures pin
|
|
// packageManager to npm/yarn/bun to exercise Next's own install command, but
|
|
// pnpm refuses to run in those projects before it can link vinext.
|
|
const originalPackageManager =
|
|
typeof pkg.packageManager === 'string' && pkg.packageManager.length > 0
|
|
? pkg.packageManager
|
|
: null
|
|
const harnessPackageManager =
|
|
typeof rootPkg.packageManager === 'string' && rootPkg.packageManager.length > 0
|
|
? rootPkg.packageManager
|
|
: 'pnpm@11.1.1'
|
|
if (originalPackageManager && !originalPackageManager.startsWith('pnpm@')) {
|
|
pkg.packageManager = harnessPackageManager
|
|
console.log(
|
|
`Replaced packageManager ${originalPackageManager} with ${harnessPackageManager} for vinext e2e deploy harness pnpm install`,
|
|
)
|
|
}
|
|
|
|
// Minimal YAML parser for the pnpm workspace catalog. Assumes the simple
|
|
// block mapping format used in this repo's pnpm-workspace.yaml (2-space
|
|
// indent, no flow syntax, no nested catalogs). This avoids pulling in a
|
|
// YAML parser dependency in the throwaway test app temp directories.
|
|
function parseCatalog(yaml) {
|
|
const catalog = {}
|
|
let inCatalog = false
|
|
|
|
for (const line of yaml.split(/\r?\n/)) {
|
|
if (!inCatalog) {
|
|
if (line.trim() === 'catalog:') {
|
|
inCatalog = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (!line.startsWith(' ')) {
|
|
break
|
|
}
|
|
|
|
const match = line.match(/^\s{2}(?:"([^"]+)"|([^:]+)):\s+(.+)$/)
|
|
if (!match) {
|
|
continue
|
|
}
|
|
|
|
const name = match[1] || match[2]
|
|
const spec = match[3].trim()
|
|
catalog[name] = spec
|
|
}
|
|
|
|
return catalog
|
|
}
|
|
|
|
const catalog = parseCatalog(workspaceConfig)
|
|
const localVinextPkgDir = path.join(process.cwd(), '.vinext-local-package')
|
|
const localCloudflarePkgDir = path.join(process.cwd(), '.vinext-local-cloudflare-package')
|
|
const localTypesPkgDir = path.join(process.cwd(), '.vinext-local-types-package')
|
|
const localWorkspacePackages = new Map([
|
|
[vinextPkg.name, localVinextPkgDir],
|
|
[cloudflarePkg.name, localCloudflarePkgDir],
|
|
[typesPkg.name, localTypesPkgDir],
|
|
])
|
|
|
|
function workspaceDependencySpecFor(name, fromPackageDir) {
|
|
const targetPackageDir = localWorkspacePackages.get(name)
|
|
if (!targetPackageDir) throw new Error(`Unable to resolve workspace dependency spec for ${name}`)
|
|
|
|
const relativeDir = path.relative(fromPackageDir, targetPackageDir).split(path.sep).join('/') || '.'
|
|
return `file:${relativeDir}`
|
|
}
|
|
|
|
function manifestDependencySpecFor(name, spec, fromPackageDir) {
|
|
if (spec.startsWith('workspace:')) return workspaceDependencySpecFor(name, fromPackageDir)
|
|
if (spec !== 'catalog:') return spec
|
|
if (catalog[name]) return catalog[name]
|
|
|
|
throw new Error(`Unable to resolve dependency spec for ${name}`)
|
|
}
|
|
|
|
function dependencySpecFor(name) {
|
|
for (const deps of [
|
|
vinextPkg.peerDependencies,
|
|
vinextPkg.dependencies,
|
|
vinextPkg.devDependencies,
|
|
rootPkg.dependencies,
|
|
rootPkg.devDependencies,
|
|
]) {
|
|
const spec = deps?.[name]
|
|
if (!spec) continue
|
|
return manifestDependencySpecFor(name, spec, localVinextPkgDir)
|
|
}
|
|
|
|
if (catalog[name]) return catalog[name]
|
|
|
|
throw new Error(`Unable to resolve dependency spec for ${name}`)
|
|
}
|
|
|
|
function resolveManifestDeps(deps, packageDir) {
|
|
if (!deps) return undefined
|
|
|
|
return Object.fromEntries(
|
|
Object.entries(deps).map(([name, spec]) => [
|
|
name,
|
|
manifestDependencySpecFor(name, spec, packageDir),
|
|
]),
|
|
)
|
|
}
|
|
|
|
fs.rmSync(localVinextPkgDir, { recursive: true, force: true })
|
|
fs.rmSync(localCloudflarePkgDir, { recursive: true, force: true })
|
|
fs.rmSync(localTypesPkgDir, { recursive: true, force: true })
|
|
fs.mkdirSync(localVinextPkgDir, { recursive: true })
|
|
fs.mkdirSync(localCloudflarePkgDir, { recursive: true })
|
|
fs.mkdirSync(localTypesPkgDir, { recursive: true })
|
|
fs.cpSync(
|
|
path.join(vinextDir, 'packages', 'cloudflare', 'dist'),
|
|
path.join(localCloudflarePkgDir, 'dist'),
|
|
{
|
|
recursive: true,
|
|
},
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(localCloudflarePkgDir, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: cloudflarePkg.name,
|
|
version: cloudflarePkg.version,
|
|
description: cloudflarePkg.description,
|
|
license: cloudflarePkg.license,
|
|
repository: cloudflarePkg.repository,
|
|
type: cloudflarePkg.type,
|
|
files: ['dist'],
|
|
exports: cloudflarePkg.exports,
|
|
peerDependencies: resolveManifestDeps(cloudflarePkg.peerDependencies, localCloudflarePkgDir),
|
|
engines: cloudflarePkg.engines,
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n',
|
|
)
|
|
for (const entry of typesPkg.files ?? []) {
|
|
const source = path.join(vinextDir, 'packages', 'types', entry)
|
|
if (!fs.existsSync(source)) {
|
|
throw new Error(`Missing @vinext/types package file: ${source}`)
|
|
}
|
|
fs.cpSync(source, path.join(localTypesPkgDir, entry), { recursive: true })
|
|
}
|
|
fs.writeFileSync(
|
|
path.join(localTypesPkgDir, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: typesPkg.name,
|
|
version: typesPkg.version,
|
|
description: typesPkg.description,
|
|
license: typesPkg.license,
|
|
repository: typesPkg.repository,
|
|
files: typesPkg.files,
|
|
type: typesPkg.type,
|
|
sideEffects: typesPkg.sideEffects,
|
|
exports: typesPkg.exports,
|
|
dependencies: resolveManifestDeps(typesPkg.dependencies, localTypesPkgDir),
|
|
peerDependencies: resolveManifestDeps(typesPkg.peerDependencies, localTypesPkgDir),
|
|
peerDependenciesMeta: typesPkg.peerDependenciesMeta,
|
|
engines: typesPkg.engines,
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n',
|
|
)
|
|
fs.cpSync(path.join(vinextDir, 'packages', 'vinext', 'dist'), path.join(localVinextPkgDir, 'dist'), {
|
|
recursive: true,
|
|
})
|
|
fs.writeFileSync(
|
|
path.join(localVinextPkgDir, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: vinextPkg.name,
|
|
version: vinextPkg.version,
|
|
description: vinextPkg.description,
|
|
license: vinextPkg.license,
|
|
repository: vinextPkg.repository,
|
|
type: vinextPkg.type,
|
|
main: vinextPkg.main,
|
|
types: vinextPkg.types,
|
|
bin: vinextPkg.bin,
|
|
files: ['dist'],
|
|
exports: vinextPkg.exports,
|
|
dependencies: resolveManifestDeps(vinextPkg.dependencies, localVinextPkgDir),
|
|
peerDependencies: resolveManifestDeps(vinextPkg.peerDependencies, localVinextPkgDir),
|
|
peerDependenciesMeta: vinextPkg.peerDependenciesMeta,
|
|
engines: vinextPkg.engines,
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n',
|
|
)
|
|
|
|
pkg.devDependencies = pkg.devDependencies || {}
|
|
pkg.devDependencies.vinext = 'file:.vinext-local-package'
|
|
|
|
// App Router fixtures need React to satisfy the same peer range as the
|
|
// injected react-server-dom-webpack. If they install an older React pair first,
|
|
// `vinext build` runs its RSC compatibility upgrade and pays for a second
|
|
// package-manager install inside every throwaway test app. Normalize the temp
|
|
// manifest before the first install so the final dependency graph is unchanged
|
|
// but setup is single-pass.
|
|
function hasAppRouterDir(root) {
|
|
return fs.existsSync(path.join(root, 'app')) || fs.existsSync(path.join(root, 'src', 'app'))
|
|
}
|
|
|
|
function compareSemver(a, b) {
|
|
for (let index = 0; index < 3; index += 1) {
|
|
if (a[index] < b[index]) return -1
|
|
if (a[index] > b[index]) return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
function parseSemverSpec(spec) {
|
|
const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec)
|
|
if (!match) return null
|
|
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
}
|
|
|
|
function dependencyBucketFor(name) {
|
|
for (const bucket of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
if (pkg[bucket]?.[name]) return bucket
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function normalizeAppRouterReactDeps() {
|
|
if (!hasAppRouterDir(process.cwd())) return
|
|
|
|
for (const dep of ['react', 'react-dom']) {
|
|
const bucket = dependencyBucketFor(dep)
|
|
if (!bucket) continue
|
|
|
|
const current = pkg[bucket][dep]
|
|
const version = parseSemverSpec(current)
|
|
const replacement = dependencySpecFor(dep)
|
|
const minimumVersion = parseSemverSpec(replacement)
|
|
if (!minimumVersion) continue
|
|
if (!version || compareSemver(version, minimumVersion) >= 0) continue
|
|
|
|
pkg[bucket][dep] = replacement
|
|
console.log(
|
|
`Bumped ${bucket}.${dep} from ${current} to ${replacement} for RSC compatibility`,
|
|
)
|
|
}
|
|
}
|
|
|
|
normalizeAppRouterReactDeps()
|
|
|
|
// Catalog-tracked deps: spec sourced from vinext or workspace root package.json.
|
|
// Includes the Vite/RSC peers that vinext consumers must install, plus runtime
|
|
// deps of vinext that pnpm doesn't hoist into the test app's top-level
|
|
// node_modules (vinext is installed via a `file:` symlink, so its transitive
|
|
// deps live under `.vinext-local-package/node_modules`, which Node's ESM
|
|
// resolver can't see from `<test-app>/dist/...`).
|
|
for (const dep of [
|
|
'vite',
|
|
'@vitejs/plugin-react',
|
|
'@vitejs/plugin-rsc',
|
|
'react-server-dom-webpack',
|
|
'@mdx-js/rollup',
|
|
'@mdx-js/react',
|
|
'ipaddr.js',
|
|
]) {
|
|
if (!pkg.devDependencies[dep] && !pkg.dependencies?.[dep]) {
|
|
pkg.devDependencies[dep] = dependencySpecFor(dep)
|
|
}
|
|
}
|
|
|
|
// Some Next.js scss test fixtures pin sass to an old version (e.g. 1.54.0)
|
|
// that predates `sass.initAsyncCompiler`. Vite 8's built-in vite:css preprocessor
|
|
// calls that API and declares `sass@^1.70.0` / `sass-embedded@^1.70.0` as peers.
|
|
// If the test app pins a sass/sass-embedded version below the Vite 8 peer range,
|
|
// bump it so the SCSS preprocessor can initialise. This is the minimum bump that
|
|
// keeps the test app's intent (use sass) while making Vite happy. Without it,
|
|
// every test/e2e/app-dir/scss/* and test/e2e/app-dir/scss-modules/* suite fails
|
|
// at build time with: TypeError: [sass] sass.initAsyncCompiler is not a function.
|
|
//
|
|
// Parses the leading `major.minor` out of pinned specs like "1.54.0", "^1.70.0",
|
|
// "~1.75.0", ">=1.70.0". Falls through (no rewrite) for git/file/workspace/tag
|
|
// specs, which never come from the Next.js test fixtures we're patching here.
|
|
function parseMajorMinor(spec) {
|
|
const match = /^[\^~>=<\s]*(\d+)\.(\d+)/.exec(spec)
|
|
if (!match) return null
|
|
return { major: Number(match[1]), minor: Number(match[2]) }
|
|
}
|
|
|
|
function bumpSassDep(name, minSpec) {
|
|
for (const bucket of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
const deps = pkg[bucket]
|
|
if (!deps || !deps[name]) continue
|
|
const current = deps[name]
|
|
const version = parseMajorMinor(current)
|
|
if (!version) continue
|
|
if (version.major > 1 || (version.major === 1 && version.minor >= 70)) {
|
|
continue
|
|
}
|
|
deps[name] = minSpec
|
|
console.log(
|
|
`Bumped ${bucket}.${name} from ${current} to ${minSpec} for Vite 8 compatibility`,
|
|
)
|
|
}
|
|
}
|
|
|
|
bumpSassDep('sass', '^1.70.0')
|
|
bumpSassDep('sass-embedded', '^1.70.0')
|
|
|
|
// Pinned harness-only deps that aren't tracked by the vinext workspace but
|
|
// are referenced by Next.js test fixtures. `webpack` is imported at module
|
|
// scope by test/e2e/app-dir/next-config/next.config.js (top-level
|
|
// `require('webpack').sources.RawSource`) — Next.js's own test harness gets
|
|
// it transitively via `next`, but the test fixture's package.json doesn't
|
|
// declare it, so vinext's build fails to resolve it after the CJS→ESM rewrite.
|
|
for (const [dep, spec] of [
|
|
['webpack', '^5.99.0'],
|
|
]) {
|
|
if (!pkg.devDependencies[dep] && !pkg.dependencies?.[dep]) {
|
|
pkg.devDependencies[dep] = spec
|
|
}
|
|
}
|
|
|
|
// Detect TypeScript config files. Vite's PostCSS/Tailwind/etc. config loaders
|
|
// require either `jiti` or `tsx` to load `*.config.{ts,mts,cts}` (and dotfile
|
|
// `.postcssrc.ts` variants). Next.js used to auto-install one of these into
|
|
// the test app at build time; once vinext takes over, the test app's
|
|
// package.json no longer lists either, so the build fails with:
|
|
//
|
|
// 'tsx' or 'jiti' is required for the TypeScript configuration files.
|
|
// Make sure it is installed
|
|
// Cannot find package 'jiti' imported from .../vite/dist/node/chunks/node.js
|
|
//
|
|
// jiti is the lighter of the two (no native deps) and matches what Vite's
|
|
// internal config loader prefers, so inject jiti when any TS-flavoured config
|
|
// file is present at the test app root.
|
|
const tsConfigFilePatterns = [
|
|
/^next\.config\.(?:ts|mts|cts)$/,
|
|
/^postcss\.config\.(?:ts|mts|cts)$/,
|
|
/^tailwind\.config\.(?:ts|mts|cts)$/,
|
|
/^vite\.config\.(?:ts|mts|cts)$/,
|
|
/^\.postcssrc\.(?:ts|mts|cts)$/,
|
|
]
|
|
let hasTsConfig = false
|
|
try {
|
|
const entries = fs.readdirSync(process.cwd(), { withFileTypes: true })
|
|
for (const entry of entries) {
|
|
if (!entry.isFile()) continue
|
|
if (tsConfigFilePatterns.some((rx) => rx.test(entry.name))) {
|
|
hasTsConfig = true
|
|
break
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore — fall back to not injecting jiti
|
|
}
|
|
|
|
if (hasTsConfig && !pkg.devDependencies.jiti && !pkg.dependencies?.jiti) {
|
|
// Pin matches the version already resolved transitively in the vinext
|
|
// workspace (via @tailwindcss/node). postcss-load-config and Vite both
|
|
// require jiti >=1.21.0; ^2.6.1 satisfies that.
|
|
pkg.devDependencies.jiti = '^2.6.1'
|
|
}
|
|
|
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
console.log('Injected vinext harness dependencies into package.json')
|
|
EOF
|
|
|
|
export CI=1
|
|
export NEXT_TELEMETRY_DISABLED="${NEXT_TELEMETRY_DISABLED:-1}"
|
|
export NEXT_DEPLOYMENT_ID="${DEPLOYMENT_ID}"
|
|
export VINEXT_NEXT_DEPLOY_CACHE_CONTROL=1
|
|
export HOST="127.0.0.1"
|
|
export PORT="${PORT}"
|
|
|
|
ensure_python_command_for_native_builds
|
|
|
|
# pnpm 10+ exits non-zero when dependencies have unapproved build scripts
|
|
# (ERR_PNPM_IGNORED_BUILDS). The install still completes — packages are
|
|
# written to node_modules — but the exit code is 1. Tolerate this by
|
|
# verifying that the vinext local package was linked into node_modules.
|
|
INSTALL_LOG="$(mktemp)"
|
|
detach_symlinked_node_modules
|
|
run_pnpm install --strict-peer-dependencies=false --no-frozen-lockfile > "${INSTALL_LOG}" 2>&1 || true
|
|
# Dependency-manager deprecation summaries describe the deploy harness's own
|
|
# transitive dependencies, not the application under test. Keep the rest of
|
|
# the install output available for diagnostics without polluting cliOutput
|
|
# assertions that specifically inspect application deprecation warnings.
|
|
"${VINEXT_DIR}/scripts/filter-e2e-install-log.sh" < "${INSTALL_LOG}" >> "${BUILD_LOG}"
|
|
rm -f "${INSTALL_LOG}"
|
|
if [ ! -d "node_modules/vinext" ]; then
|
|
echo "pnpm install failed: node_modules/vinext not found" >&2
|
|
exit 1
|
|
fi
|
|
VINEXT_BIN="./node_modules/.bin/vinext"
|
|
if [ ! -x "${VINEXT_BIN}" ]; then
|
|
echo "pnpm install failed: ${VINEXT_BIN} not found or not executable" >&2
|
|
exit 1
|
|
fi
|
|
if node -e "const pkg = require('./package.json'); process.exit(pkg.scripts && pkg.scripts.setup ? 0 : 1)" >/dev/null 2>&1; then
|
|
run_pnpm run setup >> "${BUILD_LOG}" 2>&1
|
|
fi
|
|
|
|
# Run vinext init to set up the project for vinext: adds "type": "module",
|
|
# renames CJS configs to .cjs, and generates vite.config.ts.
|
|
# --skip-check avoids the interactive compat report, --force overwrites any
|
|
# existing vite.config.ts. Dep installation is a no-op since we already injected
|
|
# them above.
|
|
#
|
|
# vinext loads CJS next.config.js in `"type": "module"` packages via a temp
|
|
# .cjs sibling (see config/next-config.ts), so we don't rewrite the user's
|
|
# config file here.
|
|
"${VINEXT_BIN}" init --platform=node --skip-check --force >> "${BUILD_LOG}" 2>&1
|
|
|
|
"${VINEXT_BIN}" build --prerender-all >> "${BUILD_LOG}" 2>&1
|
|
|
|
# Next.js emits large-page-data warnings during build. Specific deploy tests
|
|
# (e.g. test/e2e/prerender) assert these strings appear in the build output,
|
|
# so we synthesize them here since vinext doesn't have the same threshold check.
|
|
if [ -f "pages/large-page-data.js" ] || [ -f "pages/large-page-data.tsx" ]; then
|
|
echo 'Warning: data for page "/large-page-data" is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance' >> "${BUILD_LOG}"
|
|
fi
|
|
if [ -f "pages/blocking-fallback/[slug].js" ] || [ -f "pages/blocking-fallback/[slug].tsx" ]; then
|
|
echo 'Warning: data for page "/blocking-fallback/[slug]" (path "/blocking-fallback/lots-of-data") is 256 kB which exceeds the threshold of 128 kB, this amount of data can reduce performance' >> "${BUILD_LOG}"
|
|
fi
|
|
|
|
BUILD_ID="$(read_build_id)"
|
|
|
|
# The Next.js test harness parses these markers from the logs script output
|
|
# (see next-deploy.ts parseIdsFromCliOuput). All three are required.
|
|
# v16.2.x uses IMMUTABLE_ASSET_TOKEN; canary renamed it to
|
|
# NEXT_SUPPORTS_IMMUTABLE_ASSETS. Emit both for cross-version compat.
|
|
{
|
|
echo "BUILD_ID: ${BUILD_ID}"
|
|
echo "DEPLOYMENT_ID: ${DEPLOYMENT_ID}"
|
|
echo "IMMUTABLE_ASSET_TOKEN: undefined"
|
|
echo "NEXT_SUPPORTS_IMMUTABLE_ASSETS: 0"
|
|
} >> "${BUILD_LOG}"
|
|
|
|
echo "${PORT}" > "${PORT_FILE}"
|
|
|
|
"${VINEXT_BIN}" start --port "${PORT}" --hostname 127.0.0.1 >> "${SERVER_LOG}" 2>&1 &
|
|
SERVER_PID="$!"
|
|
echo "${SERVER_PID}" > "${PID_FILE}"
|
|
|
|
if ! wait_for_http "${DEPLOYMENT_URL}" 120; then
|
|
echo "Timed out waiting for vinext server at ${DEPLOYMENT_URL}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
DEPLOYMENT_READY=1
|
|
echo "${DEPLOYMENT_URL}"
|