Skip to content

Instantly share code, notes, and snippets.

@NateScarlet
Created January 28, 2022 10:04
Show Gist options
  • Save NateScarlet/81ef62f4a8aac297f5e9bb305792f2db to your computer and use it in GitHub Desktop.
Save NateScarlet/81ef62f4a8aac297f5e9bb305792f2db to your computer and use it in GitHub Desktop.
Vue SFC setup script migration
#!/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