Created
July 1, 2022 17:29
-
-
Save runspired/5d951e242568655431bf4b990c9ed666 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-console */ | |
import execa from "execa"; | |
import { globby } from "globby"; | |
import codeshift from "jscodeshift"; | |
import fs from "node:fs"; | |
import path from "node:path"; | |
const TYPES = [ | |
{ fileName: "adapter", usePods: false }, | |
{ fileName: "component", usePods: false, alsoMoveTemplate: true }, | |
{ fileName: "helper", usePods: false }, | |
{ fileName: "mixin", usePods: false }, | |
{ fileName: "initializer", usePods: false }, | |
{ fileName: "instance-initializer", usePods: false }, | |
{ fileName: "model", usePods: false }, | |
{ | |
fileName: "route", | |
usePods: true, | |
preserveName: true, | |
alsoMoveTemplate: true, | |
}, | |
{ | |
fileName: "controller", | |
usePods: true, | |
preserveName: true, | |
baseName: "routes", | |
alsoMoveTemplate: true, | |
}, | |
{ fileName: "serializer", usePods: false }, | |
{ fileName: "service", usePods: false }, | |
{ fileName: "transform", usePods: false }, | |
// unknown templates ? | |
]; | |
const SEEN_MIGRATIONS = new Set(); | |
const CONVERSIONS = {}; | |
const MIGRATIONS = []; | |
const COMPLETED = {}; | |
const APP_NAME = "frontend"; | |
const REWRITE_RELATIVE_TO_ABSOLUTE = false; | |
const podModulePrefix = "routes"; | |
const DRY_RUN = false; | |
function toImportPath(str) { | |
str = str.replace("app/", `${APP_NAME}/`); | |
const ext = path.extname(str); | |
if (ext) { | |
str = str.replace(ext, ""); | |
} | |
return str; | |
} | |
function filePathToImportPath(from) { | |
let f = path.dirname(from); | |
f = f.replace("app/", `${APP_NAME}/`); | |
return f; | |
} | |
function buildFullPath(filePath, relativePath) { | |
const f = filePathToImportPath(filePath); | |
if (relativePath.startsWith(".")) { | |
return toImportPath(path.join(f, relativePath)); | |
} | |
return relativePath; | |
} | |
function getRelativeImportPath(from, to) { | |
const f = filePathToImportPath(from); | |
const r = path.relative(f, to); | |
if (!r.startsWith(".")) { | |
return "./" + r; | |
} | |
return r; | |
} | |
function updateImportPaths(migration, conversions) { | |
const filePath = DRY_RUN ? migration.from : migration.to; | |
const code = fs.readFileSync(filePath, { encoding: "utf-8" }); | |
let hasChanges = false; | |
const output = codeshift(code) | |
.find(codeshift.ImportDeclaration) | |
.forEach((path) => { | |
const { value } = path.value.source; | |
const importPath = buildFullPath(migration.from, value); | |
const updatedPath = conversions[importPath]; | |
const isRelativePath = value.startsWith("."); | |
let updatedValue; | |
// if we want to rewrite all relative to absolute then we have less work to do | |
if (isRelativePath && REWRITE_RELATIVE_TO_ABSOLUTE) { | |
updatedValue = updatedPath || importPath; | |
console.log( | |
"\tupdate to absolute import location", | |
value, | |
updatedValue | |
); | |
// check if the file has moved relative to us | |
} else if (updatedPath) { | |
let newPath = updatedPath; | |
if (isRelativePath) { | |
newPath = getRelativeImportPath(migration.to, newPath); | |
} | |
updatedValue = newPath; | |
console.log("\tupdate import location", value, newPath); | |
// check if we have moved relative to the file | |
} else if (isRelativePath) { | |
const newRelativePath = getRelativeImportPath(migration.to, importPath); | |
const oldRelativePath = getRelativeImportPath( | |
migration.from, | |
importPath | |
); | |
if (newRelativePath !== oldRelativePath) { | |
updatedValue = newRelativePath; | |
console.log("\tupdate relative path", value, newRelativePath); | |
} | |
} | |
if (updatedValue) { | |
hasChanges = true; | |
path.value.source.value = updatedValue; | |
} | |
}) | |
.toSource(); | |
migration.hasChanges = hasChanges; | |
if (!DRY_RUN && hasChanges) { | |
fs.writeFileSync(filePath, output); | |
} else if (hasChanges) { | |
console.log("\n\n", output, "\n\n"); | |
} else if (DRY_RUN) { | |
console.log(`no changes needed for ${filePath}`); | |
} else { | |
console.log(`no changes needed for ${filePath}`); | |
} | |
} | |
function getSiblingTemplatePath(target) { | |
const potentialTemplatePath = path.join(path.dirname(target), "template.hbs"); | |
let exists = false; | |
try { | |
fs.lstatSync(potentialTemplatePath); | |
exists = true; | |
} catch { | |
exists = false; | |
} | |
if (exists) { | |
return potentialTemplatePath; | |
} | |
} | |
function makeDir(target) { | |
const dir = path.dirname(target); | |
fs.mkdirSync(dir, { recursive: true }); | |
} | |
async function migrateTemplateOnlyComponents(options) { | |
let filesToMigrate; | |
try { | |
filesToMigrate = await globby(`app/${podModulePrefix}/components/**/*.hbs`); | |
} catch (error) { | |
if (!error.message.includes("No such file or directory")) { | |
throw error; | |
} | |
console.log( | |
`No files found for the glob 'app/${podModulePrefix}/components/**/*.hbs'` | |
); | |
return; | |
} | |
const migrations = MIGRATIONS; | |
const conversions = CONVERSIONS; | |
filesToMigrate.forEach((from) => { | |
let dirname = path.dirname(from); | |
const podPath = `app/${podModulePrefix}/`; | |
const isPodsPath = dirname.startsWith(podPath); | |
const podsTypePath = `${podPath}${options.fileName}s/`; | |
const toDirectoryBase = options.usePods | |
? podPath | |
: `app/${options.fileName}s/`; | |
// scrub directory path | |
// replace | |
// - app/<podModulePrefix>/ | |
// - app/<type>s/ | |
// - app/<podModulePrefix>/<type>s/ | |
if (dirname.startsWith(podsTypePath)) { | |
dirname = dirname.replace(podsTypePath, ""); | |
} else if (dirname.startsWith(toDirectoryBase)) { | |
dirname = dirname.replace(toDirectoryBase, ""); | |
} else if (isPodsPath) { | |
dirname = dirname.replace(podPath, ""); | |
} | |
const to = options.preserveName | |
? `${toDirectoryBase}${dirname}/template.hbs` | |
: `${toDirectoryBase}${dirname}.hbs`; | |
if (from === to) { | |
return; | |
} | |
if (SEEN_MIGRATIONS.has(from)) { | |
return; | |
} | |
SEEN_MIGRATIONS.add(from); | |
conversions[toImportPath(from)] = toImportPath(to); | |
migrations.push({ | |
from, | |
to, | |
}); | |
}); | |
} | |
async function run(options) { | |
let filesToMigrate; | |
try { | |
filesToMigrate = await globby(`app/**/${options.fileName}.js`); | |
} catch (error) { | |
if (!error.message.includes("No such file or directory")) { | |
throw error; | |
} | |
console.log(`No files found for the glob 'app/**/${options.fileName}.js'`); | |
return; | |
} | |
const migrations = MIGRATIONS; | |
const conversions = CONVERSIONS; | |
filesToMigrate.forEach((from) => { | |
let dirname = path.dirname(from); | |
const podPath = `app/${podModulePrefix}/`; | |
const isPodsPath = dirname.startsWith(podPath); | |
const podsTypePath = `${podPath}${options.fileName}s/`; | |
const toDirectoryBase = options.usePods | |
? podPath | |
: `app/${options.fileName}s/`; | |
// scrub directory path | |
// replace | |
// - app/<podModulePrefix>/ | |
// - app/<type>s/ | |
// - app/<podModulePrefix>/<type>s/ | |
if (dirname.startsWith(podsTypePath)) { | |
dirname = dirname.replace(podsTypePath, ""); | |
} else if (dirname.startsWith(toDirectoryBase)) { | |
dirname = dirname.replace(toDirectoryBase, ""); | |
} else if (isPodsPath) { | |
dirname = dirname.replace(podPath, ""); | |
} | |
const to = options.preserveName | |
? `${toDirectoryBase}${dirname}/${options.fileName}.js` | |
: `${toDirectoryBase}${dirname}.js`; | |
if (from === to) { | |
return; | |
} | |
conversions[toImportPath(from)] = toImportPath(to); | |
SEEN_MIGRATIONS.add(from); | |
migrations.push({ | |
from, | |
to, | |
}); | |
if (options.alsoMoveTemplate) { | |
const templatePath = getSiblingTemplatePath(from); | |
const to = options.preserveName | |
? `${toDirectoryBase}${dirname}/template.hbs` | |
: `${toDirectoryBase}${dirname}.hbs`; | |
if (templatePath) { | |
conversions[toImportPath(templatePath)] = toImportPath(to); | |
migrations.push({ | |
from: templatePath, | |
to, | |
}); | |
} | |
} | |
}); | |
if (options.fileName === "component") { | |
await migrateTemplateOnlyComponents(options); | |
} | |
} | |
async function fixImportPaths(migrations, conversions) { | |
console.log("fixing import paths"); | |
let hasFixedImports = false; | |
migrations.forEach((m) => { | |
if (m.from.endsWith(".js")) { | |
updateImportPaths(m, conversions); | |
hasFixedImports = hasFixedImports || m.hasChanges; | |
} | |
}); | |
if (hasFixedImports) { | |
console.log("done fixing import paths"); | |
if (!DRY_RUN) { | |
await execa( | |
`git add -A && git commit -m "migration: update file import paths"`, | |
{ shell: true, preferLocal: true } | |
); | |
} | |
} else { | |
console.log("no imports required fixing"); | |
} | |
} | |
async function renameFiles(migrations) { | |
console.log("renaming files"); | |
migrations.forEach((m) => { | |
// templates may be added twice when both route and controller exist | |
if (COMPLETED[m.from]) { | |
return; | |
} | |
COMPLETED[m.from] = true; | |
try { | |
fs.statSync(m.to); | |
throw new Error(`A File Already Exists ${m.to}`); | |
} catch (error) { | |
if (error.msg === `A File Already Exists ${m.to}`) { | |
throw error; | |
} else { | |
console.log(`\tsafely moving ${m.from} => ${m.to}`); | |
} | |
} | |
if (!DRY_RUN) { | |
makeDir(m.to); | |
fs.renameSync(m.from, m.to); | |
} | |
}); | |
console.log("done renaming files"); | |
if (!DRY_RUN) { | |
await execa( | |
`git add -A && git commit -m "migration: restructure file locations"`, | |
{ shell: true, preferLocal: true } | |
); | |
} | |
} | |
async function runAll() { | |
const status = await execa("git status", { shell: true, preferLocal: true }); | |
if (!/^nothing to commit/m.test(status.stdout)) { | |
console.log( | |
`Directory is not in a clean working state. Commiting any outstanding changes prior to running this script.` | |
); | |
if (!DRY_RUN) { | |
try { | |
await execa( | |
`git add -A && git commit -m "pre-migration: unsaved changes from working state prior to script exec"`, | |
{ shell: true, preferLocal: true } | |
); | |
} catch (error) { | |
console.log(error); | |
return; | |
} | |
} | |
} | |
for (let i = 0; i < TYPES.length; i++) { | |
const config = TYPES[i]; | |
console.log("Analyzing " + config.fileName); | |
await run(config); | |
} | |
await renameFiles(MIGRATIONS); | |
await fixImportPaths(MIGRATIONS, CONVERSIONS); | |
} | |
runAll(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment