upgrading/app/upgrades/0.11-to-0.12/re-emit-domain-namespaced-contracts.ts
upgrading/app/upgrades/0.11-to-0.12/re-emit-domain-namespaced-contracts.tsBrowse 76 files
1,595 tokens
6,334 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1/**2 * Re-emits every contract artefact still on the pre-0.12 flat domain plane3 * (`contract.models` / `contract.valueObjects` at the contract root) so4 * emitted JSON and `contract.d.ts` pick up `contract.domain.namespaces.<ns>`.5 *6 * Background: starting at 0.12 (symmetric domain plane, ADR 221), models7 * and value objects live under `domain.namespaces`. The supported read8 * paths are `contract.domain.namespaces` plus helpers such as9 * `contractModels()` / `ContractModelsMap`.10 *11 * Dispatch: walks the project root for `prisma.config.ts` directories,12 * resolves each space's committed `contract.json` / `contract.d.ts`, and13 * re-emits when the flat domain shape remains. Uses the nearest ancestor14 * `package.json` `scripts.emit` when present; otherwise runs15 * `prisma-next contract emit --config <path>`.16 *17 * Flags:18 * --check dry-run; lists contract-spaces that still need re-emitting19 * and exits 1 if any remain.20 */21import { execFile } from 'node:child_process';22import { access, readdir, readFile } from 'node:fs/promises';23import { dirname, join } from 'node:path';24import { promisify } from 'node:util';25 26const execFileAsync = promisify(execFile);27 28const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);29 30const dryRun = process.argv.includes('--check');31const projectRoot = process.cwd();32 33async function pathExists(path: string): Promise<boolean> {34 try {35 await access(path);36 return true;37 } catch {38 return false;39 }40}41 42function isJsonObject(value: unknown): value is Record<string, unknown> {43 return typeof value === 'object' && value !== null && !Array.isArray(value);44}45 46async function findPrismaNextConfigDirs(root: string): Promise<string[]> {47 const out: string[] = [];48 49 async function walk(dir: string): Promise<void> {50 let entries: Awaited<ReturnType<typeof readdir>>;51 try {52 entries = await readdir(dir, { withFileTypes: true });53 } catch {54 return;55 }56 for (const entry of entries) {57 if (entry.isDirectory()) {58 if (SKIP_DIRS.has(entry.name)) continue;59 await walk(join(dir, entry.name));60 } else if (entry.isFile() && entry.name === 'prisma.config.ts') {61 out.push(dir);62 }63 }64 }65 66 await walk(root);67 return out.sort();68}69 70function contractJsonCandidates(configDir: string): string[] {71 return [72 join(configDir, 'src', 'contract.json'),73 join(configDir, 'src', 'prisma', 'contract.json'),74 join(configDir, 'prisma', 'contract.json'),75 join(configDir, 'contract.json'),76 ];77}78 79function contractDtsCandidates(configDir: string): string[] {80 return [81 join(configDir, 'src', 'contract.d.ts'),82 join(configDir, 'src', 'prisma', 'contract.d.ts'),83 join(configDir, 'prisma', 'contract.d.ts'),84 join(configDir, 'contract.d.ts'),85 ];86}87 88function contractJsonNeedsDomainPlaneMigration(raw: string): boolean {89 let parsed: unknown;90 try {91 parsed = JSON.parse(raw);92 } catch {93 return false;94 }95 if (!isJsonObject(parsed)) return false;96 const domain = parsed['domain'];97 if (!isJsonObject(domain)) return true;98 const namespaces = domain['namespaces'];99 return !isJsonObject(namespaces) || Object.keys(namespaces).length === 0;100}101 102function contractDtsNeedsDomainPlaneMigration(raw: string): boolean {103 return raw.includes("Contract['models']");104}105 106async function packageJsonHasEmitScript(dir: string): Promise<boolean> {107 const pkgPath = join(dir, 'package.json');108 if (!(await pathExists(pkgPath))) return false;109 const raw = await readFile(pkgPath, 'utf-8');110 try {111 const parsed: unknown = JSON.parse(raw);112 if (!isJsonObject(parsed)) return false;113 const scripts = parsed['scripts'];114 if (!isJsonObject(scripts)) return false;115 return typeof scripts['emit'] === 'string' && scripts['emit'].length > 0;116 } catch {117 return false;118 }119}120 121async function resolveEmitInvocation(configDir: string): Promise<{122 readonly cwd: string;123 readonly args: string[];124 readonly key: string;125}> {126 let dir = configDir;127 while (dir.startsWith(projectRoot)) {128 if (await packageJsonHasEmitScript(dir)) {129 return { cwd: dir, args: ['emit'], key: `script:${dir}` };130 }131 const parent = dirname(dir);132 if (parent === dir) break;133 dir = parent;134 }135 const configPath = join(configDir, 'prisma.config.ts');136 return {137 cwd: projectRoot,138 args: ['exec', 'prisma-next', 'contract', 'emit', '--config', configPath],139 key: `config:${configPath}`,140 };141}142 143async function runEmit(configDir: string): Promise<void> {144 const { cwd, args } = await resolveEmitInvocation(configDir);145 await execFileAsync('pnpm', args, { cwd, env: process.env });146}147 148async function configDirNeedsDomainPlaneMigration(configDir: string): Promise<boolean> {149 for (const candidate of contractJsonCandidates(configDir)) {150 if (!(await pathExists(candidate))) continue;151 const raw = await readFile(candidate, 'utf-8');152 if (contractJsonNeedsDomainPlaneMigration(raw)) return true;153 }154 for (const candidate of contractDtsCandidates(configDir)) {155 if (!(await pathExists(candidate))) continue;156 const raw = await readFile(candidate, 'utf-8');157 if (contractDtsNeedsDomainPlaneMigration(raw)) return true;158 }159 return false;160}161 162const configDirs = await findPrismaNextConfigDirs(projectRoot);163const emitKeys = new Set<string>();164const targets: string[] = [];165 166for (const configDir of configDirs) {167 if (!(await configDirNeedsDomainPlaneMigration(configDir))) continue;168 const { key } = await resolveEmitInvocation(configDir);169 if (emitKeys.has(key)) continue;170 emitKeys.add(key);171 targets.push(configDir);172}173 174if (targets.length === 0) {175 console.error(`No flat-domain contract candidates under ${projectRoot}.`);176 process.exit(dryRun ? 0 : 1);177}178 179let needsFix = 0;180 181for (const configDir of targets) {182 const rel = configDir.slice(projectRoot.length + 1) || '.';183 if (!(await configDirNeedsDomainPlaneMigration(configDir))) {184 console.log(`OK ${rel}`);185 continue;186 }187 needsFix += 1;188 if (dryRun) {189 console.log(`WOULD RE-EMIT ${rel}`);190 continue;191 }192 console.log(`EMIT ${rel}`);193 await runEmit(configDir);194}195 196console.log();197console.log(198 `${targets.length} contract-space(s): ${needsFix} ${dryRun ? 'needing re-emit' : 're-emitted'}.`,199);200 201if (dryRun && needsFix > 0) process.exit(1);202