import fs from "node:fs"; import path from "node:path"; const root = path.resolve(import.meta.dirname, ".."); const src = path.join(root, "src"); const app = path.join(src, "app"); const routesSrc = path.join(app, "routes"); function read(file) { return fs.readFileSync(file, "utf8"); } function write(file, content) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, content, "utf8"); } function rmrf(dir) { if (!fs.existsSync(dir)) return; fs.rmSync(dir, { recursive: true, force: true }); } function toDefaultExport(content, exportName) { let result = content; if (exportName) { result = result.replace( new RegExp(`export function ${exportName}\\b`), `export default function ${exportName}`, ); result = result.replace( new RegExp(`export const ${exportName}\\b`), `export default function ${exportName}`, ); } return result.replace(/^export \{ ([A-Za-z]+) as default \} from .+;\s*$/m, "").trimEnd() + "\n"; } function copyRoutesTree() { const entries = ["(auth)", "(protected)", "v"]; for (const entry of entries) { const from = path.join(routesSrc, entry); const to = path.join(app, entry); if (!fs.existsSync(from)) continue; rmrf(to); fs.cpSync(from, to, { recursive: true }); } } function updateProtectedLayout() { const layoutPath = path.join(app, "(protected)", "_layout.tsx"); let content = read(layoutPath); content = content.replace( "export default function ProtectedLayout()", "export default function AdminShellLayout()", ); write(layoutPath, content); } function mergePage( sourceRel, destRel, exportName, importReplacements = [], { namedOnly = false } = {}, ) { const source = path.join(app, sourceRel); if (!fs.existsSync(source)) { console.warn("Missing source:", sourceRel); return; } let content = read(source); for (const [from, to] of importReplacements) { content = content.split(from).join(to); } if (!namedOnly) { content = toDefaultExport(content, exportName); } write(path.join(app, destRel), content.endsWith("\n") ? content : content + "\n"); } function copyFile(sourceRel, destRel, importReplacements = []) { mergePage(sourceRel, destRel, null, importReplacements, { namedOnly: true }); } function writeReExport(destRel, componentPath, { nestedEdit = false } = {}) { const importPath = nestedEdit ? componentPath.replace(/^\.\//, "../") : componentPath; write(path.join(app, destRel), `export { default } from "${importPath}";\n`); } // --- infra --- write(path.join(src, "app-root.tsx"), read(path.join(app, "app-root.tsx"))); let main = read(path.join(src, "main.tsx")); main = main.replace("@/app/app-root", "@/app-root"); write(path.join(src, "main.tsx"), main); let vite = read(path.join(root, "vite.config.ts")); vite = vite.replace( 'routes: "./src/app/routes/**/[\\w[-]*.{jsx,tsx}"', 'routes: "./src/app/**/[\\w[-]*.{jsx,tsx}"', ); vite = vite.replace( 'modals: "./src/app/routes/**/[+]*.{jsx,tsx}"', 'modals: "./src/app/**/[+]*.{jsx,tsx}"', ); write(path.join(root, "vite.config.ts"), vite); copyRoutesTree(); updateProtectedLayout(); // --- accounts pilot --- mergePage("accounts/accounts-list-page.tsx", "(protected)/accounts/index.tsx", "AccountsListPage"); mergePage( "accounts/account-form-page.tsx", "(protected)/accounts/_components/account-form.tsx", "AccountFormPage", ); writeReExport("(protected)/accounts/new.tsx", "./_components/account-form"); writeReExport("(protected)/accounts/edit/[id].tsx", "./_components/account-form"); // --- contacts, employees, locations --- mergePage("contacts/contacts-list-page.tsx", "(protected)/contacts/index.tsx", "ContactsListPage"); mergePage( "contacts/contact-form-page.tsx", "(protected)/contacts/_components/contact-form.tsx", "ContactFormPage", ); writeReExport("(protected)/contacts/new.tsx", "./_components/contact-form"); writeReExport("(protected)/contacts/edit/[id].tsx", "./_components/contact-form"); mergePage( "employees/employees-list-page.tsx", "(protected)/employees/index.tsx", "EmployeesListPage", ); mergePage( "employees/employee-form-page.tsx", "(protected)/employees/_components/employee-form.tsx", "EmployeeFormPage", ); writeReExport("(protected)/employees/new.tsx", "./_components/employee-form"); writeReExport("(protected)/employees/edit/[id].tsx", "./_components/employee-form"); mergePage( "locations/locations-list-page.tsx", "(protected)/locations/index.tsx", "LocationsListPage", ); mergePage( "locations/location-form-page.tsx", "(protected)/locations/_components/location-form.tsx", "LocationFormPage", ); writeReExport("(protected)/locations/new.tsx", "./_components/location-form"); writeReExport("(protected)/locations/edit/[id].tsx", "./_components/location-form"); // --- assets, vendors, followups, pmschedules --- mergePage("assets/assets-list-page.tsx", "(protected)/assets/index.tsx", "AssetsListPage"); mergePage( "assets/asset-form-page.tsx", "(protected)/assets/_components/asset-form.tsx", "AssetFormPage", ); writeReExport("(protected)/assets/new.tsx", "./_components/asset-form"); writeReExport("(protected)/assets/edit/[id].tsx", "./_components/asset-form"); mergePage("vendors/vendors-list-page.tsx", "(protected)/vendors/index.tsx", "VendorsListPage"); mergePage( "vendors/vendor-form-page.tsx", "(protected)/vendors/_components/vendor-form.tsx", "VendorFormPage", [["@/app/vendors/components/vendor-portal-token-panel", "./vendor-portal-token-panel"]], ); copyFile( "vendors/components/vendor-portal-token-panel.tsx", "(protected)/vendors/_components/vendor-portal-token-panel.tsx", ); writeReExport("(protected)/vendors/new.tsx", "./_components/vendor-form"); writeReExport("(protected)/vendors/edit/[id].tsx", "./_components/vendor-form"); mergePage( "follow-ups/follow-ups-list-page.tsx", "(protected)/followups/index.tsx", "FollowUpsListPage", ); mergePage( "follow-ups/follow-up-form-page.tsx", "(protected)/followups/_components/follow-up-form.tsx", "FollowUpFormPage", ); writeReExport("(protected)/followups/new.tsx", "./_components/follow-up-form"); writeReExport("(protected)/followups/edit/[id].tsx", "./_components/follow-up-form"); mergePage( "pm-schedules/pm-schedules-list-page.tsx", "(protected)/pmschedules/index.tsx", "PmSchedulesListPage", ); mergePage( "pm-schedules/pm-schedule-form-page.tsx", "(protected)/pmschedules/_components/pm-schedule-form.tsx", "PmScheduleFormPage", ); writeReExport("(protected)/pmschedules/new.tsx", "./_components/pm-schedule-form"); writeReExport("(protected)/pmschedules/edit/[id].tsx", "./_components/pm-schedule-form"); // --- calendar, uplifts, vendor-pos, settings, dashboard, login --- mergePage("calendar/calendar-page.tsx", "(protected)/calendar/index.tsx", "CalendarPage"); mergePage( "calendar/event-form-page.tsx", "(protected)/calendar/_components/event-form.tsx", "EventFormPage", ); writeReExport("(protected)/calendar/new.tsx", "./_components/event-form"); writeReExport("(protected)/calendar/edit/[id].tsx", "./_components/event-form"); mergePage("uplifts/uplift-queue-page.tsx", "(protected)/uplifts/index.tsx", "UpliftQueuePage"); mergePage( "vendor-pos/vendor-pos-list-page.tsx", "(protected)/vendor-pos/index.tsx", "VendorPosListPage", ); mergePage( "vendor-pos/vendor-po-view-page.tsx", "(protected)/vendor-pos/[id].tsx", "VendorPoViewPage", ); mergePage( "settings/dropdown-options-page.tsx", "(protected)/settings/dropdowns.tsx", "DropdownOptionsPage", ); mergePage( "settings/task-templates-page.tsx", "(protected)/settings/task-templates.tsx", "TaskTemplatesPage", ); mergePage("dashboard/dashboard-page.tsx", "(protected)/index.tsx", "DashboardPage"); mergePage("dashboard/dashboard-page.tsx", "(protected)/dashboard.tsx", "DashboardPage"); mergePage("login/login-page.tsx", "(auth)/login.tsx", "LoginPage"); // --- workorders --- mergePage( "work-orders/work-orders-list-page.tsx", "(protected)/workorders/index.tsx", "WorkOrdersListPage", ); mergePage( "work-orders/work-order-form-page.tsx", "(protected)/workorders/_components/work-order-form.tsx", "WorkOrderFormPage", ); writeReExport("(protected)/workorders/new.tsx", "./_components/work-order-form"); writeReExport("(protected)/workorders/edit/[id].tsx", "./_components/work-order-form"); mergePage( "work-orders/work-order-view-page.tsx", "(protected)/workorders/[id].tsx", "WorkOrderViewPage", [ ["@/app/work-orders/components/dispatch-create-modal", "./_components/dispatch-create-modal"], ["@/app/work-orders/components/dispatch-detail-modal", "./_components/dispatch-detail-modal"], ], ); copyFile( "work-orders/components/dispatch-create-modal.tsx", "(protected)/workorders/_components/dispatch-create-modal.tsx", ); copyFile( "work-orders/components/dispatch-detail-modal.tsx", "(protected)/workorders/_components/dispatch-detail-modal.tsx", ); // --- vendor portal --- const vpComponents = path.join(app, "v", "_components"); fs.mkdirSync(vpComponents, { recursive: true }); copyFile("vendor-portal/vendor-portal-provider.tsx", "v/_components/vendor-portal-provider.tsx"); copyFile("vendor-portal/components/signature-pad.tsx", "v/_components/signature-pad.tsx"); fs.copyFileSync( path.join(app, "vendor-portal/vendor-portal.css"), path.join(vpComponents, "vendor-portal.css"), ); let vpLayout = read(path.join(app, "vendor-portal/vendor-portal-layout.tsx")); vpLayout = vpLayout .replace( "@/app/vendor-portal/vendor-portal-provider", "@/app/v/_components/vendor-portal-provider", ) .replace("@/app/vendor-portal/vendor-portal.css", "@/app/v/_components/vendor-portal.css") .replace("export function VendorPortalLayout", "export default function VendorPortalLayout"); write(path.join(app, "v", "[token]", "_layout.tsx"), vpLayout); mergePage( "vendor-portal/vendor-portal-dashboard-page.tsx", "v/[token]/dashboard.tsx", "VendorPortalDashboardPage", [["@/app/vendor-portal/vendor-portal-provider", "@/app/v/_components/vendor-portal-provider"]], ); mergePage("vendor-portal/vendor-portal-pos-page.tsx", "v/[token]/pos.tsx", "VendorPortalPosPage", [ ["@/app/vendor-portal/vendor-portal-provider", "@/app/v/_components/vendor-portal-provider"], ]); mergePage( "vendor-portal/vendor-portal-dispatch-page.tsx", "v/[token]/dispatch/[id].tsx", "VendorPortalDispatchPage", [ ["@/app/vendor-portal/components/signature-pad", "@/app/v/_components/signature-pad"], ["@/app/vendor-portal/vendor-portal-provider", "@/app/v/_components/vendor-portal-provider"], ], ); // --- cleanup old dirs --- const toDelete = [ path.join(app, "routes"), path.join(app, "app-root.tsx"), path.join(app, "store.js"), path.join(app, "slices"), path.join(app, "accounts"), path.join(app, "contacts"), path.join(app, "employees"), path.join(app, "locations"), path.join(app, "assets"), path.join(app, "vendors"), path.join(app, "follow-ups"), path.join(app, "pm-schedules"), path.join(app, "calendar"), path.join(app, "uplifts"), path.join(app, "vendor-pos"), path.join(app, "settings"), path.join(app, "dashboard"), path.join(app, "login"), path.join(app, "work-orders"), path.join(app, "vendor-portal"), ]; for (const dir of toDelete) { rmrf(dir); } console.log("Migration complete.");