Skip to content

Instantly share code, notes, and snippets.

@runspired
Created July 1, 2022 17:29
Show Gist options
  • Save runspired/5d951e242568655431bf4b990c9ed666 to your computer and use it in GitHub Desktop.
Save runspired/5d951e242568655431bf4b990c9ed666 to your computer and use it in GitHub Desktop.
/* 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