Useful or not, from you.
relay Hack Idea: Runtime Relay Compiler

In this issue I'm proposing an in-browser version of the Relay compiler which could allow people to try out Relay (with some restrictions) without needing to setup Babel transform or compiler. I'm curious to hear from the community how valuable (or not) and option like this would be given the limitations.


Motivation: We believe that Relay’s main impediment to broader OSS adoption, and thus broader industry impact is the complexity of the initial setup. The largest part of this is the requirement that new users must get the compiler and Babel transform working for their code base before they even start to try Relay. If we could allow users to try Relay’s APIs first and only configure the compiler and Babel transform after/if they are convinced of its value, we could increase our industry impact.

Hypothesis If a user can abide by the following restrictions, we can support most of Relay’s existing APIs.

  1. All graphql tagged template literals must be in the module scope
  2. Schema must be available at runtime
  3. No TypeScript/Flow support
  4. No code is lazily loaded*

Summary: We could provide a new NPM module which exposes a custom graphql tagged template literal which collects module scoped GraphQL strings and — right before we begin rendering — transforms them to Relay runtime modules using a Wasm version of the compiler.

Proposed Implementation

We create a new NPM module which exposes a graphql tagged template and a compile(schema: string) function. The new module maintains a module scope registry (map object) from each GraphQL document string to that document’s runtime object. When the graphql tagged template is called it immediately creates an empty object (to be mutated later), adds it to the registry and returns that empty object.

After all modules have been loaded, but before we start to render — for example right before the initial render of the app — the user must call the new module’s compile(schema) function.

When the compile(schema) function is called, it collects up all the documents that have been seen so far and the schema string and calls a method on a Wasm version of the compiler. The compiler returns an array of strings (one for each document, in the same order), each string contains the JavaScript for that document’s runtime module. Those modules are eval()ed and the resulting module is merged into the empty object (Object.assign()) created by the call to the graphql tagged template.

The result is that the in-browser compiler would have access to all GraphQL documents and by the time any of the Relay runtime data is read by Relay, it would be available.

Rough Code for the New Module:

import * as wasm from 'relay-compiler-playground';

const DOCUMENT_MAP = {};

function graphql(args) {
  const text = args[0];
  const runtimeValue = {};
  DOCUMENT_MAP[text] = runtimeValue;
  return runtimeValue;
}

export function compile(schema) {
  const docs = [];
  const runtimes = [];
  for (const [doc, runtime] of Object.entries(DOCUMENT_MAP)) {
    docs.push(doc);
    runtimes.push(runtime);
  }
  const compiled = wasm.compile(schema, docs);
  compiled.forEach((runtime, i) => {
    Object.assign(runtimes[i], runtime);
  });
}

export default graphql;

A Component Using the New Module

import { useLazyLoadQuery } from 'react-relay/hooks';
import graphql from 'NEW_MODULE'; // Differet import
import Issues from './Issues';
import React from 'react';

// Only constraint: We define the query in module scope
const QUERY = graphql`
  query HomeRootIssuesQuery($owner: String!, $name: String!) {
    repository(owner: $owner, name: $name) {
      ...Issues_repository
    }
  }
`;

export default function HomeRoot() {
  const data = useLazyLoadQuery(QUERY, {
    owner: 'facebook',
    name: 'relay',
  });
  const { repository } = data;

  return <Issues repository={repository} />;
}

A Project Root Using the New Module

import React from 'react';
import ReactDOM from 'react-dom';
import { RelayEnvironmentProvider } from 'react-relay/hooks';
import RelayEnvironment from './RelayEnvironment';
import HomeRoot from './HomeRoot';

// These three lines can be removed once
// you upgrade to the real compiler
import { compile } from 'NEW_MODULE';
import SCHEMA from './schema';
compile(SCHEMA);

ReactDOM.createRoot(document.getElementById('root')).render(
  <RelayEnvironmentProvider environment={RelayEnvironment}>
    <React.Suspense fallback={'Loading...'}>
      <HomeRoot />
    </React.Suspense>
  </RelayEnvironmentProvider>,
);

Arguments In Favor

Features of this proposal:

  1. Can be implemented as a separate module and does not involve any changes to the Relay Runtime
  2. Upgrading to the real compiler — once you have the build-time stuff setup — is just a matter of changing how you import the graphql tagged template and removing the runtime call to compile(schema)

Arguments Against

Reasons we might not want to consider this proposal:

  1. Part of Relay’s value proposition is the entrypoints-like behavior where code and data are loaded in parallel. This is not possible to implement without lazy loading component code and thus would not work with this simple proposal. (That said, it may be possible to allow lazy loaded components by rerunning the compiler after each entrypoint is loaded)
  2. Relay features which create non-type imports/require calls in the runtime module would not work, and it may be difficult to communicate that limitation.
  3. Compiler errors would need to be presented in the console and could not point directly to file/line numbers
  4. Optimizations like lazy module evaluation would break this approach

Overall, the limitations of this setup might be too complex to communicate via documentation or runtime checks.

That's a useful answer
Without any help

The idea would be to allow people to try it out in their own app without needing to setup the compiler or Babel transform.

For trying out a demo app we have https://codesandbox.io/s/relay-sandbox-nxl7i?file=/src/TodoApp.tsx

Good point about the pain of spinning up a server being a big pain point. Are there any public GraphQL servers that have permissive CORS headers?