Skip to content

Instantly share code, notes, and snippets.

@fernandoabolafio
Created March 26, 2024 19:30
Show Gist options
  • Save fernandoabolafio/ee02543405b1ed3e2e50bda2db0ec7de to your computer and use it in GitHub Desktop.
Save fernandoabolafio/ee02543405b1ed3e2e50bda2db0ec7de to your computer and use it in GitHub Desktop.
Rewrite rmx-cli imports to their orginal packages
/* 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