@sanity/migrate
Version:
Tooling for running data migrations on Sanity.io projects
103 lines (90 loc) • 3.33 kB
text/typescript
import path from 'node:path'
import {fileExists} from '@sanity/cli-core'
import {getTsconfig} from 'get-tsconfig'
import {isPlainObject} from 'lodash-es'
import {tsImport} from 'tsx/esm/api'
import {Migration} from '../../types.js'
import {MIGRATION_SCRIPT_EXTENSIONS, MIGRATIONS_DIRECTORY} from './constants.js'
interface ResolvedMigrationScript {
/**
* Absolute path to the migration script
*/
absolutePath: string
/**
* Relative path from the working directory to the migration script
*/
relativePath: string
/**
* The migration module, if it could be resolved - otherwise `undefined`
*/
mod?: {default: Migration; down?: unknown; up?: unknown}
}
/**
* Resolves the potential paths to a migration script.
* Considers the following paths (where `<ext>` is 'mjs', 'js', 'ts' or 'cjs'):
*
* - `<migrationsDir>/<migrationName>.<ext>`
* - `<migrationsDir>/<migrationName>/index.<ext>`
*
* Note that all possible paths are returned, even if the files do not exist.
* Check the `mod` property to see if a module could actually be loaded.
*
* @param workDir - Working directory of the studio
* @param migrationName - The name of the migration directory to resolve
* @returns An array of potential migration scripts
* @internal
*/
export async function resolveMigrationScript(
workDir: string,
migrationName: string,
): Promise<ResolvedMigrationScript[]> {
const tsconfig = getTsconfig(workDir)
const locations = [migrationName, path.join(migrationName, 'index')]
const results: ResolvedMigrationScript[] = []
for (const location of locations) {
for (const ext of MIGRATION_SCRIPT_EXTENSIONS) {
const relativePath = path.join(MIGRATIONS_DIRECTORY, `${location}.${ext}`)
const absolutePath = path.resolve(workDir, relativePath)
let mod: {default: Migration; down?: unknown; up?: unknown} | undefined
// Check if the file exists before trying to import it
const exists = await fileExists(absolutePath)
if (!exists) {
continue
}
try {
const imported = await tsImport(absolutePath, {
parentURL: import.meta.url,
tsconfig: tsconfig?.path,
})
// Handle both ESM and CJS default exports
mod = imported as {default: Migration; down?: unknown; up?: unknown}
} catch (err) {
// Ignore MODULE_NOT_FOUND errors, but throw others
if (
(err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND' &&
(err as NodeJS.ErrnoException).code !== 'ERR_MODULE_NOT_FOUND'
) {
throw new Error(`Error loading migration: ${(err as Error).message}`)
}
}
results.push({absolutePath, mod, relativePath})
}
}
return results
}
/**
* Checks whether or not the passed resolved migration script is actually loadable (eg has a default export)
*
* @param script - The resolved migration script to check
* @returns `true` if the script is loadable, `false` otherwise
* @internal
*/
export function isLoadableMigrationScript(
script: ResolvedMigrationScript,
): script is Required<ResolvedMigrationScript> {
if (script.mod === undefined || !isPlainObject(script.mod.default)) {
return false
}
const mod = script.mod.default
return typeof mod.title === 'string' && mod.migrate !== undefined
}