Created
March 26, 2024 19:30
-
-
Save fernandoabolafio/ee02543405b1ed3e2e50bda2db0ec7de to your computer and use it in GitHub Desktop.
Rewrite rmx-cli imports to their orginal packages
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-param-reassign */ | |
/** | |
* DESCRIPTION: | |
* This script reverts the imports from the centralized file created by https://github.com/kiliman/rmx-cli | |
*/ | |
import fs from 'fs'; | |
import path from 'path'; | |
import ts from 'typescript'; | |
// ===> Change these values to match your project <=== | |
const APP_SRC_DIR = path.join(process.cwd(), '../app'); | |
const CURRENT_IMPORT_ALIAS = '~/remix'; | |
const REGEX_EXP = new RegExp(/import\s+\{[^}]+\}\s+from\s+['"]~\/remix['"];/g); | |
function createNamedImports(typeImports, regularImports, pkg) { | |
return ts.factory.createImportDeclaration( | |
undefined, | |
ts.factory.createImportClause( | |
false, | |
undefined, | |
ts.factory.createNamedImports([ | |
...typeImports.map((imp) => | |
ts.factory.createImportSpecifier( | |
true, | |
undefined, | |
ts.factory.createIdentifier(imp), | |
), | |
), | |
...regularImports.map((imp) => | |
ts.factory.createImportSpecifier( | |
false, | |
undefined, | |
ts.factory.createIdentifier(imp), | |
), | |
), | |
]), | |
), | |
ts.factory.createStringLiteral(pkg), | |
undefined, | |
); | |
} | |
const importMap = { | |
'@remix-run/node': { | |
regular: [ | |
'MaxPartSizeExceededError', | |
'NodeOnDiskFile', | |
'broadcastDevReady', | |
'createCookie', | |
'createCookieSessionStorage', | |
'createFileSessionStorage', | |
'createMemorySessionStorage', | |
'createReadableStreamFromReadable', | |
'createRequestHandler', | |
'createSession', | |
'createSessionStorage', | |
'defer', | |
'installGlobals', | |
'isCookie', | |
'isSession', | |
'json', | |
'logDevReady', | |
'readableStreamToString', | |
'redirect', | |
'redirectDocument', | |
'unstable_composeUploadHandlers', | |
'unstable_createFileUploadHandler', | |
'unstable_createMemoryUploadHandler', | |
'unstable_parseMultipartFormData', | |
'writeAsyncIterableToWritable', | |
'writeReadableStreamToWritable', | |
], | |
types: [ | |
'ActionFunction', | |
'ActionFunctionArgs', | |
'AppLoadContext', | |
'Cookie', | |
'CookieOptions', | |
'CookieParseOptions', | |
'CookieSerializeOptions', | |
'CookieSignatureOptions', | |
'DataFunctionArgs', | |
'EntryContext', | |
'ErrorResponse', | |
'HandleDataRequestFunction', | |
'HandleDocumentRequestFunction', | |
'HandleErrorFunction', | |
'HeadersArgs', | |
'HeadersFunction', | |
'HtmlLinkDescriptor', | |
'JsonFunction', | |
'LinkDescriptor', | |
'LinksFunction', | |
'LoaderFunction', | |
'LoaderFunctionArgs', | |
'MemoryUploadHandlerFilterArgs', | |
'MemoryUploadHandlerOptions', | |
'MetaArgs', | |
'MetaDescriptor', | |
'MetaFunction', | |
'PageLinkDescriptor', | |
'RequestHandler', | |
'SerializeFrom', | |
'ServerBuild', | |
'ServerEntryModule', | |
'Session', | |
'SessionData', | |
'SessionIdStorageStrategy', | |
'SessionStorage', | |
'SignFunction', | |
'TypedDeferredData', | |
'TypedResponse', | |
'UnsignFunction', | |
'UploadHandler', | |
'UploadHandlerPart', | |
], | |
}, | |
'@remix-run/react': { | |
regular: [ | |
'Await', | |
'Form', | |
'Link', | |
'Links', | |
'LiveReload', | |
'Meta', | |
'NavLink', | |
'Navigate', | |
'NavigationType', | |
'Outlet', | |
'PrefetchPageLinks', | |
'RemixBrowser', | |
'RemixServer', | |
'Route', | |
'Routes', | |
'Scripts', | |
'ScrollRestoration', | |
'UNSAFE_RemixContext', | |
'createPath', | |
'createRoutesFromChildren', | |
'createRoutesFromElements', | |
'createSearchParams', | |
'generatePath', | |
'isRouteErrorResponse', | |
'matchPath', | |
'matchRoutes', | |
'parsePath', | |
'renderMatches', | |
'resolvePath', | |
'unstable_usePrompt', | |
'unstable_useViewTransitionState', | |
'useActionData', | |
'useAsyncError', | |
'useAsyncValue', | |
'useBeforeUnload', | |
'useBlocker', | |
'useFetcher', | |
'useFetchers', | |
'useFormAction', | |
'useHref', | |
'useInRouterContext', | |
'useLinkClickHandler', | |
'useLoaderData', | |
'useLocation', | |
'useMatch', | |
'useMatches', | |
'useNavigate', | |
'useNavigation', | |
'useNavigationType', | |
'useOutlet', | |
'useOutletContext', | |
'useParams', | |
'useResolvedPath', | |
'useRevalidator', | |
'useRouteError', | |
'useRouteLoaderData', | |
'useRoutes', | |
'useSearchParams', | |
'useSubmit', | |
], | |
types: [ | |
'AwaitProps', | |
'Blocker', | |
'BlockerFunction', | |
'ClientActionFunction', | |
'ClientActionFunctionArgs', | |
'ClientLoaderFunction', | |
'ClientLoaderFunctionArgs', | |
'Fetcher', | |
'FetcherWithComponents', | |
'FormEncType', | |
'FormMethod', | |
'FormProps', | |
'LinkProps', | |
'Location', | |
'NavLinkProps', | |
'NavigateFunction', | |
'Navigation', | |
'Params', | |
'Path', | |
'RemixBrowserProps', | |
'RemixServerProps', | |
'ShouldRevalidateFunction', | |
'ShouldRevalidateFunctionArgs', | |
'SubmitFunction', | |
'SubmitOptions', | |
'UIMatch', | |
'UNSAFE_AssetsManifest', | |
'UNSAFE_EntryRoute', | |
], | |
}, | |
'remix-typedjson': { | |
regular: [ | |
'TypedAwait', | |
'applyMeta', | |
'deserialize', | |
'deserializeRemix', | |
'parse', | |
'registerCustomType', | |
'serialize', | |
'stringify', | |
'stringifyRemix', | |
'typeddefer', | |
'typedjson', | |
'useTypedActionData', | |
'useTypedFetcher', | |
'useTypedLoaderData', | |
'useTypedRouteLoaderData', | |
], | |
types: [ | |
'MetaType', | |
'RemixSerializedType', | |
'TypedAwaitProps', | |
'TypedFetcherWithComponents', | |
'TypedJsonResponse', | |
'TypedJsonResult', | |
'TypedMetaFunction', | |
'UseDataFunctionReturn', | |
], | |
}, | |
}; | |
/** | |
* for a given file, capture all named imports from 'currentImportAlias' and transform them to the correct import path | |
* by matching each named import with the respective import package. | |
* For example: | |
* import { SerializeFrom, useLoaderData } from '~/remix' | |
* should become: | |
* import { type SerializeFrom } from '@remix-run/node' | |
* import { useLoaderData } from '@remix-run/react' | |
*/ | |
function getNewImports(sourceFile) { | |
let newImports = []; | |
const transformer = (context) => { | |
const visit = (node) => { | |
node = ts.visitEachChild(node, visit, context); | |
if ( | |
ts.isStatement(node) && | |
ts.isImportDeclaration(node) && | |
ts.isStringLiteral(node.moduleSpecifier) && | |
node.moduleSpecifier.text === CURRENT_IMPORT_ALIAS | |
) { | |
const allImports = node.importClause.namedBindings.elements.map( | |
(element) => element.name.escapedText, | |
); | |
// generate a new import map containing only the imports that are in the current file | |
const filteredImportMap = Object.entries(importMap).reduce( | |
(acc, [pkg, { types, regular }]) => { | |
const filteredTypes = types.filter((type) => | |
allImports.includes(type), | |
); | |
const filteredRegular = regular.filter((regularImport) => | |
allImports.includes(regularImport), | |
); | |
if (filteredTypes.length || filteredRegular.length) { | |
acc[pkg] = { | |
types: filteredTypes, | |
regular: filteredRegular, | |
}; | |
} | |
return acc; | |
}, | |
{}, | |
); | |
// create new import declarations for each package | |
newImports = Object.entries(filteredImportMap).flatMap( | |
([pkg, { types, regular }]) => { | |
return createNamedImports(types, regular, pkg, true); | |
}, | |
); | |
} | |
return node; | |
}; | |
return (node) => ts.visitNode(node, visit); | |
}; | |
ts.transform(sourceFile, [transformer]); | |
return newImports; | |
} | |
// this function needs to walk deep into the directory and find all files that are using the currentImportAlias | |
// make sure to only match ts or tsx files | |
function findAllTargetFiles(dir, callback) { | |
fs.readdirSync(dir).forEach((file) => { | |
const filePath = path.join(dir, file); | |
if (fs.statSync(filePath).isDirectory()) { | |
findAllTargetFiles(filePath, callback); | |
return; | |
} | |
if ( | |
file.endsWith('.ts') || | |
(file.endsWith('.tsx') && | |
!file.endsWith('.test.ts') && | |
!file.endsWith('.test.tsx')) | |
) { | |
callback(filePath); | |
} | |
}); | |
} | |
function updateFileImports(file) { | |
console.log('updating file: ', file); | |
const program = ts.createProgram([file], {}); | |
const source = program.getSourceFile(file); | |
const newImports = getNewImports(source); | |
// replace new imports in the source file by using a regex replace | |
const content = fs.readFileSync(file, 'utf-8'); | |
const matches = content.match(REGEX_EXP); | |
if (!matches) { | |
return; | |
} | |
console.log('file matched!'); | |
const newContent = content.replace( | |
REGEX_EXP, | |
newImports | |
.map((imp) => | |
ts.createPrinter().printNode(ts.EmitHint.Unspecified, imp, source), | |
) | |
.join('\n'), | |
); | |
const final = newContent; | |
fs.writeFileSync(file, final, 'utf-8'); | |
} | |
findAllTargetFiles(APP_SRC_DIR, updateFileImports); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment