@data-eden/codegen
Advanced tools
@@ -1,7 +0,13 @@ | ||
import type { PluginObj } from '@babel/core'; | ||
interface PluginOptions { | ||
tagName: string; | ||
} | ||
declare const babelPlugin: (api: object, options: PluginOptions | null | undefined, dirname: string) => PluginObj<import("@babel/core").PluginPass>; | ||
import type { NodePath } from '@babel/traverse'; | ||
import type { Program } from '@babel/types'; | ||
import * as t from '@babel/types'; | ||
declare const babelPlugin: (api: object, options: Record<string, any> | null | undefined, dirname: string) => { | ||
name: string; | ||
visitor: { | ||
Program(this: import("@babel/core").PluginPass, path: NodePath<Program>): void; | ||
ImportDeclaration(this: import("@babel/core").PluginPass, path: NodePath<t.ImportDeclaration>): void; | ||
TaggedTemplateExpression(this: import("@babel/core").PluginPass, path: NodePath<t.TaggedTemplateExpression>, state: import("@babel/core").PluginPass): void; | ||
}; | ||
}; | ||
export { babelPlugin }; | ||
//# sourceMappingURL=babel-plugin.d.ts.map |
@@ -6,15 +6,44 @@ import { template } from '@babel/core'; | ||
import { changeExtension } from './utils.js'; | ||
/** | ||
* | ||
This takes the following example: | ||
``` | ||
const memberFeedDetailPageQuery = gql<MemberFeedDetailPageQuery>` | ||
query MemberFeedDetailPage($urn: Urn!) {} | ||
` | ||
``` | ||
and converts it to: | ||
export const memberFeedDetailPageQuery = MemberFeedDetailPageDocument | ||
*/ | ||
// TODO: we should parse the fragment, query or mutation name from the gql tag content via regex | ||
function createImportName(name) { | ||
if (name.endsWith('Fragment')) { | ||
return `${name}Doc`; | ||
/* | ||
since we want to have the ability to have the following | ||
export const memberFeedDetailPageQuery = gql<MemberFeedDetailPageQuery>` | ||
query MemberFeedDetailPage {} | ||
` | ||
We want to uppercase the first character of the name to avoid collisions | ||
``` | ||
*/ | ||
let importName = name; | ||
importName = importName.charAt(0).toUpperCase() + importName.slice(1); | ||
if (importName.endsWith('Fragment')) { | ||
return `${importName}Doc`; | ||
} | ||
else if (name.endsWith('Query')) { | ||
return name.replace('Query', 'Document'); | ||
else if (importName.endsWith('Query')) { | ||
return importName.replace('Query', 'Document'); | ||
} | ||
else { | ||
return name.replace('Mutation', 'Document'); | ||
return importName.replace('Mutation', 'Document'); | ||
} | ||
} | ||
const babelPlugin = declare((api, options) => { | ||
const babelPlugin = declare((api) => { | ||
let program; | ||
let gqlImportIdentifier = null; | ||
let currentFileName = null; | ||
return { | ||
@@ -26,26 +55,47 @@ name: 'data-eden-codegen', | ||
}, | ||
TaggedTemplateExpression(path, state) { | ||
ImportDeclaration(path) { | ||
const { node } = path; | ||
if (!(t.isIdentifier(node.tag) && node.tag.name === options.tagName)) { | ||
return; | ||
if (currentFileName !== this.file.opts.filename) { | ||
currentFileName = this.file.opts.filename; | ||
gqlImportIdentifier = null; | ||
} | ||
const parentNode = path.parentPath.node; | ||
if (!t.isVariableDeclarator(parentNode)) { | ||
return; | ||
if (t.isStringLiteral(node.source) && | ||
node.source.value === '@data-eden/codegen/gql') { | ||
const gqlSpecifier = node.specifiers.find((specifier) => { | ||
if (t.isImportSpecifier(specifier)) { | ||
return (t.isIdentifier(specifier.imported) && | ||
specifier.imported.name === 'gql'); | ||
} | ||
return false; | ||
}); | ||
if (gqlSpecifier) { | ||
gqlImportIdentifier = gqlSpecifier.local; | ||
} | ||
} | ||
t.assertIdentifier(parentNode.id); | ||
const operationOrFragmentName = createImportName(parentNode.id.name); | ||
const filePath = state.filename; | ||
if (!filePath) { | ||
throw new Error('@data-eden/codegen: Unable to resolve filepath.'); | ||
}, | ||
TaggedTemplateExpression(path, state) { | ||
const { node } = path; | ||
if (t.isIdentifier(node.tag) && | ||
gqlImportIdentifier && | ||
node.tag.name === gqlImportIdentifier.name) { | ||
const parentNode = path.parentPath.node; | ||
if (!t.isVariableDeclarator(parentNode)) { | ||
return; | ||
} | ||
t.assertIdentifier(parentNode.id); | ||
const operationOrFragmentName = createImportName(parentNode.id.name); | ||
const filePath = state.filename; | ||
if (!filePath) { | ||
throw new Error('@data-eden/codegen: Unable to resolve filepath.'); | ||
} | ||
const importPath = getRelativeImportPath(filePath); | ||
const newImportDeclaration = template(` | ||
import { %%importName%% } from %%importPath%% | ||
`); | ||
program.unshiftContainer('body', newImportDeclaration({ | ||
importName: api.types.identifier(operationOrFragmentName), | ||
importPath: api.types.stringLiteral(importPath), | ||
})); | ||
path.replaceWith(api.types.identifier(operationOrFragmentName)); | ||
} | ||
const importPath = getRelativeImportPath(filePath); | ||
const importDeclaration = template(` | ||
import { %%importName%% } from %%importPath%% | ||
`); | ||
program.unshiftContainer('body', importDeclaration({ | ||
importName: api.types.identifier(operationOrFragmentName), | ||
importPath: api.types.stringLiteral(importPath), | ||
})); | ||
path.replaceWith(api.types.identifier(operationOrFragmentName)); | ||
}, | ||
@@ -52,0 +102,0 @@ }, |
import { athenaCodegen } from './codegen.js'; | ||
import { program } from 'commander'; | ||
import { resolve } from 'path'; | ||
function isResolverModule(o) { | ||
return typeof o === 'object'; | ||
} | ||
export async function binMain() { | ||
@@ -7,10 +11,22 @@ program.name('athena-codegen').description('codegen for @data-eden/athena'); | ||
.requiredOption('--schema-path <schemaPath>', 'path to your schema file') | ||
.requiredOption('--documents <patterns...>', 'one or more glob patterns that match the documents you want to process') | ||
.requiredOption('--documents <patterns...>', 'one or more glob patterns that match the documents you want to process', ',') | ||
.option('--debug', 'output additional debugging information', false) | ||
.option('--production', 'optimize codegen outputs for production builds', process.env.NODE_ENV === 'prod') | ||
.option('--disable-schema-types-generation', 'skip generating schema types', false) | ||
.option('--base-dir <baseDir>', 'directory where codegen should begin searching for documents', process.cwd()) | ||
.option('--extension <extension>', 'extension for newly generated files', '.graphql.ts'); | ||
.option('--extension <extension>', 'extension for newly generated files', '.graphql.ts') | ||
.option('--resolver <resolverImportPath>', 'path to resolver script', async (resolverImportPath) => { | ||
const resolverPath = resolve(process.cwd(), resolverImportPath); | ||
return import(resolverPath); | ||
}); | ||
program.parse(); | ||
const options = program.opts(); | ||
await athenaCodegen(options); | ||
let resolver = isResolverModule(options.resolver) | ||
? (await options.resolver).default | ||
: undefined; | ||
await athenaCodegen({ | ||
...options, | ||
resolver, | ||
}); | ||
} | ||
//# sourceMappingURL=bin.js.map |
import type { CodegenArgs } from './types.js'; | ||
export declare function athenaCodegen({ schemaPath: schemaFile, documents, baseDir, extension, hash, production, }: CodegenArgs): Promise<void>; | ||
export declare function athenaCodegen({ schemaPath: schemaFile, documents, baseDir, extension, hash, debug, disableSchemaTypesGeneration, production, resolver, }: CodegenArgs): Promise<void>; | ||
//# sourceMappingURL=codegen.d.ts.map |
@@ -7,2 +7,3 @@ import { codegen } from '@graphql-codegen/core'; | ||
import fs from 'fs-extra'; | ||
import { hrtime } from 'node:process'; | ||
import { readFile } from 'fs/promises'; | ||
@@ -15,4 +16,9 @@ import { globby } from 'globby'; | ||
import { generateSchemaTypes } from './generate-schema-types.js'; | ||
import { changeExtension } from './utils.js'; | ||
export async function athenaCodegen({ schemaPath: schemaFile, documents, baseDir, extension, hash, production, }) { | ||
import { changeExtension, extensionAwareResolver } from './utils.js'; | ||
import { enable as enableDebugging } from './debug.js'; | ||
export async function athenaCodegen({ schemaPath: schemaFile, documents, baseDir, extension, hash, debug, disableSchemaTypesGeneration, production, resolver, }) { | ||
const startTime = hrtime.bigint(); | ||
if (debug === true) { | ||
enableDebugging(); | ||
} | ||
const outputFiles = []; | ||
@@ -26,10 +32,35 @@ const persistedQueries = {}; | ||
const hashFn = hash || defaultHash; | ||
// Generate schema types and write to root schema file | ||
// User can disable schema type generation (e.g. for cases where they have | ||
// already compiled their schema types) | ||
const schemaTypesOutputPath = changeExtension(schemaPath, extension); | ||
const schemaTypes = await generateSchemaTypes(parsedSchema, schemaTypesOutputPath); | ||
outputFiles.push(schemaTypes); | ||
if (!disableSchemaTypesGeneration) { | ||
// Generate schema types and write to root schema file | ||
const schemaTypes = await generateSchemaTypes(parsedSchema, schemaTypesOutputPath); | ||
outputFiles.push(schemaTypes); | ||
} | ||
// Expand glob patterns and find matching files | ||
const paths = await globby([...documents, `!${schemaPath}`]); | ||
const paths = await globby([...documents, `!${schemaPath}`], { | ||
absolute: true, | ||
}); | ||
// we want to wrap any resolver we are passed with an extension aware resolver | ||
const defaultResolver = (importPath, options) => { | ||
const extensions = ['.tsx', '.jsx', '.js', '.ts']; | ||
const potentiallyResolvedValueFromResolver = resolver | ||
? resolver(importPath, options) | ||
: undefined; | ||
return potentiallyResolvedValueFromResolver | ||
? extensionAwareResolver(potentiallyResolvedValueFromResolver, extensions) | ||
: extensionAwareResolver(path.resolve(options.basedir, importPath), extensions); | ||
}; | ||
// Create DocumentFile objects for use in the preset builder | ||
const documentFiles = generateDocumentFiles(graphqlSchema, paths); | ||
const { gqlTagDocuments, gqlFileDocuments, dependencyGraph } = generateDocumentFiles(graphqlSchema, paths, defaultResolver); | ||
const exportedGqlTagFragments = Array.from(dependencyGraph.fragments.values()).map((fragment) => { | ||
return { | ||
name: fragment.ast.name.value, | ||
isExternal: true, | ||
importFrom: fragment.filePath, | ||
node: fragment.ast, | ||
onType: fragment.ast.typeCondition.name.value, | ||
}; | ||
}); | ||
function onExecutableDocumentNode(documentNode) { | ||
@@ -48,35 +79,76 @@ const materializedDocumentString = printExecutableGraphQLDocument(documentNode); | ||
} | ||
const configs = await preset.buildGeneratesSection({ | ||
baseOutputDir: baseDir, | ||
config: { | ||
useTypeImports: true, | ||
}, | ||
presetConfig: { | ||
baseTypesPath: path.relative(baseDir, schemaTypesOutputPath), | ||
folder: '__generated', | ||
extension, | ||
}, | ||
schema: parsedSchema, | ||
documents: documentFiles, | ||
pluginMap: { | ||
typescriptOperations: typescriptOperationsPlugin, | ||
typedDocumentNode: typedDocumentNodePlugin, | ||
}, | ||
plugins: [ | ||
{ | ||
typescriptOperations: { | ||
nonOptionalTypename: true, | ||
useTypeImports: true, | ||
inlineFragmentTypes: 'combine', | ||
const configs = [ | ||
...(await preset.buildGeneratesSection({ | ||
baseOutputDir: baseDir, | ||
config: { | ||
useTypeImports: true, | ||
}, | ||
presetConfig: { | ||
baseTypesPath: path.relative(baseDir, schemaTypesOutputPath), | ||
folder: '__generated', | ||
extension, | ||
}, | ||
schema: parsedSchema, | ||
documents: gqlFileDocuments, | ||
pluginMap: { | ||
typescriptOperations: typescriptOperationsPlugin, | ||
typedDocumentNode: typedDocumentNodePlugin, | ||
}, | ||
plugins: [ | ||
{ | ||
typescriptOperations: { | ||
nonOptionalTypename: true, | ||
useTypeImports: true, | ||
inlineFragmentTypes: 'combine', | ||
}, | ||
}, | ||
{ | ||
typedDocumentNode: { | ||
addTypenameToSelectionSets: true, | ||
unstable_omitDefinitions: true, | ||
unstable_onExecutableDocumentNode: onExecutableDocumentNode, | ||
}, | ||
}, | ||
], | ||
})), | ||
...(await preset.buildGeneratesSection({ | ||
baseOutputDir: baseDir, | ||
config: { | ||
useTypeImports: true, | ||
}, | ||
{ | ||
typedDocumentNode: { | ||
addTypenameToSelectionSets: true, | ||
unstable_omitDefinitions: true, | ||
unstable_onExecutableDocumentNode: onExecutableDocumentNode, | ||
presetConfig: { | ||
baseTypesPath: path.relative(baseDir, schemaTypesOutputPath), | ||
folder: '__generated', | ||
extension, | ||
}, | ||
schema: parsedSchema, | ||
documents: gqlTagDocuments, | ||
pluginMap: { | ||
typescriptOperations: typescriptOperationsPlugin, | ||
typedDocumentNode: typedDocumentNodePlugin, | ||
}, | ||
plugins: [ | ||
{ | ||
typescriptOperations: { | ||
nonOptionalTypename: true, | ||
useTypeImports: true, | ||
inlineFragmentTypes: 'combine', | ||
}, | ||
}, | ||
}, | ||
], | ||
}); | ||
{ | ||
typedDocumentNode: { | ||
// Tell typedDocumentNode that all fragments are external to | ||
// prevent it from generating ASTs for fragments | ||
// | ||
// We'll still get the fragment types by way of | ||
// typescriptOperations | ||
externalFragments: exportedGqlTagFragments, | ||
addTypenameToSelectionSets: true, | ||
unstable_omitDefinitions: true, | ||
unstable_onExecutableDocumentNode: onExecutableDocumentNode, | ||
}, | ||
}, | ||
], | ||
})), | ||
]; | ||
// Generate types for each document + import statements for Schema types | ||
@@ -107,3 +179,7 @@ const documentTypes = await Promise.all(configs.map(async (config) => { | ||
})); | ||
const endTime = hrtime.bigint(); | ||
const totalTime = Number(endTime - startTime); | ||
const timeInSeconds = totalTime / 1000000000; | ||
console.log(`Successfully generated ${outputFiles.length} files in ${timeInSeconds} seconds.`); | ||
} | ||
//# sourceMappingURL=codegen.js.map |
import type { Types } from '@graphql-codegen/plugin-helpers'; | ||
import type { GraphQLSchema } from 'graphql'; | ||
export declare function generateDocumentFiles(schema: GraphQLSchema, documentPaths: Array<string>): Array<Types.DocumentFile>; | ||
import type { DependencyGraph } from './gql/dependency-graph.js'; | ||
import { type Resolver } from './types.js'; | ||
export declare function generateDocumentFiles(schema: GraphQLSchema, documentPaths: Array<string>, resolver: Resolver): { | ||
gqlFileDocuments: Array<Types.DocumentFile>; | ||
gqlTagDocuments: Array<Types.DocumentFile>; | ||
dependencyGraph: DependencyGraph; | ||
}; | ||
//# sourceMappingURL=generate-document-files.d.ts.map |
@@ -5,6 +5,7 @@ import { parse } from 'graphql'; | ||
import { resolveForeignReferences } from './gql/foreign-ref-resolver.js'; | ||
import { resolveNameCollisions } from './gql/name-collision-resolver.js'; | ||
import { outputOperations } from './gql/output-operations.js'; | ||
import * as path from 'node:path'; | ||
export function generateDocumentFiles(schema, documentPaths) { | ||
import { createDebug } from './debug.js'; | ||
const debug = createDebug('generate-document-files'); | ||
export function generateDocumentFiles(schema, documentPaths, resolver) { | ||
const { gqlFiles, gqlTags } = documentPaths.reduce((acc, docPath) => { | ||
@@ -24,29 +25,50 @@ const ext = path.extname(docPath); | ||
const files = handleGraphQLFiles(gqlFiles); | ||
const tags = handleGraphQLTags(schema, gqlTags); | ||
return [...files, ...tags]; | ||
const { tags, dependencyGraph } = handleGraphQLTags(schema, gqlTags, resolver); | ||
return { | ||
gqlFileDocuments: files, | ||
gqlTagDocuments: tags, | ||
dependencyGraph, | ||
}; | ||
} | ||
function handleGraphQLFiles(documentPaths) { | ||
return documentPaths.map((path) => { | ||
const contents = readFileSync(path, 'utf-8'); | ||
const parsed = parse(contents); | ||
return { | ||
location: path, | ||
document: parsed, | ||
rawSDL: contents, | ||
}; | ||
debug(`compile .graphql: ${path}`); | ||
try { | ||
const contents = readFileSync(path, 'utf-8'); | ||
const parsed = parse(contents); | ||
return { | ||
location: path, | ||
document: parsed, | ||
rawSDL: contents, | ||
}; | ||
} | ||
catch (e) { | ||
if (e instanceof Error) { | ||
e.message = `[@data-eden/codegen]: Error processing .graphql file ${path}: ${e?.message}`; | ||
} | ||
throw e; | ||
} | ||
}); | ||
} | ||
function handleGraphQLTags(schema, documentPaths) { | ||
function handleGraphQLTags(schema, documentPaths, resolver) { | ||
const extractedQueriesMap = new Map(); | ||
documentPaths.forEach((filePath) => { | ||
const extractedQueries = extractDefinitions(schema, filePath); | ||
if (extractedQueries.definitions.length > 0) { | ||
extractedQueriesMap.set(filePath, extractedQueries); | ||
debug(`compile gql tags in : ${filePath}`); | ||
try { | ||
const extractedQueries = extractDefinitions(schema, filePath, resolver); | ||
if (extractedQueries.definitions.length > 0) { | ||
extractedQueriesMap.set(filePath, extractedQueries); | ||
} | ||
} | ||
catch (e) { | ||
if (e instanceof Error) { | ||
e.message = `[@data-eden/codegen]: Error processing gql tags in ${filePath}: ${e?.message}`; | ||
} | ||
throw e; | ||
} | ||
}); | ||
const moduleAliasMap = {}; | ||
const dependencyGraph = resolveForeignReferences(extractedQueriesMap, moduleAliasMap, schema); | ||
resolveNameCollisions(dependencyGraph); | ||
return outputOperations(dependencyGraph); | ||
return { tags: outputOperations(dependencyGraph), dependencyGraph }; | ||
} | ||
//# sourceMappingURL=generate-document-files.js.map |
@@ -1,9 +0,11 @@ | ||
import type { Definition } from './types.js'; | ||
import type { Definition, Fragment, UnresolvedFragment } from './types.js'; | ||
export type DependencyGraphNode = Definition | UnresolvedFragment; | ||
export declare class DependencyGraph { | ||
#private; | ||
addDefinition(node: Definition): void; | ||
addDefinitions(nodes: Definition[]): void; | ||
get definitions(): Set<Definition>; | ||
addDependency(from: Definition, to: Definition): void; | ||
addDefinition(node: DependencyGraphNode): void; | ||
addDefinitions(nodes: DependencyGraphNode[]): void; | ||
get fragments(): Set<Fragment>; | ||
get definitions(): Set<DependencyGraphNode>; | ||
addDependency(from: DependencyGraphNode, to: DependencyGraphNode): void; | ||
} | ||
//# sourceMappingURL=dependency-graph.d.ts.map |
@@ -6,5 +6,6 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
}; | ||
var _DependencyGraph_nodes, _DependencyGraph_outgoingEdges, _DependencyGraph_incomingEdges; | ||
var _DependencyGraph_fragment, _DependencyGraph_nodes, _DependencyGraph_outgoingEdges, _DependencyGraph_incomingEdges; | ||
export class DependencyGraph { | ||
constructor() { | ||
_DependencyGraph_fragment.set(this, new Set()); | ||
_DependencyGraph_nodes.set(this, new Set()); | ||
@@ -15,2 +16,5 @@ _DependencyGraph_outgoingEdges.set(this, new Map()); | ||
addDefinition(node) { | ||
if (node.type === 'fragment') { | ||
__classPrivateFieldGet(this, _DependencyGraph_fragment, "f").add(node); | ||
} | ||
__classPrivateFieldGet(this, _DependencyGraph_nodes, "f").add(node); | ||
@@ -25,2 +29,5 @@ __classPrivateFieldGet(this, _DependencyGraph_outgoingEdges, "f").set(node, new Set()); | ||
} | ||
get fragments() { | ||
return __classPrivateFieldGet(this, _DependencyGraph_fragment, "f"); | ||
} | ||
get definitions() { | ||
@@ -40,3 +47,3 @@ return __classPrivateFieldGet(this, _DependencyGraph_nodes, "f"); | ||
} | ||
_DependencyGraph_nodes = new WeakMap(), _DependencyGraph_outgoingEdges = new WeakMap(), _DependencyGraph_incomingEdges = new WeakMap(); | ||
_DependencyGraph_fragment = new WeakMap(), _DependencyGraph_nodes = new WeakMap(), _DependencyGraph_outgoingEdges = new WeakMap(), _DependencyGraph_incomingEdges = new WeakMap(); | ||
//# sourceMappingURL=dependency-graph.js.map |
import type { GraphQLSchema } from 'graphql'; | ||
import type { Definition } from './types.js'; | ||
export declare function extractDefinitions(schema: GraphQLSchema, fileName: string): { | ||
import type { Definition, UnresolvedFragment } from './types.js'; | ||
import { type Resolver } from '../types.js'; | ||
export declare function extractDefinitions(schema: GraphQLSchema, fileName: string, resolver: Resolver): { | ||
definitions: Definition[]; | ||
exportedDefinitionMap: Map<string, Definition>; | ||
exportedDefinitionMap: Map<string, UnresolvedFragment | Definition>; | ||
}; | ||
//# sourceMappingURL=extract-definitions.d.ts.map |
@@ -5,3 +5,3 @@ import { traverse } from '@babel/core'; | ||
import { createExtractor } from './extractor.js'; | ||
export function extractDefinitions(schema, fileName) { | ||
export function extractDefinitions(schema, fileName, resolver) { | ||
const document = readFileSync(fileName, 'utf-8'); | ||
@@ -17,3 +17,3 @@ const parsed = parse(document, { | ||
const definitions = []; | ||
traverse(parsed, createExtractor(schema, fileName, definitions, 'graphql')); | ||
traverse(parsed, createExtractor(schema, fileName, definitions, resolver)); | ||
const exportedDefinitionMap = new Map(); | ||
@@ -24,2 +24,18 @@ for (const def of definitions) { | ||
} | ||
if (def.foreignReferences) { | ||
for (const foreignReference of def.foreignReferences.entries()) { | ||
if (foreignReference[1].exportName && | ||
foreignReference[1].type && | ||
foreignReference[1].type === 'unresolvedFragment') { | ||
// we are in a file that exports fragments, these are local and not external, but are accessible externally | ||
exportedDefinitionMap.set(foreignReference[1].exportName, { | ||
location: foreignReference[1].location, | ||
filePath: foreignReference[1].filePath, | ||
exportName: foreignReference[1].exportName, | ||
isExternal: false, | ||
type: 'unresolvedFragment', | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
@@ -26,0 +42,0 @@ return { |
@@ -5,4 +5,5 @@ import type { Visitor } from '@babel/traverse'; | ||
import type { Definition } from './types.js'; | ||
import { type Resolver } from '../types.js'; | ||
export declare function getSortedTemplateElements(tmplLiteral: TemplateLiteral): (TemplateElement | Identifier)[]; | ||
export declare function createExtractor(schema: GraphQLSchema, filePath: string, definitions: Array<Definition>, tagName?: string): Visitor<{}>; | ||
export declare function createExtractor(schema: GraphQLSchema, filePath: string, definitions: Array<Definition>, resolver: Resolver): Visitor<{}>; | ||
//# sourceMappingURL=extractor.d.ts.map |
import * as t from '@babel/types'; | ||
import * as assert from 'node:assert'; | ||
import { parse as graphqlParse, specifiedRules, KnownFragmentNamesRule, NoUnusedFragmentsRule, validate, } from 'graphql'; | ||
import { dirname } from 'node:path'; | ||
import { createHash } from 'crypto'; | ||
import { existsSync } from 'node:fs'; | ||
const VALIDATION_RULES = [...specifiedRules].filter( | ||
// This rules will be applied once we have full depedency graph for the queries resolvedx | ||
(rule) => rule !== KnownFragmentNamesRule && rule !== NoUnusedFragmentsRule); | ||
function getDefinition(documentAst) { | ||
function getDefinition(documentAst, filePath) { | ||
if (documentAst.definitions.length !== 1) { | ||
throw new Error('Only single definitions allowed per usage of the graphql tag'); | ||
throw new Error(`Only single definitions allowed per usage of the graphql tag at "${filePath}"`); | ||
} | ||
@@ -39,3 +42,3 @@ const definition = documentAst.definitions.at(0); | ||
} | ||
function getForeignReference(elem, path, localDefinitionDeclaratorMap) { | ||
function getForeignReference(elem, path, localDefinitionDeclaratorMap, resolver) { | ||
const binding = path.scope.getBinding(elem.name); | ||
@@ -67,4 +70,17 @@ if (!binding) { | ||
?.node; | ||
// todo: figure out why this doesn't exist on the source node | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access | ||
const currentDir = dirname(importDeclaration.source.loc.filename); | ||
const filename = resolver(importDeclaration.source.value, { | ||
basedir: currentDir, | ||
}); | ||
if (!filename || !existsSync(filename)) { | ||
throw new Error(`Can not resolve the file ${importDeclaration.source.value} in ${ | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
importDeclaration.source.loc.filename}`); | ||
} | ||
referencedFragment = { | ||
location: importDeclaration.source.value, | ||
filePath: filename, | ||
isExternal: true, | ||
exportName: importSpecifier.imported.name, | ||
@@ -82,8 +98,27 @@ type: 'unresolvedFragment', | ||
} | ||
export function createExtractor(schema, filePath, definitions, tagName = 'gql') { | ||
export function createExtractor(schema, filePath, definitions, resolver) { | ||
const localDefinitionDeclaratorMap = new Map(); | ||
let gqlImportIdentifier = null; | ||
const extractor = { | ||
ImportDeclaration(path) { | ||
const { node } = path; | ||
if (t.isStringLiteral(node.source) && | ||
node.source.value === '@data-eden/codegen/gql') { | ||
const gqlSpecifier = node.specifiers.find((specifier) => { | ||
if (t.isImportSpecifier(specifier)) { | ||
return (t.isIdentifier(specifier.imported) && | ||
specifier.imported.name === 'gql'); | ||
} | ||
return false; | ||
}); | ||
if (gqlSpecifier) { | ||
gqlImportIdentifier = gqlSpecifier.local; | ||
} | ||
} | ||
}, | ||
TaggedTemplateExpression(path) { | ||
const node = path.node; | ||
if (!(t.isIdentifier(node.tag) && node.tag.name === tagName)) { | ||
if (!(t.isIdentifier(node.tag) && | ||
gqlImportIdentifier && | ||
node.tag.name === gqlImportIdentifier.name)) { | ||
return; | ||
@@ -100,4 +135,6 @@ } | ||
else { | ||
const referencedDefinition = getForeignReference(elem, path, localDefinitionDeclaratorMap); | ||
const fragmentPlaceholder = `__FRAGMENT_${placeholder++}__`; | ||
const referencedDefinition = getForeignReference(elem, path, localDefinitionDeclaratorMap, resolver); | ||
const fragmentPlaceholder = `__FRAGMENT_${createHash('sha256') | ||
.update(filePath) | ||
.digest('hex')}_${placeholder++}__`; | ||
foreignReferences.set(fragmentPlaceholder, referencedDefinition); | ||
@@ -109,3 +146,3 @@ return `...${fragmentPlaceholder}`; | ||
const documentNode = graphqlParse(generatedDefinitionString); | ||
const defAst = getDefinition(documentNode); | ||
const defAst = getDefinition(documentNode, filePath); | ||
let def; | ||
@@ -112,0 +149,0 @@ const errors = validate(schema, documentNode, VALIDATION_RULES); |
@@ -7,2 +7,3 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ | ||
import { DependencyGraph } from './dependency-graph.js'; | ||
import { resolvedUnresolvedFragment } from '../utils.js'; | ||
function PossibleFragmentSpreadRules(foreignReferences) { | ||
@@ -39,2 +40,5 @@ return (context) => { | ||
for (const definition of dependencyGraph.definitions) { | ||
if (definition.type === 'unresolvedFragment') { | ||
continue; | ||
} | ||
for (const [placeholder, foreignReference,] of definition.foreignReferences.entries()) { | ||
@@ -55,10 +59,26 @@ if (foreignReference.type !== 'unresolvedFragment') { | ||
projectDefinitions.get(join(foreignFilePath, 'index.ts')) || | ||
projectDefinitions.get(join(foreignFilePath, 'index.tsx')); | ||
projectDefinitions.get(join(foreignFilePath, 'index.tsx')) || | ||
projectDefinitions.get(definition.filePath) || | ||
projectDefinitions.get(definition.filePath + '.ts') || | ||
projectDefinitions.get(definition.filePath + '.tsx') || | ||
projectDefinitions.get(join(definition.filePath, 'index.ts')) || | ||
projectDefinitions.get(join(definition.filePath, 'index.tsx')); | ||
if (!foreignDefinitionMap) { | ||
throw new Error(`Could not find ${foreignFilePath} in project or it doesn't contain any graphql definitions`); | ||
} | ||
const foreignDefinition = foreignDefinitionMap.exportedDefinitionMap.get(exportName); | ||
let foreignDefinition = foreignDefinitionMap.exportedDefinitionMap.get(exportName); | ||
if (!foreignDefinition) { | ||
throw new Error(`Could not find an exported definition at ${exportName} in ${definition.filePath}`); | ||
} | ||
if (foreignDefinition && | ||
foreignDefinition.type === 'unresolvedFragment') { | ||
const oldforeignDefinition = foreignDefinition; | ||
foreignDefinition = resolvedUnresolvedFragment(dependencyGraph.definitions, foreignDefinition); | ||
if (!foreignDefinition) { | ||
throw new Error(`Foreign operation reference failed to resolve for ${oldforeignDefinition.exportName} at ${definition.ast.name} in ${definition.filePath} is unresolveable`); | ||
} | ||
} | ||
if (!foreignDefinition) { | ||
throw new Error(`Foreign operation reference at ${definition.ast.name} in ${definition.filePath} is unresolveable`); | ||
} | ||
if (foreignDefinition.type !== 'fragment') { | ||
@@ -68,2 +88,4 @@ throw new Error(`Foreign operation reference found at ${definition.ast.name} in ${definition.filePath}`); | ||
definition.foreignReferences.set(placeholder, foreignDefinition); | ||
// we want to make sure we have the placeholder and also the reoslved value | ||
definition.foreignReferences.set(foreignDefinition.outputName, foreignDefinition); | ||
dependencyGraph.addDependency(definition, foreignDefinition); | ||
@@ -70,0 +92,0 @@ } |
@@ -32,3 +32,3 @@ import { parse, print, visit } from 'graphql'; | ||
if (!foreignRef) { | ||
throw new Error(`Cannot find foreign reference for definition in ${definition.filePath}`); | ||
throw new Error(`Cannot find foreign reference for definition in ${definition.filePath} for ${node.name.value}`); | ||
} | ||
@@ -48,3 +48,3 @@ if (foreignRef.type !== 'fragment') { | ||
} | ||
function generateFinalOperationString(operation, outputStringsByDefinition) { | ||
function generateFinalOperationString(fragments, operation, outputStringsByDefinition) { | ||
const visitedSet = new Set(); | ||
@@ -59,3 +59,2 @@ const visitQueue = [operation]; | ||
opStr = outputStringsByDefinition.get(def) + '\n\n' + opStr; | ||
visitQueue.push(...def.foreignReferences.values()); | ||
visitedSet.add(def); | ||
@@ -67,8 +66,27 @@ } | ||
const outputStringsByDefinition = new Map(); | ||
for (const definition of dependencyGraph.definitions) { | ||
for (let definition of dependencyGraph.definitions) { | ||
if (definition.type === 'unresolvedFragment') { | ||
throw new Error(`Could not resolve ${definition.exportName} from ${definition.filePath}`); | ||
} | ||
outputStringsByDefinition.set(definition, print(replaceForeignReferencesWithOutputName(definition))); | ||
} | ||
const operations = [...dependencyGraph.definitions].filter((def) => def.type === 'operation'); | ||
const updatedFragments = new Set(); | ||
// TODO: we need to clean this up. Since we use these fragments later on in the build we need to resolve the foreign refs in them here | ||
for (let fragment of dependencyGraph.fragments) { | ||
updatedFragments.add({ | ||
...fragment, | ||
ast: { | ||
...fragment.ast, | ||
...replaceForeignReferencesWithOutputName(fragment), | ||
}, | ||
}); | ||
} | ||
dependencyGraph.fragments.clear(); | ||
// TODO: this needs to be fixed | ||
for (let fragment of updatedFragments) { | ||
dependencyGraph.fragments.add(fragment); | ||
} | ||
const operations = [...dependencyGraph.definitions].filter((def) => def.type === 'operation' || def.type === 'fragment'); | ||
return operations.map((operation) => { | ||
const opStr = generateFinalOperationString(operation, outputStringsByDefinition); | ||
const opStr = generateFinalOperationString(dependencyGraph.fragments, operation, outputStringsByDefinition); | ||
return { | ||
@@ -75,0 +93,0 @@ location: operation.filePath, |
import type { OperationDefinitionNode, NameNode, FragmentDefinitionNode } from 'graphql'; | ||
export interface UnresolvedFragment { | ||
location: string; | ||
filePath: string; | ||
exportName: string; | ||
isExternal: boolean; | ||
type: 'unresolvedFragment'; | ||
@@ -37,5 +39,5 @@ } | ||
definitions: Definition[]; | ||
exportedDefinitionMap: Map<string, Definition>; | ||
exportedDefinitionMap: Map<string, Definition | UnresolvedFragment>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=types.d.ts.map |
export { athenaCodegen } from './codegen.js'; | ||
export type { CodegenArgs, OutputFile, Source } from './types.js'; | ||
export type { CodegenArgs, OutputFile, Source, Resolver } from './types.js'; | ||
export { generateDocumentFiles } from './generate-document-files.js'; | ||
@@ -7,2 +7,3 @@ export { generateSchemaTypes } from './generate-schema-types.js'; | ||
export { babelPlugin } from './babel-plugin.js'; | ||
export { gql } from './gql.js'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -6,2 +6,3 @@ export { athenaCodegen } from './codegen.js'; | ||
export { babelPlugin } from './babel-plugin.js'; | ||
export { gql } from './gql.js'; | ||
//# sourceMappingURL=index.js.map |
@@ -8,2 +8,6 @@ import type { DocumentNode, GraphQLSchema } from 'graphql'; | ||
} | ||
interface ResolveOptions { | ||
basedir: string; | ||
} | ||
export type Resolver = (importPath: string, options: ResolveOptions) => string | undefined; | ||
export interface CodegenArgs { | ||
@@ -14,4 +18,7 @@ schemaPath: string; | ||
extension: string; | ||
disableSchemaTypesGeneration: boolean; | ||
debug?: boolean; | ||
production: boolean; | ||
hash?: (document: DocumentNode) => string; | ||
resolver?: Resolver; | ||
} | ||
@@ -22,2 +29,3 @@ export type OutputFile = { | ||
}; | ||
export {}; | ||
//# sourceMappingURL=types.d.ts.map |
@@ -0,2 +1,6 @@ | ||
import type { DependencyGraphNode } from './gql/dependency-graph.js'; | ||
import type { Definition, UnresolvedFragment } from './gql/types.js'; | ||
export declare function changeExtension(fileName: string, ext: string): string; | ||
export declare function resolvedUnresolvedFragment(definitions: Set<DependencyGraphNode>, unresolvedFragment: UnresolvedFragment): Definition | undefined; | ||
export declare function extensionAwareResolver(path: string, extensions: string[]): string | undefined; | ||
//# sourceMappingURL=utils.d.ts.map |
@@ -1,6 +0,7 @@ | ||
import * as path from 'node:path'; | ||
import { extname, parse, format } from 'node:path'; | ||
import { existsSync } from 'fs'; | ||
export function changeExtension(fileName, ext) { | ||
const parts = path.parse(fileName); | ||
const parts = parse(fileName); | ||
ext = ext.startsWith('.') ? ext : `.${ext}`; | ||
return path.format({ | ||
return format({ | ||
...parts, | ||
@@ -11,2 +12,26 @@ ext, | ||
} | ||
export function resolvedUnresolvedFragment(definitions, unresolvedFragment) { | ||
return Array.from(definitions).find((potentialDefinitionLocations) => { | ||
if (unresolvedFragment && | ||
unresolvedFragment.type === 'unresolvedFragment' && | ||
potentialDefinitionLocations.type !== 'unresolvedFragment' && | ||
potentialDefinitionLocations.filePath === unresolvedFragment.filePath) { | ||
return (potentialDefinitionLocations.exportName === | ||
unresolvedFragment.exportName); | ||
} | ||
}); | ||
} | ||
export function extensionAwareResolver(path, extensions) { | ||
const startingExtensionName = extname(path); | ||
const fileNameWithNoExtension = path.replace(`${startingExtensionName}`, ''); | ||
return (extensions | ||
.map((ext) => { | ||
const potentialPath = fileNameWithNoExtension + `${ext}`; | ||
if (existsSync(potentialPath)) { | ||
return potentialPath; | ||
} | ||
return false; | ||
}) | ||
.filter((path) => path !== false)[0] || undefined); | ||
} | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "@data-eden/codegen", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"repository": { | ||
@@ -18,2 +18,16 @@ "type": "git", | ||
"default": "./dist/index.js" | ||
}, | ||
"./babel-plugin": { | ||
"import": { | ||
"types": "./dist/babel-plugin.d.ts", | ||
"default": "./dist/babel-plugin.js" | ||
}, | ||
"default": "./dist/babel-plugin.js" | ||
}, | ||
"./gql": { | ||
"import": { | ||
"types": "./dist/gql.d.ts", | ||
"default": "./dist/gql.js" | ||
}, | ||
"default": "./dist/gql.js" | ||
} | ||
@@ -40,2 +54,3 @@ }, | ||
"build": "tsc --build", | ||
"dev": "tsc --watch", | ||
"test": "vitest run" | ||
@@ -54,5 +69,8 @@ }, | ||
"@graphql-tools/documents": "^0.1.0", | ||
"@swc/core": "^1.3.58", | ||
"@types/debug": "^4.1.7", | ||
"babel-plugin-graphql-tag": "^3.3.0", | ||
"babel-plugin-jsx": "^1.2.0", | ||
"commander": "^10.0.1", | ||
"debug": "^4.3.4", | ||
"fs-extra": "^11.1.1", | ||
@@ -59,0 +77,0 @@ "globby": "^13.1.4" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
Found 1 instance in 1 package
97414
45.5%75
13.64%1174
52.07%1
-50%46
Infinity%19
18.75%4
33.33%1
Infinity%