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.
Files changed (32) hide show
  1. package/converter/README.md +66 -11
  2. package/converter/package-lock.json +1363 -146
  3. package/converter/package.json +9 -4
  4. package/converter/scripts/check-tests.ts +58 -0
  5. package/converter/scripts/smoke-test.ts +234 -0
  6. package/converter/src/cli.ts +4 -1
  7. package/converter/src/convert.test.ts +292 -0
  8. package/converter/src/convert.ts +5 -3
  9. package/converter/src/presets.test.ts +37 -0
  10. package/converter/src/registry-validation.test.ts +127 -0
  11. package/converter/src/scanner.test.ts +67 -0
  12. package/converter/src/scanner.ts +1 -1
  13. package/converter/src/steps/bridge.test.ts +72 -0
  14. package/converter/src/steps/language-client.test.ts +131 -0
  15. package/converter/src/steps/language-client.ts +1 -1
  16. package/converter/src/steps/mark-unsupported.test.ts +109 -0
  17. package/converter/src/steps/snippets.test.ts +114 -0
  18. package/converter/src/steps/snippets.ts +12 -3
  19. package/converter/src/steps/source.test.ts +117 -0
  20. package/converter/src/steps/source.ts +2 -4
  21. package/converter/src/transforms/class-to-factory.test.ts +60 -0
  22. package/converter/src/transforms/enum-offset.test.ts +27 -0
  23. package/converter/src/transforms/import-mapping.test.ts +227 -0
  24. package/converter/src/transforms/import-mapping.ts +32 -11
  25. package/converter/src/transforms/language-client.test.ts +48 -0
  26. package/converter/src/transforms/provider-register.test.ts +65 -0
  27. package/converter/src/transforms/provider-register.ts +1 -1
  28. package/converter/src/transforms/strip-volar.test.ts +35 -0
  29. package/converter/src/types.ts +2 -0
  30. package/converter/vitest.config.ts +8 -0
  31. package/lib/index.js +99 -50
  32. package/package.json +1 -1
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "converter",
3
- "version": "1.2.9",
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) })
@@ -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
+ })
@@ -199,7 +199,9 @@ export async function convert(opts: ConvertOptions): Promise<void> {
199
199
  changed = true
200
200
  }
201
201
 
202
- if (content.includes('.fileName') || content.includes('.uri.fsPath')) {
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.dependencies, ...origPkg.devDependencies }
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
  }