Created
January 28, 2022 10:04
-
-
Save NateScarlet/81ef62f4a8aac297f5e9bb305792f2db to your computer and use it in GitHub Desktop.
Vue SFC setup script migration
This file contains 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
#!/usr/bin/env ts-node-script | |
import { program } from 'commander'; | |
import * as _fs from 'fs'; | |
import { promisify } from 'util'; | |
import * as ts from 'typescript'; | |
import { ESLint } from 'eslint'; | |
const readFile = promisify(_fs.readFile); | |
const writeFile = promisify(_fs.writeFile); | |
let DEBUG = false; | |
function getPropertyNameText(name?: ts.PropertyName): string { | |
if (!name) { | |
return ''; | |
} | |
if (ts.isStringLiteral(name)) { | |
return name.text; | |
} | |
if (ts.isIdentifier(name)) { | |
return name.escapedText.toString(); | |
} | |
throw new Error( | |
`unsupported property assignment kind: ${ts.SyntaxKind[name.kind]}` | |
); | |
} | |
function getObjectElementInitializer( | |
prop: ts.ObjectLiteralElementLike | |
): ts.Expression { | |
if (ts.isPropertyAssignment(prop)) { | |
return prop.initializer; | |
} | |
if (ts.isMethodDeclaration(prop)) { | |
return ts.factory.createArrowFunction( | |
[], | |
[], | |
prop.parameters, | |
prop.type, | |
undefined, | |
prop.body | |
); | |
} | |
throw new Error( | |
`unsupported object element kind: ${getPropertyNameText(prop.name)}: ${ | |
ts.SyntaxKind[prop.kind] | |
}` | |
); | |
} | |
function transformEmitObjectToType(node: ts.Node): ts.TypeNode[] { | |
if (ts.isObjectLiteralExpression(node)) { | |
return [ | |
ts.factory.createTypeLiteralNode( | |
node.properties.flatMap((prop) => { | |
const initializer = getObjectElementInitializer(prop); | |
const params = [ | |
ts.factory.createParameterDeclaration( | |
undefined, | |
undefined, | |
undefined, | |
'e', | |
undefined, | |
ts.factory.createLiteralTypeNode( | |
ts.factory.createStringLiteral(getPropertyNameText(prop.name)) | |
) | |
), | |
]; | |
if (initializer.kind === ts.SyntaxKind.NullKeyword) { | |
params.push( | |
ts.factory.createParameterDeclaration( | |
undefined, | |
undefined, | |
ts.factory.createToken(ts.SyntaxKind.DotDotDotToken), | |
'args', | |
undefined, | |
undefined | |
) | |
); | |
} else if (ts.isArrowFunction(initializer)) { | |
params.push(...initializer.parameters); | |
} else { | |
throw new Error( | |
`unsupported emit initializer kind: ${ | |
ts.SyntaxKind[initializer.kind] | |
}` | |
); | |
} | |
return [ | |
ts.factory.createCallSignature( | |
[], | |
params, | |
ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword) | |
), | |
]; | |
}) | |
), | |
]; | |
} | |
return []; | |
} | |
async function transformTypeScript( | |
setupTypescript: string, | |
exportTypeScript = '' | |
) { | |
const exportProperties: ts.ObjectLiteralElementLike[] = []; | |
const exportFileStatements: ts.Statement[] = []; | |
const setupFileStatements: ts.Statement[] = []; | |
let { | |
transformed: [exportFile], | |
} = ts.transform( | |
ts.createSourceFile('temp.ts', exportTypeScript, ts.ScriptTarget.Latest), | |
[ | |
(ctx) => (root) => { | |
const visitor1: ts.Visitor = (node) => { | |
if (ts.isSourceFile(node)) { | |
return ts.visitEachChild(node, visitor1, ctx); | |
} | |
if ( | |
ts.isExportAssignment(node) && | |
ts.isCallExpression(node.expression) && | |
ts.isIdentifier(node.expression.expression) && | |
node.expression.expression.escapedText === 'defineComponent' | |
) { | |
const callExpr = node.expression; | |
const options = callExpr.arguments[0]; | |
if (!ts.isObjectLiteralExpression(options)) { | |
throw new Error( | |
`unsupported defineComponent options kind: ${ | |
ts.SyntaxKind[options.kind] | |
}` | |
); | |
} | |
const setupStmts = setupFileStatements; | |
for (const prop of options.properties) { | |
const nameText = prop.name ? getPropertyNameText(prop.name) : ''; | |
switch (nameText) { | |
case 'props': | |
setupStmts.push( | |
ts.factory.createVariableStatement( | |
[], | |
ts.factory.createVariableDeclarationList( | |
[ | |
ts.factory.createVariableDeclaration( | |
'props', | |
undefined, | |
undefined, | |
ts.factory.createCallExpression( | |
ts.factory.createIdentifier('defineProps'), | |
[], | |
[getObjectElementInitializer(prop)] | |
) | |
), | |
], | |
ts.NodeFlags.Const | |
) | |
), | |
ts.factory.createVariableStatement( | |
[], | |
ts.factory.createVariableDeclarationList( | |
[ | |
ts.factory.createVariableDeclaration( | |
'_', | |
undefined, | |
undefined, | |
ts.factory.createIdentifier('props') | |
), | |
], | |
ts.NodeFlags.Const | |
) | |
) | |
); | |
break; | |
case 'emits': | |
setupStmts.push( | |
ts.factory.createVariableStatement( | |
[], | |
ts.factory.createVariableDeclarationList( | |
[ | |
ts.factory.createVariableDeclaration( | |
'emit', | |
undefined, | |
undefined, | |
ts.factory.createCallExpression( | |
ts.factory.createIdentifier('defineEmits'), | |
transformEmitObjectToType( | |
getObjectElementInitializer(prop) | |
), | |
[] | |
) | |
), | |
], | |
ts.NodeFlags.Const | |
) | |
), | |
ts.factory.createVariableStatement( | |
[], | |
ts.factory.createVariableDeclarationList( | |
[ | |
ts.factory.createVariableDeclaration( | |
'ctx', | |
undefined, | |
undefined, | |
ts.factory.createObjectLiteralExpression([ | |
ts.factory.createShorthandPropertyAssignment( | |
'emit' | |
), | |
]) | |
), | |
], | |
ts.NodeFlags.Const | |
) | |
) | |
); | |
break; | |
case 'setup': | |
const initializer = getObjectElementInitializer(prop); | |
if ( | |
!( | |
ts.isArrowFunction(initializer) || | |
ts.isMethodDeclaration(initializer) | |
) | |
) { | |
throw new Error( | |
`unsupported setup syntax: ${ | |
ts.SyntaxKind[initializer.kind] | |
}` | |
); | |
} | |
if (ts.isBlock(initializer.body)) { | |
initializer.body.statements.forEach((stmt) => { | |
if (ts.isReturnStatement(stmt)) { | |
setupStmts.push( | |
ts.factory.createExpressionStatement( | |
ts.factory.createCallExpression( | |
ts.factory.createIdentifier('defineExpose'), | |
[], | |
[stmt.expression] | |
) | |
) | |
); | |
return; | |
} | |
setupStmts.push(stmt); | |
}); | |
} else { | |
throw new Error( | |
`unsupported setup body syntax: ${ | |
ts.SyntaxKind[initializer.body.kind] | |
}` | |
); | |
} | |
break; | |
case 'components': | |
break; | |
default: | |
exportProperties.push(prop); | |
} | |
} | |
return; | |
} | |
return node; | |
}; | |
const visitor2: ts.Visitor = (node) => { | |
if ( | |
exportProperties.length === 0 && | |
ts.isImportDeclaration(node) && | |
node.importClause.namedBindings && | |
ts.isNamedImports(node.importClause.namedBindings) && | |
node.importClause.namedBindings.elements.some( | |
(i) => | |
ts.isIdentifier(i.name) && | |
i.name.escapedText === 'defineComponent' | |
) | |
) { | |
return ts.factory.updateImportDeclaration( | |
node, | |
node.decorators, | |
node.modifiers, | |
ts.factory.updateImportClause( | |
node.importClause, | |
node.importClause.isTypeOnly, | |
node.importClause.name, | |
ts.factory.updateNamedImports( | |
node.importClause.namedBindings, | |
node.importClause.namedBindings.elements.filter( | |
(i) => | |
!( | |
ts.isIdentifier(i.name) && | |
i.name.escapedText === 'defineComponent' | |
) | |
) | |
) | |
), | |
node.moduleSpecifier, | |
undefined | |
); | |
} | |
return node; | |
}; | |
return ts.visitNode(ts.visitNode(root, visitor1), visitor2); | |
}, | |
], | |
{ | |
target: ts.ScriptTarget.Latest, | |
} | |
); | |
let { | |
transformed: [setupFile], | |
} = ts.transform( | |
ts.createSourceFile('temp.ts', setupTypescript, ts.ScriptTarget.Latest), | |
[ | |
(ctx) => (root) => { | |
const visitor1: ts.Visitor = (node) => { | |
if (ts.isSourceFile(node)) { | |
return ts.visitEachChild(node, visitor1, ctx); | |
} | |
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { | |
exportFileStatements.push(node); | |
return undefined; | |
} | |
return node; | |
}; | |
return ts.visitNode(root, visitor1); | |
}, | |
], | |
{ | |
target: ts.ScriptTarget.Latest, | |
} | |
); | |
exportFile = ts.factory.updateSourceFile(exportFile, [ | |
...exportFile.statements, | |
...exportFileStatements, | |
...(exportProperties.length > 0 | |
? [ | |
ts.factory.createExportDefault( | |
ts.factory.createCallExpression( | |
ts.factory.createIdentifier('defineComponent'), | |
[], | |
[ts.factory.createObjectLiteralExpression(exportProperties)] | |
) | |
), | |
] | |
: []), | |
]); | |
setupFile = ts.factory.updateSourceFile(setupFile, [ | |
...setupFile.statements, | |
...setupFileStatements, | |
]); | |
const printer = ts.createPrinter({ | |
newLine: ts.NewLineKind.LineFeed, | |
}); | |
const printFile = async (f: ts.SourceFile) => { | |
// XXX: printer will escape unicode | |
// https://github.com/microsoft/TypeScript/issues/36174 | |
return printer.printFile(f).replace(/\\u[0-9A-F]{4}/g, (code) => { | |
return String.fromCodePoint(Number.parseInt(code.slice(2), 16)); | |
}); | |
}; | |
return { | |
exportFile: await printFile(exportFile), | |
setupFile: await printFile(setupFile), | |
}; | |
} | |
async function updateFile(path: string, dryRun = false): Promise<void> { | |
const data = (await readFile(path)).toString(); | |
let setupTypescript = ''; | |
let exportTypeScript = ''; | |
let otherContent = ''; | |
let pushLine: (s: string) => void = (s) => { | |
otherContent += s + '\n'; | |
}; | |
const defaultPushLine = pushLine; | |
for (const line of data.split('\n')) { | |
if (line.match(/<script setup lang=('ts'|"ts")>/)) { | |
pushLine = (s) => { | |
setupTypescript += s + '\n'; | |
}; | |
} else if (line.match(/<script lang=('ts'|"ts")>/)) { | |
pushLine = (s) => { | |
exportTypeScript += s + '\n'; | |
}; | |
} else if (line === '</script>') { | |
pushLine = defaultPushLine; | |
} else { | |
pushLine(line); | |
} | |
} | |
const { setupFile, exportFile } = await transformTypeScript( | |
setupTypescript, | |
exportTypeScript | |
); | |
const updated = `\ | |
${otherContent.trim()} | |
<script lang="ts"> | |
${exportFile.trim()} | |
</script> | |
<script setup lang="ts"> | |
${setupFile.trim()} | |
</script> | |
`; | |
const [{ output = updated }] = await new ESLint({ fix: true }).lintText( | |
updated, | |
{ | |
filePath: path, | |
} | |
); | |
if (DEBUG) { | |
console.log({ | |
exportTypeScript, | |
setupTypescript, | |
}); | |
} | |
if (dryRun) { | |
console.log(output); | |
} else { | |
await writeFile(path, output); | |
} | |
} | |
(async (): Promise<void> => { | |
program.usage('[file ...]').option('-d --dry-run').option('--debug'); | |
const { args } = program.parse(); | |
const { dryRun, debug } = program.opts(); | |
DEBUG = debug; | |
for (const i of args) { | |
console.log(i); | |
await updateFile(i, dryRun); | |
} | |
})().catch((err) => { | |
console.error(err); | |
process.exit(1); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment