coc-vscode-loader 1.2.9 → 1.4.0
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/converter/README.md +66 -11
- package/converter/package-lock.json +1363 -146
- package/converter/package.json +9 -4
- package/converter/scripts/check-tests.ts +58 -0
- package/converter/scripts/smoke-test.ts +234 -0
- package/converter/src/cli.ts +4 -1
- package/converter/src/convert.test.ts +292 -0
- package/converter/src/convert.ts +5 -3
- package/converter/src/presets.test.ts +37 -0
- package/converter/src/registry-validation.test.ts +127 -0
- package/converter/src/scanner.test.ts +67 -0
- package/converter/src/scanner.ts +1 -1
- package/converter/src/steps/bridge.test.ts +72 -0
- package/converter/src/steps/language-client.test.ts +131 -0
- package/converter/src/steps/language-client.ts +1 -1
- package/converter/src/steps/mark-unsupported.test.ts +109 -0
- package/converter/src/steps/snippets.test.ts +114 -0
- package/converter/src/steps/snippets.ts +12 -3
- package/converter/src/steps/source.test.ts +117 -0
- package/converter/src/steps/source.ts +2 -4
- package/converter/src/transforms/class-to-factory.test.ts +60 -0
- package/converter/src/transforms/enum-offset.test.ts +27 -0
- package/converter/src/transforms/import-mapping.test.ts +227 -0
- package/converter/src/transforms/import-mapping.ts +32 -11
- package/converter/src/transforms/language-client.test.ts +48 -0
- package/converter/src/transforms/provider-register.test.ts +65 -0
- package/converter/src/transforms/provider-register.ts +1 -1
- package/converter/src/transforms/strip-volar.test.ts +35 -0
- package/converter/src/types.ts +2 -0
- package/converter/vitest.config.ts +8 -0
- package/lib/index.js +99 -50
- package/package.json +1 -1
package/converter/package.json
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "converter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "vscode → coc.nvim converter prototype",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"convert": "tsx src/cli.ts"
|
|
8
|
+
"convert": "tsx src/cli.ts",
|
|
9
|
+
"test": "npm run check:tests && vitest run",
|
|
10
|
+
"test:watch": "vitest",
|
|
11
|
+
"test:smoke": "tsx scripts/smoke-test.ts",
|
|
12
|
+
"check:tests": "tsx scripts/check-tests.ts"
|
|
9
13
|
},
|
|
10
14
|
"dependencies": {
|
|
11
|
-
"ts-morph": "^28.0.0",
|
|
12
15
|
"commander": "^15.0.0",
|
|
16
|
+
"ts-morph": "^28.0.0",
|
|
13
17
|
"typescript": "^6.0.3"
|
|
14
18
|
},
|
|
15
19
|
"devDependencies": {
|
|
16
20
|
"@types/node": "^25.9.3",
|
|
17
|
-
"tsx": "^4.19.0"
|
|
21
|
+
"tsx": "^4.19.0",
|
|
22
|
+
"vitest": "^4.1.8"
|
|
18
23
|
}
|
|
19
24
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync } from 'fs'
|
|
2
|
+
import { join, relative } from 'path'
|
|
3
|
+
|
|
4
|
+
const srcDir = new URL('../src', import.meta.url).pathname
|
|
5
|
+
|
|
6
|
+
const EXEMPT = ['types.ts', 'index.ts', 'cli.ts']
|
|
7
|
+
const MIN_TEST_SIZE = 50
|
|
8
|
+
|
|
9
|
+
function walk(dir: string): string[] {
|
|
10
|
+
const files: string[] = []
|
|
11
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
12
|
+
const p = join(dir, entry.name)
|
|
13
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
|
|
14
|
+
files.push(...walk(p))
|
|
15
|
+
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
|
|
16
|
+
files.push(p)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return files
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = walk(srcDir)
|
|
23
|
+
let exitCode = 0
|
|
24
|
+
|
|
25
|
+
for (const fp of files) {
|
|
26
|
+
const name = relative(srcDir, fp)
|
|
27
|
+
if (name.endsWith('.test.ts')) continue
|
|
28
|
+
if (EXEMPT.some(e => name === e || name.endsWith('/' + e))) continue
|
|
29
|
+
|
|
30
|
+
const testFile = fp.replace(/\.ts$/, '.test.ts')
|
|
31
|
+
if (!existsSync(testFile)) {
|
|
32
|
+
console.error(` MISSING TEST: ${name}`)
|
|
33
|
+
exitCode = 1
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate test file is not empty or placeholder
|
|
38
|
+
const testContent = readFileSync(testFile, 'utf-8')
|
|
39
|
+
if (testContent.length < MIN_TEST_SIZE) {
|
|
40
|
+
console.error(` EMPTY TEST: ${relative(srcDir, testFile)} (${testContent.length} bytes)`)
|
|
41
|
+
exitCode = 1
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if (!/\b(it|test)\s*\(/.test(testContent)) {
|
|
45
|
+
console.error(` NO TEST CASES: ${relative(srcDir, testFile)} (no it() or test() calls)`)
|
|
46
|
+
exitCode = 1
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (exitCode === 0) {
|
|
52
|
+
const sourceFiles = files.filter(f => {
|
|
53
|
+
const name = relative(srcDir, f)
|
|
54
|
+
return !name.endsWith('.test.ts') && !EXEMPT.some(e => name === e || name.endsWith('/' + e))
|
|
55
|
+
})
|
|
56
|
+
console.log(`All ${sourceFiles.length} source files have valid tests`)
|
|
57
|
+
}
|
|
58
|
+
process.exit(exitCode)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Registry smoke test: clone ALL registry entries, run the converter,
|
|
4
|
+
* validate output structure. Runs concurrently for speed.
|
|
5
|
+
*
|
|
6
|
+
* Usage: npm run test:smoke
|
|
7
|
+
* CONCURRENCY=8 npm run test:smoke
|
|
8
|
+
* NO_CACHE=1 npm run test:smoke
|
|
9
|
+
* VERBOSE=1 npm run test:smoke
|
|
10
|
+
*
|
|
11
|
+
* Proxy: respects HTTPS_PROXY / HTTP_PROXY / ALL_PROXY
|
|
12
|
+
* Cache: ~/.cache/coc-converter-smoke/
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'fs'
|
|
16
|
+
import * as path from 'path'
|
|
17
|
+
import * as os from 'os'
|
|
18
|
+
import { execFileSync } from 'child_process'
|
|
19
|
+
import { convert } from '../src/convert.js'
|
|
20
|
+
import { fileURLToPath } from 'url'
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const REGISTRY_PATH = path.resolve(__dirname, '../../coc-vscode-registry/registry.json')
|
|
24
|
+
const PRESETS_PATH = path.resolve(__dirname, '../../coc-vscode-registry/presets.json')
|
|
25
|
+
const CACHE_DIR = path.join(os.homedir(), '.cache', 'coc-converter-smoke')
|
|
26
|
+
const TEST_OUTPUT = path.join(os.tmpdir(), 'coc-smoke-output')
|
|
27
|
+
const CONCURRENCY = parseInt(process.env.CONCURRENCY || '8', 10)
|
|
28
|
+
const CACHE_TTL_DAYS = parseInt(process.env.CACHE_TTL || '7', 10)
|
|
29
|
+
|
|
30
|
+
interface RegistryEntry {
|
|
31
|
+
name: string; displayName: string; type: string
|
|
32
|
+
source: { type: string; repo?: string; package?: string; subdir?: string }
|
|
33
|
+
convert: any[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function cacheDir(e: RegistryEntry): string {
|
|
37
|
+
return path.join(CACHE_DIR, e.name.replace(/[^a-z0-9_-]/gi, '_'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function downloadOrCached(entry: RegistryEntry): string {
|
|
41
|
+
const dest = cacheDir(entry)
|
|
42
|
+
const src = entry.source
|
|
43
|
+
const isSnippets = entry.convert.some(s => s.type === 'snippets')
|
|
44
|
+
|
|
45
|
+
if (src.type === 'github' && src.repo) {
|
|
46
|
+
const url = `https://github.com/${src.repo}.git`
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(path.join(dest, '.git'))) {
|
|
49
|
+
// Fast incremental update
|
|
50
|
+
try {
|
|
51
|
+
execFileSync('git', ['fetch', '--depth', '1', 'origin', 'main'], { cwd: dest, stdio: 'pipe', timeout: 30000 })
|
|
52
|
+
execFileSync('git', ['reset', '--hard', 'origin/main'], { cwd: dest, stdio: 'pipe', timeout: 30000 })
|
|
53
|
+
} catch {
|
|
54
|
+
// Fetch failed, re-clone
|
|
55
|
+
fs.rmSync(dest, { recursive: true, force: true })
|
|
56
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
57
|
+
execFileSync('git', ['clone', '--depth', '1', '--single-branch', url, dest], { stdio: 'pipe', timeout: 300000 })
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// Fresh clone
|
|
61
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true })
|
|
62
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
63
|
+
execFileSync('git', ['clone', '--depth', '1', '--single-branch', url, dest], { stdio: 'pipe', timeout: 300000 })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate expected files exist
|
|
67
|
+
const checkDir = src.subdir ? path.join(dest, src.subdir) : dest
|
|
68
|
+
if (isSnippets) {
|
|
69
|
+
// Snippets need contributes.snippets in package.json
|
|
70
|
+
if (!fs.existsSync(path.join(checkDir, 'package.json')))
|
|
71
|
+
throw new Error(`package.json not found in ${src.subdir ? src.subdir : 'root'} of ${src.repo}`)
|
|
72
|
+
}
|
|
73
|
+
return checkDir
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (src.type === 'npm' && src.package) {
|
|
77
|
+
// npm packages: re-download if stale or missing
|
|
78
|
+
const metaPath = path.join(dest, '.npm-meta.json')
|
|
79
|
+
let stale = !fs.existsSync(dest) || !fs.existsSync(path.join(dest, 'package.json'))
|
|
80
|
+
if (!stale) {
|
|
81
|
+
try {
|
|
82
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
|
|
83
|
+
const age = (Date.now() - new Date(meta.fetched).getTime()) / 1000 / 86400
|
|
84
|
+
if (age > CACHE_TTL_DAYS) stale = true
|
|
85
|
+
} catch { stale = true }
|
|
86
|
+
}
|
|
87
|
+
if (stale || process.env.NO_CACHE) {
|
|
88
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true })
|
|
89
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
90
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-'))
|
|
91
|
+
try {
|
|
92
|
+
execFileSync('npm', ['pack', src.package], { cwd: tmp, stdio: 'pipe', timeout: 60000 })
|
|
93
|
+
} catch (e) {
|
|
94
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
95
|
+
fs.rmSync(dest, { recursive: true, force: true })
|
|
96
|
+
throw e
|
|
97
|
+
}
|
|
98
|
+
const tarball = fs.readdirSync(tmp).find(f => f.endsWith('.tgz'))
|
|
99
|
+
if (!tarball) { fs.rmSync(tmp, { recursive: true, force: true }); throw new Error(`npm pack failed for ${src.package}`) }
|
|
100
|
+
execFileSync('tar', ['xzf', path.join(tmp, tarball)], { cwd: dest, stdio: 'pipe' })
|
|
101
|
+
if (fs.existsSync(path.join(dest, 'package'))) {
|
|
102
|
+
for (const f of fs.readdirSync(path.join(dest, 'package')))
|
|
103
|
+
fs.cpSync(path.join(dest, 'package', f), path.join(dest, f), { recursive: true })
|
|
104
|
+
fs.rmSync(path.join(dest, 'package'), { recursive: true, force: true })
|
|
105
|
+
}
|
|
106
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
107
|
+
fs.writeFileSync(metaPath, JSON.stringify({ fetched: new Date().toISOString() }))
|
|
108
|
+
}
|
|
109
|
+
return dest
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error(`Unknown source type: ${src.type}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function testOne(entry: RegistryEntry, presets: any): Promise<string | null> {
|
|
116
|
+
let inputDir: string
|
|
117
|
+
try {
|
|
118
|
+
inputDir = downloadOrCached(entry)
|
|
119
|
+
} catch (e: any) {
|
|
120
|
+
return `download: ${e.message}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const outputDir = path.join(TEST_OUTPUT, entry.name)
|
|
124
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
125
|
+
try {
|
|
126
|
+
await convert({ input: inputDir, output: outputDir, convert: entry.convert, presets })
|
|
127
|
+
} catch (e: any) {
|
|
128
|
+
return `convert: ${e.message}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hasPkg = fs.existsSync(path.join(outputDir, 'package.json'))
|
|
132
|
+
const hasNonSource = entry.convert.some(s => s.type !== 'source' && s.type !== 'mark-unsupported')
|
|
133
|
+
const isSnippets = entry.convert.some(s => s.type === 'snippets')
|
|
134
|
+
|
|
135
|
+
// Read output package.json if it exists
|
|
136
|
+
let pkg: any = null
|
|
137
|
+
if (hasPkg) {
|
|
138
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(outputDir, 'package.json'), 'utf-8')) } catch {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (isSnippets) {
|
|
143
|
+
// Snippets: must have package.json + activationEvents + src/index.ts + snippet files copied
|
|
144
|
+
if (!pkg) return 'missing package.json'
|
|
145
|
+
if (!pkg.name) return 'package.json missing name'
|
|
146
|
+
if (!pkg.activationEvents?.length) return 'package.json missing activationEvents'
|
|
147
|
+
if (!fs.existsSync(path.join(outputDir, 'src', 'index.ts'))) return 'missing src/index.ts'
|
|
148
|
+
// Check contributed snippet files actually exist in output
|
|
149
|
+
const contributed = pkg.contributes?.snippets
|
|
150
|
+
if (contributed?.length) {
|
|
151
|
+
const missing = contributed.filter((s: any) => !fs.existsSync(path.join(outputDir, s.path)))
|
|
152
|
+
if (missing.length > 0) {
|
|
153
|
+
return `${missing.length} snippet files not copied (e.g. ${missing[0].path})`
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else if (hasNonSource) {
|
|
157
|
+
// language-client / bridge: must have package.json + entry point + esbuild.mjs
|
|
158
|
+
if (!pkg) return 'no output (package.json missing)'
|
|
159
|
+
if (!pkg.name) return 'package.json missing name'
|
|
160
|
+
if (!pkg.main) return 'package.json missing main'
|
|
161
|
+
if (!fs.existsSync(path.join(outputDir, 'esbuild.mjs'))) return 'missing esbuild.mjs'
|
|
162
|
+
// Check generated entry point exists
|
|
163
|
+
if (!fs.existsSync(path.join(outputDir, 'src', 'index.ts')) && !fs.existsSync(path.join(outputDir, 'src', 'bridge.ts'))) {
|
|
164
|
+
return 'missing generated entry (src/index.ts or src/bridge.ts)'
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Source-only: conversion may produce no output if no vscode imports found (expected)
|
|
168
|
+
if (pkg) {
|
|
169
|
+
if (!pkg.name) return 'package.json missing name'
|
|
170
|
+
// Check at least some source files were copied
|
|
171
|
+
const srcDir = path.join(outputDir, 'src')
|
|
172
|
+
if (fs.existsSync(srcDir)) {
|
|
173
|
+
const files = fs.readdirSync(srcDir).filter(f => f.endsWith('.ts') || f.endsWith('.js'))
|
|
174
|
+
if (files.length === 0) return 'no source files in src/'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (e: any) {
|
|
179
|
+
return `validate: ${e.message}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null // success
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function main() {
|
|
186
|
+
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY)
|
|
187
|
+
console.log(`Proxy: ${process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY}`)
|
|
188
|
+
console.log(`Concurrency: ${CONCURRENCY}\n`)
|
|
189
|
+
|
|
190
|
+
const registry: RegistryEntry[] = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8'))
|
|
191
|
+
const presets = fs.existsSync(PRESETS_PATH) ? JSON.parse(fs.readFileSync(PRESETS_PATH, 'utf-8')) : undefined
|
|
192
|
+
console.log(`Total: ${registry.length} entries\n`)
|
|
193
|
+
|
|
194
|
+
if (fs.existsSync(TEST_OUTPUT)) fs.rmSync(TEST_OUTPUT, { recursive: true, force: true })
|
|
195
|
+
|
|
196
|
+
let completed = 0
|
|
197
|
+
const failures: Array<{ name: string; error: string }> = []
|
|
198
|
+
const startTime = Date.now()
|
|
199
|
+
const total = registry.length
|
|
200
|
+
|
|
201
|
+
// Process with concurrency
|
|
202
|
+
const pool = new Set<Promise<void>>()
|
|
203
|
+
for (const entry of registry) {
|
|
204
|
+
const p = (async () => {
|
|
205
|
+
const err = await testOne(entry, presets)
|
|
206
|
+
completed++
|
|
207
|
+
const pct = (completed / total * 100).toFixed(0)
|
|
208
|
+
if (err) {
|
|
209
|
+
failures.push({ name: entry.name, error: err })
|
|
210
|
+
process.stdout.write(`\r[${pct}%] ${completed}/${total} FAIL ${entry.name} \n`)
|
|
211
|
+
} else {
|
|
212
|
+
process.stdout.write(`\r[${pct}%] ${completed}/${total} PASS ${entry.name} `)
|
|
213
|
+
}
|
|
214
|
+
})()
|
|
215
|
+
pool.add(p)
|
|
216
|
+
p.finally(() => pool.delete(p))
|
|
217
|
+
if (pool.size >= CONCURRENCY) await Promise.race(pool)
|
|
218
|
+
}
|
|
219
|
+
await Promise.allSettled(pool)
|
|
220
|
+
console.log('')
|
|
221
|
+
|
|
222
|
+
// Cleanup
|
|
223
|
+
if (fs.existsSync(TEST_OUTPUT)) fs.rmSync(TEST_OUTPUT, { recursive: true, force: true })
|
|
224
|
+
|
|
225
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0)
|
|
226
|
+
console.log(`\n${completed} done in ${elapsed}s — ${total - failures.length} passed, ${failures.length} failed`)
|
|
227
|
+
if (failures.length > 0) {
|
|
228
|
+
console.log('\nFailures:')
|
|
229
|
+
for (const f of failures) console.log(` ${f.name}: ${f.error}`)
|
|
230
|
+
process.exit(1)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main().catch(e => { console.error('\nFatal:', e); process.exit(1) })
|
package/converter/src/cli.ts
CHANGED
|
@@ -23,7 +23,10 @@ program
|
|
|
23
23
|
let steps: any[]
|
|
24
24
|
let presets: any
|
|
25
25
|
if (opts.presetsFile) {
|
|
26
|
-
try { presets = JSON.parse(fs.readFileSync(opts.presetsFile, 'utf-8')) } catch {
|
|
26
|
+
try { presets = JSON.parse(fs.readFileSync(opts.presetsFile, 'utf-8')) } catch (e: any) {
|
|
27
|
+
console.error(`invalid --presets-file: ${e.message}`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
if (opts.convertFile) {
|
|
29
32
|
try {
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
import * as fs from 'fs'
|
|
5
|
+
|
|
6
|
+
describe('convert main flow', () => {
|
|
7
|
+
let tmpdir: string
|
|
8
|
+
let outdir: string
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'convert-test-'))
|
|
12
|
+
outdir = tmpdir + '/output'
|
|
13
|
+
// Create minimal input
|
|
14
|
+
fs.mkdirSync(path.join(tmpdir, 'src'), { recursive: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(tmpdir, { recursive: true, force: true })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
function writeInput(rel: string, content: string) {
|
|
22
|
+
const fp = path.join(tmpdir, rel)
|
|
23
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true })
|
|
24
|
+
fs.writeFileSync(fp, content)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
it('produces package.json with correct structure', async () => {
|
|
28
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext', description: 'Test' }))
|
|
29
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
30
|
+
const { convert } = await import('./convert.js')
|
|
31
|
+
await convert({
|
|
32
|
+
input: tmpdir,
|
|
33
|
+
output: outdir,
|
|
34
|
+
convert: [{ type: 'source', transforms: ['import-mapping'] }],
|
|
35
|
+
})
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(outdir, 'package.json'), 'utf-8'))
|
|
37
|
+
expect(pkg.name).toBe('coc-test-ext')
|
|
38
|
+
expect(pkg.main).toBe('lib/index.js')
|
|
39
|
+
expect(pkg.engines.coc).toBe('^0.0.82')
|
|
40
|
+
expect(pkg.activationEvents).toEqual(['onLanguage'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('generates esbuild.mjs config', async () => {
|
|
44
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
45
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
46
|
+
const { convert } = await import('./convert.js')
|
|
47
|
+
await convert({
|
|
48
|
+
input: tmpdir,
|
|
49
|
+
output: outdir,
|
|
50
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
51
|
+
})
|
|
52
|
+
expect(fs.existsSync(path.join(outdir, 'esbuild.mjs'))).toBe(true)
|
|
53
|
+
const esbuild = fs.readFileSync(path.join(outdir, 'esbuild.mjs'), 'utf-8')
|
|
54
|
+
expect(esbuild).toContain("entryPoints: ['src/extension.ts']")
|
|
55
|
+
expect(esbuild).toContain('external:')
|
|
56
|
+
expect(esbuild).toContain("outfile: 'lib/index.js'")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('generates coc-convert.json metadata', async () => {
|
|
60
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
61
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
62
|
+
const { convert } = await import('./convert.js')
|
|
63
|
+
await convert({
|
|
64
|
+
input: tmpdir,
|
|
65
|
+
output: outdir,
|
|
66
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
67
|
+
})
|
|
68
|
+
const meta = JSON.parse(fs.readFileSync(path.join(outdir, 'coc-convert.json'), 'utf-8'))
|
|
69
|
+
expect(meta.entryPoint).toBeTruthy()
|
|
70
|
+
expect(meta.activationEvents).toBeDefined()
|
|
71
|
+
expect(meta.hasLanguageClient).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('replaces getWordRangeAtPosition in source files', async () => {
|
|
75
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
76
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nconst range = document.getWordRangeAtPosition(pos, /\\w+/)`)
|
|
77
|
+
const { convert } = await import('./convert.js')
|
|
78
|
+
await convert({
|
|
79
|
+
input: tmpdir,
|
|
80
|
+
output: outdir,
|
|
81
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
82
|
+
})
|
|
83
|
+
const content = fs.readFileSync(path.join(outdir, 'src', 'extension.ts'), 'utf-8')
|
|
84
|
+
expect(content).toContain('document.getText()')
|
|
85
|
+
expect(content).not.toContain('const range = document.getWordRangeAtPosition(')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('replaces .fileName with Uri.parse().fsPath', async () => {
|
|
89
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
90
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nconst name = document.fileName`)
|
|
91
|
+
const { convert } = await import('./convert.js')
|
|
92
|
+
await convert({
|
|
93
|
+
input: tmpdir,
|
|
94
|
+
output: outdir,
|
|
95
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
96
|
+
})
|
|
97
|
+
const content = fs.readFileSync(path.join(outdir, 'src', 'extension.ts'), 'utf-8')
|
|
98
|
+
expect(content).toContain('Uri.parse(document.uri).fsPath')
|
|
99
|
+
expect(content).not.toContain('document.fileName')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('replaces .uri.fsPath with Uri.parse().fsPath', async () => {
|
|
103
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
104
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nconst p = document.uri.fsPath`)
|
|
105
|
+
const { convert } = await import('./convert.js')
|
|
106
|
+
await convert({
|
|
107
|
+
input: tmpdir,
|
|
108
|
+
output: outdir,
|
|
109
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
110
|
+
})
|
|
111
|
+
const content = fs.readFileSync(path.join(outdir, 'src', 'extension.ts'), 'utf-8')
|
|
112
|
+
expect(content).toContain('Uri.parse(document.uri).fsPath')
|
|
113
|
+
expect(content).not.toContain('document.uri.fsPath')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('injects Uri import when Uri.parse() is introduced', async () => {
|
|
117
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
118
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nimport { workspace } from 'coc.nvim'\nconst p = document.uri.fsPath`)
|
|
119
|
+
const { convert } = await import('./convert.js')
|
|
120
|
+
await convert({
|
|
121
|
+
input: tmpdir,
|
|
122
|
+
output: outdir,
|
|
123
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
124
|
+
})
|
|
125
|
+
const content = fs.readFileSync(path.join(outdir, 'src', 'extension.ts'), 'utf-8')
|
|
126
|
+
expect(content).toContain('Uri')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('handles fileName destructuring', async () => {
|
|
130
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
131
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nconst { fileName } = document;`)
|
|
132
|
+
const { convert } = await import('./convert.js')
|
|
133
|
+
await convert({
|
|
134
|
+
input: tmpdir,
|
|
135
|
+
output: outdir,
|
|
136
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
137
|
+
})
|
|
138
|
+
const content = fs.readFileSync(path.join(outdir, 'src', 'extension.ts'), 'utf-8')
|
|
139
|
+
expect(content).toContain('fileName = Uri.parse')
|
|
140
|
+
expect(content).not.toContain('{ fileName }')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('preserves source plugin contributes.configuration in output package.json', async () => {
|
|
144
|
+
writeInput('package.json', JSON.stringify({
|
|
145
|
+
name: 'test-ext',
|
|
146
|
+
contributes: {
|
|
147
|
+
configuration: {
|
|
148
|
+
title: 'Test',
|
|
149
|
+
properties: {
|
|
150
|
+
'test.enable': { type: 'boolean', default: true },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}))
|
|
155
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
156
|
+
const { convert } = await import('./convert.js')
|
|
157
|
+
await convert({
|
|
158
|
+
input: tmpdir,
|
|
159
|
+
output: outdir,
|
|
160
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
161
|
+
})
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(outdir, 'package.json'), 'utf-8'))
|
|
163
|
+
expect(pkg.contributes.configuration.properties['test.enable']).toBeTruthy()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('preserves source contributes.commands in output package.json', async () => {
|
|
167
|
+
writeInput('package.json', JSON.stringify({
|
|
168
|
+
name: 'test-ext',
|
|
169
|
+
contributes: {
|
|
170
|
+
commands: [{ command: 'test.doStuff', title: 'Do Stuff' }],
|
|
171
|
+
},
|
|
172
|
+
}))
|
|
173
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
174
|
+
const { convert } = await import('./convert.js')
|
|
175
|
+
await convert({
|
|
176
|
+
input: tmpdir,
|
|
177
|
+
output: outdir,
|
|
178
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
179
|
+
})
|
|
180
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(outdir, 'package.json'), 'utf-8'))
|
|
181
|
+
expect(pkg.contributes.commands).toHaveLength(1)
|
|
182
|
+
expect(pkg.contributes.commands[0].command).toBe('test.doStuff')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('preserves contributes.snippets in output', async () => {
|
|
186
|
+
writeInput('package.json', JSON.stringify({
|
|
187
|
+
name: 'test-snippets',
|
|
188
|
+
contributes: {
|
|
189
|
+
snippets: [{ language: 'javascript', path: './snippets/javascript.json' }],
|
|
190
|
+
},
|
|
191
|
+
}))
|
|
192
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
193
|
+
const { convert } = await import('./convert.js')
|
|
194
|
+
await convert({
|
|
195
|
+
input: tmpdir,
|
|
196
|
+
output: outdir,
|
|
197
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
198
|
+
})
|
|
199
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(outdir, 'package.json'), 'utf-8'))
|
|
200
|
+
expect(pkg.contributes.snippets).toHaveLength(1)
|
|
201
|
+
expect(pkg.contributes.snippets[0].language).toBe('javascript')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('handles language-client step with module server', async () => {
|
|
205
|
+
writeInput('package.json', JSON.stringify({
|
|
206
|
+
name: 'test-ls',
|
|
207
|
+
description: 'Test LS',
|
|
208
|
+
dependencies: { 'test-server': '^1.0.0' },
|
|
209
|
+
}))
|
|
210
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
211
|
+
const { convert } = await import('./convert.js')
|
|
212
|
+
await convert({
|
|
213
|
+
input: tmpdir,
|
|
214
|
+
output: outdir,
|
|
215
|
+
convert: [
|
|
216
|
+
{
|
|
217
|
+
type: 'language-client',
|
|
218
|
+
server: { kind: 'module', package: 'test-server' },
|
|
219
|
+
languages: ['test'],
|
|
220
|
+
},
|
|
221
|
+
{ type: 'source', transforms: ['import-mapping'] },
|
|
222
|
+
],
|
|
223
|
+
})
|
|
224
|
+
expect(fs.existsSync(path.join(outdir, 'src', 'index.ts'))).toBe(true)
|
|
225
|
+
const index = fs.readFileSync(path.join(outdir, 'src', 'index.ts'), 'utf-8')
|
|
226
|
+
expect(index).toContain('LanguageClient')
|
|
227
|
+
expect(index).toContain("from 'coc.nvim'")
|
|
228
|
+
|
|
229
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(outdir, 'package.json'), 'utf-8'))
|
|
230
|
+
expect(pkg.dependencies['test-server']).toBe('^1.0.0')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('handles binary server language-client step', async () => {
|
|
234
|
+
writeInput('package.json', JSON.stringify({ name: 'binary-ls' }))
|
|
235
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
236
|
+
const { convert } = await import('./convert.js')
|
|
237
|
+
await convert({
|
|
238
|
+
input: tmpdir,
|
|
239
|
+
output: outdir,
|
|
240
|
+
convert: [
|
|
241
|
+
{
|
|
242
|
+
type: 'language-client',
|
|
243
|
+
server: {
|
|
244
|
+
kind: 'binary',
|
|
245
|
+
package: 'my-server',
|
|
246
|
+
binary: { repo: 'user/repo', asset: 'server-{{version}}.tar.gz', binaryPath: 'bin/srv' },
|
|
247
|
+
},
|
|
248
|
+
languages: ['test'],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
})
|
|
252
|
+
const meta = JSON.parse(fs.readFileSync(path.join(outdir, 'coc-convert.json'), 'utf-8'))
|
|
253
|
+
expect(meta.serverBinary).toBeTruthy()
|
|
254
|
+
expect(meta.serverBinary.repo).toBe('user/repo')
|
|
255
|
+
expect(meta.hasLanguageClient).toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('copies .ts and .js source files to output', async () => {
|
|
259
|
+
writeInput('package.json', JSON.stringify({ name: 'test-ext' }))
|
|
260
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
261
|
+
writeInput('src/helper.ts', `export function helper() {}`)
|
|
262
|
+
writeInput('src/legacy.js', `module.exports = {}`)
|
|
263
|
+
const { convert } = await import('./convert.js')
|
|
264
|
+
await convert({
|
|
265
|
+
input: tmpdir,
|
|
266
|
+
output: outdir,
|
|
267
|
+
convert: [{ type: 'source', transforms: [] }],
|
|
268
|
+
})
|
|
269
|
+
expect(fs.existsSync(path.join(outdir, 'src', 'extension.ts'))).toBe(true)
|
|
270
|
+
expect(fs.existsSync(path.join(outdir, 'src', 'helper.ts'))).toBe(true)
|
|
271
|
+
expect(fs.existsSync(path.join(outdir, 'src', 'legacy.js'))).toBe(true)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('outputs entry point src/index.ts for language-client', async () => {
|
|
275
|
+
writeInput('package.json', JSON.stringify({ name: 'ls-ext' }))
|
|
276
|
+
writeInput('src/extension.ts', `import * as vscode from 'vscode'\nexport function activate() {}`)
|
|
277
|
+
const { convert } = await import('./convert.js')
|
|
278
|
+
await convert({
|
|
279
|
+
input: tmpdir,
|
|
280
|
+
output: outdir,
|
|
281
|
+
convert: [
|
|
282
|
+
{
|
|
283
|
+
type: 'language-client',
|
|
284
|
+
server: { kind: 'module', package: 'ls-srv' },
|
|
285
|
+
languages: ['test'],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
})
|
|
289
|
+
const esbuild = fs.readFileSync(path.join(outdir, 'esbuild.mjs'), 'utf-8')
|
|
290
|
+
expect(esbuild).toContain("entryPoints: ['src/index.ts']")
|
|
291
|
+
})
|
|
292
|
+
})
|
package/converter/src/convert.ts
CHANGED
|
@@ -199,7 +199,9 @@ export async function convert(opts: ConvertOptions): Promise<void> {
|
|
|
199
199
|
changed = true
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
const hasFileNameRef = content.includes('.fileName') || content.includes('.uri.fsPath')
|
|
203
|
+
const hasFileNameDestructuring = /(const|let|var)\s*\{[^}]*\bfileName\b[^}]*\}\s*=\s*(document|doc|textDocument)/.test(content)
|
|
204
|
+
if (hasFileNameRef || hasFileNameDestructuring) {
|
|
203
205
|
// .fileName → Uri.parse($1.uri).fsPath (coc's TextDocument#uri returns a file:// URI string)
|
|
204
206
|
content = content.replace(/(document|this\.document|textDocument|scope|doc)\.fileName/g, 'Uri.parse($1.uri).fsPath')
|
|
205
207
|
// Handle destructuring: const { fileName, ...rest } = document/doc/textDocument
|
|
@@ -285,12 +287,12 @@ export async function convert(opts: ConvertOptions): Promise<void> {
|
|
|
285
287
|
// Collect all runtime deps from origPkg (deps + devDeps, filtered)
|
|
286
288
|
const origDeps: Record<string, string> = {}
|
|
287
289
|
{
|
|
288
|
-
const allPkgDeps = { ...origPkg.
|
|
290
|
+
const allPkgDeps = { ...origPkg.devDependencies, ...origPkg.dependencies }
|
|
289
291
|
for (const [dep, ver] of Object.entries(allPkgDeps)) {
|
|
290
292
|
const v = ver as string
|
|
291
293
|
if (v.startsWith('workspace:')) continue
|
|
292
294
|
if (dep.startsWith('@types/')) continue
|
|
293
|
-
if (['typescript', 'mocha', 'c8', 'prettier', 'rollup', 'esbuild', '@vscode/'].some(p => dep.startsWith(p))) continue
|
|
295
|
+
if (['typescript', 'mocha', 'c8', 'prettier', 'rollup', 'esbuild', '@vscode/test-electron'].some(p => dep.startsWith(p))) continue
|
|
294
296
|
origDeps[dep] = v
|
|
295
297
|
}
|
|
296
298
|
}
|