au-rogue
Version:
Conservative Aurelia 1 to 2 codemods. Changes only what is safe, reports everything.
120 lines (119 loc) • 5.37 kB
JavaScript
import * as fs from 'node:fs';
import { parseFragment, serialize } from 'parse5';
function isElement(n) {
return n.tagName !== undefined;
}
/**
* Check if an event handler on a specific element might need preventDefault behavior
* This helps identify cases where Aurelia 1's automatic preventDefault might be missed in v2
*/
function isPotentiallyProblematicEvent(element, eventName, eventValue) {
const tagName = element.tagName.toLowerCase();
// Form submission events on buttons inside forms
if (eventName === 'click' && tagName === 'button') {
// Check if button is inside a form or has type="submit"
const typeAttr = element.attrs.find(attr => attr.name === 'type');
if (typeAttr?.value === 'submit' || isInsideForm(element)) {
return true;
}
}
// Form submission events
if (eventName === 'submit' && tagName === 'form') {
return true;
}
// Link navigation events that might need prevention
if (eventName === 'click' && tagName === 'a') {
const hrefAttr = element.attrs.find(attr => attr.name === 'href');
// If there's an href but we're handling click, might need preventDefault
if (hrefAttr && hrefAttr.value && !hrefAttr.value.startsWith('javascript:')) {
return true;
}
}
// Key events that commonly need preventDefault (like Enter in forms)
if (eventName === 'keydown' || eventName === 'keyup' || eventName === 'keypress') {
return true;
}
return false;
}
/**
* Check if an element is inside a form (simplified check - only looks at direct ancestors)
*/
function isInsideForm(element) {
// This is a simplified check - in a full implementation we'd walk up the DOM tree
// For now, we'll be conservative and assume buttons might be in forms
return true; // Conservative approach - warn about all button clicks
}
export function transformTemplates(files, reporter, options) {
for (const file of files) {
const html = fs.readFileSync(file, 'utf8');
const doc = parseFragment(html, { sourceCodeLocationInfo: true });
let edits = 0;
let warnings = 0;
function visit(node) {
if (isElement(node)) {
// tag transforms
if (node.tagName === 'require') {
node.tagName = 'import';
edits++;
reporter.edit(file, '<require> -> <import>');
}
if (node.tagName === 'router-view') {
node.tagName = 'au-viewport';
edits++;
reporter.edit(file, '<router-view> -> <au-viewport>');
}
if (node.tagName === 'compose') {
node.tagName = 'au-compose';
edits++;
reporter.edit(file, '<compose> -> <au-compose>');
for (const a of node.attrs) {
if (a.name === 'view')
a.name = 'template';
if (a.name === 'view-model')
a.name = 'component';
}
}
for (const a of node.attrs) {
if (a.name.endsWith('.delegate')) {
a.name = a.name.replace('.delegate', '.trigger');
edits++;
reporter.edit(file, '*.delegate -> *.trigger');
}
if (a.name === 'view-model.ref') {
a.name = 'component.ref';
edits++;
reporter.edit(file, 'view-model.ref -> component.ref');
}
if (a.name.endsWith('.call')) {
warnings++;
reporter.warn(file, 'Found *.call binding. v2 removed .call. Convert to a lambda or method with .trigger as needed.');
}
// Check for event handlers that may need :prevent modifier in Aurelia 2
if (a.name.endsWith('.trigger') || a.name.endsWith('.delegate')) {
const eventName = a.name.split('.')[0];
const eventValue = a.value;
// Check if this is a potentially problematic event handler
if (isPotentiallyProblematicEvent(node, eventName, eventValue)) {
warnings++;
reporter.warn(file, `Event handler '${a.name}="${eventValue}"' may need :prevent modifier in Aurelia 2. In v1, preventDefault was called automatically, but not in v2. Consider '${eventName}.trigger:prevent' if needed.`);
}
}
}
}
// recurse
const anyNode = node;
if (Array.isArray(anyNode.childNodes)) {
for (const child of anyNode.childNodes)
visit(child);
}
if (Array.isArray(anyNode.content?.childNodes)) {
for (const child of anyNode.content.childNodes)
visit(child);
}
}
visit(doc);
if (edits > 0 && options.write) {
fs.writeFileSync(file, serialize(doc), 'utf8');
}
}
}