@tobiastengler/create-relay-app
Version:
Easy configuration of Relay for existing projects
117 lines (112 loc) • 5.67 kB
JavaScript
import traverse from "@babel/traverse";
import path from "path";
import { REACT_RELAY_PACKAGE, RELAY_ENV_PROVIDER, RELAY_RUNTIME_PACKAGE } from "../../consts.js";
import { RelativePath } from "../../misc/RelativePath.js";
import { astToString, insertNamedImport, insertNamedImports, parseAst, prettifyCode } from "../../utils/ast.js";
import { bold } from "../../utils/cli.js";
import { TaskBase, TaskSkippedError } from "../TaskBase.js";
import t from "@babel/types";
import { removeExtension, hasRelayProvider, wrapJsxInRelayProvider } from "../cra/Cra_AddRelayEnvironmentProvider.js";
import { Next_AddTypeHelpers } from "./Next_AddTypeHelpers.js";
// todo: test this
const envCreationAndHydration = `
const environment = useMemo(initRelayEnvironment, []);
useEffect(() => {
const store = environment.getStore();
// Hydrate the store.
store.publish(new RecordSource(pageProps.initialRecords));
// Notify any existing subscribers.
store.notify();
}, [environment, pageProps.initialRecords])
`;
const APP_PROPS = "AppProps";
const RELAY_PAGE_PROPS = "RelayPageProps";
export class Next_AddRelayEnvironmentProvider extends TaskBase {
constructor(context) {
super();
this.context = context;
this.message = "Add " + RELAY_ENV_PROVIDER;
}
isEnabled() {
return this.context.is("next");
}
async run() {
const pagesMainFilename = "_app" + (this.context.args.typescript ? ".tsx" : ".js");
const possiblePagesMainFileLocations = [
path.join("pages", pagesMainFilename),
path.join("src", "pages", pagesMainFilename),
];
let pagesMainFile = null;
for (const possibleMainFileLocation of possiblePagesMainFileLocations) {
const file = this.context.env.rel(possibleMainFileLocation);
if (this.context.fs.exists(file.abs)) {
pagesMainFile = file;
break;
}
}
if (pagesMainFile) {
await this.setupPages(pagesMainFile);
}
else {
throw new Error(`${pagesMainFilename} could not be found`);
}
}
async setupPages(mainFile) {
this.updateMessage(this.message + " in " + bold(mainFile.rel));
const code = await this.context.fs.readFromFile(mainFile.abs);
const ast = parseAst(code);
let providerWrapped = false;
traverse.default(ast, {
JSXElement: (path) => {
// Find first JSX being returned *somewhere*.
if (providerWrapped || !path.parentPath.isReturnStatement()) {
return;
}
const functionReturn = path.parentPath;
const isProviderConfigured = hasRelayProvider(path);
if (isProviderConfigured) {
throw new TaskSkippedError("Already added");
}
// We need to modify the type of the _app arguments,
// starting with Next 12.3.
if (this.context.args.typescript) {
// Import RelayPageProps.
const relayTypesPath = Next_AddTypeHelpers.getRelayTypesPath(this.context);
const relayTypesImportPath = new RelativePath(mainFile.parentDirectory, removeExtension(relayTypesPath.abs));
insertNamedImport(path, RELAY_PAGE_PROPS, relayTypesImportPath.rel);
// Change argument of type AppProps to AppProps<RelayPageProps>.
const functionBodyPath = functionReturn.parentPath;
if (!functionBodyPath.isBlockStatement()) {
throw new Error("Expected parentPath to be a block statement.");
}
const functionPath = functionBodyPath.parentPath;
if (!functionPath.isFunctionDeclaration() || !t.isFunctionDeclaration(functionPath.node)) {
throw new Error("Expected parentPath to be a function declaration.");
}
const appPropsArg = functionPath.node.params[0];
if (!appPropsArg) {
throw new Error("Expected function to have one argument.");
}
const genericAppProps = t.genericTypeAnnotation(t.identifier(APP_PROPS), t.typeParameterInstantiation([t.genericTypeAnnotation(t.identifier(RELAY_PAGE_PROPS))]));
appPropsArg.typeAnnotation = t.typeAnnotation(genericAppProps);
}
insertNamedImports(path, ["useMemo", "useEffect"], "react");
insertNamedImport(path, "RecordSource", RELAY_RUNTIME_PACKAGE);
const relayEnvImportPath = new RelativePath(mainFile.parentDirectory, removeExtension(this.context.relayEnvFile.abs));
insertNamedImport(path, "initRelayEnvironment", relayEnvImportPath.rel);
functionReturn.addComment("leading", "--MARKER", true);
const envProviderId = t.jsxIdentifier(insertNamedImport(path, RELAY_ENV_PROVIDER, REACT_RELAY_PACKAGE).name);
wrapJsxInRelayProvider(path, envProviderId, t.identifier("environment"));
providerWrapped = true;
path.skip();
},
});
if (!providerWrapped) {
throw new Error("Could not find JSX");
}
let updatedCode = astToString(ast, code);
updatedCode = updatedCode.replace("//--MARKER", envCreationAndHydration);
updatedCode = prettifyCode(updatedCode);
await this.context.fs.writeToFile(mainFile.abs, updatedCode);
}
}