Skip to content

Instantly share code, notes, and snippets.

@ktsn
Last active June 23, 2021 15:00
Show Gist options
  • Save ktsn/15f8f92a84085b777f4491a8f75b9289 to your computer and use it in GitHub Desktop.
Save ktsn/15f8f92a84085b777f4491a8f75b9289 to your computer and use it in GitHub Desktop.
Migration script from Apollo codegen to GraphQL Code Generator
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()
{
"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"
}
}
{
"compilerOptions": {
"module": "commonjs"
},
"include": ["**/*.ts"]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment