@apollo/generate-persisted-query-manifest
Version:
Creates a Persisted Query Manifest from an Apollo Client Web project
473 lines • 19.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaults = void 0;
exports.fromGraphQLCodegenPersistedDocuments = fromGraphQLCodegenPersistedDocuments;
exports.getFilepaths = getFilepaths;
exports.generatePersistedQueryManifest = generatePersistedQueryManifest;
const cache_1 = require("@apollo/client/cache");
const semver_1 = __importDefault(require("semver"));
const core_1 = require("@apollo/client/core");
const persisted_query_lists_1 = require("@apollo/persisted-query-lists");
const graphql_tag_pluck_1 = require("@graphql-tools/graphql-tag-pluck");
const globby_1 = __importDefault(require("globby"));
const graphql_1 = require("graphql");
const lodash_1 = require("lodash");
const node_crypto_1 = require("node:crypto");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const vfile_1 = __importDefault(require("vfile"));
const vfile_reporter_1 = __importDefault(require("vfile-reporter"));
const chalk_1 = __importDefault(require("chalk"));
const CUSTOM_DOCUMENTS_SOURCE = Symbol.for("apollo.generate-persisted-query-manifest.documents-source");
/**
* Source documents from a persisted documents manifest generated by [GraphQL
* Codegen](https://the-guild.dev/graphql/codegen). Using this utility skips all file system traversal and uses the
* documents defined in the persisted documents file.
*
* For more information see the [Persisted documents](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#persisted-documents) documentation.
*
* @example
* ```ts
* import {
* fromGraphQLCodegenPersistedDocuments
* } from '@apollo/generate-persisted-query-manifest';
*
* const config = {
* documents: fromGraphQLCodegenPersistedDocuments('./path/to/persisted-documents.json')
* };
* ```
*
* @since 1.2.0
*/
function fromGraphQLCodegenPersistedDocuments(filepath) {
return {
[CUSTOM_DOCUMENTS_SOURCE]: () => {
const file = (0, vfile_1.default)({ path: filepath });
function getSourceWithError(message) {
addError({ file }, message);
return [{ file, node: null, location: undefined }];
}
if (!(0, node_fs_1.existsSync)(file.path)) {
return {
sources: getSourceWithError(ERROR_MESSAGES.graphqlCodegenManifestFileNotFound(filepath)),
};
}
try {
const manifest = JSON.parse((0, node_fs_1.readFileSync)(filepath, "utf-8"));
if (isParseableGraphQLCodegenManifest(manifest)) {
// We don't run any validation on unique entries because we assume
// GraphQL Codegen has already handled this in the persisted documents
// generation.
return {
sources: Object.values(manifest).map((query) => ({
file,
node: (0, graphql_1.parse)(query),
location: undefined,
})),
};
}
else {
return {
sources: getSourceWithError(ERROR_MESSAGES.malformedGraphQLCodegenManifest()),
};
}
}
catch (e) {
return {
sources: getSourceWithError(ERROR_MESSAGES.parseError(e)),
};
}
},
};
}
exports.defaults = {
documents: [
"src/**/*.{graphql,gql,js,jsx,ts,tsx}",
"!**/*.d.ts",
"!**/*.spec.{js,jsx,ts,tsx}",
"!**/*.story.{js,jsx,ts,tsx}",
"!**/*.test.{js,jsx,ts,tsx}",
],
output: "persisted-query-manifest.json",
createOperationId: (query) => {
return (0, node_crypto_1.createHash)("sha256").update(query).digest("hex");
},
};
const COLORS = {
identifier: chalk_1.default.magenta,
filepath: chalk_1.default.underline.cyan,
name: chalk_1.default.yellow,
};
const ERROR_MESSAGES = {
anonymousOperation: (node) => {
return `Anonymous GraphQL operations are not supported. Please name your ${node.operation}.`;
},
graphqlCodegenManifestFileNotFound: (filepath) => {
return `ENOENT: GraphQL Codegen persisted documents file not found: '${filepath}'`;
},
malformedGraphQLCodegenManifest: () => {
return "GraphQL Codegen persisted documents manifest is malformed. Either the file was not generated by GraphQL Codegen or the format has been updated and is no longer compatible with this utility.";
},
uniqueFragment: (name, source) => {
return `Fragment named "${COLORS.name(name)}" already defined in: ${COLORS.filepath(source.file.path)}`;
},
uniqueOperation: (name, source) => {
return `Operation named "${COLORS.name(name)}" already defined in: ${COLORS.filepath(source.file.path)}`;
},
uniqueOperationId: (id, operationName, definedOperationName) => {
return `\`createOperationId\` created an ID (${COLORS.identifier(id)}) for operation named "${COLORS.name(operationName)}" that has already been used for operation named "${COLORS.name(definedOperationName)}".`;
},
parseError(error) {
return formatErrorMessage(error);
},
multipleOperations() {
return "Cannot declare multiple operations in a single document.";
},
};
async function enableDevMessages() {
const { loadDevMessages, loadErrorMessages } = await Promise.resolve().then(() => __importStar(require("@apollo/client/dev")));
loadDevMessages();
loadErrorMessages();
}
function addError(source, message) {
const vfileMessage = source.file.message(message, source.location);
vfileMessage.fatal = true;
}
function isCustomDocumentsSource(documentsConfig) {
return (typeof documentsConfig === "object" &&
documentsConfig !== null &&
Object.prototype.hasOwnProperty.call(documentsConfig, CUSTOM_DOCUMENTS_SOURCE));
}
function isParseableGraphQLCodegenManifest(manifest) {
return (typeof manifest === "object" &&
manifest !== null &&
!Array.isArray(manifest) &&
Object.entries(manifest).every(([key, value]) => typeof key === "string" && typeof value === "string"));
}
function parseLocationFromError(error) {
if (error instanceof graphql_1.GraphQLError && error.locations) {
return error.locations[0];
}
const loc = "loc" in error &&
typeof error.loc === "object" &&
error.loc !== null &&
error.loc;
const line = loc && "line" in loc && typeof loc.line === "number" && loc.line;
const column = loc && "column" in loc && typeof loc.column === "number" && loc.column;
if (typeof line === "number" && typeof column === "number") {
return { line, column };
}
return;
}
function getDocumentSources(filepath) {
const file = (0, vfile_1.default)({
path: filepath,
contents: (0, node_fs_1.readFileSync)(filepath, "utf-8"),
});
try {
if (file.extname === ".graphql" || file.extname === ".gql") {
return [
{
node: (0, graphql_1.parse)(file.toString()),
file,
location: { line: 1, column: 1 },
},
];
}
return (0, graphql_tag_pluck_1.gqlPluckFromCodeStringSync)(filepath, file.toString()).map((source) => ({
node: (0, graphql_1.parse)(source.body),
file,
location: source.locationOffset,
}));
}
catch (e) {
const error = e;
const source = {
node: null,
file,
location: parseLocationFromError(error),
};
addError(source, ERROR_MESSAGES.parseError(error));
return [source];
}
}
function maybeReportErrorsAndExit(files) {
if (!Array.isArray(files)) {
files = [files];
}
if (files.some((file) => file.messages.length > 0)) {
console.error((0, vfile_reporter_1.default)(files, { quiet: true }));
process.exit(1);
}
}
function uniq(arr) {
return [...new Set(arr)];
}
function formatErrorMessage(error) {
return `${error.name}: ${error.message}`;
}
function getClientVersion() {
return new core_1.ApolloClient({
cache: new core_1.InMemoryCache(),
link: core_1.ApolloLink.empty(),
}).version;
}
async function fromFilepathList(documents) {
const filepaths = await getFilepaths(documents);
const sources = filepaths.flatMap(getDocumentSources);
const fragmentsByName = new Map();
const operationsByName = new Map();
for (const source of sources) {
if (!source.node) {
continue;
}
let documentCount = 0;
(0, graphql_1.visit)(source.node, {
FragmentDefinition(node) {
const name = node.name.value;
const sources = fragmentsByName.get(name) ?? [];
if (sources.length) {
sources.forEach((sibling) => {
addError(source, ERROR_MESSAGES.uniqueFragment(name, sibling));
addError(sibling, ERROR_MESSAGES.uniqueFragment(name, source));
});
}
fragmentsByName.set(name, [...sources, source]);
return false;
},
OperationDefinition(node) {
const name = node.name?.value;
if (++documentCount > 1) {
addError(source, ERROR_MESSAGES.multipleOperations());
return graphql_1.BREAK;
}
if (!name) {
addError(source, ERROR_MESSAGES.anonymousOperation(node));
return false;
}
const sources = operationsByName.get(name) ?? [];
if (sources.length) {
sources.forEach((sibling) => {
addError(source, ERROR_MESSAGES.uniqueOperation(name, sibling));
addError(sibling, ERROR_MESSAGES.uniqueOperation(name, source));
});
}
operationsByName.set(name, [...sources, source]);
return false;
},
});
}
return {
fragmentRegistry: (0, cache_1.createFragmentRegistry)(...sources.map(({ node }) => node).filter(Boolean)),
sources,
};
}
// Unfortunately globby does not guarantee deterministic file sorting so we
// apply some sorting on the files in this function.
//
// https://github.com/sindresorhus/globby/issues/131
/** @internal */
async function getFilepaths(documents) {
if (isCustomDocumentsSource(documents)) {
const { sources } = documents[CUSTOM_DOCUMENTS_SOURCE]();
return [
...new Set(sources.filter(({ file }) => file.path).map(({ file }) => file.path)),
];
}
return [...uniq(await (0, globby_1.default)(documents))].sort((a, b) => a.localeCompare(b));
}
/** @internal */
async function generatePersistedQueryManifest(config = {}, configFilePath) {
const clientVersion = getClientVersion();
const { documents = exports.defaults.documents, createOperationId = exports.defaults.createOperationId, } = config;
const configFile = (0, vfile_1.default)({
path: configFilePath
? (0, node_path_1.relative)(process.cwd(), configFilePath)
: "<virtual>",
});
const { fragmentRegistry, sources } = isCustomDocumentsSource(documents)
? documents[CUSTOM_DOCUMENTS_SOURCE]()
: await fromFilepathList(documents);
const operationsByName = new Map();
for (const source of sources) {
if (!source.node) {
continue;
}
// We delegate validation to the functions that return the document sources.
// We just need to record the operations here to sort them in the manifest
// output.
(0, graphql_1.visit)(source.node, {
OperationDefinition(node) {
const name = node.name?.value;
if (!name) {
return false;
}
const sources = operationsByName.get(name) ?? [];
operationsByName.set(name, [...sources, source]);
return false;
},
});
}
maybeReportErrorsAndExit(uniq(sources.map((source) => source.file)));
const cacheConfig = {};
if ("addTypename" in config) {
if (clientVersion.startsWith("4")) {
console.warn("`addTypename` was removed in Apollo Client 4 and is ignored. Please remove this option from your config.");
}
else {
// @ts-ignore
cacheConfig.addTypename = config.addTypename;
}
}
// Using createFragmentRegistry means our minimum AC version is 3.7. We can
// probably go back to 3.2 (original createPersistedQueryLink) if we just
// reimplement/copy the fragment registry code here.
if (fragmentRegistry) {
cacheConfig.fragments = fragmentRegistry;
}
const manifestOperationIds = new Map();
const manifestOperations = [];
// @ts-ignore
const clientConfig = {};
if (config.documentTransform) {
clientConfig.documentTransform = config.documentTransform;
}
try {
// @ts-ignore
const { LocalState } = await Promise.resolve().then(() => __importStar(require("@apollo/client/local-state")));
clientConfig.localState = new LocalState();
}
catch (e) {
// this is a v3 client
}
const client = new core_1.ApolloClient({
...clientConfig,
cache: new core_1.InMemoryCache(cacheConfig),
link: new core_1.ApolloLink((operation) => {
const body = (0, graphql_1.print)((0, persisted_query_lists_1.sortTopLevelDefinitions)(operation.query));
// We can assume the operation name is present since we check for
// anonymous operations before this is executed
const name = operation.operationName;
const type = operation.query.definitions.find((d) => d.kind === "OperationDefinition").operation;
const id = createOperationId(body, {
operationName: name,
type,
createDefaultId() {
return exports.defaults.createOperationId(body);
},
});
// We only need to validate the `id` when using a config file. Without
// a config file, our default id function will be used which is
// guaranteed to create unique IDs.
if (manifestOperationIds.has(id)) {
addError({ file: configFile }, ERROR_MESSAGES.uniqueOperationId(id, name, manifestOperationIds.get(id)));
}
else {
manifestOperationIds.set(id, name);
}
manifestOperations.push({ id, name, type, body });
// Use `new Observable` instead of an `of` helper so that we can target
// both v3 and v4 which use different observable implementations.
return new core_1.Observable((observer) => {
observer.next({ data: null });
observer.complete();
});
}),
});
if (semver_1.default.gte(clientVersion, "3.8.0")) {
await enableDevMessages();
}
for (const [_, sources] of (0, lodash_1.sortBy)([...operationsByName.entries()], lodash_1.first)) {
for (const source of sources) {
if (source.node) {
const opDef = source.node.definitions.find((d) => d.kind === graphql_1.Kind.OPERATION_DEFINITION);
if (!opDef)
continue;
try {
switch (opDef.operation) {
case graphql_1.OperationTypeNode.QUERY:
await client.query({
query: source.node,
fetchPolicy: "no-cache",
});
break;
case graphql_1.OperationTypeNode.MUTATION:
await client.mutate({
mutation: source.node,
fetchPolicy: "no-cache",
});
break;
case graphql_1.OperationTypeNode.SUBSCRIPTION:
await new Promise((resolve, reject) => {
if (!source.node) {
return resolve();
}
const sub = client
.subscribe({ query: source.node, fetchPolicy: "no-cache" })
.subscribe({
next() {
sub.unsubscribe();
resolve();
},
error: reject,
complete: resolve,
});
});
break;
}
}
catch (error) {
if (error instanceof Error) {
addError(source, formatErrorMessage(error));
}
else {
addError(source, "Unknown error occured. Please file a bug.");
}
}
}
}
}
maybeReportErrorsAndExit(uniq(sources.map((source) => source.file).concat(configFile)));
return {
format: "apollo-persisted-query-manifest",
version: 1,
operations: manifestOperations,
};
}
//# sourceMappingURL=index.js.map