@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
250 lines (249 loc) • 12.6 kB
JavaScript
import path from 'node:path';
import fs from 'fs-extra';
import * as toolbox from 'gluegun';
import * as graphql from 'graphql/language/index.js';
import immutable from 'immutable';
import prettier from 'prettier';
// @ts-expect-error TODO: type out if necessary
import uncrashable from '@float-capital/float-subgraph-uncrashable/src/Index.bs.js';
import DataSourceTemplateCodeGenerator from './codegen/template.js';
import { GENERATED_FILE_NOTE } from './codegen/typescript.js';
import { appendApiVersionForGraph } from './command-helpers/compiler.js';
import { displayPath } from './command-helpers/fs.js';
import { step, withSpinner } from './command-helpers/spinner.js';
import { GRAPH_CLI_SHARED_HEADERS } from './constants.js';
import debug from './debug.js';
import { applyMigrations } from './migrations.js';
import Schema from './schema.js';
import Subgraph from './subgraph.js';
import { createIpfsClient, loadSubgraphSchemaFromIPFS } from './utils.js';
import Watcher from './watcher.js';
const typeGenDebug = debug('graph-cli:type-generator');
export default class TypeGenerator {
sourceDir;
options;
protocol;
protocolTypeGenerator;
constructor(options) {
this.options = options;
this.sourceDir =
this.options.sourceDir ||
(this.options.subgraphManifest && path.dirname(this.options.subgraphManifest));
this.protocol = this.options.protocol;
this.protocolTypeGenerator = this.protocol?.getTypeGenerator?.({
sourceDir: this.sourceDir,
outputDir: this.options.outputDir,
});
process.on('uncaughtException', e => {
toolbox.print.error(`UNCAUGHT EXCEPTION: ${e}`);
});
}
async generateTypes() {
if (this.protocol.name === 'substreams') {
typeGenDebug.extend('generateTypes')('Subgraph uses a substream datasource. Skipping code generation.');
toolbox.print.success('Subgraph uses a substream datasource. Codegeneration is not required.');
process.exit(0);
}
if (this.options.subgraphSources.length > 0) {
typeGenDebug.extend('generateTypes')('Subgraph uses subgraph datasources.');
toolbox.print.success('Subgraph uses subgraph datasources.');
}
try {
if (!this.options.skipMigrations && this.options.subgraphManifest) {
await applyMigrations({
sourceDir: this.sourceDir,
manifestFile: this.options.subgraphManifest,
});
}
const subgraph = await this.loadSubgraph();
// Not all protocols support/have ABIs.
if (this.protocol.hasABIs()) {
typeGenDebug.extend('generateTypes')('Generating types for ABIs');
const abis = await this.protocolTypeGenerator.loadABIs(subgraph);
await this.protocolTypeGenerator.generateTypesForABIs(abis);
}
typeGenDebug.extend('generateTypes')('Generating types for templates');
await this.generateTypesForDataSourceTemplates(subgraph);
// Not all protocols support/have ABIs.
if (this.protocol.hasABIs()) {
const templateAbis = await this.protocolTypeGenerator.loadDataSourceTemplateABIs(subgraph);
await this.protocolTypeGenerator.generateTypesForDataSourceTemplateABIs(templateAbis);
}
const schema = await this.loadSchema(subgraph);
typeGenDebug.extend('generateTypes')('Generating types for schema');
await this.generateTypesForSchema({ schema });
if (this.options.subgraphSources.length > 0) {
const ipfsClient = createIpfsClient({
url: appendApiVersionForGraph(this.options.ipfsUrl.toString()),
headers: {
...GRAPH_CLI_SHARED_HEADERS,
},
});
await Promise.all(this.options.subgraphSources.map(async (manifest) => {
const subgraphSchemaFile = await loadSubgraphSchemaFromIPFS(ipfsClient, manifest);
const subgraphSchema = await Schema.loadFromString(subgraphSchemaFile);
typeGenDebug.extend('generateTypes')(`Generating types for subgraph datasource ${manifest}`);
await this.generateTypesForSchema({
schema: subgraphSchema,
fileName: `subgraph-${manifest}.ts`,
generateStoreMethods: false,
});
}));
}
toolbox.print.success('\nTypes generated successfully\n');
if (this.options.uncrashable && this.options.uncrashableConfig) {
await this.generateUncrashableEntities(schema);
toolbox.print.success('\nUncrashable Helpers generated successfully\n');
}
return true;
}
catch (e) {
return false;
}
}
async generateUncrashableEntities(graphSchema) {
const ast = graphql.parse(graphSchema.document);
const entityDefinitions = ast['definitions'];
return await withSpinner(`Generate Uncrashable Entity Helpers`, `Failed to generate Uncrashable Entity Helpers`, `Warnings while generating Uncrashable Entity Helpers`, async (spinner) => {
uncrashable.run(entityDefinitions, this.options.uncrashableConfig, this.options.outputDir);
const outputFile = path.join(this.options.outputDir, 'UncrashableEntityHelpers.ts');
step(spinner, 'Save uncrashable entities to', displayPath(outputFile));
});
}
async loadSubgraph({ quiet } = { quiet: false }) {
const subgraphLoadOptions = {
protocol: this.protocol,
skipValidation: false,
};
if (quiet) {
return (this.options.subgraph ||
(await Subgraph.load(this.options.subgraphManifest, subgraphLoadOptions)).result);
}
const manifestPath = displayPath(this.options.subgraphManifest);
return await withSpinner(`Load subgraph from ${manifestPath}`, `Failed to load subgraph from ${manifestPath}`, `Warnings while loading subgraph from ${manifestPath}`, async (_spinner) => {
return (this.options.subgraph || Subgraph.load(this.options.subgraphManifest, subgraphLoadOptions));
});
}
async loadSchema(subgraph) {
const maybeRelativePath = subgraph.getIn(['schema', 'file']);
const absolutePath = path.resolve(this.sourceDir, maybeRelativePath);
return await withSpinner(`Load GraphQL schema from ${displayPath(absolutePath)}`, `Failed to load GraphQL schema from ${displayPath(absolutePath)}`, `Warnings while loading GraphQL schema from ${displayPath(absolutePath)}`, async (_spinner) => {
const absolutePath = path.resolve(this.sourceDir, maybeRelativePath);
return Schema.load(absolutePath);
});
}
async generateTypesForSchema({ schema, fileName = 'schema.ts', // Default file name
outputDir = this.options.outputDir, // Default output directory
generateStoreMethods = true, }) {
return await withSpinner(`Generate types for GraphQL schema`, `Failed to generate types for GraphQL schema`, `Warnings while generating types for GraphQL schema`, async (spinner) => {
// Generate TypeScript module from schema
const codeGenerator = schema.codeGenerator();
const code = await prettier.format([
GENERATED_FILE_NOTE,
...codeGenerator.generateModuleImports(),
...(await codeGenerator.generateTypes(generateStoreMethods)),
...codeGenerator.generateDerivedLoaders(),
].join('\n'), {
parser: 'typescript',
});
const outputFile = path.join(outputDir, fileName); // Use provided outputDir and fileName
step(spinner, 'Write types to', displayPath(outputFile));
await fs.mkdirs(path.dirname(outputFile));
await fs.writeFile(outputFile, code);
});
}
async generateTypesForDataSourceTemplates(subgraph) {
const moduleImports = [];
return await withSpinner(`Generate types for data source templates`, `Failed to generate types for data source templates`, `Warnings while generating types for data source templates`, async (spinner) => {
// Combine the generated code for all templates
const codeSegments = subgraph
.get('templates', immutable.List())
.reduce((codeSegments, template) => {
step(spinner, 'Generate types for data source template', String(template.get('name')));
const codeGenerator = new DataSourceTemplateCodeGenerator(template, this.protocol);
// we want to get all the imports from the templates
moduleImports.push(...codeGenerator.generateModuleImports());
return codeSegments.concat(codeGenerator.generateTypes());
}, immutable.List());
// we want to dedupe the imports from the templates
const dedupeModulesImports = moduleImports.reduce((acc, curr) => {
const found = acc.find(item => item.module === curr.module);
if (found) {
const foundNames = Array.isArray(found.nameOrNames)
? found.nameOrNames
: [found.nameOrNames];
const currNames = Array.isArray(curr.nameOrNames)
? curr.nameOrNames
: [curr.nameOrNames];
const names = new Set([...foundNames, ...currNames]);
found.nameOrNames = Array.from(names);
}
else {
acc.push(curr);
}
return acc;
}, []);
if (!codeSegments.isEmpty()) {
const code = await prettier.format([GENERATED_FILE_NOTE, ...dedupeModulesImports, ...codeSegments].join('\n'), {
parser: 'typescript',
});
const outputFile = path.join(this.options.outputDir, 'templates.ts');
step(spinner, `Write types for templates to`, displayPath(outputFile));
await fs.mkdirs(path.dirname(outputFile));
await fs.writeFile(outputFile, code);
}
});
}
async getFilesToWatch() {
try {
const files = [];
const subgraph = await this.loadSubgraph({ quiet: true });
// Add the subgraph manifest file
files.push(this.options.subgraphManifest);
// Add the GraphQL schema to the watched files
files.push(subgraph.getIn(['schema', 'file']));
// Add all file paths specified in manifest
subgraph.get('dataSources').map((dataSource) => {
dataSource.getIn(['mapping', 'abis']).map((abi) => {
files.push(abi.get('file'));
});
});
// Make paths absolute
return files.map(file => path.resolve(file));
}
catch (e) {
throw Error(`Failed to load subgraph: ${e.message}`);
}
}
async watchAndGenerateTypes() {
let spinner;
// Create watcher and generate types once and then on every change to a watched file
const watcher = new Watcher({
onReady: () => (spinner = toolbox.print.spin('Watching subgraph files')),
onTrigger: async (changedFile) => {
if (changedFile !== undefined) {
spinner.info(`File change detected: ${displayPath(changedFile)}\n`);
}
await this.generateTypes();
spinner.start();
},
onCollectFiles: async () => await this.getFilesToWatch(),
onError: error => {
spinner.stop();
toolbox.print.error(`${error}\n`);
spinner.start();
},
});
// Catch keyboard interrupt: close watcher and exit process
process.on('SIGINT', () => {
watcher.close();
process.exit();
});
try {
await watcher.watch();
}
catch (e) {
toolbox.print.error(String(e.message));
}
}
}