← 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,
})