Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

scripts/smoke-migrations.mjs

138 lines · 3.6 KB · JavaScript
#!/usr/bin/env node
/**
 * Lightweight smoke for the storage-migrations contract (no TS loader required).
 * Asserts the kit subpath re-exports `@kentcdodds/package-storage-migrations`,
 * and runs an in-memory twin of the migration contract.
 *
 * Run: node scripts/smoke-migrations.mjs
 */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const here = path.dirname(fileURLToPath(import.meta.url))
const tsPath = path.join(here, '../src/storage-migrations.ts')
const src = fs.readFileSync(tsPath, 'utf8')
for (const token of [
	"from 'kody:@kentcdodds/package-storage-migrations'",
	'export async function runPackageStorageMigrations',
	'export function createMigrationRunner',
	'export type {',
	'export default runPackageStorageMigrations',
]) {
	if (!src.includes(token)) {
		console.error('Missing export/surface in storage-migrations.ts:', token)
		process.exit(1)
	}
}

function memoryStorage() {
	const map = new Map()
	return {
		async get(key) {
			return map.has(key) ? map.get(key) : undefined
		},
		async set(key, value) {
			map.set(key, value)
		},
		async delete(key) {
			map.delete(key)
		},
	}
}

function readVersion(raw) {
	if (typeof raw === 'number' && Number.isFinite(raw) && raw >= 0) return Math.floor(raw)
	if (typeof raw === 'string' && raw.trim() !== '') {
		const n = Number(raw)
		if (Number.isFinite(n) && n >= 0) return Math.floor(n)
	}
	return 0
}

/** Inline twin of runPackageStorageMigrations for smoke (keep in sync with .ts). */
async function runPackageStorageMigrations(input) {
	const versionKey = input.versionKey ?? 'pak:schema-version'
	const sorted = [...input.migrations].sort((a, b) => a.version - b.version)
	const fromVersion = readVersion(await input.storage.get(versionKey))
	let current = fromVersion
	const applied = []
	for (const step of sorted) {
		if (step.version <= current) continue
		await step.up(input.storage)
		current = step.version
		await input.storage.set(versionKey, current)
		applied.push({ version: step.version, name: step.name })
	}
	return { fromVersion, toVersion: current, applied }
}

function createMigrationRunner(input) {
	let pending = null
	return function ensureMigrated() {
		if (!pending) {
			pending = runPackageStorageMigrations(input).catch((error) => {
				pending = null
				throw error
			})
		}
		return pending
	}
}

const storage = memoryStorage()
await storage.set('notes-v1', [{ id: 'a', text: 'hi', createdAt: '2026-01-01T00:00:00.000Z' }])
const migrations = [
	{
		version: 1,
		name: 'notes-to-document',
		async up(s) {
			const legacy = await s.get('notes-v1')
			if (Array.isArray(legacy)) {
				await s.set('notes', { items: legacy })
				await s.delete('notes-v1')
			}
		},
	},
]

const first = await runPackageStorageMigrations({
	storage,
	versionKey: 'demo:schema-version',
	migrations,
})
const second = await runPackageStorageMigrations({
	storage,
	versionKey: 'demo:schema-version',
	migrations,
})
const ensure = createMigrationRunner({
	storage,
	versionKey: 'demo:schema-version',
	migrations,
})
const third = await ensure()
const fourth = await ensure()

const notes = await storage.get('notes')
const ok =
	first.fromVersion === 0 &&
	first.toVersion === 1 &&
	first.applied.length === 1 &&
	second.applied.length === 0 &&
	second.toVersion === 1 &&
	third.applied.length === 0 &&
	fourth === third &&
	Array.isArray(notes?.items) &&
	(await storage.get('notes-v1')) == null

if (!ok) {
	console.error('FAIL', { first, second, third })
	process.exit(1)
}
console.log('OK', {
	first,
	secondApplied: second.applied.length,
	sharedEnsure: third === fourth,
	tsExports: true,
})