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

enhancement/issue 629 cache unchanged assets in development #760

Merged
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"acorn-walk": "^8.0.0",
"commander": "^2.20.0",
"cssnano": "^5.0.11",
"es-module-shims": "^0.5.2",
"es-module-shims": "^1.2.0",
"front-matter": "^4.0.2",
"koa": "^2.13.0",
"livereload": "^0.9.1",
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/lib/hashing-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0#gistcomment-2775538
function hashString(inputString) {
let h = 0;

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

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

export {
hashString
};
33 changes: 32 additions & 1 deletion packages/cli/src/lifecycles/serve.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BrowserRunner } from '../lib/browser.js';
import fs from 'fs';
import { hashString } from '../lib/hashing-utils.js';
import path from 'path';
import Koa from 'koa';
import { ResourceInterface } from '../lib/resource-interface.js';
Expand Down Expand Up @@ -88,7 +89,7 @@ async function getDevServer(compilation) {
});

// allow intercepting of urls (response)
app.use(async (ctx) => {
app.use(async (ctx, next) => {
const responseAccumulator = {
body: ctx.body,
contentType: ctx.response.headers['content-type']
Expand Down Expand Up @@ -120,6 +121,36 @@ async function getDevServer(compilation) {

ctx.set('Content-Type', reducedResponse.contentType);
ctx.body = reducedResponse.body;

await next();
});

// ETag Support - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
// https://stackoverflow.com/questions/43659756/chrome-ignores-the-etag-header-and-just-uses-the-in-memory-cache-disk-cache
app.use(async (ctx) => {
const body = ctx.response.body;
const { url } = ctx;

// don't interfere with external requests or API calls
// and only run in development
if (process.env.__GWD_COMMAND__ === 'develop' && path.extname(url) !== '' && url.indexOf('http') !== 0) { // eslint-disable-line no-underscore-dangle
if (Buffer.isBuffer(body)) {
// console.warn(`no body for => ${ctx.url}`);
} else {
const inm = ctx.headers['if-none-match'];
const etagHash = hashString(body);

if (inm && inm === etagHash) {
ctx.status = 304;
ctx.body = null;
ctx.set('Etag', etagHash);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there's a reason to not ever to set the ETag, or what the implications of setting it all the time would be? 🤔

ctx.set('Cache-Control', 'no-cache');
} else if (!inm || inm !== etagHash) {
ctx.set('Etag', etagHash);
}
}
}

});

return Promise.resolve(app);
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/plugins/resource/plugin-node-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ class NodeModulesResource extends ResourceInterface {
// for each entry found in dependencies, find its entry point
// then walk its entry point (e.g. index.js) for imports / exports to add to the importMap
// and then walk its package.json for transitive dependencies and all those import / exports
await walkPackageJson(userPackageJson);
if (Object.keys(importMap).length === 0) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if there is something more elegant other than initializing importMap = null, but effectively walking no dependencies should be almost zero cost as it is? Ideally for someone with a handful of dependencies, walking that everytime would be annoying.

Another thought, if we only need to do this once, then in theory we should also be able to include this in the should intercept logic instead?

 async shouldIntercept(url, body, headers) {
    return Promise.resolve(Object.keys(importMap).length === 0 && headers.response['content-type'] === 'text/html');
  }

await walkPackageJson(userPackageJson);
}

// apply import map and shim for users
newContents = newContents.replace('<head>', `
Expand Down
11 changes: 1 addition & 10 deletions packages/plugin-graphql/src/core/common.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
// 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();
}
import { hashString } from '@greenwood/cli/src/lib/hashing-utils.js';

function getQueryHash(query, variables = {}) {
const queryKeys = query;
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-graphql/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class GraphQLResource extends ResourceInterface {
: ',';
const shimmedBody = body.replace('"imports": {', `
"imports": {
"@greenwood/cli/src/lib/hashing-utils.js": "/node_modules/@greenwood/cli/src/lib/hashing-utils.js",
"@greenwood/plugin-graphql/core/client": "/node_modules/@greenwood/plugin-graphql/src/core/client.js",
"@greenwood/plugin-graphql/core/common": "/node_modules/@greenwood/plugin-graphql/src/core/common.js",
"@greenwood/plugin-graphql/queries/children": "/node_modules/@greenwood/plugin-graphql/src/queries/children.gql",
Expand Down
1 change: 1 addition & 0 deletions www/pages/about/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ During _development_ the CLI will:
- Process requests on the fly only for the content or code you need for a given page.
- Supports loading dependencies from _node_modules_ using an [`importMap`](https://github.com/WICG/import-maps) to avoid bundling.
- While Greenwood is ESM first, we have a [plugin](/plugins/custom-plugins/) to transform CommonJS into ESM (🤞)
- Leverage `E-Tag` headers to apply [caching techniques for unchanged assets](/blog/release/v0-24-0/#local-development-enhancements)

For _production_ builds:
- Combine all your code and dependencies into efficient modern bundles including minifying your JavaScript and CSS.
Expand Down
10 changes: 6 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5173,10 +5173,12 @@ es-abstract@^1.18.0-next.2:
string.prototype.trimstart "^1.0.4"
unbox-primitive "^1.0.0"

es-module-shims@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-0.5.2.tgz#9bea003e84a11bdc052b9f52464671176509f520"
integrity sha512-cXSy7EPyZc6Iq4AjyWXMqMURXgFguduPN7nxtfI0WsV4C83wNHrdxf0oxhzDPZU/8zB6YFllR24HMG/EBVN/GQ==
es-module-shims@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-1.2.0.tgz#f3b447826c4f4b9d9597b24a7aaa0c639893de2f"
integrity sha512-Kupc9HFwmScot1v8vO/3CX9MjNprarG4wdlBk2bv5R76SD63UBhXG53MUTjg8USgHaCMLBOQyGSl7lAEqGUb1g==
dependencies:
rimraf "^3.0.2"

es-to-primitive@^1.2.1:
version "1.2.1"
Expand Down