503 lines
17 KiB
JavaScript
503 lines
17 KiB
JavaScript
import { parse } from '@babel/parser';
|
|
import { isVariableDeclarator, isIdentifier, isTemplateLiteral, isImportDefaultSpecifier, isImportSpecifier } from '@babel/types';
|
|
import { asArray } from '@graphql-tools/utils';
|
|
import traverse from '@babel/traverse';
|
|
|
|
const getExtNameFromFilePath = (filePath) => {
|
|
const partials = filePath.split('.');
|
|
let ext = '.' + partials.pop();
|
|
if (partials.length > 1 && partials[partials.length - 1] === 'flow') {
|
|
ext = '.' + partials.pop() + ext;
|
|
}
|
|
return ext;
|
|
};
|
|
|
|
function generateConfig(filePath, code, _options) {
|
|
const plugins = [
|
|
'asyncGenerators',
|
|
'bigInt',
|
|
'classProperties',
|
|
'classPrivateProperties',
|
|
'classPrivateMethods',
|
|
'decorators-legacy',
|
|
'doExpressions',
|
|
'dynamicImport',
|
|
'exportDefaultFrom',
|
|
'exportNamespaceFrom',
|
|
'functionBind',
|
|
'functionSent',
|
|
'importMeta',
|
|
'logicalAssignment',
|
|
'nullishCoalescingOperator',
|
|
'numericSeparator',
|
|
'objectRestSpread',
|
|
'optionalCatchBinding',
|
|
'optionalChaining',
|
|
['pipelineOperator', { proposal: 'smart' }],
|
|
'throwExpressions',
|
|
];
|
|
// { all: true } option is bullshit thus I do it manually, just in case
|
|
// I still specify it
|
|
const flowPlugins = [['flow', { all: true }], 'flowComments'];
|
|
// If line has @flow header, include flow plug-ins
|
|
const dynamicFlowPlugins = code.includes('@flow') ? flowPlugins : [];
|
|
const fileExt = getExtNameFromFilePath(filePath);
|
|
switch (fileExt) {
|
|
case '.ts':
|
|
plugins.push('typescript');
|
|
break;
|
|
case '.tsx':
|
|
plugins.push('typescript', 'jsx');
|
|
break;
|
|
// Adding .jsx extension by default because it doesn't affect other syntax features
|
|
// (unlike .tsx) and because people are seem to use it with regular file extensions
|
|
// (e.g. .js) see https://github.com/dotansimha/graphql-code-generator/issues/1967
|
|
case '.js':
|
|
plugins.push('jsx', ...dynamicFlowPlugins);
|
|
break;
|
|
case '.jsx':
|
|
plugins.push('jsx', ...dynamicFlowPlugins);
|
|
break;
|
|
case '.flow.js':
|
|
plugins.push('jsx', ...flowPlugins);
|
|
break;
|
|
case '.flow.jsx':
|
|
plugins.push('jsx', ...flowPlugins);
|
|
break;
|
|
case '.flow':
|
|
plugins.push('jsx', ...flowPlugins);
|
|
break;
|
|
case '.vue':
|
|
plugins.push('typescript', 'vue');
|
|
break;
|
|
default:
|
|
plugins.push('jsx', ...dynamicFlowPlugins);
|
|
break;
|
|
}
|
|
// The _options filed will be used to retrieve the original options.
|
|
// Useful when we wanna get not config related options later on
|
|
return {
|
|
sourceType: 'module',
|
|
plugins,
|
|
allowUndeclaredExports: true,
|
|
};
|
|
}
|
|
|
|
// Will use the shortest indention as an axis
|
|
const freeText = (text, skipIndentation = false) => {
|
|
if (text instanceof Array) {
|
|
text = text.join('');
|
|
}
|
|
// This will allow inline text generation with external functions, same as ctrl+shift+c
|
|
// As long as we surround the inline text with ==>text<==
|
|
text = text.replace(/( *)==>((?:.|\n)*?)<==/g, (_match, baseIndent, content) => {
|
|
return content
|
|
.split('\n')
|
|
.map(line => `${baseIndent}${line}`)
|
|
.join('\n');
|
|
});
|
|
if (skipIndentation) {
|
|
return text;
|
|
}
|
|
const lines = text.split('\n');
|
|
const minIndent = lines
|
|
.filter(line => line.trim())
|
|
.reduce((minIndent, line) => {
|
|
const currIndent = line.match(/^ */)[0].length;
|
|
return currIndent < minIndent ? currIndent : minIndent;
|
|
}, Infinity);
|
|
return lines
|
|
.map(line => line.slice(minIndent))
|
|
.join('\n')
|
|
.trim()
|
|
.replace(/\n +\n/g, '\n\n');
|
|
};
|
|
|
|
const defaults = {
|
|
modules: [
|
|
{
|
|
name: 'graphql-tag',
|
|
},
|
|
{
|
|
name: 'graphql-tag.macro',
|
|
},
|
|
{
|
|
name: '@apollo/client',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: '@apollo/client/core',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-angular',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'gatsby',
|
|
identifier: 'graphql',
|
|
},
|
|
{
|
|
name: 'apollo-server-express',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'react-relay',
|
|
identifier: 'graphql',
|
|
},
|
|
{
|
|
name: 'react-relay/hooks',
|
|
identifier: 'graphql',
|
|
},
|
|
{
|
|
name: 'relay-runtime',
|
|
identifier: 'graphql',
|
|
},
|
|
{
|
|
name: 'babel-plugin-relay/macro',
|
|
identifier: 'graphql',
|
|
},
|
|
{
|
|
name: 'apollo-boost',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-koa',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-hapi',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-fastify',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: ' apollo-server-lambda',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-micro',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-azure-functions',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-cloud-functions',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'apollo-server-cloudflare',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'graphql.macro',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: '@urql/core',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: 'urql',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: '@urql/preact',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: '@urql/svelte',
|
|
identifier: 'gql',
|
|
},
|
|
{
|
|
name: '@urql/vue',
|
|
identifier: 'gql',
|
|
},
|
|
],
|
|
gqlMagicComment: 'graphql',
|
|
globalGqlIdentifierName: ['gql', 'graphql'],
|
|
};
|
|
const createVisitor = (code, out, options = {}) => {
|
|
// Apply defaults to options
|
|
let { modules, globalGqlIdentifierName, gqlMagicComment } = {
|
|
...defaults,
|
|
...options,
|
|
};
|
|
// Prevent case related potential errors
|
|
gqlMagicComment = gqlMagicComment.toLowerCase();
|
|
// normalize `name` and `identifier` values
|
|
modules = modules.map(mod => {
|
|
return {
|
|
name: mod.name,
|
|
identifier: mod.identifier && mod.identifier.toLowerCase(),
|
|
};
|
|
});
|
|
globalGqlIdentifierName = asArray(globalGqlIdentifierName).map(s => s.toLowerCase());
|
|
// Keep imported identifiers
|
|
// import gql from 'graphql-tag' -> gql
|
|
// import { graphql } from 'gatsby' -> graphql
|
|
// Will result with ['gql', 'graphql']
|
|
const definedIdentifierNames = [];
|
|
// Will accumulate all template literals
|
|
const gqlTemplateLiterals = [];
|
|
// Check if package is registered
|
|
function isValidPackage(name) {
|
|
return modules.some(pkg => pkg.name && name && pkg.name.toLowerCase() === name.toLowerCase());
|
|
}
|
|
// Check if identifier is defined and imported from registered packages
|
|
function isValidIdentifier(name) {
|
|
return definedIdentifierNames.some(id => id === name) || globalGqlIdentifierName.includes(name);
|
|
}
|
|
const pluckStringFromFile = ({ start, end }) => {
|
|
return freeText(code
|
|
// Slice quotes
|
|
.slice(start + 1, end - 1)
|
|
// Erase string interpolations as we gonna export everything as a single
|
|
// string anyway
|
|
.replace(/\$\{[^}]*\}/g, '')
|
|
.split('\\`')
|
|
.join('`'), options.skipIndent);
|
|
};
|
|
// Push all template literals leaded by graphql magic comment
|
|
// e.g. /* GraphQL */ `query myQuery {}` -> query myQuery {}
|
|
const pluckMagicTemplateLiteral = (node, takeExpression = false) => {
|
|
const leadingComments = node.leadingComments;
|
|
if (!leadingComments) {
|
|
return;
|
|
}
|
|
if (!leadingComments.length) {
|
|
return;
|
|
}
|
|
const leadingComment = leadingComments[leadingComments.length - 1];
|
|
const leadingCommentValue = leadingComment.value.trim().toLowerCase();
|
|
if (leadingCommentValue !== gqlMagicComment) {
|
|
return;
|
|
}
|
|
const nodeToUse = takeExpression ? node.expression : node;
|
|
const gqlTemplateLiteral = pluckStringFromFile(nodeToUse);
|
|
if (gqlTemplateLiteral) {
|
|
gqlTemplateLiterals.push({
|
|
content: gqlTemplateLiteral,
|
|
loc: node.loc,
|
|
end: node.end,
|
|
start: node.start,
|
|
});
|
|
}
|
|
};
|
|
return {
|
|
CallExpression: {
|
|
enter(path) {
|
|
// Find the identifier name used from graphql-tag, commonJS
|
|
// e.g. import gql from 'graphql-tag' -> gql
|
|
if (path.node.callee.name === 'require' && isValidPackage(path.node.arguments[0].value)) {
|
|
if (!isVariableDeclarator(path.parent)) {
|
|
return;
|
|
}
|
|
if (!isIdentifier(path.parent.id)) {
|
|
return;
|
|
}
|
|
definedIdentifierNames.push(path.parent.id.name);
|
|
return;
|
|
}
|
|
const arg0 = path.node.arguments[0];
|
|
// Push strings template literals to gql calls
|
|
// e.g. gql(`query myQuery {}`) -> query myQuery {}
|
|
if (isIdentifier(path.node.callee) && isValidIdentifier(path.node.callee.name) && isTemplateLiteral(arg0)) {
|
|
const gqlTemplateLiteral = pluckStringFromFile(arg0);
|
|
// If the entire template was made out of interpolations it should be an empty
|
|
// string by now and thus should be ignored
|
|
if (gqlTemplateLiteral) {
|
|
gqlTemplateLiterals.push({
|
|
content: gqlTemplateLiteral,
|
|
loc: arg0.loc,
|
|
end: arg0.end,
|
|
start: arg0.start,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
},
|
|
ImportDeclaration: {
|
|
enter(path) {
|
|
// Find the identifier name used from graphql-tag, es6
|
|
// e.g. import gql from 'graphql-tag' -> gql
|
|
if (!isValidPackage(path.node.source.value)) {
|
|
return;
|
|
}
|
|
const moduleNode = modules.find(pkg => pkg.name.toLowerCase() === path.node.source.value.toLowerCase());
|
|
const gqlImportSpecifier = path.node.specifiers.find((importSpecifier) => {
|
|
// When it's a default import and registered package has no named identifier
|
|
if (isImportDefaultSpecifier(importSpecifier) && !moduleNode.identifier) {
|
|
return true;
|
|
}
|
|
// When it's a named import that matches registered package's identifier
|
|
if (isImportSpecifier(importSpecifier) &&
|
|
'name' in importSpecifier.imported &&
|
|
importSpecifier.imported.name === moduleNode.identifier) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
if (!gqlImportSpecifier) {
|
|
return;
|
|
}
|
|
definedIdentifierNames.push(gqlImportSpecifier.local.name);
|
|
},
|
|
},
|
|
ExpressionStatement: {
|
|
exit(path) {
|
|
// Push all template literals leaded by graphql magic comment
|
|
// e.g. /* GraphQL */ `query myQuery {}` -> query myQuery {}
|
|
if (!isTemplateLiteral(path.node.expression)) {
|
|
return;
|
|
}
|
|
pluckMagicTemplateLiteral(path.node, true);
|
|
},
|
|
},
|
|
TemplateLiteral: {
|
|
exit(path) {
|
|
pluckMagicTemplateLiteral(path.node);
|
|
},
|
|
},
|
|
TaggedTemplateExpression: {
|
|
exit(path) {
|
|
// Push all template literals provided to the found identifier name
|
|
// e.g. gql `query myQuery {}` -> query myQuery {}
|
|
if (!isIdentifier(path.node.tag) || !isValidIdentifier(path.node.tag.name)) {
|
|
return;
|
|
}
|
|
const gqlTemplateLiteral = pluckStringFromFile(path.node.quasi);
|
|
if (gqlTemplateLiteral) {
|
|
gqlTemplateLiterals.push({
|
|
content: gqlTemplateLiteral,
|
|
end: path.node.quasi.end,
|
|
start: path.node.quasi.start,
|
|
loc: path.node.quasi.loc,
|
|
});
|
|
}
|
|
},
|
|
},
|
|
exit() {
|
|
out.returnValue = gqlTemplateLiterals;
|
|
},
|
|
};
|
|
};
|
|
|
|
const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.flow', '.flow.js', '.flow.jsx', '.vue'];
|
|
// tslint:disable-next-line: no-implicit-dependencies
|
|
function parseWithVue(vueTemplateCompiler, fileData) {
|
|
const { descriptor } = vueTemplateCompiler.parse(fileData);
|
|
return descriptor.script || descriptor.scriptSetup
|
|
? vueTemplateCompiler.compileScript(descriptor, { id: Date.now().toString() }).content
|
|
: '';
|
|
}
|
|
/**
|
|
* Asynchronously plucks GraphQL template literals from a single file.
|
|
*
|
|
* Supported file extensions include: `.js`, `.jsx`, `.ts`, `.tsx`, `.flow`, `.flow.js`, `.flow.jsx`, `.vue`
|
|
*
|
|
* @param filePath Path to the file containing the code. Required to detect the file type
|
|
* @param code The contents of the file being parsed.
|
|
* @param options Additional options for determining how a file is parsed.
|
|
*/
|
|
const gqlPluckFromCodeString = async (filePath, code, options = {}) => {
|
|
validate({ code, options });
|
|
const fileExt = extractExtension(filePath);
|
|
if (fileExt === '.vue') {
|
|
code = await pluckVueFileScript(code);
|
|
}
|
|
return parseCode({ code, filePath, options })
|
|
.map(t => t.content)
|
|
.join('\n\n');
|
|
};
|
|
/**
|
|
* Synchronously plucks GraphQL template literals from a single file
|
|
*
|
|
* Supported file extensions include: `.js`, `.jsx`, `.ts`, `.tsx`, `.flow`, `.flow.js`, `.flow.jsx`, `.vue`
|
|
*
|
|
* @param filePath Path to the file containing the code. Required to detect the file type
|
|
* @param code The contents of the file being parsed.
|
|
* @param options Additional options for determining how a file is parsed.
|
|
*/
|
|
const gqlPluckFromCodeStringSync = (filePath, code, options = {}) => {
|
|
validate({ code, options });
|
|
const fileExt = extractExtension(filePath);
|
|
if (fileExt === '.vue') {
|
|
code = pluckVueFileScriptSync(code);
|
|
}
|
|
return parseCode({ code, filePath, options })
|
|
.map(t => t.content)
|
|
.join('\n\n');
|
|
};
|
|
function parseCode({ code, filePath, options, }) {
|
|
const out = { returnValue: null };
|
|
const ast = parse(code, generateConfig(filePath, code));
|
|
const visitor = createVisitor(code, out, options);
|
|
traverse(ast, visitor);
|
|
return out.returnValue || [];
|
|
}
|
|
function validate({ code, options }) {
|
|
if (typeof code !== 'string') {
|
|
throw TypeError('Provided code must be a string');
|
|
}
|
|
if (!(options instanceof Object)) {
|
|
throw TypeError(`Options arg must be an object`);
|
|
}
|
|
}
|
|
function extractExtension(filePath) {
|
|
const fileExt = getExtNameFromFilePath(filePath);
|
|
if (fileExt) {
|
|
if (!supportedExtensions.includes(fileExt)) {
|
|
throw TypeError(`Provided file type must be one of ${supportedExtensions.join(', ')} `);
|
|
}
|
|
}
|
|
return fileExt;
|
|
}
|
|
const MissingVueTemplateCompilerError = new Error(freeText(`
|
|
GraphQL template literals cannot be plucked from a Vue template code without having the "@vue/compiler-sfc" package installed.
|
|
Please install it and try again.
|
|
|
|
Via NPM:
|
|
|
|
$ npm install @vue/compiler-sfc
|
|
|
|
Via Yarn:
|
|
|
|
$ yarn add @vue/compiler-sfc
|
|
`));
|
|
async function pluckVueFileScript(fileData) {
|
|
// tslint:disable-next-line: no-implicit-dependencies
|
|
let vueTemplateCompiler;
|
|
try {
|
|
// tslint:disable-next-line: no-implicit-dependencies
|
|
vueTemplateCompiler = await import('@vue/compiler-sfc');
|
|
}
|
|
catch (e) {
|
|
throw MissingVueTemplateCompilerError;
|
|
}
|
|
return parseWithVue(vueTemplateCompiler, fileData);
|
|
}
|
|
function pluckVueFileScriptSync(fileData) {
|
|
// tslint:disable-next-line: no-implicit-dependencies
|
|
let vueTemplateCompiler;
|
|
try {
|
|
// tslint:disable-next-line: no-implicit-dependencies
|
|
vueTemplateCompiler = require('@vue/compiler-sfc');
|
|
}
|
|
catch (e) {
|
|
throw MissingVueTemplateCompilerError;
|
|
}
|
|
return parseWithVue(vueTemplateCompiler, fileData);
|
|
}
|
|
|
|
export { gqlPluckFromCodeString, gqlPluckFromCodeStringSync, parseCode };
|
|
//# sourceMappingURL=index.esm.js.map
|