Last active
June 23, 2021 15:00
-
-
Save ktsn/15f8f92a84085b777f4491a8f75b9289 to your computer and use it in GitHub Desktop.
Migration script from Apollo codegen to GraphQL Code Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as fs from 'fs' | |
import * as path from 'path' | |
import * as globby from 'globby' | |
import { parseComponent } from 'vue-template-compiler' | |
import { format } from 'prettier' | |
import { parse } from '@babel/parser' | |
import traverse from '@babel/traverse' | |
import generate from '@babel/generator' | |
const graphqlCodeGenFile = '../src/generated/graphql.ts' | |
const graphqlCodeGenImportSource = '@/generated/graphql' | |
const prettierConfig = { | |
singleQuote: true, | |
semi: false, | |
} | |
function availableNames(): string[] { | |
const code = fs.readFileSync(graphqlCodeGenFile, 'utf-8') | |
const ast = parse(code, { | |
sourceType: 'module', | |
plugins: ['typescript'], | |
}) | |
const exported: string[] = [] | |
traverse(ast as any, { | |
ExportNamedDeclaration(path) { | |
if (!path.node.declaration) { | |
return | |
} | |
path.traverse({ | |
TSTypeAliasDeclaration(path) { | |
exported.push(path.node.id.name) | |
}, | |
TSEnumDeclaration(path) { | |
exported.push(path.node.id.name) | |
}, | |
}) | |
}, | |
}) | |
return exported | |
} | |
function transformVueScript( | |
code: string, | |
scriptTransformer: (script: string) => string | |
): string { | |
const parsed = parseComponent(code) | |
if (!parsed.script) { | |
return code | |
} | |
const transformed = scriptTransformer(parsed.script.content) | |
const { start, end } = parsed.script | |
return code.slice(0, start) + transformed + code.slice(end) | |
} | |
function transformApolloGeneratedCode( | |
code: string, | |
availableNames: string[] | |
): string { | |
const ast = parse(code, { | |
sourceType: 'module', | |
plugins: ['typescript'], | |
}) | |
traverse(ast as any, { | |
ImportDeclaration(path) { | |
if (!/\/__generated__\//.test(path.node.source.value)) { | |
return | |
} | |
path.node.source.value = graphqlCodeGenImportSource | |
path.node.specifiers.forEach((specifier) => { | |
if (specifier.type !== 'ImportSpecifier') { | |
return | |
} | |
const importedName = | |
specifier.imported.type === 'Identifier' | |
? specifier.imported.name | |
: specifier.imported.value | |
const convertedName = convertSpecifierName(importedName, availableNames) | |
if (!convertedName) { | |
return | |
} | |
specifier.local.name = convertedName | |
if (specifier.imported.type === 'Identifier') { | |
specifier.imported.name = convertedName | |
} else { | |
specifier.imported.value = convertedName | |
} | |
const binding = path.scope.bindings[importedName] | |
binding?.referencePaths.forEach((p) => { | |
if (p.node.type === 'Identifier') { | |
p.node.name = convertedName | |
} | |
}) | |
}) | |
}, | |
}) | |
const { code: generated } = generate(ast as any, { | |
compact: false, | |
retainLines: true, | |
}) | |
return generated | |
} | |
function convertSpecifierName( | |
name: string, | |
availableNames: string[] | |
): string | null { | |
// Unsupported | |
if (name.includes('_')) { | |
return null | |
} | |
const matched = /^(.+?)(Variables)?$/.exec(name) | |
if (!matched) { | |
return null | |
} | |
const prefix = matched[1][0].toUpperCase() + matched[1].slice(1) | |
const suffix = matched[2] ?? '' | |
const types = ['Query', 'Mutation', 'Subscription', 'Fragment', ''] | |
for (let i = 0; i < types.length; i++) { | |
const type = types[i] | |
const candidate = prefix + type + suffix | |
if (availableNames.includes(candidate)) { | |
return candidate | |
} | |
} | |
return null | |
} | |
function main() { | |
const folder = process.argv[2] | |
const files = globby.sync([ | |
path.join(folder, '**/*.ts'), | |
path.join(folder, '**/*.vue'), | |
]) | |
const names = availableNames() | |
files.forEach((file) => { | |
const code = fs.readFileSync(file, 'utf-8') | |
if (!code.includes('/__generated__/')) { | |
return | |
} | |
let result: string | undefined | |
switch (path.extname(file)) { | |
case '.vue': { | |
const transformed = transformVueScript(code, (script) => | |
transformApolloGeneratedCode(script, names) | |
) | |
result = format(transformed, { | |
...prettierConfig, | |
parser: 'vue', | |
}) | |
break | |
} | |
case '.ts': { | |
result = format(transformApolloGeneratedCode(code, names), { | |
...prettierConfig, | |
parser: 'typescript', | |
}) | |
break | |
} | |
} | |
if (!result) { | |
return | |
} | |
fs.writeFileSync(file, result) | |
console.log('Wrote: ' + file) | |
}) | |
} | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "migrator", | |
"private": true, | |
"dependencies": { | |
"@babel/generator": "^7.14.3", | |
"@babel/parser": "^7.14.4", | |
"@babel/traverse": "^7.14.2", | |
"@types/node": "^15.6.1", | |
"globby": "^11.0.3", | |
"prettier": "^2.3.0", | |
"ts-node": "^10.0.0", | |
"typescript": "^4.3.2", | |
"vue-template-compiler": "^2.6.12" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
{ | |
"compilerOptions": { | |
"module": "commonjs" | |
}, | |
"include": ["**/*.ts"] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment