Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | 2x 2x 2x 2x 8x 8x 8x 1x 15x 15x 24x 23x 1x 1x 1x 1x 1x 14x 24x 9x 1x 1x 1x 14x 14x 13x 1x 1x 1x 1x 1x 3x 1x 2x 2x 1x 1x 2x 2x 2x 1x 1x | /**
* Formatter integration for generated code
*
* Task 2.1.4: Implement formatter integration (Prettier)
*
* Formats generated code using Prettier to ensure consistent style,
* pass linting, and provide readable output.
*/
// Use Prettier standalone with explicit plugins for browser compatibility
import prettier from 'prettier/standalone';
// Prettier v3 standalone built-in plugins (no Babel)
import * as pluginEstree from 'prettier/plugins/estree';
import * as pluginTypescript from 'prettier/plugins/typescript';
import * as pluginHtml from 'prettier/plugins/html';
import * as pluginPostcss from 'prettier/plugins/postcss';
import type { GeneratedCode, GeneratedFile, FormatterOptions } from '../types/index.js';
/**
* Custom error for formatting failures
*/
export class UifFormatterError extends Error {
constructor(
message: string,
public readonly filePath?: string,
public readonly syntaxError?: string,
) {
super(message);
this.name = 'UifFormatterError';
}
}
/**
* Parser mapping for file types
* Note: 'lwc' is intentionally omitted - LWC templates have special binding syntax
* (e.g., {property}) that HTML formatters incorrectly quote, breaking the template.
*/
const PARSER_MAP: Record<string, string> = {
typescript: 'typescript',
html: 'html',
css: 'css',
json: 'json',
// lwc: skipped - special syntax not compatible with HTML formatter
};
/**
* Plugin mapping per parser for Prettier standalone
* Using 'any' here because Prettier's Plugin type is complex and varies by version
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const PLUGINS_FOR_PARSER: Record<string, any[]> = {
typescript: [pluginEstree, pluginTypescript],
html: [pluginHtml],
css: [pluginPostcss],
json: [], // JSON handled via manual fallback when possible
};
/**
* Default Prettier configuration
*/
const DEFAULT_PRETTIER_CONFIG = {
semi: true,
singleQuote: true,
trailingComma: 'es5' as const,
printWidth: 100,
tabWidth: 2,
useTabs: false,
arrowParens: 'always' as const,
};
/**
* Format generated code with Prettier
*
* @param code - Generated code from generator stage
* @param options - Formatter options
* @returns Formatted code with same structure
*/
export async function formatGeneratedCode(
code: GeneratedCode,
options: FormatterOptions = {},
): Promise<GeneratedCode> {
if (options.skip) {
return code;
}
const prettierConfig = {
...DEFAULT_PRETTIER_CONFIG,
...options.prettier,
};
const formattedFiles: GeneratedFile[] = [];
for (const file of code.files) {
try {
const formatted = await formatFile(file, prettierConfig);
formattedFiles.push(formatted);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
const filePath = file.path;
// Enhance error message with file context
let detail = `File: ${filePath}`;
if (errorMsg.includes('Syntax error')) {
detail += ' — The generated code has invalid TypeScript/JSX syntax';
} else if (errorMsg.includes('at')) {
detail += ` — Error ${errorMsg}`;
}
throw new UifFormatterError(`Failed to format file: ${detail}`, filePath, errorMsg);
}
}
return {
...code,
files: formattedFiles,
};
}
/**
* Format a single file
*/
async function formatFile(file: GeneratedFile, config: Record<string, any>): Promise<GeneratedFile> {
const parser = PARSER_MAP[file.type];
if (!parser) {
// Skip formatting for unknown types
return file;
}
try {
// Special-case JSON for robustness if plugins are unavailable
if (parser === 'json') {
try {
const obj = JSON.parse(file.content);
return { ...file, content: JSON.stringify(obj, null, 2) };
} catch {
// Fall through to Prettier if content isn't pure JSON
}
}
const plugins = PLUGINS_FOR_PARSER[parser] ?? [];
const formatted = await prettier.format(file.content, {
...config,
parser,
plugins,
filepath: file.path,
});
return {
...file,
content: formatted,
};
} catch (error) {
// If formatting fails, it's likely a syntax error in generated code
const details = error instanceof Error ? error.message : String(error);
const errorInfo = error instanceof Error ? (error as any).loc || (error as any).index : undefined;
// Try to extract line/column information from Prettier error
let location = '';
if (errorInfo) {
location = ` at ${errorInfo.line || '?'}:${errorInfo.column || '?'}`;
}
throw new UifFormatterError(`Syntax error in generated code${location}: ${details}`, file.path, details);
}
}
/**
* Validate generated code syntax without formatting
*
* Useful for debugging generator issues before formatting.
*
* @param file - Generated file to validate
* @returns True if valid, false otherwise
*/
export async function validateSyntax(file: GeneratedFile): Promise<boolean> {
const parser = PARSER_MAP[file.type];
if (!parser) {
// Unknown type, assume valid
return true;
}
try {
const plugins = PLUGINS_FOR_PARSER[parser] ?? [];
await prettier.format(file.content, {
parser,
plugins,
filepath: file.path,
});
return true;
} catch {
return false;
}
}
/**
* Get syntax errors from generated code
*
* @param file - Generated file to check
* @returns Array of syntax error messages
*/
export async function getSyntaxErrors(file: GeneratedFile): Promise<string[]> {
const parser = PARSER_MAP[file.type];
if (!parser) {
return [];
}
try {
const plugins = PLUGINS_FOR_PARSER[parser] ?? [];
await prettier.format(file.content, {
parser,
plugins,
filepath: file.path,
});
return [];
} catch (error) {
return [error instanceof Error ? error.message : String(error)];
}
}
/**
* Format multiple code outputs in parallel
*
* @param codes - Array of generated code
* @param options - Formatter options
* @returns Array of formatted code
*/
export async function formatMultipleOutputs(
codes: GeneratedCode[],
options: FormatterOptions = {},
): Promise<GeneratedCode[]> {
return Promise.all(codes.map((code) => formatGeneratedCode(code, options)));
}
|