@consilioweb/spellcheck 0.10.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.
- package/LICENSE +21 -0
- package/README.md +567 -0
- package/dist/client.cjs +1711 -0
- package/dist/client.d.cts +77 -0
- package/dist/client.d.ts +77 -0
- package/dist/client.js +1702 -0
- package/dist/index.cjs +1691 -0
- package/dist/index.d.cts +268 -0
- package/dist/index.d.ts +268 -0
- package/dist/index.js +1677 -0
- package/dist/views.cjs +32 -0
- package/dist/views.d.cts +11 -0
- package/dist/views.d.ts +11 -0
- package/dist/views.js +30 -0
- package/package.json +102 -0
- package/scripts/debug-check.mjs +295 -0
- package/scripts/install.mjs +236 -0
- package/scripts/uninstall.mjs +350 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-install setup for @consilioweb/spellcheck
|
|
5
|
+
* Automatically adds the plugin to the Payload config.
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx spellcheck-install
|
|
8
|
+
* or: npx spellcheck-install --collections pages,posts --language fr
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { execSync } from 'node:child_process'
|
|
14
|
+
|
|
15
|
+
const PACKAGE_NAME = '@consilioweb/spellcheck'
|
|
16
|
+
|
|
17
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function detectPackageManager(dir) {
|
|
20
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
21
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
|
|
22
|
+
if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun'
|
|
23
|
+
return 'npm'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function run(cmd, cwd) {
|
|
27
|
+
console.log(` \x1b[90m$ ${cmd}\x1b[0m`)
|
|
28
|
+
try {
|
|
29
|
+
execSync(cmd, { cwd, stdio: 'inherit' })
|
|
30
|
+
return true
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findSourceFiles(dir) {
|
|
37
|
+
const results = []
|
|
38
|
+
if (!fs.existsSync(dir)) return results
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = path.join(dir, entry.name)
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
44
|
+
results.push(...findSourceFiles(fullPath))
|
|
45
|
+
} else if (/\.(ts|tsx)$/.test(entry.name)) {
|
|
46
|
+
results.push(fullPath)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return results
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find the plugins file (src/plugins/index.ts or similar)
|
|
54
|
+
*/
|
|
55
|
+
function findPluginsFile(srcDir) {
|
|
56
|
+
// Common locations
|
|
57
|
+
const candidates = [
|
|
58
|
+
path.join(srcDir, 'plugins', 'index.ts'),
|
|
59
|
+
path.join(srcDir, 'plugins.ts'),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (fs.existsSync(candidate)) return candidate
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Search for a file containing `Plugin[]` or `plugins:`
|
|
67
|
+
const files = findSourceFiles(srcDir)
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const content = fs.readFileSync(file, 'utf-8')
|
|
70
|
+
if (content.includes('export const plugins') && content.includes('Plugin[]')) {
|
|
71
|
+
return file
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if the plugin is already imported/used
|
|
80
|
+
*/
|
|
81
|
+
function isAlreadyInstalled(content) {
|
|
82
|
+
return content.includes(PACKAGE_NAME)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse CLI args for --collections and --language
|
|
87
|
+
*/
|
|
88
|
+
function parseArgs() {
|
|
89
|
+
const args = process.argv.slice(2)
|
|
90
|
+
const config = {
|
|
91
|
+
collections: ['pages', 'posts'],
|
|
92
|
+
language: 'fr',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < args.length; i++) {
|
|
96
|
+
if (args[i] === '--collections' && args[i + 1]) {
|
|
97
|
+
config.collections = args[++i].split(',').map(s => s.trim())
|
|
98
|
+
}
|
|
99
|
+
if (args[i] === '--language' && args[i + 1]) {
|
|
100
|
+
config.language = args[++i].trim()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return config
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Main ──────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function main() {
|
|
110
|
+
const projectDir = process.env.INIT_CWD || process.cwd()
|
|
111
|
+
const srcDir = path.join(projectDir, 'src')
|
|
112
|
+
const pm = detectPackageManager(projectDir)
|
|
113
|
+
const config = parseArgs()
|
|
114
|
+
|
|
115
|
+
console.log('')
|
|
116
|
+
console.log(' \x1b[36m@consilioweb/spellcheck\x1b[0m — Install')
|
|
117
|
+
console.log(' ─────────────────────────────────────────────')
|
|
118
|
+
console.log(` Project: \x1b[33m${projectDir}\x1b[0m`)
|
|
119
|
+
console.log(` Package manager: \x1b[33m${pm}\x1b[0m`)
|
|
120
|
+
console.log(` Collections: \x1b[33m${config.collections.join(', ')}\x1b[0m`)
|
|
121
|
+
console.log(` Language: \x1b[33m${config.language}\x1b[0m`)
|
|
122
|
+
console.log('')
|
|
123
|
+
|
|
124
|
+
// ── Step 1: Find plugins file ──
|
|
125
|
+
console.log(' \x1b[36m[1/3]\x1b[0m Finding plugins configuration...')
|
|
126
|
+
|
|
127
|
+
const pluginsFile = findPluginsFile(srcDir)
|
|
128
|
+
if (!pluginsFile) {
|
|
129
|
+
console.log(' \x1b[33m⚠\x1b[0m No plugins file found in src/.')
|
|
130
|
+
console.log(' \x1b[33m⚠\x1b[0m You need to manually add the plugin to your Payload config:')
|
|
131
|
+
console.log('')
|
|
132
|
+
console.log(` \x1b[90mimport { spellcheckPlugin } from '${PACKAGE_NAME}'\x1b[0m`)
|
|
133
|
+
console.log(` \x1b[90mspellcheckPlugin({ collections: ${JSON.stringify(config.collections)}, language: '${config.language}' })\x1b[0m`)
|
|
134
|
+
console.log('')
|
|
135
|
+
} else {
|
|
136
|
+
const relPath = path.relative(projectDir, pluginsFile)
|
|
137
|
+
console.log(` \x1b[32m✓\x1b[0m Found: ${relPath}`)
|
|
138
|
+
|
|
139
|
+
const content = fs.readFileSync(pluginsFile, 'utf-8')
|
|
140
|
+
|
|
141
|
+
if (isAlreadyInstalled(content)) {
|
|
142
|
+
console.log(` \x1b[32m✓\x1b[0m Plugin already configured — skipping.`)
|
|
143
|
+
} else {
|
|
144
|
+
// Add import at the top (after last import line)
|
|
145
|
+
const importLine = `import { spellcheckPlugin } from '${PACKAGE_NAME}'`
|
|
146
|
+
const lines = content.split('\n')
|
|
147
|
+
let lastImportIndex = -1
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
if (lines[i].trim().startsWith('import ')) {
|
|
151
|
+
lastImportIndex = i
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (lastImportIndex >= 0) {
|
|
156
|
+
lines.splice(lastImportIndex + 1, 0, importLine)
|
|
157
|
+
} else {
|
|
158
|
+
lines.unshift(importLine)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add plugin call before the closing bracket of the plugins array
|
|
162
|
+
let joined = lines.join('\n')
|
|
163
|
+
|
|
164
|
+
// Build plugin config string
|
|
165
|
+
const collectionsStr = config.collections.map(c => `'${c}'`).join(', ')
|
|
166
|
+
const pluginCall = ` spellcheckPlugin({
|
|
167
|
+
collections: [${collectionsStr}],
|
|
168
|
+
language: '${config.language}',
|
|
169
|
+
checkOnSave: true,
|
|
170
|
+
addSidebarField: true,
|
|
171
|
+
addDashboardView: true,
|
|
172
|
+
skipRules: ['FR_SPELLING_RULE', 'WHITESPACE_RULE'],
|
|
173
|
+
skipCategories: ['TYPOGRAPHY', 'STYLE'],
|
|
174
|
+
customDictionary: ['Next.js', 'Payload', 'TypeScript', 'SEO'],
|
|
175
|
+
}),`
|
|
176
|
+
|
|
177
|
+
// Find the plugins array closing bracket and insert before it
|
|
178
|
+
const pluginsArrayMatch = joined.match(/export\s+const\s+plugins\s*:\s*Plugin\[\]\s*=\s*\[/)
|
|
179
|
+
if (pluginsArrayMatch) {
|
|
180
|
+
// Find the last `]` that closes this array
|
|
181
|
+
const arrayStart = joined.indexOf(pluginsArrayMatch[0])
|
|
182
|
+
const afterStart = joined.indexOf('[', arrayStart + pluginsArrayMatch[0].length - 1)
|
|
183
|
+
|
|
184
|
+
// Find matching closing bracket
|
|
185
|
+
let depth = 0
|
|
186
|
+
let closingIdx = -1
|
|
187
|
+
for (let i = afterStart; i < joined.length; i++) {
|
|
188
|
+
if (joined[i] === '[') depth++
|
|
189
|
+
else if (joined[i] === ']') {
|
|
190
|
+
depth--
|
|
191
|
+
if (depth === 0) {
|
|
192
|
+
closingIdx = i
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (closingIdx > 0) {
|
|
199
|
+
joined = joined.slice(0, closingIdx) + pluginCall + '\n' + joined.slice(closingIdx)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fs.writeFileSync(pluginsFile, joined, 'utf-8')
|
|
204
|
+
console.log(` \x1b[32m✓\x1b[0m Plugin added to ${relPath}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('')
|
|
209
|
+
|
|
210
|
+
// ── Step 2: Regenerate importmap ──
|
|
211
|
+
console.log(' \x1b[36m[2/3]\x1b[0m Regenerating importmap...')
|
|
212
|
+
const importmapCmd = pm === 'npm' ? 'npx payload' : `${pm} payload`
|
|
213
|
+
run(`${importmapCmd} generate:importmap`, projectDir)
|
|
214
|
+
|
|
215
|
+
console.log('')
|
|
216
|
+
|
|
217
|
+
// ── Step 3: Summary ──
|
|
218
|
+
console.log(' \x1b[36m[3/3]\x1b[0m Post-install checks...')
|
|
219
|
+
console.log(' \x1b[32m✓\x1b[0m Collection \x1b[33mspellcheck-results\x1b[0m will be auto-created on first boot')
|
|
220
|
+
console.log(' \x1b[32m✓\x1b[0m Endpoints registered: /api/spellcheck/validate, /api/spellcheck/fix, /api/spellcheck/bulk')
|
|
221
|
+
console.log(' \x1b[32m✓\x1b[0m Sidebar field added to editor')
|
|
222
|
+
console.log(' \x1b[32m✓\x1b[0m Dashboard view at /admin/spellcheck')
|
|
223
|
+
|
|
224
|
+
console.log('')
|
|
225
|
+
console.log(' \x1b[32m✓ Install complete!\x1b[0m')
|
|
226
|
+
console.log('')
|
|
227
|
+
console.log(' \x1b[36mNext steps:\x1b[0m')
|
|
228
|
+
console.log(' 1. Start your dev server to create the DB table')
|
|
229
|
+
console.log(' 2. Visit /admin/spellcheck to scan your content')
|
|
230
|
+
console.log(' 3. (Optional) Add "Correcteur" to your admin nav')
|
|
231
|
+
console.log('')
|
|
232
|
+
console.log(' \x1b[36mTo uninstall:\x1b[0m npx spellcheck-uninstall')
|
|
233
|
+
console.log('')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
main()
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Full uninstall for @consilioweb/spellcheck
|
|
5
|
+
* Removes all imports, plugin calls, DB tables, and the package itself.
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx spellcheck-uninstall
|
|
8
|
+
* or: npx spellcheck-uninstall --keep-data (skip DB cleanup)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { execSync } from 'node:child_process'
|
|
14
|
+
|
|
15
|
+
const PACKAGE_NAME = '@consilioweb/spellcheck'
|
|
16
|
+
|
|
17
|
+
// Tables and indexes created by the plugin
|
|
18
|
+
const DB_TABLES = ['spellcheck_results', 'spellcheck_dictionary']
|
|
19
|
+
const DB_INDEXES = [
|
|
20
|
+
'spellcheck_results_doc_id_idx',
|
|
21
|
+
'spellcheck_results_collection_idx',
|
|
22
|
+
'spellcheck_results_last_checked_idx',
|
|
23
|
+
'spellcheck_dictionary_word_idx',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
// Regex to match any import line from @consilioweb/spellcheck
|
|
27
|
+
const IMPORT_RE = /^\s*import\s+(?:type\s+)?(?:\{[^}]*\}|[\w]+)\s+from\s+['"]@consilioweb\/spellcheck(?:\/[^'"]*)?['"]\s*;?\s*$/gm
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
function detectPackageManager(dir) {
|
|
32
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
33
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn'
|
|
34
|
+
if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun'
|
|
35
|
+
return 'npm'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(cmd, cwd) {
|
|
39
|
+
console.log(` \x1b[90m$ ${cmd}\x1b[0m`)
|
|
40
|
+
try {
|
|
41
|
+
execSync(cmd, { cwd, stdio: 'inherit' })
|
|
42
|
+
return true
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runSilent(cmd, cwd) {
|
|
49
|
+
try {
|
|
50
|
+
return execSync(cmd, { cwd, encoding: 'utf-8' }).trim()
|
|
51
|
+
} catch {
|
|
52
|
+
return ''
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findSourceFiles(dir) {
|
|
57
|
+
const results = []
|
|
58
|
+
if (!fs.existsSync(dir)) return results
|
|
59
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullPath = path.join(dir, entry.name)
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
64
|
+
results.push(...findSourceFiles(fullPath))
|
|
65
|
+
} else if (/\.(ts|tsx)$/.test(entry.name)) {
|
|
66
|
+
results.push(fullPath)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract imported names from @consilioweb/spellcheck imports
|
|
74
|
+
*/
|
|
75
|
+
function extractImportedNames(content) {
|
|
76
|
+
const names = []
|
|
77
|
+
const re = /^\s*import\s+(?:type\s+)?\{([^}]*)\}\s+from\s+['"]@consilioweb\/spellcheck(?:\/[^'"]*)?['"]\s*;?\s*$/gm
|
|
78
|
+
let match
|
|
79
|
+
while ((match = re.exec(content)) !== null) {
|
|
80
|
+
for (const spec of match[1].split(',')) {
|
|
81
|
+
const trimmed = spec.trim()
|
|
82
|
+
if (!trimmed) continue
|
|
83
|
+
const asParts = trimmed.split(/\s+as\s+/)
|
|
84
|
+
names.push(asParts.length > 1 ? asParts[1].trim() : trimmed)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return names
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove plugin calls (handles nested braces/parens across multiple lines)
|
|
92
|
+
*/
|
|
93
|
+
function removePluginCalls(content, callNames) {
|
|
94
|
+
let modified = content
|
|
95
|
+
|
|
96
|
+
for (const fnName of callNames) {
|
|
97
|
+
let searchFrom = 0
|
|
98
|
+
while (true) {
|
|
99
|
+
const callIndex = modified.indexOf(`${fnName}(`, searchFrom)
|
|
100
|
+
if (callIndex === -1) break
|
|
101
|
+
|
|
102
|
+
// Verify not part of a larger identifier
|
|
103
|
+
if (callIndex > 0 && /[\w$]/.test(modified[callIndex - 1])) {
|
|
104
|
+
searchFrom = callIndex + fnName.length
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Find line start
|
|
109
|
+
let lineStart = callIndex
|
|
110
|
+
while (lineStart > 0 && modified[lineStart - 1] !== '\n') lineStart--
|
|
111
|
+
|
|
112
|
+
// Find matching closing paren
|
|
113
|
+
const openParen = callIndex + fnName.length
|
|
114
|
+
let depth = 0
|
|
115
|
+
let endIndex = openParen
|
|
116
|
+
for (let i = openParen; i < modified.length; i++) {
|
|
117
|
+
if (modified[i] === '(') depth++
|
|
118
|
+
else if (modified[i] === ')') {
|
|
119
|
+
depth--
|
|
120
|
+
if (depth === 0) {
|
|
121
|
+
endIndex = i + 1
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle trailing comma
|
|
128
|
+
let removeEnd = endIndex
|
|
129
|
+
const afterCall = modified.slice(endIndex)
|
|
130
|
+
const trailingMatch = afterCall.match(/^\s*,/)
|
|
131
|
+
if (trailingMatch) {
|
|
132
|
+
removeEnd = endIndex + trailingMatch[0].length
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Determine full range
|
|
136
|
+
let removeStart = lineStart
|
|
137
|
+
if (removeStart > 0 && modified[removeStart - 1] === '\n') removeStart--
|
|
138
|
+
|
|
139
|
+
// If no trailing comma, try removing leading comma
|
|
140
|
+
if (!trailingMatch) {
|
|
141
|
+
let lookBack = removeStart
|
|
142
|
+
while (lookBack > 0 && /[\s\n]/.test(modified[lookBack - 1])) lookBack--
|
|
143
|
+
if (lookBack > 0 && modified[lookBack - 1] === ',') removeStart = lookBack - 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
modified = modified.slice(0, removeStart) + modified.slice(removeEnd)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return modified
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cleanEmptyLines(content) {
|
|
154
|
+
return content.replace(/\n{3,}/g, '\n\n')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cleanOrphanCommas(content) {
|
|
158
|
+
return content.replace(/,(\s*\n\s*[)\]])/g, '$1')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Process a single source file: remove imports and plugin calls
|
|
163
|
+
*/
|
|
164
|
+
function processFile(filePath) {
|
|
165
|
+
const original = fs.readFileSync(filePath, 'utf-8')
|
|
166
|
+
if (!original.includes(PACKAGE_NAME)) return null
|
|
167
|
+
|
|
168
|
+
let content = original
|
|
169
|
+
|
|
170
|
+
// Extract names before removing imports
|
|
171
|
+
const importedNames = extractImportedNames(content)
|
|
172
|
+
|
|
173
|
+
// Remove import lines
|
|
174
|
+
content = content.replace(IMPORT_RE, '')
|
|
175
|
+
|
|
176
|
+
// Remove plugin calls
|
|
177
|
+
if (importedNames.length > 0) {
|
|
178
|
+
content = removePluginCalls(content, importedNames)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Also remove any admin-nav items referencing /admin/spellcheck
|
|
182
|
+
content = content.replace(/\s*\{[^}]*href:\s*['"]\/admin\/spellcheck['"][^}]*\},?\s*/g, '')
|
|
183
|
+
|
|
184
|
+
// Clean up
|
|
185
|
+
content = cleanOrphanCommas(content)
|
|
186
|
+
content = cleanEmptyLines(content)
|
|
187
|
+
|
|
188
|
+
return content === original ? null : content
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find SQLite DB files in the project
|
|
193
|
+
*/
|
|
194
|
+
function findDatabaseFiles(projectDir) {
|
|
195
|
+
const dbFiles = []
|
|
196
|
+
const entries = fs.readdirSync(projectDir, { withFileTypes: true })
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (entry.isFile() && entry.name.endsWith('.db')) {
|
|
199
|
+
dbFiles.push(path.join(projectDir, entry.name))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Also check data/ subdirectory
|
|
203
|
+
const dataDir = path.join(projectDir, 'data')
|
|
204
|
+
if (fs.existsSync(dataDir)) {
|
|
205
|
+
const dataEntries = fs.readdirSync(dataDir, { withFileTypes: true })
|
|
206
|
+
for (const entry of dataEntries) {
|
|
207
|
+
if (entry.isFile() && entry.name.endsWith('.db')) {
|
|
208
|
+
dbFiles.push(path.join(dataDir, entry.name))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return dbFiles
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Drop spellcheck tables and indexes from a SQLite database
|
|
217
|
+
*/
|
|
218
|
+
function cleanDatabase(dbPath) {
|
|
219
|
+
const statements = []
|
|
220
|
+
|
|
221
|
+
// Drop indexes first
|
|
222
|
+
for (const idx of DB_INDEXES) {
|
|
223
|
+
statements.push(`DROP INDEX IF EXISTS \`${idx}\`;`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Drop tables
|
|
227
|
+
for (const table of DB_TABLES) {
|
|
228
|
+
statements.push(`DROP TABLE IF EXISTS \`${table}\`;`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Also clean up payload_locked_documents_rels
|
|
232
|
+
statements.push(
|
|
233
|
+
`DELETE FROM \`payload_locked_documents_rels\` WHERE \`spellcheck_results_id\` IS NOT NULL;`,
|
|
234
|
+
`DELETE FROM \`payload_locked_documents_rels\` WHERE \`spellcheck_dictionary_id\` IS NOT NULL;`,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Remove the column from payload_locked_documents_rels (SQLite doesn't support DROP COLUMN easily)
|
|
238
|
+
// Just log it as manual step
|
|
239
|
+
|
|
240
|
+
const sql = statements.join('\n')
|
|
241
|
+
const result = runSilent(`sqlite3 "${dbPath}" "${sql}"`, path.dirname(dbPath))
|
|
242
|
+
return result !== undefined
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Main ──────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
function main() {
|
|
248
|
+
const projectDir = process.env.INIT_CWD || process.cwd()
|
|
249
|
+
const srcDir = path.join(projectDir, 'src')
|
|
250
|
+
const pm = detectPackageManager(projectDir)
|
|
251
|
+
const keepData = process.argv.includes('--keep-data')
|
|
252
|
+
|
|
253
|
+
console.log('')
|
|
254
|
+
console.log(' \x1b[36m@consilioweb/spellcheck\x1b[0m — Full Uninstall')
|
|
255
|
+
console.log(' ─────────────────────────────────────────────')
|
|
256
|
+
console.log(` Project: \x1b[33m${projectDir}\x1b[0m`)
|
|
257
|
+
console.log(` Package manager: \x1b[33m${pm}\x1b[0m`)
|
|
258
|
+
if (keepData) console.log(` \x1b[33m--keep-data: DB tables will NOT be dropped\x1b[0m`)
|
|
259
|
+
console.log('')
|
|
260
|
+
|
|
261
|
+
// ── Step 1: Clean source files ──
|
|
262
|
+
console.log(' \x1b[36m[1/4]\x1b[0m Cleaning source files...')
|
|
263
|
+
|
|
264
|
+
if (!fs.existsSync(srcDir)) {
|
|
265
|
+
console.log(' \x1b[33m⚠\x1b[0m No src/ directory found. Skipping code cleanup.')
|
|
266
|
+
} else {
|
|
267
|
+
const files = findSourceFiles(srcDir)
|
|
268
|
+
const modified = []
|
|
269
|
+
|
|
270
|
+
for (const filePath of files) {
|
|
271
|
+
const result = processFile(filePath)
|
|
272
|
+
if (result !== null) {
|
|
273
|
+
fs.writeFileSync(filePath, result, 'utf-8')
|
|
274
|
+
const rel = path.relative(projectDir, filePath)
|
|
275
|
+
modified.push(rel)
|
|
276
|
+
console.log(` \x1b[32m✓\x1b[0m Cleaned: ${rel}`)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (modified.length === 0) {
|
|
281
|
+
console.log(' \x1b[32m✓\x1b[0m No references found in source files.')
|
|
282
|
+
} else {
|
|
283
|
+
console.log(` \x1b[32m✓\x1b[0m ${modified.length} file(s) cleaned.`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log('')
|
|
288
|
+
|
|
289
|
+
// ── Step 2: Clean database ──
|
|
290
|
+
if (!keepData) {
|
|
291
|
+
console.log(' \x1b[36m[2/4]\x1b[0m Cleaning database...')
|
|
292
|
+
|
|
293
|
+
const dbFiles = findDatabaseFiles(projectDir)
|
|
294
|
+
if (dbFiles.length === 0) {
|
|
295
|
+
console.log(' \x1b[33m⚠\x1b[0m No .db files found. Skipping DB cleanup.')
|
|
296
|
+
} else {
|
|
297
|
+
for (const dbFile of dbFiles) {
|
|
298
|
+
const rel = path.relative(projectDir, dbFile)
|
|
299
|
+
const success = cleanDatabase(dbFile)
|
|
300
|
+
if (success) {
|
|
301
|
+
console.log(` \x1b[32m✓\x1b[0m Cleaned: ${rel}`)
|
|
302
|
+
for (const table of DB_TABLES) {
|
|
303
|
+
console.log(` \x1b[90m- Dropped table: ${table}\x1b[0m`)
|
|
304
|
+
}
|
|
305
|
+
for (const idx of DB_INDEXES) {
|
|
306
|
+
console.log(` \x1b[90m- Dropped index: ${idx}\x1b[0m`)
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
console.log(` \x1b[33m⚠\x1b[0m Could not clean ${rel} (sqlite3 not found or DB locked)`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
console.log(' \x1b[36m[2/4]\x1b[0m Skipping database cleanup (--keep-data)')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log('')
|
|
318
|
+
|
|
319
|
+
// ── Step 3: Remove the package ──
|
|
320
|
+
console.log(' \x1b[36m[3/4]\x1b[0m Removing package...')
|
|
321
|
+
const removeCmd = pm === 'npm' ? 'npm uninstall' : `${pm} remove`
|
|
322
|
+
run(`${removeCmd} ${PACKAGE_NAME}`, projectDir)
|
|
323
|
+
|
|
324
|
+
console.log('')
|
|
325
|
+
|
|
326
|
+
// ── Step 4: Regenerate importmap ──
|
|
327
|
+
console.log(' \x1b[36m[4/4]\x1b[0m Regenerating importmap...')
|
|
328
|
+
const importmapCmd = pm === 'npm' ? 'npx payload' : `${pm} payload`
|
|
329
|
+
run(`${importmapCmd} generate:importmap`, projectDir)
|
|
330
|
+
|
|
331
|
+
console.log('')
|
|
332
|
+
|
|
333
|
+
// ── Done ──
|
|
334
|
+
console.log(' \x1b[32m✓ Uninstall complete!\x1b[0m')
|
|
335
|
+
console.log('')
|
|
336
|
+
|
|
337
|
+
if (keepData) {
|
|
338
|
+
console.log(' \x1b[36mNote:\x1b[0m Database tables were preserved (--keep-data).')
|
|
339
|
+
console.log(' To manually drop them:')
|
|
340
|
+
console.log(' \x1b[90m sqlite3 your.db "DROP TABLE IF EXISTS spellcheck_results; DROP TABLE IF EXISTS spellcheck_dictionary;"\x1b[0m')
|
|
341
|
+
} else {
|
|
342
|
+
console.log(' \x1b[36mNote:\x1b[0m If columns \x1b[33mspellcheck_results_id\x1b[0m or \x1b[33mspellcheck_dictionary_id\x1b[0m')
|
|
343
|
+
console.log(' remain in \x1b[33mpayload_locked_documents_rels\x1b[0m, they will be ignored by Payload.')
|
|
344
|
+
console.log(' SQLite does not support DROP COLUMN natively.')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log('')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
main()
|