Files
plane/packages/codemods/remove-directives.ts

98 lines
4.5 KiB
TypeScript
Raw Normal View History

import { API, FileInfo, Options } from "jscodeshift";
import type { CommentKind, DirectiveKind } from "ast-types/gen/kinds";
export default function transform(file: FileInfo, api: API, options: Options) {
const j = api.jscodeshift;
const root = j(file.source);
const directivesToRemove = new Set(["use client", "use server", "use-client", "use-server"]);
root.find(j.Program).forEach((path) => {
// Handle ASTs where directives are stored in 'directives' property (Babel/TS)
if (path.node.directives) {
const newDirectives: DirectiveKind[] = [];
let capturedComments: CommentKind[] = [];
path.node.directives.forEach((directive) => {
if (directive.type === 'Directive' && directive.value.type === 'DirectiveLiteral' && typeof directive.value.value === 'string' && directivesToRemove.has(directive.value.value)) {
// Directive is being removed. Capture its comments.
if (directive.comments) {
capturedComments.push(...directive.comments);
}
} else {
// Keep this directive
// If we have captured comments from previous removed directives, attach them here?
// Usually comments belong to the next node.
if (capturedComments.length > 0) {
directive.comments = directive.comments || [];
directive.comments.unshift(...capturedComments);
capturedComments = [];
}
newDirectives.push(directive);
}
});
path.node.directives = newDirectives;
// If we still have captured comments (e.g. all directives removed, or last one removed),
// attach them to the first body node.
if (capturedComments.length > 0) {
if (path.node.body.length > 0) {
const firstBodyNode = path.node.body[0];
if (firstBodyNode) {
firstBodyNode.comments = firstBodyNode.comments || [];
firstBodyNode.comments.unshift(...capturedComments);
}
} else {
// If empty body, attach to Program?
// jscodeshift might not print Program comments easily if they are not attached to children.
// But let's try attaching to the Program node itself if possible, or leave them (they might be lost).
path.node.comments = path.node.comments || [];
path.node.comments.push(...capturedComments);
}
}
}
// Also handle the case where they might be in body as ExpressionStatements
if (path.node.body) {
const newBody: any[] = [];
let capturedComments: any[] = [];
path.node.body.forEach((node) => {
let shouldRemove = false;
if (j.ExpressionStatement.check(node)) {
const expression = node.expression;
if (j.StringLiteral.check(expression) && directivesToRemove.has(expression.value)) {
shouldRemove = true;
} else if (j.Literal.check(expression) && typeof expression.value === 'string' && directivesToRemove.has(expression.value)) {
shouldRemove = true;
}
}
if (shouldRemove) {
if (node.comments) {
capturedComments.push(...node.comments);
}
} else {
if (capturedComments.length > 0) {
node.comments = node.comments || [];
node.comments.unshift(...capturedComments);
capturedComments = [];
}
newBody.push(node);
}
});
// If comments left at the end (e.g. removed last statement), usually they are trailing comments of the file.
// But if we removed the ONLY statement, they become dangling.
// We attached to next node, so if we are at the end, there is no next node.
// If we removed a directive at the top, and there are more statements, they got attached.
path.node.body = newBody;
}
});
return root.toSource(options);
}