Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/issue 371 similar query calls invariant error #383

Merged
3 changes: 1 addition & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,10 @@
"css-loader": "^2.1.1",
"css-to-string-loader": "^0.1.3",
"cssnano": "^4.1.10",
"deepmerge": "^4.2.2",
"file-loader": "^3.0.1",
"filewatcher-webpack-plugin": "^1.2.0",
"front-matter": "^3.0.1",
"fs-extra": "^8.1.0",
"glob-promise": "^3.4.0",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"html-webpack-plugin": "^3.2.0",
Expand All @@ -74,6 +72,7 @@
"webpack-merge": "^4.2.1"
},
"devDependencies": {
"glob-promise": "^3.4.0",
"rehype-autolink-headings": "^4.0.0",
"rehype-slug": "^3.0.0"
}
Expand Down
25 changes: 12 additions & 13 deletions packages/cli/src/data/cache.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const { ApolloClient } = require('apollo-client');
const createHttpLink = require('apollo-link-http').createHttpLink;
const crypto = require('crypto');
const fetch = require('node-fetch');
const fs = require('fs-extra');
const { gql } = require('apollo-server');
const InMemoryCache = require('apollo-cache-inmemory').InMemoryCache;
const path = require('path');
const { getQueryHash } = require('./common');

/* Extract cache server-side */
module.exports = async (req, context) => {
Expand All @@ -22,26 +22,25 @@ module.exports = async (req, context) => {

/* Take the same query from request, and repeat the query for our server side cache */
const { query, variables } = req.body;
const queryObj = gql`${query}`;

const { data } = await client.query({
query: gql`${query}`,
query: queryObj,
variables
});

if (data) {
const cache = JSON.stringify(client.extract());
const md5 = crypto.createHash('md5').update(query + JSON.stringify(variables)).digest('hex');

/* Get the requests entire (full) route and rootRoute to use as reference for designated cache directory */
const { origin, referer } = req.headers;
const fullRoute = referer.substring(origin.length, referer.length);
const rootRoute = fullRoute.substring(0, fullRoute.substring(1, fullRoute.length).indexOf('/') + 1);
const targetDir = path.join(context.publicDir, rootRoute);
const targetFile = path.join(targetDir, `${md5}-cache.json`);
const queryHash = getQueryHash(queryObj, variables);
const hashFilename = `${queryHash}-cache.json`;
const cachePath = `${context.publicDir}/${queryHash}-cache.json`;

if (!fs.existsSync(context.publicDir)) {
fs.mkdirSync(context.publicDir);
}

if (!fs.existsSync(targetFile)) {
fs.mkdirsSync(targetDir, { recursive: true });
fs.writeFileSync(path.join(targetFile), cache, 'utf8');
if (!fs.existsSync(cachePath)) {
fs.writeFileSync(path.join(context.publicDir, hashFilename), cache, 'utf8');
}
}
resolve();
Expand Down
14 changes: 6 additions & 8 deletions packages/cli/src/data/client.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { getQueryHash } from '@greenwood/cli/data/common';

const APOLLO_STATE = window.__APOLLO_STATE__; // eslint-disable-line no-underscore-dangle
const client = new ApolloClient({
cache: new InMemoryCache().restore(APOLLO_STATE),
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://localhost:4000'
})
});
const backupQuery = client.query;

client.query = (params) => {

if (APOLLO_STATE) {
// __APOLLO_STATE__ defined, in "SSG" mode...
const root = window.location.pathname.split('/')[1];
const rootSuffix = root === ''
? ''
: '/';

return fetch(`/${root}${rootSuffix}cache.json`)
const queryHash = getQueryHash(params.query, params.variables);
const cachePath = `/${queryHash}-cache.json`;

return fetch(cachePath)
.then(response => response.json())
.then((response) => {
// mock client.query response
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/data/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0#gistcomment-2775538
function hashString(queryKeysString) {
let h = 0;

for (let i = 0; i < queryKeysString.length; i += 1) {
h = Math.imul(31, h) + queryKeysString.charCodeAt(i) | 0; // eslint-disable-line no-bitwise
}

return Math.abs(h).toString();
}

function getQueryKeysFromSelectionSet(selectionSet) {
let queryKeys = '';

for (let key in selectionSet) {

if (key === 'selections') {
queryKeys += selectionSet[key]
.filter(selection => selection.name.value !== '__typename') // __typename is added by server.js
.map(selection => selection.name.value).join('');
}
}

if (selectionSet.kind === 'SelectionSet') {
selectionSet.selections.forEach(selection => {
if (selection.selectionSet) {
queryKeys += getQueryKeysFromSelectionSet(selection.selectionSet);
}
});
}

return queryKeys;
}

function getQueryHash(query, variables = {}) {
const queryKeys = getQueryKeysFromSelectionSet(query.definitions[0].selectionSet);
const variableValues = Object.keys(variables).length > 0
? `_${Object.values(variables).join('').replace(/\//g, '')}` // handle / which will translate to filepaths
: '';

return hashString(`${queryKeys}${variableValues}`);
}

module.exports = {
getQueryHash
};
30 changes: 1 addition & 29 deletions packages/cli/src/lifecycles/serialize.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,9 @@
const BrowserRunner = require('../lib/browser');
const dataServer = require('../data/server');
const deepmerge = require('deepmerge');
const fs = require('fs-extra');
const glob = require('glob-promise');
const LocalWebServer = require('local-web-server');
const path = require('path');

const setDataForPages = async (context) => {
const { publicDir } = context;
const pages = await glob.promise(path.join(publicDir, '**/**/index.html'));

pages.forEach((pagePath) => {
const contents = fs.readFileSync(pagePath, 'utf-8');
const pageRoot = pagePath.replace(publicDir, '').split('/')[1];
const cacheRoot = pageRoot === 'index.html'
? ''
: `${pageRoot}`;
let cacheContents = {};

glob.sync(`${publicDir}/${cacheRoot}/*-cache.json`).forEach((file) => {
cacheContents = deepmerge(cacheContents, require(file));
});

const serialzedCacheContents = JSON.stringify(cacheContents);

fs.writeFileSync(`${publicDir}/${cacheRoot}/cache.json`, serialzedCacheContents);
fs.writeFileSync(pagePath, contents.replace('___DATA___', serialzedCacheContents));
});
};

module.exports = serializeBuild = async (compilation) => {
const browserRunner = new BrowserRunner();
const localWebServer = new LocalWebServer();
Expand All @@ -50,7 +25,7 @@ module.exports = serializeBuild = async (compilation) => {
.replace(polyfill, '')
.replace('<script></script>', `
<script data-state="apollo">
window.__APOLLO_STATE__=___DATA___;
window.__APOLLO_STATE__ = true;
</script>
`);

Expand Down Expand Up @@ -93,9 +68,6 @@ module.exports = serializeBuild = async (compilation) => {
browserRunner.close();
webServer.close();

// loop through all index.html files and inject cache
await setDataForPages(compilation.context);

resolve();
} catch (err) {
reject(err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const TestBed = require('../../../../../test/test-bed');

describe('Build Greenwood With: ', function() {
const LABEL = 'Data from GraphQL and using Custom Frontmatter Data';
const apolloStateRegex = /window.__APOLLO_STATE__=({.*?});/;
const apolloStateRegex = /window.__APOLLO_STATE__ = true/;
let setup;

before(async function() {
Expand All @@ -54,14 +54,18 @@ describe('Build Greenwood With: ', function() {
expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'first-post', 'index.html'))).to.be.true;
});

it('should output one cache.json file', async function() {
expect(await glob.promise(path.join(this.context.publicDir, 'blog', 'cache.json'))).to.have.lengthOf(1);
it('should output a (partial) *-cache.json file, one per each query made', async function() {
expect(await glob.promise(path.join(this.context.publicDir, './*-cache.json'))).to.have.lengthOf(3);
});

it('should output one cache.json file to be defined', function() {
const cacheContents = require(path.join(this.context.publicDir, 'blog', 'cache.json'));
it('should output a (partial) *-cache.json files, one per each query made, that are all defined', async function() {
const cacheFiles = await glob.promise(path.join(this.context.publicDir, './*-cache.json'));

expect(cacheContents).to.not.be.undefined;
cacheFiles.forEach(file => {
const cache = require(file);

expect(cache).to.not.be.undefined;
});
});

it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => {
Expand Down
Loading