@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/bin/elements.cjs +3 -5
- package/dist/codemods/at-a-glance-article-card/transform.js +253570 -0
- package/dist/codemods/bin.js +315 -0
- package/dist/codemods/codemods.js +44 -0
- package/{codemods → dist/codemods}/manifest.json +1 -1
- package/dist/codemods/runner.js +147 -0
- package/package.json +8 -6
- package/codemods/__tests__/codemods.test.ts +0 -178
- package/codemods/__tests__/generate-manifest.test.ts +0 -240
- package/codemods/__tests__/readme-parser.test.ts +0 -218
- package/codemods/__tests__/runner.test.ts +0 -530
- package/codemods/at-a-glance-article-card/README.md +0 -122
- package/codemods/at-a-glance-article-card/__tests__/transform.test.ts +0 -390
- package/codemods/at-a-glance-article-card/transform.ts +0 -291
- package/codemods/bin.ts +0 -205
- package/codemods/codemods.ts +0 -75
- package/codemods/generate-manifest.ts +0 -120
- package/codemods/manifest.schema.json +0 -39
- package/codemods/readme-parser.ts +0 -37
- package/codemods/runner.ts +0 -196
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
|
-
})
|
package/codemods/codemods.ts
DELETED
|
@@ -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
|
-
}
|
package/codemods/runner.ts
DELETED
|
@@ -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
|
-
}
|