af / apollo-local-query

Simpler server rendering with apollo-client 1.x, using a local GraphQL networkInterface
MIT License
65 stars 7 forks source link

Installation problem: Dependencies not available after installing the package (context: Nextjs / Redux / Apollo / Koa) #10

Closed TLevesque closed 7 years ago

TLevesque commented 7 years ago

Hi, Apollo-local-query looks to be a great package for me to avoid go out and back through the networking stack, before I'm looking to gain some speed to render the data of my queries.

Unfortunately, I'm facing some troubles to install it in my context:

After doing the installation as presented in the readme, I'm getting the following error, which says that the dependencies related to the Koa server:

ERROR  Failed to compile with 18 errors

These modules were not found:
* child_process in ./node_modules/stripe/lib/stripe.js, ./node_modules/os-locale/index.js
* net in ./node_modules/forever-agent/index.js, ./node_modules/tunnel-agent/index.js and 2 others
* tls in ./node_modules/forever-agent/index.js, ./node_modules/tunnel-agent/index.js and 1 other
* module in ./node_modules/require_optional/node_modules/resolve-from/index.js
* fs in ./node_modules/nconf/lib/nconf.js, ./node_modules/y18n/index.js and 6 others

To install them, you can run: npm install --save child_process net tls module fs

My working code before the installation was looking like this:

import { ApolloClient, createNetworkInterface } from 'react-apollo';
import fetch from 'isomorphic-fetch';

let apolloClient = null;

// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch;
}

function create() {
  return new ApolloClient({
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    networkInterface: createNetworkInterface({
      uri: '/graphql',
      opts: {
        credentials: 'include'
      }
    })
  });
}

export default function initApollo() {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)
  if (!process.browser) {
    return create();
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create();
  }

  return apolloClient;
}

After the installation of apollo-local-query, it was looking like this:

import { ApolloClient, createNetworkInterface } from 'react-apollo';
//  >>I've tried with it too import fetch from 'isomorphic-fetch';
import { createLocalInterface } from 'apollo-local-query';
import * as graphql from 'graphql'; >> // I've also tried with: import graphql from 'graphql';
import schema from '../../server/graphql/schema';

const isServer = !process.browser;
const options = { credentials: 'include' };

if (isServer) {
  options.networkInterface = createLocalInterface(graphql, schema);
  options.ssrMode = true;
}

const myClient = new ApolloClient(options)

export default function initApollo() {
  return myClient;
}

For information, my schema.js is looking like this:

const { makeExecutableSchema } = require('graphql-tools');

const resolvers = require('./resolvers');
const Schemas = require('./schemas');
const { User, Service } = require('./types');

const Queries = `
  type Query {
    ${User.queries}
    ${Service.queries}
  }
`;

const Mutations = `
  type Mutation {
    ${User.mutations}
    ${Service.mutations}
  }
`;

const Scalars = `
  scalar Date
`;

const Inputs = `
  input TranslationInput {
    en: String
    es: String
    ca: String
    fr: String
  }
// ... With others inputs
`;

module.exports = makeExecutableSchema({
  typeDefs: [Queries, Mutations, Scalars, Inputs, ...Schemas],
  resolvers
});

The file where I call the "initApollo" function to pass the ApolloClient to the Apollo Provider is looking like this:

import React from 'react'
import PropTypes from 'prop-types'
import { ApolloProvider, getDataFromTree } from 'react-apollo'
import Head from 'next/head'
import initApollo from './apollo'
import initRedux from './store'

// Gets the display name of a JSX component for dev tools
function getComponentDisplayName (Component) {
  return Component.displayName || Component.name || 'Unknown'
}

export default ComposedComponent => {
  return class WithData extends React.Component {
    static displayName = `WithData(${getComponentDisplayName(ComposedComponent)})`
    static propTypes = {
      serverState: PropTypes.object.isRequired
    }

    static async getInitialProps (ctx) {
      let serverState = {}

      // Evaluate the composed component's getInitialProps()
      let composedInitialProps = {}
      if (ComposedComponent.getInitialProps) {
        composedInitialProps = await ComposedComponent.getInitialProps(ctx)
      }

      // Run all GraphQL queries in the component tree
      // and extract the resulting data
      if (!process.browser) {
        const apollo = initApollo()
        const redux = initRedux(apollo, { test: { also: 'this' } })
        // Provide the `url` prop data in case a GraphQL query uses it
        const url = {query: ctx.query, pathname: ctx.pathname}

        try {
          // Run all GraphQL queries
          await getDataFromTree(
            // No need to use the Redux Provider
            // because Apollo sets up the store for us
            <ApolloProvider client={apollo} store={redux}>
              <ComposedComponent url={url} {...composedInitialProps} />
            </ApolloProvider>
          )
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error
        }
        // getDataFromTree does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind()

        // Extract query data from the store
        const state = redux.getState()

        // No need to include other initial Redux state because when it
        // initialises on the client-side it'll create it again anyway
        serverState = {
          apollo: { // Only include the Apollo data state
            data: state.apollo.data
          }
        }
      }

      return {
        serverState,
        ...composedInitialProps
      }
    }

    constructor (props) {
      super(props)
      this.apollo = initApollo()
      this.redux = initRedux(this.apollo, this.props.serverState)
    }

    render () {
      return (
        // No need to use the Redux Provider
        // because Apollo sets up the store for us
        <ApolloProvider client={this.apollo} store={this.redux}>
          <ComposedComponent {...this.props} />
        </ApolloProvider>
      )
    }
  }
}

