UNPKG

@apollo/generate-persisted-query-manifest

Version:

Creates a Persisted Query Manifest from an Apollo Client Web project

416 lines 17.1 kB
"use strict"; 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}`; } 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 { 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 inMemoryCacheConfig = {}; // Note that the ability to pass `addTypename: false` will be removed in // Apollo Client v4. When we upgrade this package to work with AC4 as well, // we'll want to do a version check and throw an error if the version is at // least 4.0.0. if ("addTypename" in config) { inMemoryCacheConfig.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) { inMemoryCacheConfig.fragments = fragmentRegistry; } const cache = new core_1.InMemoryCache(inMemoryCacheConfig); const manifestOperationIds = new Map(); const manifestOperations = []; const clientConfig = {}; if (config.documentTransform) { clientConfig.documentTransform = config.documentTransform; } const client = new core_1.ApolloClient({ ...clientConfig, cache, link: new core_1.ApolloLink((operation) => { const body = (0, graphql_1.print)((0, persisted_query_lists_1.sortTopLevelDefinitions)(operation.query)); 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 }); return core_1.Observable.of({ data: null }); }), }); if (semver_1.default.gte(client.version, "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) { try { await client.query({ query: source.node, fetchPolicy: "no-cache" }); } 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