/** * scaffold-ui-primitives/version.ts — Which @atlashub/smartstack a project actually has. * * One generated primitive — `useModuleAvailability` — is a thin adapter over a hook the * PACKAGE exports. A project still on an older package would import a symbol that does not * exist there and fail to build. So the emitter asks this module first, and emits a * permissive stub instead when the floor is not met: the guarded surfaces then render as * they always did, the CLI warns, and an audit rule reports it — a visible degradation * rather than a broken build or a silent behaviour change. */ import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; /** * First @atlashub/smartstack release exporting `useModuleAvailability`. * Keep in step with the socle CHANGELOG entry when its release is cut. */ export const MODULE_AVAILABILITY_MIN_VERSION = '3.65.0'; /** `^3.64.0` / `~3.64.0` / `3.64.0-localtest18` → `[3, 64, 0]`; null when unreadable. */ export function parseVersionTriple(range: string | undefined | null): [number, number, number] | null { if (!range) return null; const m = /(\d+)\.(\d+)\.(\d+)/.exec(range.trim()); if (!m) return null; return [Number(m[1]), Number(m[2]), Number(m[3])]; } /** * Whether `range` is at least `floor`, comparing major.minor.patch ONLY. * * The pre-release suffix is deliberately ignored: this project ships and installs * `X.Y.Z-localtestN` builds, and under strict semver every one of them would rank BELOW * `X.Y.Z` — so a local build carrying the hook would be treated as too old and silently * stubbed, which is exactly the confusion this gate exists to avoid. */ export function meetsFloor(range: string | undefined | null, floor: string): boolean { const v = parseVersionTriple(range); const f = parseVersionTriple(floor); if (!v || !f) return false; for (let i = 0; i < 3; i++) { if (v[i] > f[i]) return true; if (v[i] < f[i]) return false; } return true; } /** * The @atlashub/smartstack version declared by the web project, from any dependency * section. Undefined when there is no package.json, no such dependency, or it is * unreadable — the caller then treats the floor as unmet (fail-safe: stub, warn, never * emit an import that might not resolve). */ export function readInstalledSmartstackVersion(projectPath: string): string | undefined { const pkgPath = path.join(path.resolve(projectPath), 'package.json'); if (!existsSync(pkgPath)) return undefined; let pkg: Record | undefined>; try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return undefined; } for (const section of ['dependencies', 'devDependencies', 'peerDependencies'] as const) { const found = pkg[section]?.['@atlashub/smartstack']; if (typeof found === 'string' && found.length > 0) return found; } return undefined; }