/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
tests/migration/framework/shared.ts
309 строк
10 KB
Ben Irvin
test(migration): version migration testing framework (#26060)
06 авг 2026, 15:39
Не верифицирован
06 авг 2026, 15:39
ac0c928
Код
Авторство
О чём код?
const fs = require('fs'); const path = require('path'); const execa = require('execa'); type MigrationContext = { REPO_ROOT: string; COMPLEX_DIR: string; MIGRATION_ROOT: string; V4_APP_DIR: string; SQLITE_PATH: string; DOCKER_COMPOSE_FILE: string; DOTENV_PATH: string; }; type EnvRecord = Record<string, string | undefined>; type DbClient = 'postgres' | 'mysql' | 'mariadb' | 'sqlite' | string; function composeEnv(ctx: MigrationContext, extra: EnvRecord = {}): NodeJS.ProcessEnv { const composeProject = process.env.STRAPI_MIGRATION_COMPOSE_PROJECT || 'strapi_migration_v5'; return { ...process.env, COMPOSE_PROJECT_NAME: composeProject, ...extra, }; } /** * Env for yarn install in ephemeral apps outside the monorepo workspace. * CI PR "hardened mode" forbids lockfile updates unless immutable installs are disabled. */ function nestedYarnInstallEnv(extra: EnvRecord = {}): NodeJS.ProcessEnv { return { ...process.env, YARN_ENABLE_IMMUTABLE_INSTALLS: 'false', ...extra, }; } async function dockerComposeDownVolumes(ctx: MigrationContext): Promise<void> { // Dynamic require of examples/complex compose (.ts; parent process loads via tsx) const { runCompose } = require(path.join(ctx.COMPLEX_DIR, 'scripts', 'compose.ts')); try { runCompose(['-f', ctx.DOCKER_COMPOSE_FILE, 'down', '-v'], { cwd: ctx.COMPLEX_DIR, stdio: 'pipe', env: composeEnv(ctx), }); } catch { // No project / already clean } } async function resolveDockerHostPorts(client: DbClient): Promise<void> { const getPort = require('get-port'); if (client === 'postgres') { if (!process.env.POSTGRES_PORT && !process.env.DATABASE_PORT) { const p = await getPort({ port: 5432 }); process.env.POSTGRES_PORT = String(p); process.env.DATABASE_PORT = String(p); console.log( `\n🔌 Using host port ${p} for Postgres → container 5432 (set POSTGRES_PORT or DATABASE_PORT to pin).` ); } else if (!process.env.POSTGRES_PORT && process.env.DATABASE_PORT) { process.env.POSTGRES_PORT = process.env.DATABASE_PORT; } else if (process.env.POSTGRES_PORT && !process.env.DATABASE_PORT) { process.env.DATABASE_PORT = process.env.POSTGRES_PORT; } return; } if (client === 'mysql') { if (!process.env.MYSQL_PORT && !process.env.DATABASE_PORT) { const p = await getPort({ port: 3306 }); process.env.MYSQL_PORT = String(p); process.env.DATABASE_PORT = String(p); console.log( `\n🔌 Using host port ${p} for MySQL → container 3306 (set MYSQL_PORT or DATABASE_PORT to pin).` ); } else if (!process.env.MYSQL_PORT && process.env.DATABASE_PORT) { process.env.MYSQL_PORT = process.env.DATABASE_PORT; } else if (process.env.MYSQL_PORT && !process.env.DATABASE_PORT) { process.env.DATABASE_PORT = process.env.MYSQL_PORT; } } if (client === 'mariadb') { if (!process.env.MARIADB_PORT && !process.env.DATABASE_PORT) { const p = await getPort({ port: 3307 }); process.env.MARIADB_PORT = String(p); process.env.DATABASE_PORT = String(p); console.log( `\n🔌 Using host port ${p} for MariaDB → container 3306 (set MARIADB_PORT or DATABASE_PORT to pin).` ); } else if (!process.env.MARIADB_PORT && process.env.DATABASE_PORT) { process.env.MARIADB_PORT = process.env.DATABASE_PORT; } else if (process.env.MARIADB_PORT && !process.env.DATABASE_PORT) { process.env.DATABASE_PORT = process.env.MARIADB_PORT; } } } function buildDatabaseEnvForClient( ctx: MigrationContext, client: DbClient ): Record<string, string> { const postgresPort = process.env.POSTGRES_PORT || '5432'; const mysqlPort = process.env.MYSQL_PORT || '3306'; const mariadbPort = process.env.MARIADB_PORT || '3307'; // Strapi exposes postgres | mysql | sqlite only (Knex mysql2 against MariaDB uses client "mysql"). const base = { DATABASE_CLIENT: client === 'mariadb' ? 'mysql' : client, DATABASE_HOST: process.env.DATABASE_HOST || 'localhost', DATABASE_NAME: process.env.DATABASE_NAME || 'strapi', DATABASE_USERNAME: process.env.DATABASE_USERNAME || 'strapi', DATABASE_PASSWORD: process.env.DATABASE_PASSWORD || 'strapi', DATABASE_SSL: process.env.DATABASE_SSL || 'false', }; if (client === 'postgres') { return { ...base, DATABASE_PORT: process.env.DATABASE_PORT || postgresPort, }; } if (client === 'mysql') { return { ...base, DATABASE_PORT: process.env.DATABASE_PORT || mysqlPort, }; } if (client === 'mariadb') { return { ...base, DATABASE_CLIENT: 'mysql', DATABASE_PORT: process.env.DATABASE_PORT || mariadbPort, }; } return { ...base, DATABASE_FILENAME: ctx.SQLITE_PATH, }; } function writeAppDotenv(ctx: MigrationContext, dbEnv: Record<string, string>): void { const keys = ['toBeModified1', 'toBeModified2', 'toBeModified3', 'toBeModified4'].join(','); let body = `HOST=0.0.0.0 PORT=1337 APP_KEYS=${keys} API_TOKEN_SALT=toBeModified ADMIN_JWT_SECRET=toBeModified TRANSFER_TOKEN_SALT=toBeModified JWT_SECRET=toBeModified `; if (dbEnv.DATABASE_CLIENT === 'sqlite') { body += `DATABASE_CLIENT=sqlite DATABASE_FILENAME=${dbEnv.DATABASE_FILENAME} `; } else { body += `DATABASE_CLIENT=${dbEnv.DATABASE_CLIENT} DATABASE_HOST=${dbEnv.DATABASE_HOST} DATABASE_PORT=${dbEnv.DATABASE_PORT} DATABASE_NAME=${dbEnv.DATABASE_NAME} DATABASE_USERNAME=${dbEnv.DATABASE_USERNAME} DATABASE_PASSWORD=${dbEnv.DATABASE_PASSWORD} `; } fs.writeFileSync(path.join(ctx.V4_APP_DIR, '.env'), body); } async function prepareDockerDatabase(ctx: MigrationContext, client: DbClient): Promise<void> { // Dynamic requires of examples/complex scripts (.ts; parent process loads via tsx) const dbUtils = require(path.join(ctx.COMPLEX_DIR, 'scripts', 'db-utils.ts')); const { startContainer, getContainerId, waitForPostgresReady, waitForMysqlReady } = dbUtils; const env = composeEnv(ctx); if (client === 'postgres') { await execa( process.execPath, ['--import', 'tsx', path.join(ctx.COMPLEX_DIR, 'scripts', 'db-postgres.ts'), 'wipe'], { cwd: ctx.COMPLEX_DIR, stdio: 'inherit', env: { ...env, NODE_PATH: [path.join(ctx.REPO_ROOT, 'node_modules'), env.NODE_PATH] .filter(Boolean) .join(path.delimiter), }, } ); const cid = getContainerId(ctx.DOCKER_COMPOSE_FILE, ctx.COMPLEX_DIR, 'postgres', env); if (cid) { waitForPostgresReady(cid); } return; } if (client === 'mysql') { startContainer(ctx.DOCKER_COMPOSE_FILE, ctx.COMPLEX_DIR, 'mysql', env); const cid = getContainerId(ctx.DOCKER_COMPOSE_FILE, ctx.COMPLEX_DIR, 'mysql', env); if (cid) { waitForMysqlReady(cid); } await execa( process.execPath, ['--import', 'tsx', path.join(ctx.COMPLEX_DIR, 'scripts', 'db-mysql.ts'), 'wipe'], { cwd: ctx.COMPLEX_DIR, stdio: 'inherit', env: { ...env, NODE_PATH: [path.join(ctx.REPO_ROOT, 'node_modules'), env.NODE_PATH] .filter(Boolean) .join(path.delimiter), }, } ); } if (client === 'mariadb') { startContainer(ctx.DOCKER_COMPOSE_FILE, ctx.COMPLEX_DIR, 'mariadb', env); const cid = getContainerId(ctx.DOCKER_COMPOSE_FILE, ctx.COMPLEX_DIR, 'mariadb', env); if (cid) { waitForMysqlReady(cid); } await execa( process.execPath, ['--import', 'tsx', path.join(ctx.COMPLEX_DIR, 'scripts', 'db-mariadb.ts'), 'wipe'], { cwd: ctx.COMPLEX_DIR, stdio: 'inherit', env: { ...env, NODE_PATH: [path.join(ctx.REPO_ROOT, 'node_modules'), env.NODE_PATH] .filter(Boolean) .join(path.delimiter), }, } ); } } function assertNodeForV4(): void { const major = Number(process.versions.node.split('.')[0]); if (major > 20) { console.warn( `\n⚠️ Strapi v4 in this scaffold expects Node <= 20 (engines). Current Node is ${process.version}. If install or seed fails, use Node 20.\n` ); } } type WorkspaceBuildOpts = { fullBuild?: boolean; skipBuild?: boolean; }; async function runWorkspaceBuilds( repoRoot: string, { fullBuild, skipBuild }: WorkspaceBuildOpts ): Promise<void> { if (fullBuild) { console.log('\n📦 Running yarn build at repo root (first step)...'); await execa('yarn', ['build'], { cwd: repoRoot, stdio: 'inherit' }); return; } if (!skipBuild) { const strapiPkg = path.join(repoRoot, 'packages', 'core', 'strapi', 'package.json'); const databasePkg = path.join(repoRoot, 'packages', 'core', 'database', 'package.json'); if (!fs.existsSync(strapiPkg)) { console.error('Missing @strapi/strapi package; run from a full checkout.'); process.exit(1); } if (!fs.existsSync(databasePkg)) { console.error('Missing @strapi/database package; run from a full checkout.'); process.exit(1); } console.log( '\n📦 Building @strapi/core and @strapi/database (migration dist). Use --skip-build to skip.' ); await execa('yarn', ['workspace', '@strapi/core', 'build'], { cwd: repoRoot, stdio: 'inherit', }); await execa('yarn', ['workspace', '@strapi/database', 'build'], { cwd: repoRoot, stdio: 'inherit', }); } else { console.log( '\n⚠️ --skip-build: tests load compiled code from `packages/core/core/dist` and `packages/core/database/dist`; rebuild those workspaces after editing migration TS if results look stale.' ); } } module.exports = { composeEnv, nestedYarnInstallEnv, dockerComposeDownVolumes, resolveDockerHostPorts, buildDatabaseEnvForClient, writeAppDotenv, prepareDockerDatabase, assertNodeForV4, runWorkspaceBuilds, };