Skip to content

Commit

Permalink
Generate @apollo/client/invariantErrorCodes.js for each release.
Browse files Browse the repository at this point in the history
When an invariant fails in production, a cryptic numeric error of the form
`Invariant Violation: 42` is thrown, with a reference to the error codes
section of invariant-packages README.md: https://github.com/apollographql/invariant-packages#error-codes

This vague guidance has not proven adequate in many cases, to say the
least: see apollographql/invariant-packages#18,
apollographql/invariant-packages#19, #6604,

Using error codes instead of error strings remains important for
production bundle sizes, but I think we can make it substantially easier
to look up the error string corresponding to each error code, by
generating a single file containing all the invariant error codes for each
@apollo/client release.

Starting with Apollo Client 3.1.0, this manifest file can be found in
@apollo/client/invariantErrorCodes.js (using an npm/yarn-installed copy of
@apollo/client, since this file is generated in the ./dist directory, not
checked into the repository). The file contains an explanatory comment,
the @apollo/client version, and a sequential map from error numbers to the
{ file, node } responsible for the error.

I wish we could go back and republish old versions of @apollo/client to
include this file, but at least things should be easier going forward.
  • Loading branch information
benjamn committed Jul 21, 2020
1 parent f60ee39 commit 4badf2f
Showing 1 changed file with 51 additions and 16 deletions.
67 changes: 51 additions & 16 deletions config/processInvariants.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,78 @@
import { readFileSync, writeFileSync } from "fs";
import { resolve, relative } from "path";
import * as fs from "fs";
import * as path from "path";
import glob = require("glob");

const distDir = resolve(__dirname, "..", "dist");
const distDir = path.resolve(__dirname, "..", "dist");

glob(`${distDir}/**/*.js`, (error, files) => {
if (error) throw error;

files.sort().forEach(file => {
const relPath = relative(distDir, file);
const relPath = path.relative(distDir, file);

// Outside the distDir, somehow.
if (relPath.startsWith("../")) return;

// Avoid re-transforming CommonJS bundle files.
if (relPath.endsWith(".cjs.js")) return;

const source = readFileSync(file, "utf8");
const source = fs.readFileSync(file, "utf8");
const output = transform(source, relPath);
if (source !== output) {
console.log("transformed invariants in " + relPath);
writeFileSync(file, output, "utf8");
fs.writeFileSync(file, output, "utf8");
}
});

fs.writeFileSync(
path.join(distDir, "invariantErrorCodes.js"),
recast.print(errorCodeManifest, {
tabWidth: 2,
}).code + "\n",
);
});

import * as recast from "recast";
import * as parser from "recast/parsers/babel";
const b = recast.types.builders;
const n = recast.types.namedTypes;
type Node = recast.types.namedTypes.Node;
type NumericLiteral = recast.types.namedTypes.NumericLiteral;
type CallExpression = recast.types.namedTypes.CallExpression;
type NewExpression = recast.types.namedTypes.NewExpression;
let nextErrorCode = 1;

function transform(code: string, id: string) {
const errorCodeManifest = b.objectExpression([
b.property("init",
b.stringLiteral("@apollo/client version"),
b.stringLiteral(require("../package.json").version),
),
]);

errorCodeManifest.comments = [
b.commentLine(' This file is meant to help with looking up the source of errors like', true),
b.commentLine(' "Invariant Violation: 35" and is automatically generated by the file', true),
b.commentLine(' @apollo/client/config/processInvariants.ts for each @apollo/client', true),
b.commentLine(' release. The numbers may change from release to release, so please', true),
b.commentLine(' consult the @apollo/client/invariantErrorCodes.js file specific to', true),
b.commentLine(' your @apollo/client version. This file is not meant to be imported.', true),
];

function getErrorCode(
file: string,
expr: CallExpression | NewExpression,
): NumericLiteral {
const numLit = b.numericLiteral(nextErrorCode++);
errorCodeManifest.properties.push(
b.property("init", numLit, b.objectExpression([
b.property("init", b.identifier("file"), b.stringLiteral(file)),
b.property("init", b.identifier("node"), expr),
])),
);
return numLit;
}

function transform(code: string, file: string) {
// If the code doesn't seem to contain anything invariant-related, we
// can skip parsing and transforming it.
if (!/invariant/i.test(code)) {
Expand All @@ -50,7 +92,7 @@ function transform(code: string, id: string) {
}

const newArgs = node.arguments.slice(0, 1);
newArgs.push(b.numericLiteral(nextErrorCode++));
newArgs.push(getErrorCode(file, node));

return b.conditionalExpression(
makeNodeEnvTest(),
Expand Down Expand Up @@ -80,9 +122,7 @@ function transform(code: string, id: string) {
return;
}

const newArgs = [
b.numericLiteral(nextErrorCode++),
];
const newArgs = [getErrorCode(file, node)];

return b.conditionalExpression(
makeNodeEnvTest(),
Expand All @@ -99,11 +139,6 @@ function transform(code: string, id: string) {
return recast.print(ast).code;
}

const n = recast.types.namedTypes;
type Node = recast.types.namedTypes.Node;
type CallExpression = recast.types.namedTypes.CallExpression;
type NewExpression = recast.types.namedTypes.NewExpression;

function isIdWithName(node: Node, ...names: string[]) {
return n.Identifier.check(node) &&
names.some(name => name === node.name);
Expand Down

0 comments on commit 4badf2f

Please sign in to comment.