@consilioweb/payload-seo-analyzer 1.7.1

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.
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Cleanup script for @consilioweb/seo-analyzer
4
+ // Removes all imports and plugin calls from source files before uninstalling the package.
5
+ // Usage: npx seo-analyzer-uninstall
6
+
7
+ import fs from 'node:fs'
8
+ import path from 'node:path'
9
+ import { execSync } from 'node:child_process'
10
+
11
+ const PACKAGE_NAME = '@consilioweb/seo-analyzer'
12
+
13
+ // Regex to match any import line from @consilioweb/seo-analyzer (value + type imports)
14
+ const IMPORT_RE = /^\s*import\s+(?:type\s+)?(?:\{[^}]*\}|[\w]+)\s+from\s+['"]@consilioweb\/seo-analyzer(?:\/[^'"]*)?['"]\s*;?\s*$/gm
15
+
16
+ /**
17
+ * Extract imported names from a file that come from @consilioweb/seo-analyzer.
18
+ * Returns the list of identifiers (after "as" renaming if any).
19
+ * e.g. `import { seoPlugin as myPlugin, seoFields } from '...'` → ['myPlugin', 'seoFields']
20
+ */
21
+ function extractImportedNames(content) {
22
+ const names = []
23
+ const re = /^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]@consilioweb\/seo-analyzer(?:\/[^'"]*)?['"]\s*;?\s*$/gm
24
+ let match
25
+ while ((match = re.exec(content)) !== null) {
26
+ const specifiers = match[1]
27
+ for (const spec of specifiers.split(',')) {
28
+ const trimmed = spec.trim()
29
+ if (!trimmed) continue
30
+ // Handle `foo as bar` → use bar (local name)
31
+ const asParts = trimmed.split(/\s+as\s+/)
32
+ names.push(asParts.length > 1 ? asParts[1].trim() : trimmed)
33
+ }
34
+ }
35
+ return names
36
+ }
37
+
38
+ /**
39
+ * Recursively find all .ts and .tsx files in a directory
40
+ */
41
+ function findSourceFiles(dir) {
42
+ const results = []
43
+ if (!fs.existsSync(dir)) return results
44
+
45
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
46
+ for (const entry of entries) {
47
+ const fullPath = path.join(dir, entry.name)
48
+ if (entry.isDirectory()) {
49
+ // Skip node_modules and hidden directories
50
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
51
+ results.push(...findSourceFiles(fullPath))
52
+ } else if (/\.(ts|tsx)$/.test(entry.name)) {
53
+ results.push(fullPath)
54
+ }
55
+ }
56
+ return results
57
+ }
58
+
59
+ /**
60
+ * Remove a plugin call like `seoAnalyzerPlugin({ ... })` from a plugins array.
61
+ * Only removes calls for function names that were actually imported from our package.
62
+ * Handles nested braces/parens across multiple lines.
63
+ */
64
+ function removePluginCalls(content, callNames) {
65
+ let modified = content
66
+
67
+ for (const fnName of callNames) {
68
+ let searchFrom = 0
69
+ while (true) {
70
+ const callIndex = modified.indexOf(`${fnName}(`, searchFrom)
71
+ if (callIndex === -1) break
72
+
73
+ // Verify this is actually our function call (not something like "mySeoAnalyzerPlugin")
74
+ if (callIndex > 0 && /[\w$]/.test(modified[callIndex - 1])) {
75
+ searchFrom = callIndex + fnName.length
76
+ continue
77
+ }
78
+
79
+ // Also check the char after fnName( isn't making a different identifier
80
+ // (already handled by the `(` in the search string)
81
+
82
+ // Find the start of this expression line
83
+ let lineStart = callIndex
84
+ while (lineStart > 0 && modified[lineStart - 1] !== '\n') {
85
+ lineStart--
86
+ }
87
+
88
+ // Find the matching closing paren for the function call
89
+ const openParen = callIndex + fnName.length
90
+ let depth = 0
91
+ let endIndex = openParen
92
+ for (let i = openParen; i < modified.length; i++) {
93
+ if (modified[i] === '(') depth++
94
+ else if (modified[i] === ')') {
95
+ depth--
96
+ if (depth === 0) {
97
+ endIndex = i + 1
98
+ break
99
+ }
100
+ }
101
+ }
102
+
103
+ // Check for trailing comma and whitespace
104
+ let removeEnd = endIndex
105
+ const afterCall = modified.slice(endIndex)
106
+ const trailingMatch = afterCall.match(/^\s*,/)
107
+ if (trailingMatch) {
108
+ removeEnd = endIndex + trailingMatch[0].length
109
+ }
110
+
111
+ // Determine the full range to remove
112
+ let removeStart = lineStart
113
+ // Include the newline before this line
114
+ if (removeStart > 0 && modified[removeStart - 1] === '\n') {
115
+ removeStart--
116
+ }
117
+
118
+ // If no trailing comma, remove a leading comma instead
119
+ if (!trailingMatch) {
120
+ let lookBack = removeStart
121
+ while (lookBack > 0 && /[\s\n]/.test(modified[lookBack - 1])) {
122
+ lookBack--
123
+ }
124
+ if (lookBack > 0 && modified[lookBack - 1] === ',') {
125
+ removeStart = lookBack - 1
126
+ }
127
+ }
128
+
129
+ // Remove the block
130
+ modified = modified.slice(0, removeStart) + modified.slice(removeEnd)
131
+ // Don't advance searchFrom since content shifted
132
+ }
133
+ }
134
+
135
+ return modified
136
+ }
137
+
138
+ /**
139
+ * Clean up consecutive empty lines (max 1 empty line between content)
140
+ */
141
+ function cleanEmptyLines(content) {
142
+ return content.replace(/\n{3,}/g, '\n\n')
143
+ }
144
+
145
+ /**
146
+ * Clean orphan trailing commas before closing brackets/parens
147
+ * e.g., `,\n]` becomes `\n]`
148
+ */
149
+ function cleanOrphanCommas(content) {
150
+ return content.replace(/,(\s*\n\s*[)\]])/g, '$1')
151
+ }
152
+
153
+ /**
154
+ * Process a single file: remove imports and plugin calls
155
+ */
156
+ function processFile(filePath) {
157
+ const original = fs.readFileSync(filePath, 'utf-8')
158
+
159
+ // Check if this file references the package at all
160
+ if (!original.includes(PACKAGE_NAME)) {
161
+ return null
162
+ }
163
+
164
+ let content = original
165
+
166
+ // 1. Extract the names actually imported from our package (before removing imports)
167
+ const importedNames = extractImportedNames(content)
168
+
169
+ // 2. Remove import lines
170
+ content = content.replace(IMPORT_RE, '')
171
+
172
+ // 3. Remove plugin calls only for names imported from our package
173
+ if (importedNames.length > 0) {
174
+ content = removePluginCalls(content, importedNames)
175
+ }
176
+
177
+ // 4. Clean up
178
+ content = cleanOrphanCommas(content)
179
+ content = cleanEmptyLines(content)
180
+
181
+ if (content === original) {
182
+ return null
183
+ }
184
+
185
+ return content
186
+ }
187
+
188
+ // ── Helpers ───────────────────────────────────────────────
189
+
190
+ /**
191
+ * Detect which package manager is being used in the project
192
+ */
193
+ function detectPackageManager(projectDir) {
194
+ if (fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'))) return 'pnpm'
195
+ if (fs.existsSync(path.join(projectDir, 'yarn.lock'))) return 'yarn'
196
+ if (fs.existsSync(path.join(projectDir, 'bun.lockb')) || fs.existsSync(path.join(projectDir, 'bun.lock'))) return 'bun'
197
+ return 'npm'
198
+ }
199
+
200
+ /**
201
+ * Run a shell command, print output, swallow errors
202
+ */
203
+ function run(cmd, cwd) {
204
+ console.log(` \x1b[90m$ ${cmd}\x1b[0m`)
205
+ try {
206
+ execSync(cmd, { cwd, stdio: 'inherit' })
207
+ return true
208
+ } catch {
209
+ return false
210
+ }
211
+ }
212
+
213
+ // ── Main ──────────────────────────────────────────────────
214
+
215
+ function main() {
216
+ // Determine project root
217
+ const projectDir = process.env.INIT_CWD || process.cwd()
218
+ const srcDir = path.join(projectDir, 'src')
219
+ const pm = detectPackageManager(projectDir)
220
+
221
+ console.log('')
222
+ console.log(' \x1b[36m@consilioweb/seo-analyzer\x1b[0m — Full Uninstall')
223
+ console.log(' ─────────────────────────────────────────────')
224
+ console.log(` Project: \x1b[33m${projectDir}\x1b[0m`)
225
+ console.log(` Package manager: \x1b[33m${pm}\x1b[0m`)
226
+ console.log('')
227
+
228
+ // ── Step 1: Clean source files ──
229
+ console.log(' \x1b[36m[1/3]\x1b[0m Cleaning source files...')
230
+
231
+ if (!fs.existsSync(srcDir)) {
232
+ console.log(' \x1b[33m⚠\x1b[0m No src/ directory found. Skipping code cleanup.')
233
+ } else {
234
+ const files = findSourceFiles(srcDir)
235
+ const modified = []
236
+
237
+ for (const filePath of files) {
238
+ const result = processFile(filePath)
239
+ if (result !== null) {
240
+ fs.writeFileSync(filePath, result, 'utf-8')
241
+ const rel = path.relative(projectDir, filePath)
242
+ modified.push(rel)
243
+ console.log(` \x1b[32m✓\x1b[0m Cleaned: ${rel}`)
244
+ }
245
+ }
246
+
247
+ if (modified.length === 0) {
248
+ console.log(' \x1b[32m✓\x1b[0m No references found in source files.')
249
+ } else {
250
+ console.log(` \x1b[32m✓\x1b[0m ${modified.length} file(s) cleaned.`)
251
+ }
252
+ }
253
+
254
+ console.log('')
255
+
256
+ // ── Step 2: Remove the package ──
257
+ console.log(' \x1b[36m[2/3]\x1b[0m Removing package...')
258
+ const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
259
+ run(`${removeCmd} ${PACKAGE_NAME}`, projectDir)
260
+
261
+ console.log('')
262
+
263
+ // ── Step 3: Regenerate importmap ──
264
+ console.log(' \x1b[36m[3/3]\x1b[0m Regenerating importmap...')
265
+ const importmapCmd = pm === 'npm' ? 'npx' : pm === 'yarn' ? 'yarn' : pm
266
+ run(`${importmapCmd} generate:importmap`, projectDir)
267
+
268
+ console.log('')
269
+
270
+ // ── Done ──
271
+ console.log(' \x1b[32m✓ Uninstall complete!\x1b[0m')
272
+ console.log('')
273
+ console.log(' \x1b[36mOptional:\x1b[0m Drop plugin collections from your database:')
274
+ console.log(' \x1b[90m - seo-score-history')
275
+ console.log(' - seo-settings')
276
+ console.log(' - seo-redirects')
277
+ console.log(' - seo-performance')
278
+ console.log(' - seo-logs\x1b[0m')
279
+ console.log('')
280
+ }
281
+
282
+ main()