diff --git a/scripts/fix-vue-verbatim-module-syntax.ts b/scripts/fix-vue-verbatim-module-syntax.ts new file mode 100755 index 000000000..cf7399396 --- /dev/null +++ b/scripts/fix-vue-verbatim-module-syntax.ts @@ -0,0 +1,282 @@ +#!/usr/bin/env tsx +import glob from 'fast-glob' +import { cyan, green, magenta, red, yellow } from 'kolorist' +import MagicString from 'magic-string' +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' + +interface ImportError { + file: string + line: number + column: number + importName: string + isVue: boolean +} + +interface FileToFix { + ms: MagicString + imports: Array<{ + name: string + line: number + column: number + }> + isVue: boolean +} + +const parseVueTscOutput = (output: string): ImportError[] => { + const errors: ImportError[] = [] + const lines = output.split('\n') + + // Pattern for verbatim module syntax errors + // Example: src/components/actionbar/ComfyActionbar.vue(25,10): error TS1484: 'Ref' is a type... + const errorPattern = + /^(.+?)\((\d+),(\d+)\): error TS(1484|1205): '([^']+)' is a type/ + + for (const line of lines) { + const match = errorPattern.exec(line) + if (match) { + const [, filePath, lineNum, colNum, , importName] = match + errors.push({ + file: path.resolve(filePath), + line: parseInt(lineNum, 10), + column: parseInt(colNum, 10), + importName, + isVue: filePath.endsWith('.vue') + }) + } + } + + return errors +} + +const runVueTsc = (tsconfigPath: string): Promise => { + return new Promise((resolve, reject) => { + const vueTscPath = path.join( + process.cwd(), + 'node_modules', + '.bin', + 'vue-tsc' + ) + + const child = spawn(vueTscPath, ['--noEmit', '-p', tsconfigPath], { + cwd: process.cwd(), + shell: true + }) + + let output = '' + let errorOutput = '' + + child.stdout.on('data', (data) => { + output += data.toString() + }) + + child.stderr.on('data', (data) => { + errorOutput += data.toString() + }) + + child.on('close', (code) => { + // vue-tsc returns non-zero when there are type errors, which is expected + // We want the error output which contains the diagnostics + resolve(errorOutput || output) + }) + + child.on('error', (err) => { + reject(err) + }) + }) +} + +const findImportInLine = (line: string, importName: string): number => { + // Look for the import name in various import patterns + const patterns = [ + // import { Name } from + new RegExp(`\\b${importName}\\b(?=[^']*['"])`), + // import Name from (default import) + new RegExp(`import\\s+${importName}\\s+from`), + // import { Something as Name } from + new RegExp(`as\\s+${importName}\\b`), + // export { Name } from + new RegExp(`export\\s*\\{[^}]*\\b${importName}\\b`) + ] + + for (const pattern of patterns) { + const match = pattern.exec(line) + if (match) { + return match.index! + } + } + + // Fallback: simple indexOf + return line.indexOf(importName) +} + +const fixImports = async ( + errors: ImportError[] +): Promise> => { + const fileMap = new Map() + + for (const error of errors) { + if (!fileMap.has(error.file)) { + const content = fs.readFileSync(error.file, 'utf-8') + fileMap.set(error.file, { + ms: new MagicString(content), + imports: [], + isVue: error.isVue + }) + } + + const file = fileMap.get(error.file)! + file.imports.push({ + name: error.importName, + line: error.line, + column: error.column + }) + } + + // Apply fixes + for (const [filePath, fileData] of fileMap) { + const lines = fileData.ms.original.split('\n') + + for (const imp of fileData.imports) { + const lineIndex = imp.line - 1 + if (lineIndex >= 0 && lineIndex < lines.length) { + const line = lines[lineIndex] + + // Find the actual position of the import name in the line + const nameIndex = findImportInLine(line, imp.name) + + if (nameIndex >= 0) { + // Calculate the absolute position in the file + let absolutePos = 0 + for (let i = 0; i < lineIndex; i++) { + absolutePos += lines[i].length + 1 // +1 for newline + } + absolutePos += nameIndex + + // Check if 'type' isn't already there + const beforeText = fileData.ms.original.slice( + Math.max(0, absolutePos - 5), + absolutePos + ) + if (!beforeText.includes('type ')) { + fileData.ms.appendLeft(absolutePos, 'type ') + } + } + } + } + } + + return fileMap +} + +const main = async () => { + const args = process.argv.slice(2) + const isDry = args.includes('--dry') + const helpRequested = args.includes('--help') || args.includes('-h') + + if (helpRequested) { + console.log(cyan('🔧 Fix Vue Verbatim Module Syntax')) + console.log() + console.log( + 'Usage: npx tsx scripts/fix-vue-verbatim-module-syntax.ts [options] [tsconfig]' + ) + console.log() + console.log('Options:') + console.log( + ' --dry Run in dry mode (show what would be fixed without modifying files)' + ) + console.log(' --help Show this help message') + console.log() + console.log('Examples:') + console.log(' npx tsx scripts/fix-vue-verbatim-module-syntax.ts') + console.log(' npx tsx scripts/fix-vue-verbatim-module-syntax.ts --dry') + console.log( + ' npx tsx scripts/fix-vue-verbatim-module-syntax.ts tsconfig.app.json' + ) + return + } + + // Find tsconfig path argument + let tsconfigPath = 'tsconfig.json' + for (const arg of args) { + if (!arg.startsWith('--') && arg.endsWith('.json')) { + tsconfigPath = arg + break + } + } + + console.log( + cyan( + '🔧 Fixing verbatim module syntax errors in TypeScript and Vue files...' + ) + ) + console.log(magenta(`Using: ${tsconfigPath}`)) + if (isDry) { + console.log(yellow('Dry run mode - no files will be modified')) + } + console.log() + + try { + // Run vue-tsc to get all errors + console.log(cyan('Running vue-tsc to detect errors...')) + const output = await runVueTsc(tsconfigPath) + + // Parse the errors + const errors = parseVueTscOutput(output) + + if (errors.length === 0) { + console.log(green('✨ No verbatim module syntax errors found!')) + return + } + + console.log( + yellow(`Found ${errors.length} import(s) that need 'type' modifier`) + ) + console.log() + + // Fix the imports + const fixes = await fixImports(errors) + + const cwd = process.cwd() + for (const [filePath, fileData] of fixes) { + if (!isDry) { + fs.writeFileSync(filePath, fileData.ms.toString()) + } + + const fileType = fileData.isVue ? '(Vue)' : '(TS)' + console.log(cyan(`${path.relative(cwd, filePath)} ${fileType}`)) + + const importNames = fileData.imports.map((imp) => imp.name) + console.log( + ' ', + isDry ? 'Would add type to:' : 'Adding type to:', + importNames.map((name) => magenta(name)).join(', ') + ) + console.log() + } + + const fileCount = fixes.size + const importCount = Array.from(fixes.values()).reduce( + (sum, f) => sum + f.imports.length, + 0 + ) + + console.log( + green( + `✨ ${isDry ? 'Would fix' : 'Fixed'} ${importCount} import${importCount === 1 ? '' : 's'} in ${fileCount} file${fileCount === 1 ? '' : 's'}` + ) + ) + } catch (error) { + console.error(red('Error:'), error instanceof Error ? error.message : error) + process.exit(1) + } +} + +// Run if executed directly +main().catch((error) => { + console.error(red('Unhandled error:'), error) + process.exit(1) +}) + +export { main as fixVerbatimModuleSyntax }