And my server.js with Koa is looking like this:

const IntlPolyfill = require('intl');
const Koa = require('koa');
const convert = require('koa-convert');
const router = require('./routes.js');
const session = require('koa-generic-session');
const passport = require('koa-passport');
const bodyParser = require('koa-bodyparser');
const accepts = require('accepts');
const next = require('next');
const { graphqlKoa, graphiqlKoa } = require('graphql-server-koa');
const { basename } = require('path');
const glob = require('glob');
const { readFileSync } = require('fs');
const en = require('../helpers/intl/messages/en.js');
const es = require('../helpers/intl/messages/en.js');
const ca = require('../helpers/intl/messages/en.js');
const fr = require('../helpers/intl/messages/en.js');

const messages = { en, es, ca, fr };
// const compression = require('compression');

require('./auth');

const { Company } = require('./models');
const schema = require('./graphql/schema');
const nconf = require('../env/nconf');

Intl.NumberFormat = IntlPolyfill.NumberFormat;
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;

const port = process.env.PORT || nconf.get('PORT');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

const languages = glob.sync('./helpers/intl/messages/*.js').map(f => basename(f, '.js'));
const localeDataCache = new Map();
const getLocaleDataScript = (locale) => {
  // const lang = locale.split('-')[0]
  const lang = locale;
  if (!localeDataCache.has(lang)) {
    const localeDataFile = require.resolve(`react-intl/locale-data/${lang}`);
    const localeDataScript = readFileSync(localeDataFile, 'utf8');
    localeDataCache.set(lang, localeDataScript);
  }
  return localeDataCache.get(lang);
};

const getMessages = (locale, companyMessages) =>
  Object.assign({}, messages[locale], companyMessages);

app.keys = [nconf.get('APP_KEYS')];

app.prepare()
.then(() => {
  const server = new Koa();

  server
  .use(bodyParser())
  .use(convert(session()))
  .use(passport.initialize())
  .use(passport.session())
  .use(router.routes())
  .use(router.allowedMethods());
  // .use(compression())

  router.post('/graphql', graphqlKoa(async (ctx) => {
    const host = ctx.req.headers.host;
    const company = await Company.findOne({ website_url: host.replace('www.', '') });
    return ({
      schema,
      context: {
        ctx,
        user: ctx.state.user,
        company
      }
    });
  }));

  router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' }));

  router.get('*', async (ctx) => {
    const host = ctx.req.headers.host;
    const accept = accepts(ctx.req);
    const locale = accept.language(languages);
    const company = await Company.findOne({ website_url: host.replace('www.', '') });
    ctx.req.company = company;
    ctx.req.locale = locale;
    ctx.req.localeDataScript = getLocaleDataScript(locale);
    ctx.req.messages = getMessages(locale, company.website[locale]);
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
  });

  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});

Does anyone have an idea how to fix this issue? If someone succeed to give me some hint about how to fix it, I'm proposing myself to produce some extra documentation for apollo-local-query to illustrate the setup for this context! ;)

Thank for your help!

af commented 7 years ago

That's pretty weird, from the installation error you received, it looks like the compiler process isn't recognizing the node built-in modules (fs, child_process, etc). I'm guessing that you are importing/requiring some server-side code from a client-side module, and that's where the error is coming from.

If you haven't found a fix already, try replacing these two lines:

import { createLocalInterface } from 'apollo-local-query';
import * as graphql from 'graphql'; >> // I've also tried with: import graphql from 'graphql';

with require() calls placed inside the if (isServer) block. If you're using webpack it should be smart enough to not follow those requires when bundling your client side code.

Hope that helps, I'm closing since this looks like a build system problem but let me know if that helps to resolve the issue.