@reapit/elements 5.0.0-beta.71 → 5.0.0-beta.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/codemods/bin.ts DELETED
@@ -1,205 +0,0 @@
1
- import { run } from './runner.ts'
2
- import { listCodemods, getCodemodDescription, getCodemodReadme, validateCodemodName } from './codemods.ts'
3
- import { parseFrontMatter } from './readme-parser.ts'
4
-
5
- function printHelp(): void {
6
- console.log(`
7
- Usage: yarn dlx @reapit/elements@beta codemod <command> [options]
8
-
9
- Commands:
10
- list List available codemods
11
- info <name> Show detailed info about a codemod
12
- apply <name> <dir> Apply a codemod to a directory
13
-
14
- Options:
15
- --help, -h Show this help message
16
-
17
- Examples:
18
- yarn dlx @reapit/elements@beta codemod list
19
- yarn dlx @reapit/elements@beta codemod info at-a-glance-article-card
20
- yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/
21
- yarn dlx @reapit/elements@beta codemod apply at-a-glance-article-card src/ --dry-run
22
- `)
23
- }
24
-
25
- function printApplyHelp(codemodName: string): void {
26
- console.log(`
27
- Usage: yarn dlx @reapit/elements@beta codemod apply ${codemodName} <directory> [options]
28
-
29
- Arguments:
30
- <directory> Directory to search for files to transform
31
-
32
- Options:
33
- --ext <extensions> File extensions to process (default: .tsx,.ts,.jsx,.js)
34
- --facade-package <pkg> Package name that re-exports @reapit/elements
35
- --dry-run, -d Preview changes without writing files
36
- --help, -h Show this help message
37
-
38
- Examples:
39
- yarn dlx @reapit/elements@beta codemod apply ${codemodName} src/
40
- yarn dlx @reapit/elements@beta codemod apply ${codemodName} src/ --dry-run
41
- yarn dlx @reapit/elements@beta codemod apply ${codemodName} src/ --ext .tsx,.jsx
42
- yarn dlx @reapit/elements@beta codemod apply ${codemodName} src/ --facade-package @company/ui-components
43
- `)
44
- }
45
-
46
- function printList(): void {
47
- const codemods = listCodemods()
48
-
49
- if (codemods.length === 0) {
50
- console.log('No codemods available.')
51
- return
52
- }
53
-
54
- console.log('\nAvailable codemods:\n')
55
-
56
- // Calculate padding for alignment
57
- const maxNameLength = Math.max(...codemods.map((name) => name.length))
58
-
59
- for (const name of codemods) {
60
- const description = getCodemodDescription(name)
61
- const padding = ' '.repeat(maxNameLength - name.length + 4)
62
- if (description) {
63
- console.log(` ${name}${padding}${description}`)
64
- } else {
65
- console.log(` ${name}`)
66
- }
67
- }
68
-
69
- console.log("\nRun 'yarn dlx @reapit/elements@beta codemod info <name>' for more details.")
70
- }
71
-
72
- function printInfo(name: string): void {
73
- // Security: Validate codemod name against manifest before any operations
74
- // This prevents path traversal attacks by ensuring only known codemods are accessed
75
- // validateCodemodName() checks against CODEMOD_NAMES, a compile-time constant array
76
- const sanitizedName = validateCodemodName(name)
77
-
78
- if (!sanitizedName) {
79
- console.error(`Error: Unknown codemod '${name}'`)
80
- printAvailableCodemods()
81
- process.exit(1)
82
- }
83
-
84
- // SECURITY: sanitizedName is guaranteed to be a valid CodemodName from the static manifest
85
- // It cannot contain path traversal sequences (e.g., '../') as it's validated against CODEMOD_NAMES
86
- const readme = getCodemodReadme(sanitizedName)
87
-
88
- if (!readme) {
89
- console.log(`No documentation available for codemod '${sanitizedName}'.`)
90
- return
91
- }
92
-
93
- const { body } = parseFrontMatter(readme)
94
- console.log(body)
95
- }
96
-
97
- function printAvailableCodemods(): void {
98
- const codemods = listCodemods()
99
-
100
- if (codemods.length === 0) {
101
- console.log('No codemods available.')
102
- return
103
- }
104
-
105
- console.log('\nAvailable codemods:')
106
-
107
- const maxNameLength = Math.max(...codemods.map((name) => name.length))
108
-
109
- for (const name of codemods) {
110
- const description = getCodemodDescription(name)
111
- const padding = ' '.repeat(maxNameLength - name.length + 4)
112
- if (description) {
113
- console.log(` ${name}${padding}${description}`)
114
- } else {
115
- console.log(` ${name}`)
116
- }
117
- }
118
- }
119
-
120
- async function handleApply(args: string[]): Promise<void> {
121
- const codemodName = args[0]
122
-
123
- if (!codemodName || codemodName.startsWith('-')) {
124
- console.error('Error: No codemod name provided')
125
- console.log('\nUsage: yarn dlx @reapit/elements@beta codemod apply <name> <directory> [options]')
126
- console.log("\nRun 'yarn dlx @reapit/elements@beta codemod list' to see available codemods.")
127
- process.exit(1)
128
- }
129
-
130
- // Security: Sanitize codemod name by validating it against the allowlist
131
- // This prevents path traversal attacks in the dynamic import below
132
- const sanitizedCodemodName = validateCodemodName(codemodName)
133
-
134
- if (!sanitizedCodemodName) {
135
- console.error(`Error: Unknown codemod '${codemodName}'`)
136
- printAvailableCodemods()
137
- process.exit(1)
138
- }
139
-
140
- const remainingArgs = args.slice(1)
141
-
142
- // Handle help for specific codemod
143
- if (remainingArgs.includes('--help') || remainingArgs.includes('-h')) {
144
- printApplyHelp(sanitizedCodemodName)
145
- process.exit(0)
146
- }
147
-
148
- // Load and run the codemod
149
- const codemodModule = await import(`./${sanitizedCodemodName}/transform.ts`)
150
- const transform = codemodModule.default
151
-
152
- if (typeof transform !== 'function') {
153
- console.error(`Error: Codemod '${codemodName}' does not export a default transform function`)
154
- process.exit(1)
155
- }
156
-
157
- await run({
158
- transform,
159
- codemodName: sanitizedCodemodName,
160
- args: remainingArgs,
161
- })
162
- }
163
-
164
- async function main(): Promise<void> {
165
- const args = process.argv.slice(2)
166
- const command = args[0]
167
-
168
- // Handle no command or help
169
- if (!command || command === '--help' || command === '-h') {
170
- printHelp()
171
- process.exit(0)
172
- }
173
-
174
- switch (command) {
175
- case 'list':
176
- printList()
177
- break
178
-
179
- case 'info': {
180
- const name = args[1]
181
- if (!name || name.startsWith('-')) {
182
- console.error('Error: No codemod name provided')
183
- console.log('\nUsage: yarn dlx @reapit/elements@beta codemod info <name>')
184
- console.log("\nRun 'yarn dlx @reapit/elements@beta codemod list' to see available codemods.")
185
- process.exit(1)
186
- }
187
- printInfo(name)
188
- break
189
- }
190
-
191
- case 'apply':
192
- await handleApply(args.slice(1))
193
- break
194
-
195
- default:
196
- console.error(`Error: Unknown command '${command}'`)
197
- printHelp()
198
- process.exit(1)
199
- }
200
- }
201
-
202
- main().catch((error: Error) => {
203
- console.error('Error:', error.message)
204
- process.exit(1)
205
- })
@@ -1,75 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { join, dirname } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- // Get current directory for loading manifest
6
- const __filename = fileURLToPath(import.meta.url)
7
- const __dirname = dirname(__filename)
8
-
9
- // Type definitions for the manifest
10
- export interface CodemodMetadata {
11
- readonly name: string
12
- readonly description: string | null
13
- }
14
-
15
- interface Manifest {
16
- codemods: CodemodMetadata[]
17
- }
18
-
19
- // Load manifest from JSON file
20
- const manifestPath = join(__dirname, 'manifest.json')
21
- const manifestData = JSON.parse(readFileSync(manifestPath, 'utf-8')) as Manifest
22
-
23
- export const AVAILABLE_CODEMODS: readonly CodemodMetadata[] = manifestData.codemods
24
- export const CODEMOD_NAMES: readonly string[] = manifestData.codemods.map((c) => c.name)
25
- export type CodemodName = (typeof CODEMOD_NAMES)[number]
26
-
27
- /**
28
- * Returns available codemods from the generated manifest.
29
- *
30
- * @returns Array of codemod names
31
- */
32
- export function listCodemods(): string[] {
33
- return CODEMOD_NAMES.slice() // Return copy to prevent mutation
34
- }
35
-
36
- /**
37
- * Retrieves the README content for a specific codemod.
38
- * SECURITY: This function should only be called with a validated CodemodName from validateCodemodName().
39
- * The name parameter is guaranteed to be from the static manifest, preventing path traversal.
40
- *
41
- * @param name - The validated codemod name (must come from validateCodemodName())
42
- * @returns The README content, or null if not found
43
- */
44
- export function getCodemodReadme(name: CodemodName | string): string | null {
45
- try {
46
- // SECURITY NOTE: The 'name' parameter has been validated against the static manifest
47
- // via validateCodemodName() before reaching this function. Only names from CODEMOD_NAMES
48
- // (a compile-time constant array) can be passed here, preventing path traversal attacks.
49
- return readFileSync(join(__dirname, name, 'README.md'), 'utf-8')
50
- } catch {
51
- return null
52
- }
53
- }
54
-
55
- /**
56
- * Retrieves the description from the manifest for a specific codemod.
57
- *
58
- * @param name - The codemod name
59
- * @returns The description, or null if not found
60
- */
61
- export function getCodemodDescription(name: string): string | null {
62
- const metadata = AVAILABLE_CODEMODS.find((c) => c.name === name)
63
- return metadata?.description ?? null
64
- }
65
-
66
- /**
67
- * Validates that a codemod exists in the manifest.
68
- * Returns the sanitized name to prevent path traversal attacks.
69
- *
70
- * @param name - The codemod name to validate
71
- * @returns The sanitized codemod name, or null if invalid
72
- */
73
- export function validateCodemodName(name: string): CodemodName | null {
74
- return CODEMOD_NAMES.includes(name as CodemodName) ? (name as CodemodName) : null
75
- }
@@ -1,120 +0,0 @@
1
- import { dirname, join } from 'node:path'
2
- import { fileURLToPath } from 'node:url'
3
- import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
4
- import { parseFrontMatter } from './readme-parser.ts'
5
- import { styleText } from 'node:util'
6
-
7
- const __filename = fileURLToPath(import.meta.url)
8
- const __dirname = dirname(__filename)
9
-
10
- interface CodemodMetadata {
11
- name: string
12
- description: string | null
13
- }
14
-
15
- /**
16
- * Discovers available codemods by scanning for directories with transform.ts files.
17
- * Exported for testing.
18
- *
19
- * @param codemodDir - The directory to scan for codemods
20
- * @returns Array of codemod names sorted alphabetically
21
- */
22
- export function discoverCodemods(codemodDir: string): string[] {
23
- try {
24
- const entries = readdirSync(codemodDir, { withFileTypes: true })
25
-
26
- return entries
27
- .filter((entry) => {
28
- if (!entry.isDirectory()) return false
29
- // Skip special directories
30
- if (entry.name === 'node_modules' || entry.name === '__tests__' || entry.name.startsWith('.')) {
31
- return false
32
- }
33
- // Check if directory contains a transform.ts file
34
- try {
35
- statSync(join(codemodDir, entry.name, 'transform.ts'))
36
- return true
37
- } catch {
38
- return false
39
- }
40
- })
41
- .map((entry) => entry.name)
42
- .sort() // Sort alphabetically for consistent output
43
- } catch {
44
- return []
45
- }
46
- }
47
-
48
- /**
49
- * Extracts metadata for a specific codemod from its README.
50
- * Exported for testing.
51
- *
52
- * @param codemodDir - The directory containing the codemod
53
- * @param name - The codemod name
54
- * @returns CodemodMetadata object
55
- */
56
- export function getCodemodMetadata(codemodDir: string, name: string): CodemodMetadata {
57
- try {
58
- const readmePath = join(codemodDir, name, 'README.md')
59
- const readme = readFileSync(readmePath, 'utf-8')
60
- const { description } = parseFrontMatter(readme)
61
-
62
- return {
63
- name,
64
- description: description ?? null,
65
- }
66
- } catch (error) {
67
- console.warn(
68
- styleText('yellow', `Warning: Could not read README for codemod '${name}': ${(error as Error).message}`),
69
- )
70
- return {
71
- name,
72
- description: null,
73
- }
74
- }
75
- }
76
-
77
- /**
78
- * Generates the JSON manifest content.
79
- */
80
- function generateManifestContent(codemods: CodemodMetadata[]): string {
81
- const manifest = {
82
- $schema: './manifest.schema.json',
83
- generated: new Date().toISOString(),
84
- codemods: codemods.map((c) => ({
85
- name: c.name,
86
- description: c.description,
87
- })),
88
- }
89
-
90
- return JSON.stringify(manifest, null, 2) + '\n'
91
- }
92
-
93
- /**
94
- * Main function to generate the manifest file.
95
- */
96
- function main(): void {
97
- console.log('Generating codemod manifest...')
98
-
99
- // Discover available codemods
100
- const codemodNames = discoverCodemods(__dirname)
101
- console.log(`Found ${codemodNames.length} codemod(s): ${codemodNames.join(', ')}`)
102
-
103
- // Get metadata for each codemod
104
- const codemods = codemodNames.map((name) => getCodemodMetadata(__dirname, name))
105
-
106
- // Generate manifest content
107
- const manifestContent = generateManifestContent(codemods)
108
-
109
- // Write to file
110
- const outputPath = join(__dirname, 'manifest.json')
111
- writeFileSync(outputPath, manifestContent, 'utf-8')
112
-
113
- console.log(styleText('green', `✓ Generated manifest at ${outputPath}`))
114
- console.log(` ${codemods.length} codemod(s) registered`)
115
- console.log('\nTo regenerate this file, run: yarn generate:codemod-manifest')
116
- }
117
-
118
- if (import.meta.main) {
119
- main()
120
- }
@@ -1,39 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Codemod Manifest",
4
- "description": "Manifest file listing all available codemods with their metadata. Auto-generated by 'yarn generate:codemod-manifest'.",
5
- "type": "object",
6
- "required": ["codemods"],
7
- "properties": {
8
- "$schema": {
9
- "type": "string",
10
- "description": "JSON Schema reference"
11
- },
12
- "generated": {
13
- "type": "string",
14
- "format": "date-time",
15
- "description": "Timestamp when this manifest was generated"
16
- },
17
- "codemods": {
18
- "type": "array",
19
- "description": "List of available codemods",
20
- "items": {
21
- "type": "object",
22
- "required": ["name"],
23
- "properties": {
24
- "name": {
25
- "type": "string",
26
- "description": "Unique identifier for the codemod (must match directory name)",
27
- "pattern": "^[a-z0-9-]+$"
28
- },
29
- "description": {
30
- "type": ["string", "null"],
31
- "description": "Brief description of what the codemod does (extracted from README front matter)"
32
- }
33
- },
34
- "additionalProperties": false
35
- }
36
- }
37
- },
38
- "additionalProperties": false
39
- }
@@ -1,37 +0,0 @@
1
- export interface FrontMatter {
2
- description?: string
3
- body: string
4
- }
5
-
6
- /**
7
- * Parses front matter from a markdown file.
8
- * Front matter should be in YAML format between --- delimiters.
9
- *
10
- * @param content - The raw markdown content
11
- * @returns Parsed front matter and body
12
- *
13
- * @example
14
- * ```
15
- * const content = `---
16
- * description: My description
17
- * ---
18
- * # My Content
19
- * `
20
- * const { description, body } = parseFrontMatter(content)
21
- * // description = "My description"
22
- * // body = "# My Content"
23
- * ```
24
- */
25
- export function parseFrontMatter(content: string): FrontMatter {
26
- const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
27
- if (!match) return { body: content }
28
-
29
- const frontMatter = match[1]
30
- const body = match[2]
31
- const descMatch = frontMatter.match(/^description:\s*(.+)$/m)
32
-
33
- return {
34
- description: descMatch?.[1],
35
- body: body.trim(),
36
- }
37
- }
@@ -1,196 +0,0 @@
1
- import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'
2
- import { join, resolve, relative } from 'node:path'
3
-
4
- export type Transform = (source: string, filePath: string, options?: { facadePackage?: string }) => string
5
-
6
- export interface RunOptions {
7
- transform: Transform
8
- codemodName: string
9
- args: string[]
10
- }
11
-
12
- /**
13
- * Validates that a path is safe and doesn't attempt directory traversal.
14
- * This prevents malicious paths like "../../../etc/passwd" from being accessed.
15
- *
16
- * @param basePath - The trusted base directory path
17
- * @param targetPath - The path to validate against the base
18
- * @returns true if the target path is within the base directory, false otherwise
19
- */
20
- function isPathSafe(basePath: string, targetPath: string): boolean {
21
- const resolvedBase = resolve(basePath)
22
- const resolvedTarget = resolve(targetPath)
23
- const relativePath = relative(resolvedBase, resolvedTarget)
24
-
25
- // Empty string means target === base, which is safe
26
- if (relativePath === '') {
27
- return true
28
- }
29
-
30
- // Check if relativePath tries to escape the base directory:
31
- // - Starts with '..' means going up and out of base (e.g., '../../etc/passwd')
32
- // - Starts with '/' means absolute Unix path (e.g., '/etc/passwd')
33
- // - Windows cross-drive paths return absolute paths from relative() (e.g., 'C:\Windows')
34
- return !relativePath.startsWith('..') && !relativePath.startsWith('/') && !/^[a-zA-Z]:[\\/]/.test(relativePath)
35
- }
36
-
37
- /**
38
- * Recursively finds files matching the given patterns within a directory.
39
- * Includes path traversal protection via isPathSafe validation.
40
- *
41
- * @param dir - The directory to search
42
- * @param patterns - File patterns to match (e.g., ["*.ts", "*.tsx"])
43
- * @param results - Accumulator for matching file paths
44
- * @returns Array of absolute paths to matching files
45
- */
46
- export function findFiles(dir: string, patterns: string[], results: string[] = []): string[] {
47
- // readdirSync is safe here - dir is validated by isPathSafe, and entry names from filesystem are trusted
48
- const entries = readdirSync(dir, { withFileTypes: true })
49
-
50
- for (const entry of entries) {
51
- const fullPath = join(dir, entry.name)
52
-
53
- // Validate path safety to prevent directory traversal
54
- if (!isPathSafe(dir, fullPath)) {
55
- continue
56
- }
57
-
58
- // Skip node_modules and dist
59
- if (entry.name === 'node_modules' || entry.name === 'dist') {
60
- continue
61
- }
62
-
63
- if (entry.isDirectory()) {
64
- findFiles(fullPath, patterns, results)
65
- } else if (entry.isFile() && matchesPatterns(entry.name, patterns)) {
66
- results.push(fullPath)
67
- }
68
- }
69
-
70
- return results
71
- }
72
-
73
- export function matchesPatterns(filename: string, patterns: string[]): boolean {
74
- return patterns.some((pattern) => {
75
- // Security: Validate pattern to prevent ReDoS attacks
76
- // Limit pattern length and check for excessive wildcards
77
- if (pattern.length > 100 || (pattern.match(/\*/g) || []).length > 5) {
78
- console.warn(`Warning: Skipping potentially unsafe pattern: ${pattern}`)
79
- return false
80
- }
81
-
82
- // Simple pattern matching for common cases
83
- if (pattern.startsWith('*.')) {
84
- return filename.endsWith(pattern.slice(1))
85
- }
86
- if (pattern.includes('*')) {
87
- // Additional protection: limit filename length to prevent catastrophic backtracking
88
- // Even with wildcard limits, long filenames + multiple wildcards can cause issues
89
- if (filename.length > 500) {
90
- console.warn(`Warning: Filename too long for pattern matching: ${filename}`)
91
- return false
92
- }
93
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
94
- return regex.test(filename)
95
- }
96
- return filename === pattern
97
- })
98
- }
99
-
100
- function printUsage(codemodName: string): void {
101
- console.log(`
102
- Usage: codemod apply ${codemodName} <directory> [options]
103
-
104
- Arguments:
105
- <directory> Directory to search for files to transform
106
-
107
- Options:
108
- --ext <extensions> File extensions to process (default: .tsx,.ts,.jsx,.js)
109
- --facade-package <pkg> Package name that re-exports @reapit/elements
110
- --dry-run, -d Preview changes without writing files
111
- --help, -h Show this help message
112
-
113
- Examples:
114
- yarn dlx @reapit/elements codemod apply ${codemodName} src/
115
- yarn dlx @reapit/elements codemod apply ${codemodName} src/ --dry-run
116
- yarn dlx @reapit/elements codemod apply ${codemodName} src/ --ext .tsx,.jsx
117
- yarn dlx @reapit/elements codemod apply ${codemodName} src/ --facade-package @company/ui-components
118
- `)
119
- }
120
-
121
- export async function run({ transform, codemodName, args }: RunOptions): Promise<void> {
122
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
123
- printUsage(codemodName)
124
- process.exit(0)
125
- }
126
-
127
- const dryRun = args.includes('--dry-run') || args.includes('-d')
128
- const extIndex = args.indexOf('--ext')
129
- const extensions = extIndex !== -1 ? args[extIndex + 1].split(',') : ['.tsx', '.ts', '.jsx', '.js']
130
- const patterns = extensions.map((ext) => `*${ext}`)
131
- const facadePackageIndex = args.indexOf('--facade-package')
132
- const facadePackage = facadePackageIndex !== -1 ? args[facadePackageIndex + 1] : undefined
133
-
134
- // Get directory argument (first non-flag argument, excluding --ext value if present)
135
- const extValue = extIndex !== -1 ? args[extIndex + 1] : null
136
- const facadePackageValue = facadePackageIndex !== -1 ? args[facadePackageIndex + 1] : null
137
- const directory = args.find((arg) => !arg.startsWith('-') && arg !== extValue && arg !== facadePackageValue)
138
-
139
- if (!directory) {
140
- console.error('Error: No directory provided')
141
- process.exit(1)
142
- }
143
-
144
- const cwd = process.cwd()
145
- const resolvedDir = resolve(directory)
146
-
147
- // Security: Validate path to prevent directory traversal attacks
148
- // Use isPathSafe to ensure the resolved directory is within the current working directory
149
- // This prevents accessing files outside the project (e.g., /etc/passwd, ../../../../sensitive-file)
150
- if (!isPathSafe(cwd, resolvedDir)) {
151
- console.error('Error: Directory path is outside the current working directory')
152
- process.exit(1)
153
- }
154
-
155
- try {
156
- // NOTE: Path traversal protection implemented above via isPathSafe validation
157
- statSync(resolvedDir)
158
- } catch {
159
- console.error(`Error: Directory not found: ${directory}`)
160
- process.exit(1)
161
- }
162
-
163
- // NOTE: Path traversal protection implemented via isPathSafe validation in findFiles
164
- const files = findFiles(resolvedDir, patterns)
165
-
166
- if (files.length === 0) {
167
- console.log('No matching files found')
168
- process.exit(0)
169
- }
170
-
171
- console.log(`Found ${files.length} file(s) to process${dryRun ? ' (dry run)' : ''}...\n`)
172
-
173
- let transformedCount = 0
174
-
175
- for (const filePath of files) {
176
- try {
177
- const source = readFileSync(filePath, 'utf-8')
178
- const result = transform(source, filePath, facadePackage ? { facadePackage } : undefined)
179
-
180
- if (result !== source) {
181
- transformedCount++
182
- const relativePath = filePath.replace(process.cwd() + '/', '')
183
- console.log(` ${dryRun ? 'Would transform' : 'Transformed'}: ${relativePath}`)
184
-
185
- if (!dryRun) {
186
- writeFileSync(filePath, result, 'utf-8')
187
- }
188
- }
189
- } catch (error) {
190
- const relativePath = filePath.replace(process.cwd() + '/', '')
191
- console.error(` Error processing ${relativePath}: ${(error as Error).message}`)
192
- }
193
- }
194
-
195
- console.log(`\n${dryRun ? 'Would transform' : 'Transformed'} ${transformedCount} file(s)`)
196
- }