UNPKG

@frontboi/prettier-plugin-ramses-style

Version:

Pretty your javascript and typescript project to Ramses style.

612 lines (611 loc) 28.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.preprocessor = preprocessor; const types_1 = require("@babel/types"); const generate = require('@babel/generator').default; const babelParser = require('@babel/parser'); const traverse = require('@babel/traverse').default; /** * Fonction permettant de trier des éléments du plus court au plus long. * @param a le premier champ à comparer * @param b le second champ à comparer * @returns l'ordre de tri */ function sortByLength(a, b) { return computePropertyLength(a) - computePropertyLength(b); } /** * Permet de calculer le nombre de lignes d'un nœud. * @param node le noeud dont on veut calculer le nombre de lignes * @returns le nombre de lignes */ function getNodeNbLines(node) { return node.loc.end.line - node.loc.start.line; } /** * Permet de calculer la taille d'une propriété d'un objet selon son type: en règle générale on prendra la taille du nom et de la valeur associés, mais parfois * on ne prendra que la taille du nom (si la valeur est une référence à une variable par exemple). * @param property la propriété dont on veut calculer la taille * @returns la taille de la propriété * @throws si le type de propriété n'est pas géré */ function computePropertyLength(property) { var _a, _b, _c, _d; function getLength(start, end) { return Math.abs((end || 0) - (start || 0)); } switch (property.type) { case 'JSXAttribute': return getLength(property.name.start, property.name.end) + getLength((_a = property.value) === null || _a === void 0 ? void 0 : _a.start, (_b = property.value) === null || _b === void 0 ? void 0 : _b.end); case 'Identifier': case 'StringLiteral': case 'NumericLiteral': return getLength(property.start, property.end); case 'ArrayExpression': return getLength(property.start, property.end); case 'ObjectProperty': // les fonctions sont reclassées en dernières, et elles sont triées entre elles selon le nombre de lignes qu'elles occupent if (property.value.type === 'ArrowFunctionExpression') { const nbLines = getLength((_c = property.loc) === null || _c === void 0 ? void 0 : _c.start.line, (_d = property.loc) === null || _d === void 0 ? void 0 : _d.end.line); if (nbLines === 0) { return (1000 + getLength(property.key.start, property.key.end) + getLength(property.value.start, property.value.end)); } else if (nbLines <= 2) { // @ts-ignore const lineLength = getLength(property.value.body.body[0].start, property.value.body.body[0].end); return 1500 + lineLength; } else { return 2000 + nbLines; } } else { return getLength(property.start, property.end); } case 'ObjectMethod': return 0; default: return getLength(property.start, property.end); } } function shiftDescendantLines(node, delta) { var _a, _b, _c, _d, _e; if (!node || delta === 0) return; const children = node.properties || node.elements || node.members || node.body; if (!Array.isArray(children)) return; for (const child of children) { if (!child) continue; if (child.loc) { child.loc.start.line += delta; child.loc.end.line += delta; } if ((_a = child.key) === null || _a === void 0 ? void 0 : _a.loc) { child.key.loc.start.line += delta; child.key.loc.end.line += delta; } if ((_b = child.value) === null || _b === void 0 ? void 0 : _b.loc) { child.value.loc.start.line += delta; child.value.loc.end.line += delta; shiftDescendantLines(child.value, delta); } if ((_c = child.typeAnnotation) === null || _c === void 0 ? void 0 : _c.loc) { child.typeAnnotation.loc.start.line += delta; child.typeAnnotation.loc.end.line += delta; } if ((_d = child.id) === null || _d === void 0 ? void 0 : _d.loc) { child.id.loc.start.line += delta; child.id.loc.end.line += delta; } if ((_e = child.initializer) === null || _e === void 0 ? void 0 : _e.loc) { child.initializer.loc.start.line += delta; child.initializer.loc.end.line += delta; shiftDescendantLines(child.initializer, delta); } } } /** * Permet de mettre à jour la loc d'un nœud et de ses enfants. * @param node le nœud dont on veut mettre à jour la loc * @param startLine nouvelle ligne de début * @param endLine nouvelle ligne de fin */ function updateNodeLoc(node, startLine, endLine) { var _a, _b, _c, _d, _e; const delta = startLine - node.loc.start.line; node.loc.start.line = startLine; node.loc.end.line = endLine; if ((_a = node.key) === null || _a === void 0 ? void 0 : _a.loc) { node.key.loc.start.line = startLine; node.key.loc.end.line = endLine; } if ((_b = node.value) === null || _b === void 0 ? void 0 : _b.loc) { node.value.loc.start.line = startLine; node.value.loc.end.line = endLine; shiftDescendantLines(node.value, delta); } if ((_c = node.typeAnnotation) === null || _c === void 0 ? void 0 : _c.loc) { node.typeAnnotation.loc.start.line += delta; node.typeAnnotation.loc.end.line += delta; } if ((_d = node.id) === null || _d === void 0 ? void 0 : _d.loc) { node.id.loc.start.line = startLine; node.id.loc.end.line = endLine; } if ((_e = node.initializer) === null || _e === void 0 ? void 0 : _e.loc) { node.initializer.loc.start.line = startLine; node.initializer.loc.end.line = endLine; shiftDescendantLines(node.initializer, delta); } } /** * Il se trouve que Babel parse le javascript en partant du principe qu'un commentaire sur la même ligne qu'une ligne de code appartient en fait à la ligne de code suivante en tant que * trailingComment. * Cette fonction a pour but de corriger ce problème d'interprétation en déplaçant les commentaires de fin de ligne dans les commentaires de fin de la ligne de code précédente. * @param nodes les noeuds à traiter */ function correctEndLineComments(nodes) { var _a; for (let i = 1; i < nodes.length; i++) { const currentNode = nodes[i]; const previousNode = nodes[i - 1]; const hasABeforeCommentOnPreviousCodeLine = ((_a = currentNode.leadingComments) === null || _a === void 0 ? void 0 : _a[0].loc.start.line) === previousNode.loc.start.line; if (hasABeforeCommentOnPreviousCodeLine) { const comment = Object.assign(Object.assign({}, currentNode.leadingComments[0]), { isOnSameLine: true }); comment.loc.start.line = previousNode.loc.end.line; comment.loc.end.line = previousNode.loc.end.line; // réassignation du commentaire au noeud précédent if (!previousNode.trailingComments) { previousNode.trailingComments = []; } previousNode.trailingComments.push(comment); if (currentNode.leadingComments.length === 1) { delete currentNode.leadingComments; } else { currentNode.leadingComments.shift(); } syncCommentsLocation(previousNode); syncCommentsLocation(currentNode); } } } function isSameComment(commentA, commentB) { var _a, _b, _c, _d; if (!commentA || !commentB) return false; if (commentA === commentB) return true; return (commentA.type === commentB.type && commentA.value === commentB.value && ((_a = commentA.loc) === null || _a === void 0 ? void 0 : _a.start.line) === ((_b = commentB.loc) === null || _b === void 0 ? void 0 : _b.start.line) && ((_c = commentA.loc) === null || _c === void 0 ? void 0 : _c.end.line) === ((_d = commentB.loc) === null || _d === void 0 ? void 0 : _d.end.line)); } /** * Babel peut dupliquer un même commentaire: trailing sur le nœud précédent et leading sur le suivant. * Ce doublon provoque un décalage de lignes appliqué deux fois ensuite. * On retire la version trailing quand elle est identique à une leading du nœud suivant. * @param nodes les noeuds à normaliser */ function removeDuplicatedBoundaryComments(nodes) { var _a, _b; for (let i = 1; i < nodes.length; i++) { const previousNode = nodes[i - 1]; const currentNode = nodes[i]; if (!((_a = previousNode.trailingComments) === null || _a === void 0 ? void 0 : _a.length) || !((_b = currentNode.leadingComments) === null || _b === void 0 ? void 0 : _b.length)) continue; previousNode.trailingComments = previousNode.trailingComments.filter((trailingComment) => !currentNode.leadingComments.some((leadingComment) => isSameComment(trailingComment, leadingComment))); if (previousNode.trailingComments.length === 0) { delete previousNode.trailingComments; } } } /** * Synchronise les numéros de ligne des commentaires d'un nœud par rapport à la ligne courante de ce nœud. * @param node le nœud dont on souhaite mettre à jour les commentaires */ function syncCommentsLocation(node) { var _a, _b; const nbCommentsBefore = (_a = node.leadingComments) === null || _a === void 0 ? void 0 : _a.length; const nbCommentsAfter = (_b = node.trailingComments) === null || _b === void 0 ? void 0 : _b.length; if (nbCommentsBefore > 0) { // leading comments terminent à la ligne précédente let startIndex = node.loc.start.line - nbCommentsBefore; for (let j = 0; j < nbCommentsBefore; j++) { const comment = node.leadingComments[j]; const commentNbLines = getNodeNbLines(comment); comment.loc.start.line = startIndex + j; comment.loc.end.line = startIndex + j + commentNbLines; } } if (nbCommentsAfter > 0) { // trailings commments commencent à la ligne courante const startIndex = node.trailingComments[0].isOnSameLine ? 0 : 1; for (let j = 0; j < nbCommentsAfter; j++) { const comment = node.trailingComments[j]; const commentNbLines = getNodeNbLines(comment); comment.loc.start.line = node.loc.end.line + j + startIndex; comment.loc.end.line = node.loc.end.line + j + startIndex + commentNbLines; } } } /** * Cette fonction permet de mettre à jour les numéros de ligne d'une liste de nœuds après un tri: en effet, pour que Prettier réagisse correctement il que l'AST soit fidèle à la réalité. * Alors il devient nécessaire de mettre à jour les numéros de ligne des nœuds après un tri, et pas seulement celui du nœud lui-même mais aussi de ses enfants. * @param nodes la liste des nœuds à mettre à jour * @param initialNode nœud de référence pour la première position à prendre en compte */ function updateLoc(nodes, initialNode) { var _a; if (nodes.length === 0) return; const allOnSameLine = nodes.every(n => n.loc.start.line === nodes[0].loc.start.line) && nodes.every(n => getNodeNbLines(n) === 0); if (allOnSameLine) { const line = (initialNode === null || initialNode === void 0 ? void 0 : initialNode.loc.start.line) || nodes[0].loc.start.line; for (const node of nodes) { updateNodeLoc(node, line, line); } return; } const initialNodeRealLine = ((initialNode === null || initialNode === void 0 ? void 0 : initialNode.loc.start.line) || 0) - (((_a = initialNode === null || initialNode === void 0 ? void 0 : initialNode.leadingComments) === null || _a === void 0 ? void 0 : _a.length) || 0); let newStartLine = initialNodeRealLine; let previousNodeNbLines = -1; for (const node of nodes) { const nodeNbLines = getNodeNbLines(node); const hasCommentOnSameLine = nodeNbLines === 0 && node.trailingComments && node.trailingComments[0].loc.start.line === node.loc.start.line; newStartLine += previousNodeNbLines + 1; previousNodeNbLines = nodeNbLines; const newEndLine = newStartLine + nodeNbLines; updateNodeLoc(node, newStartLine, newEndLine); if (hasCommentOnSameLine) { node.trailingComments[0].loc.start.line = newEndLine; node.trailingComments[0].loc.end.line = newEndLine; } syncCommentsLocation(node); } } /** * Cette fonction permet de mettre à jour les numéros de ligne de chaque nœud avec commentaire: il faut éventuellement déplacer les nœud qui précèdent ou qui suivent un commentaire. * @param nodes tous les nœuds à mettre à jour */ function updateLocWithComments(nodes) { var _a, _b; for (let i = 0; i < nodes.length; i++) { const nbCommentsBefore = (_a = nodes[i].leadingComments) === null || _a === void 0 ? void 0 : _a.length; const nbCommentsAfter = (_b = nodes[i].trailingComments) === null || _b === void 0 ? void 0 : _b.length; if (nbCommentsBefore > 0) { for (let j = i; j < nodes.length; j++) { const currentNode = nodes[j]; const newFirstLine = currentNode.loc.start.line + nbCommentsBefore; const newLastLine = currentNode.loc.end.line + nbCommentsBefore; updateNodeLoc(currentNode, newFirstLine, newLastLine); syncCommentsLocation(currentNode); } } if (nbCommentsAfter > 0) { for (let j = i + 1; j < nodes.length; j++) { const currentNode = nodes[j]; const newFirstLine = currentNode.loc.start.line + nbCommentsAfter; const newLastLine = currentNode.loc.end.line + nbCommentsAfter; updateNodeLoc(currentNode, newFirstLine, newLastLine); syncCommentsLocation(currentNode); } } } } const LEADING_COMMENT_MARKER_PREFIX = '__RAMSES_LEADING_COMMENT__'; function tagLeadingComments(nodes) { var _a; for (let i = 0; i < nodes.length; i++) { const currentNode = nodes[i]; if (!((_a = currentNode.leadingComments) === null || _a === void 0 ? void 0 : _a.length)) continue; const previousNode = nodes[i - 1]; const hasBlankLineBefore = !!previousNode && currentNode.leadingComments[0].loc.start.line - previousNode.loc.end.line > 1; const marker = `${LEADING_COMMENT_MARKER_PREFIX}${hasBlankLineBefore ? '2' : '1'}__`; for (const comment of currentNode.leadingComments) { if (comment.type !== 'CommentLine') continue; if (comment.value.includes(LEADING_COMMENT_MARKER_PREFIX)) continue; comment.value = `${marker}${comment.value}`; } } } function restoreLeadingCommentsInCode(code) { const inlineTaggedCommentPattern = /([,}\]])\s*\/\/\s*__RAMSES_LEADING_COMMENT__([12])__([^\n]*)\n([ \t]*)([^\n]+)/g; let nextCode = code.replace(inlineTaggedCommentPattern, (_, separator, level, commentText, indent, nextLine) => { const blankLine = level === '2' ? '\n' : ''; return `${separator}\n${blankLine}${indent}//${commentText}\n${indent}${nextLine}`; }); nextCode = nextCode.replace(/([^\n])\n([ \t]*)\/\/\s*__RAMSES_LEADING_COMMENT__2__/g, '$1\n\n$2// __RAMSES_LEADING_COMMENT__2__'); nextCode = nextCode.replace(/\/\/\s*__RAMSES_LEADING_COMMENT__[12]__/g, '//'); return nextCode; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Surrogate pair regex: matches a single non-BMP character (high surrogate + low surrogate) const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; // Private Use Area (BMP) characters are safer placeholders than control chars. // They are still 1 code unit each, so a 2-char placeholder preserves surrogate-pair length. const PRIVATE_USE_START = 0xe000; const PRIVATE_USE_END = 0xf8ff; const PRIVATE_USE_RANGE_SIZE = PRIVATE_USE_END - PRIVATE_USE_START + 1; function buildPrivateUsePlaceholder(index) { const firstOffset = index % PRIVATE_USE_RANGE_SIZE; const secondOffset = Math.floor(index / PRIVATE_USE_RANGE_SIZE); if (secondOffset >= PRIVATE_USE_RANGE_SIZE) { throw new Error('Too many unique non-BMP characters to protect'); } const first = String.fromCharCode(PRIVATE_USE_START + firstOffset); const second = String.fromCharCode(PRIVATE_USE_START + secondOffset); return first + second; } function toUnicodeEscape(value) { return [...value].map(c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')).join(''); } /** * Replaces each unique non-BMP character in the source text with a same-length (2 char) placeholder. * This preserves all positions in the AST since each surrogate pair (2 code units) is replaced with 2 code units. * Uses private-use BMP chars to avoid control-character corruption in downstream tooling. */ function protectNonBMPInSource(code) { const charToPlaceholder = new Map(); const placeholderToChar = new Map(); let counter = 0; const result = code.replace(SURROGATE_PAIR_RE, (match) => { let placeholder = charToPlaceholder.get(match); if (!placeholder) { placeholder = buildPrivateUsePlaceholder(counter); while (code.includes(placeholder) || placeholderToChar.has(placeholder)) { counter++; placeholder = buildPrivateUsePlaceholder(counter); } charToPlaceholder.set(match, placeholder); placeholderToChar.set(placeholder, match); counter++; } return placeholder; }); return { code: result, map: placeholderToChar }; } function restoreNonBMPInOutput(code, map) { let result = code; for (const [placeholder, original] of map) { // Handle raw placeholders (when extra.raw is preserved) result = result.split(placeholder).join(original); // Handle escaped unicode forms (when extra.raw was lost). const unicodeEscaped = toUnicodeEscape(placeholder); result = result.replace(new RegExp(escapeRegExp(unicodeEscaped), 'gi'), original); } return result; } function fixEscapedSurrogatePairs(code) { return code.replace(/\\u([dD][89abAB][0-9a-fA-F]{2})\\u([dD][c-fC-F][0-9a-fA-F]{2})/g, (_, high, low) => { const highCode = parseInt(high, 16); const lowCode = parseInt(low, 16); return String.fromCharCode(highCode, lowCode); }); } function hasUnpairedSurrogates(text) { for (let i = 0; i < text.length; i++) { const code = text.charCodeAt(i); const isHigh = code >= 0xd800 && code <= 0xdbff; const isLow = code >= 0xdc00 && code <= 0xdfff; if (isHigh) { const next = text.charCodeAt(i + 1); const nextIsLow = next >= 0xdc00 && next <= 0xdfff; if (!nextIsLow) return true; i++; continue; } if (isLow) return true; } return false; } const DISABLE_MARKER = '@ramses-style-disable'; function hasDisableMarkerInHeaderComment(code) { let source = code.replace(/^\uFEFF/, ''); // Ignore shebang when present. if (source.startsWith('#!')) { const firstLineBreak = source.indexOf('\n'); source = firstLineBreak === -1 ? '' : source.slice(firstLineBreak + 1); } const headerCommentMatch = source.match(/^\s*(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/); if (!headerCommentMatch) return false; return headerCommentMatch[1].toLowerCase().includes(DISABLE_MARKER); } function collectTypeAliasesWithBlankLine(ast) { var _a, _b; const result = []; const programBody = ((_a = ast === null || ast === void 0 ? void 0 : ast.program) === null || _a === void 0 ? void 0 : _a.body) || []; for (let i = 1; i < programBody.length; i++) { const previousNode = programBody[i - 1]; const currentNode = programBody[i]; const declaration = currentNode.type === 'ExportNamedDeclaration' ? currentNode.declaration : currentNode; if (!declaration || declaration.type !== 'TSTypeAliasDeclaration') continue; if (!((_b = declaration.id) === null || _b === void 0 ? void 0 : _b.name)) continue; if (!(previousNode === null || previousNode === void 0 ? void 0 : previousNode.loc) || !(currentNode === null || currentNode === void 0 ? void 0 : currentNode.loc)) continue; const hasBlankLineBefore = currentNode.loc.start.line - previousNode.loc.end.line > 1; if (hasBlankLineBefore) { result.push(declaration.id.name); } } return result; } function restoreBlankLinesBeforeTypeAliases(code, typeAliasNames) { if (typeAliasNames.length === 0) return code; const lines = code.split('\n'); for (const name of typeAliasNames) { const typePattern = new RegExp(`^\\s*(export\\s+)?type\\s+${escapeRegExp(name)}\\b`); for (let i = 1; i < lines.length; i++) { if (!typePattern.test(lines[i])) continue; if (lines[i - 1].trim() === '') break; lines.splice(i, 0, ''); break; } } return lines.join('\n'); } /** * Permet de trier les propriétés d'un arbre AST par ordre de longueur croissante. * @param unsortedElements les nœuds AST à trier * @returns les nœuds triés */ function colocateLeadingCommentsOfMultilineNodes(nodes) { var _a; for (let i = 1; i < nodes.length; i++) { const currentNode = nodes[i]; const previousNode = nodes[i - 1]; if (!((_a = currentNode.leadingComments) === null || _a === void 0 ? void 0 : _a.length)) continue; if (getNodeNbLines(currentNode) === 0) continue; const firstComment = currentNode.leadingComments[0]; if (firstComment.loc.start.line !== previousNode.loc.end.line) continue; const comment = Object.assign(Object.assign({}, firstComment), { isOnSameLine: true }); comment.loc.start.line = previousNode.loc.end.line; comment.loc.end.line = previousNode.loc.end.line; if (!previousNode.trailingComments) { previousNode.trailingComments = []; } previousNode.trailingComments.push(comment); if (currentNode.leadingComments.length === 1) { delete currentNode.leadingComments; } else { currentNode.leadingComments.shift(); } syncCommentsLocation(previousNode); syncCommentsLocation(currentNode); } } function isTypeScriptNode(node) { return typeof (node === null || node === void 0 ? void 0 : node.type) === 'string' && node.type.startsWith('TS'); } function sortProperties(unsortedElements) { const isTypeScriptMembersList = unsortedElements.every(isTypeScriptNode); const isInlineTypeScriptMembersList = isTypeScriptMembersList && unsortedElements.length > 0 && unsortedElements.every(node => { var _a, _b, _c, _d, _e; return ((_b = (_a = node === null || node === void 0 ? void 0 : node.loc) === null || _a === void 0 ? void 0 : _a.start) === null || _b === void 0 ? void 0 : _b.line) === ((_e = (_d = (_c = unsortedElements[0]) === null || _c === void 0 ? void 0 : _c.loc) === null || _d === void 0 ? void 0 : _d.start) === null || _e === void 0 ? void 0 : _e.line) && getNodeNbLines(node) === 0; }); const shouldNormalizeCommentsAndLoc = !isInlineTypeScriptMembersList; // Les nœuds TypeScript inline (ex: Promise<{ ... }>) sont sensibles aux manipulations de loc. // On évite cette phase pour ne pas perdre des annotations de types lors du print. if (shouldNormalizeCommentsAndLoc) { correctEndLineComments(unsortedElements); removeDuplicatedBoundaryComments(unsortedElements); tagLeadingComments(unsortedElements); } // sauvegarde de l'index des éléments non-triables pour les remettre à la même place ensuite const fixedPositionNodes = {}; for (let i = 0; i < unsortedElements.length; i++) { const currentObject = unsortedElements[i]; if ((0, types_1.isSpreadElement)(currentObject) || currentObject.type === 'RestElement' || currentObject.type === 'JSXSpreadAttribute') { fixedPositionNodes[i] = currentObject; } } // tri par longueur croissante const sortedElements = unsortedElements .filter(e => !Object.values(fixedPositionNodes).includes(e) && e.type !== 'ObjectMethod') .slice() .sort(sortByLength); if (shouldNormalizeCommentsAndLoc) { updateLoc(sortedElements, unsortedElements[0]); colocateLeadingCommentsOfMultilineNodes(sortedElements); updateLocWithComments(sortedElements); } // réinsertion des éléments non-triables à leur place for (const [index, node] of Object.entries(fixedPositionNodes)) { sortedElements.splice(+index, 0, node); } // réinjection des object method for (let i = 0; i < unsortedElements.length; i++) { if (unsortedElements[i].type === 'ObjectMethod') { sortedElements.splice(i, 0, unsortedElements[i]); } } return sortedElements; } /** * Fonction permettant de d'améliorer la syntaxe d'un code en triant des champs répartis ligne par ligne par ordre de longueur croissante: * cela signifie que les champs les plus courts seront placés en premier (le nom et la valeur de la propriété sont tous deux pris en compte). * Les champs concernés sont les suivants: * - les champs d'un objet (destructuration, retour de fonction) * - les paramètres d'une fonction * - les éléments d'un tableau * @param code le code à traiter * @param options options passées à Prettier * @returns le code modifié */ function preprocessor(code, options) { if (hasDisableMarkerInHeaderComment(code)) { return code; } const { code: safeCode, map: nonBMPMap } = protectNonBMPInSource(code); const ast = babelParser.parse(safeCode, { plugins: ['jsx', 'typescript'], sourceType: 'module', }); const typeAliasesWithBlankLine = collectTypeAliasesWithBlankLine(ast); traverse(ast, { // éléments d'un objet (ex: déclaration de variable, retour de fonction) ObjectExpression(path) { const sortedElements = sortProperties(path.node.properties); path.node.properties = sortedElements; }, // destructuration d'objet ObjectPattern(path) { const sortedElements = sortProperties(path.node.properties); path.node.properties = sortedElements; }, // TypeScript interfaces TSInterfaceBody(path) { const sortedElements = sortProperties(path.node.body); path.node.body = sortedElements; }, // TypeScript type literals (e.g. type Foo = { ... }) TSTypeLiteral(path) { const sortedElements = sortProperties(path.node.members); path.node.members = sortedElements; }, // TypeScript enums TSEnumDeclaration(path) { const sortedElements = sortProperties(path.node.members); path.node.members = sortedElements; }, // JSX props JSXOpeningElement(path) { const sortedAttributes = sortProperties(path.node.attributes); path.node.attributes = sortedAttributes; }, }); const newCode = generate(ast, { retainLines: true, }).code; const fixedEscapesCode = fixEscapedSurrogatePairs(newCode); const restoredLeadingCommentsCode = restoreLeadingCommentsInCode(fixedEscapesCode); const restoredBlankLinesCode = restoreBlankLinesBeforeTypeAliases(restoredLeadingCommentsCode, typeAliasesWithBlankLine); const cleanedCode = restoredBlankLinesCode.replace(/\n{3,}/g, '\n\n'); const restoredCode = restoreNonBMPInOutput(cleanedCode, nonBMPMap); // Safety net: never emit potentially corrupted unicode output. if ((restoredCode.includes('�') && !code.includes('�')) || hasUnpairedSurrogates(restoredCode)) { return code; } return restoredCode; }