coc-vscode-loader 1.1.2 → 1.1.5
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/assets/tui-preview.png +0 -0
- package/converter/src/cli.ts +28 -0
- package/converter/src/convert.ts +501 -0
- package/converter/src/presets.ts +57 -0
- package/converter/src/scanner.ts +136 -0
- package/converter/src/transforms/class-to-factory.ts +54 -0
- package/converter/src/transforms/enum-offset.ts +49 -0
- package/converter/src/transforms/import-mapping.ts +47 -0
- package/converter/src/transforms/language-client.ts +48 -0
- package/converter/src/transforms/provider-register.ts +55 -0
- package/converter/src/types.ts +8 -0
- package/lib/index.js +50 -13
- package/package.json +7 -1
- package/converter/README.md +0 -134
- package/converter/package-lock.json +0 -693
- package/converter/pnpm-lock.yaml +0 -419
package/assets/tui-preview.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { convert } from './convert.js'
|
|
4
|
+
|
|
5
|
+
const program = new Command()
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('converter')
|
|
9
|
+
.description('vscode → coc.nvim extension converter')
|
|
10
|
+
.version('0.1.0')
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.command('convert')
|
|
14
|
+
.description('convert a VS Code extension to coc.nvim')
|
|
15
|
+
.argument('<input>', 'input directory (VS Code extension)')
|
|
16
|
+
.option('-o, --output <dir>', 'output directory', './output')
|
|
17
|
+
.option('--bridge', 'generate ts-bridge code')
|
|
18
|
+
.option('-v, --verbose', 'verbose output')
|
|
19
|
+
.action(async (input, opts) => {
|
|
20
|
+
await convert({
|
|
21
|
+
input,
|
|
22
|
+
output: opts.output,
|
|
23
|
+
bridge: opts.bridge,
|
|
24
|
+
verbose: opts.verbose,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
program.parse()
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { Project } from 'ts-morph'
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { scan } from './scanner.js'
|
|
5
|
+
import { TransformContext } from './types.js'
|
|
6
|
+
import { transformImportMapping } from './transforms/import-mapping.js'
|
|
7
|
+
import { transformLanguageClient } from './transforms/language-client.js'
|
|
8
|
+
import { transformClassToFactory } from './transforms/class-to-factory.js'
|
|
9
|
+
import { transformProviderRegister } from './transforms/provider-register.js'
|
|
10
|
+
import { transformEnumOffset } from './transforms/enum-offset.js'
|
|
11
|
+
import { getActivePresets, generateBridgeCode } from './presets.js'
|
|
12
|
+
|
|
13
|
+
const TRANSFORMS = [
|
|
14
|
+
{ name: 'import-mapping', fn: transformImportMapping },
|
|
15
|
+
{ name: 'class-to-factory', fn: transformClassToFactory },
|
|
16
|
+
{ name: 'provider-register', fn: transformProviderRegister },
|
|
17
|
+
{ name: 'enum-offset', fn: transformEnumOffset },
|
|
18
|
+
{ name: 'language-client', fn: transformLanguageClient },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
interface Options {
|
|
22
|
+
input: string
|
|
23
|
+
output: string
|
|
24
|
+
bridge?: boolean
|
|
25
|
+
verbose?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function convert(opts: Options): Promise<void> {
|
|
29
|
+
const { input, output } = opts
|
|
30
|
+
|
|
31
|
+
if (!fs.existsSync(input)) {
|
|
32
|
+
console.error(`input not found: ${input}`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 1. Scan
|
|
37
|
+
console.log('Scanning...')
|
|
38
|
+
const result = scan(input)
|
|
39
|
+
console.log(result.summary)
|
|
40
|
+
|
|
41
|
+
if (result.files.length === 0) {
|
|
42
|
+
console.log('No VS Code API usage found, nothing to convert.')
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Create output directory
|
|
47
|
+
if (fs.existsSync(output)) {
|
|
48
|
+
fs.rmSync(output, { recursive: true })
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(path.join(output, 'src'), { recursive: true })
|
|
51
|
+
|
|
52
|
+
// 3. Read original package.json
|
|
53
|
+
const origPkgPath = path.join(input, 'package.json')
|
|
54
|
+
const origPkg = fs.existsSync(origPkgPath)
|
|
55
|
+
? JSON.parse(fs.readFileSync(origPkgPath, 'utf-8'))
|
|
56
|
+
: {}
|
|
57
|
+
|
|
58
|
+
// 4. Copy source files (only .ts that have vscode usage)
|
|
59
|
+
const srcDir = path.join(input, 'src')
|
|
60
|
+
const copiedFiles: string[] = []
|
|
61
|
+
if (fs.existsSync(srcDir)) {
|
|
62
|
+
for (const file of fs.readdirSync(srcDir)) {
|
|
63
|
+
if (!file.endsWith('.ts')) continue
|
|
64
|
+
const srcPath = path.join(srcDir, file)
|
|
65
|
+
const destPath = path.join(output, 'src', file)
|
|
66
|
+
fs.copyFileSync(srcPath, destPath)
|
|
67
|
+
copiedFiles.push(file)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. Load files with ts-morph and apply transforms
|
|
72
|
+
const project = new Project()
|
|
73
|
+
const srcFiles = copiedFiles
|
|
74
|
+
.map(f => path.join(output, 'src', f))
|
|
75
|
+
.filter(f => fs.existsSync(f))
|
|
76
|
+
|
|
77
|
+
for (const fp of srcFiles) {
|
|
78
|
+
try { project.addSourceFileAtPath(fp) } catch {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const file of project.getSourceFiles()) {
|
|
82
|
+
const ctx: TransformContext = { file, project }
|
|
83
|
+
for (const t of TRANSFORMS) {
|
|
84
|
+
try {
|
|
85
|
+
t.fn(ctx)
|
|
86
|
+
if (opts.verbose) console.log(` ${t.name}: ${path.basename(file.getFilePath())}`)
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
console.warn(` ${t.name} error: ${e.message}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
file.saveSync()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 6. Mark unsupported code patterns in source files
|
|
95
|
+
for (const file of result.files) {
|
|
96
|
+
if (!file.path.startsWith('src/') || !file.path.endsWith('.ts')) continue
|
|
97
|
+
const fp = path.join(output, file.path)
|
|
98
|
+
if (!fs.existsSync(fp)) continue
|
|
99
|
+
|
|
100
|
+
let content = fs.readFileSync(fp, 'utf-8')
|
|
101
|
+
|
|
102
|
+
// Mark unsupported lines
|
|
103
|
+
for (const { pattern, label } of [
|
|
104
|
+
{ pattern: 'createTextEditorDecorationType', label: 'decoration API' },
|
|
105
|
+
{ pattern: 'createWebviewPanel', label: 'webview API' },
|
|
106
|
+
{ pattern: 'registerTreeDataProvider', label: 'tree data provider' },
|
|
107
|
+
{ pattern: 'env.openExternal', label: 'env.openExternal' },
|
|
108
|
+
]) {
|
|
109
|
+
content = content.replace(
|
|
110
|
+
new RegExp(`^(.*${pattern}.*)$`, 'gm'),
|
|
111
|
+
'// [converter] TODO: $1 — coc has no equivalent for ' + label
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Replace getWordRangeAtPosition calls with inline word boundary calculation.
|
|
116
|
+
// In coc's TextDocument there's no getWordRangeAtPosition.
|
|
117
|
+
content = content.replace(
|
|
118
|
+
/const range = document\.getWordRangeAtPosition\(([^,]+),\s*([^)]+)\)/g,
|
|
119
|
+
'const line = document.getText().split("\\n")[$1.line]; const pre = line.slice(0, $1.character); const m = pre.match($2); const range = m ? Range.create($1.line, $1.character - m[0].length, $1.line, $1.character) : undefined'
|
|
120
|
+
)
|
|
121
|
+
content = content.replace(
|
|
122
|
+
/const range = document\.getWordRangeAtPosition\(([^)]+)\)/g,
|
|
123
|
+
'const line = document.getText().split("\\n")[$1.line]; const pre = line.slice(0, $1.character); const m = pre.match(/[_a-zA-Z0-9-]+/); const range = m ? Range.create($1.line, $1.character - m[0].length, $1.line, $1.character) : undefined'
|
|
124
|
+
)
|
|
125
|
+
// Also replace usage without 'const' (already declared)
|
|
126
|
+
content = content.replace(
|
|
127
|
+
/range = document\.getWordRangeAtPosition\(([^,]+),\s*([^)]+)\)/g,
|
|
128
|
+
'(() => { const ln = document.getText().split("\\n")[$1.line]; const pr = ln.slice(0, $1.character); const mt = pr.match($2); return mt ? Range.create($1.line, $1.character - mt[0].length, $1.line, $1.character) : undefined; })()'
|
|
129
|
+
)
|
|
130
|
+
// fileName → uri (coc's DocumentUri is a string path)
|
|
131
|
+
if (content.includes('.fileName')) {
|
|
132
|
+
content = content.replace(/\.fileName/g, '.uri')
|
|
133
|
+
// uri is a full path in coc, but fileName might have had file:// prefix in vscode
|
|
134
|
+
// Add comment about potential file:// handling
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Remove unsupported import packages
|
|
138
|
+
content = content.replace(
|
|
139
|
+
/import .* from ['"]@volar\/vscode['"];?\n?/g,
|
|
140
|
+
''
|
|
141
|
+
)
|
|
142
|
+
content = content.replace(
|
|
143
|
+
/import .* from ['"]reactive-vscode['"];?\n?/g,
|
|
144
|
+
''
|
|
145
|
+
)
|
|
146
|
+
content = content.replace(
|
|
147
|
+
/import \* as lsp from ['"]@volar\/vscode\/node['"];?\n?/g,
|
|
148
|
+
''
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
fs.writeFileSync(fp, content)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 7. Detect config namespace from original properties
|
|
155
|
+
const origProps = origPkg.contributes?.configuration?.properties || {}
|
|
156
|
+
const configNamespace = Object.keys(origProps).length > 0
|
|
157
|
+
? [...new Set(Object.keys(origProps).map(k => k.split('.')[0]))][0]
|
|
158
|
+
: origPkg.name || path.basename(input)
|
|
159
|
+
|
|
160
|
+
// 8. Detect server module from original source
|
|
161
|
+
const serverModuleNames = detectServerModules(input, result)
|
|
162
|
+
if (serverModuleNames.length > 0) {
|
|
163
|
+
console.log(` detected server: ${serverModuleNames.join(', ')}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 9. Determine plugin type and entry point
|
|
167
|
+
const hasTsBridge = result.hasTsBridge
|
|
168
|
+
const hasServer = serverModuleNames.length > 0
|
|
169
|
+
const isDirectApi = !hasServer && !hasTsBridge
|
|
170
|
+
const pluginName = origPkg.name || path.basename(input)
|
|
171
|
+
const description = origPkg.description || ''
|
|
172
|
+
const activePresets = getActivePresets(hasTsBridge, result)
|
|
173
|
+
|
|
174
|
+
// Build server resolution code
|
|
175
|
+
const serverResolveCalls = serverModuleNames.map(name => {
|
|
176
|
+
return `\
|
|
177
|
+
try { serverModule = require.resolve('${escapeStr(name)}') } catch {}
|
|
178
|
+
try {
|
|
179
|
+
const _mainPath = require.resolve('${escapeStr(name)}/package.json');
|
|
180
|
+
let _dir = require('path').dirname(_mainPath);
|
|
181
|
+
while (_dir !== require('path').dirname(_dir)) {
|
|
182
|
+
const _pkgPath = require('path').join(_dir, 'package.json');
|
|
183
|
+
if (require('fs').existsSync(_pkgPath)) {
|
|
184
|
+
const _pkg = JSON.parse(require('fs').readFileSync(_pkgPath, 'utf-8'));
|
|
185
|
+
if (_pkg.bin) {
|
|
186
|
+
const _entry = typeof _pkg.bin === 'string' ? _pkg.bin : Object.values(_pkg.bin)[0];
|
|
187
|
+
serverModule = require('path').join(_dir, _entry);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
_dir = require('path').dirname(_dir);
|
|
192
|
+
}
|
|
193
|
+
} catch {}`
|
|
194
|
+
}).join('\n')
|
|
195
|
+
|
|
196
|
+
// Generate bridge code from presets
|
|
197
|
+
const bridgeCode = generateBridgeCode(activePresets)
|
|
198
|
+
const needsExtensions = activePresets.some(p => p.name === 'ts-bridge')
|
|
199
|
+
const needsCommands = activePresets.some(p => p.requiresCommand)
|
|
200
|
+
|
|
201
|
+
let fullCode = ''
|
|
202
|
+
let esbuildEntry = 'src/index.ts'
|
|
203
|
+
if (isDirectApi) {
|
|
204
|
+
esbuildEntry = 'src/extension.ts'
|
|
205
|
+
} else {
|
|
206
|
+
fullCode = `\
|
|
207
|
+
import {
|
|
208
|
+
LanguageClient,
|
|
209
|
+
TransportKind,
|
|
210
|
+
workspace,
|
|
211
|
+
window,
|
|
212
|
+
commands,
|
|
213
|
+
services as cocServices,
|
|
214
|
+
ExtensionContext${needsExtensions ? ',\n extensions' : ''},
|
|
215
|
+
} from 'coc.nvim'
|
|
216
|
+
import * as path from 'path'
|
|
217
|
+
import * as fs from 'fs'
|
|
218
|
+
|
|
219
|
+
export async function activate(context: ExtensionContext): Promise<void> {
|
|
220
|
+
try {
|
|
221
|
+
${needsExtensions ? `\
|
|
222
|
+
const tsExt = extensions.all.find(e => e.id === 'coc-tsserver')
|
|
223
|
+
if (tsExt && !tsExt.isActive) { await tsExt.activate() }
|
|
224
|
+
const tsSvc = cocServices.getService('tsserver')
|
|
225
|
+
if (tsSvc) { await tsSvc.start() }
|
|
226
|
+
` : ''}\
|
|
227
|
+
const config = workspace.getConfiguration('${escapeStr(configNamespace)}')
|
|
228
|
+
let serverModule = config.get<string>('server.path', '')
|
|
229
|
+
if (serverModule) {
|
|
230
|
+
serverModule = path.isAbsolute(serverModule) ? serverModule : path.join(workspace.root, serverModule)
|
|
231
|
+
}
|
|
232
|
+
if (!serverModule || !fs.existsSync(serverModule)) {
|
|
233
|
+
${serverResolveCalls}
|
|
234
|
+
}
|
|
235
|
+
if (!serverModule) { window.showErrorMessage('Cannot find language server.'); return }
|
|
236
|
+
|
|
237
|
+
const client = new LanguageClient(
|
|
238
|
+
'${escapeStr(pluginName)}',
|
|
239
|
+
'${escapeStr(description || pluginName)}',
|
|
240
|
+
{ module: serverModule, transport: TransportKind.ipc },
|
|
241
|
+
{
|
|
242
|
+
documentSelector: [{ language: '${escapeStr(configNamespace)}', scheme: 'file' }],
|
|
243
|
+
outputChannelName: '${escapeStr(description || pluginName)}',
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
context.subscriptions.push({ dispose: () => client.stop() })
|
|
247
|
+
context.subscriptions.push(cocServices.registerLanguageClient(client))
|
|
248
|
+
client.start()
|
|
249
|
+
|
|
250
|
+
${bridgeCode ? bridgeCode + '\n\n' : ''}\
|
|
251
|
+
// Restart command
|
|
252
|
+
context.subscriptions.push(
|
|
253
|
+
commands.registerCommand('${escapeStr(pluginName)}.restartServer', async () => {
|
|
254
|
+
await client.stop()
|
|
255
|
+
client.start()
|
|
256
|
+
}),
|
|
257
|
+
)
|
|
258
|
+
} catch (e: any) {
|
|
259
|
+
window.showErrorMessage('${escapeStr(pluginName)} error: ' + (e.message || String(e)))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`
|
|
263
|
+
}
|
|
264
|
+
if (fullCode) {
|
|
265
|
+
fs.writeFileSync(path.join(output, 'src', 'index.ts'), fullCode)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 10. Detect dependencies from original package.json + preset extras
|
|
269
|
+
const serverDeps: Record<string, string> = {}
|
|
270
|
+
// Add preset extra deps (e.g. typescript for ts-bridge)
|
|
271
|
+
for (const preset of activePresets) {
|
|
272
|
+
for (const dep of (preset.extraDeps || [])) {
|
|
273
|
+
if (!serverDeps[dep]) serverDeps[dep] = '*'
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (isDirectApi) {
|
|
277
|
+
// Non-LSP: keep all original deps (including devDeps, since they may be imported at runtime)
|
|
278
|
+
const allDeps = { ...origPkg.dependencies, ...origPkg.devDependencies }
|
|
279
|
+
for (const [dep, ver] of Object.entries(allDeps)) {
|
|
280
|
+
if (ver.startsWith('workspace:')) continue
|
|
281
|
+
if (dep.startsWith('@types/')) continue // skip type packages
|
|
282
|
+
if (['typescript', 'mocha', 'c8', 'prettier', 'rollup', '@vscode/'].some(p => dep.startsWith(p))) continue
|
|
283
|
+
if (['tslib'].includes(dep)) continue
|
|
284
|
+
serverDeps[dep] = ver as string
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
// LSP-based: only include server-related dependencies
|
|
288
|
+
const knownPatterns = ['language-server', 'language-server/node', '-server', 'languageserver', '/lsp']
|
|
289
|
+
for (const [dep, ver] of Object.entries(origPkg.dependencies || {})) {
|
|
290
|
+
if (ver.startsWith('workspace:')) {
|
|
291
|
+
// Workspace deps: if it matches a known server pattern, add with wildcard version
|
|
292
|
+
if (knownPatterns.some(p => dep.includes(p))) {
|
|
293
|
+
serverDeps[dep] = '*'
|
|
294
|
+
}
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
if (knownPatterns.some(p => dep.includes(p))) {
|
|
298
|
+
serverDeps[dep] = ver as string
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const name of serverModuleNames) {
|
|
302
|
+
const pkgName = name.startsWith('@') ? name.split('/').slice(0, 2).join('/') : name.split('/')[0]
|
|
303
|
+
if (!serverDeps[pkgName]) serverDeps[pkgName] = '*'
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const activationEvents = Array.isArray(origPkg.activationEvents) ? origPkg.activationEvents : []
|
|
307
|
+
const activationEvent = activationEvents.find((e: string) => e.startsWith('onLanguage:'))
|
|
308
|
+
|| (activationEvents.includes('*') ? '*' : undefined)
|
|
309
|
+
|| (activationEvents.includes('onStartupFinished') ? '*' : undefined)
|
|
310
|
+
|| `onLanguage:${configNamespace}`
|
|
311
|
+
|| 'onLanguage'
|
|
312
|
+
const pkg = {
|
|
313
|
+
name: pluginName.startsWith('coc-') ? pluginName : `coc-${pluginName}`,
|
|
314
|
+
version: origPkg.version || '0.1.0',
|
|
315
|
+
dependencies: {
|
|
316
|
+
...serverDeps,
|
|
317
|
+
},
|
|
318
|
+
devDependencies: {
|
|
319
|
+
esbuild: '^0.28.0',
|
|
320
|
+
},
|
|
321
|
+
description: description,
|
|
322
|
+
main: 'lib/index.js',
|
|
323
|
+
engines: { coc: '^0.0.82' },
|
|
324
|
+
activationEvents: [activationEvent],
|
|
325
|
+
contributes: {
|
|
326
|
+
configuration: origPkg.contributes?.configuration ? {
|
|
327
|
+
type: 'object',
|
|
328
|
+
title: origPkg.contributes.configuration.title || pluginName,
|
|
329
|
+
properties: origPkg.contributes.configuration.properties || {},
|
|
330
|
+
} : undefined,
|
|
331
|
+
commands: origPkg.contributes?.commands?.map((c: any) => ({
|
|
332
|
+
command: c.command,
|
|
333
|
+
title: c.title,
|
|
334
|
+
})) || undefined,
|
|
335
|
+
...(hasTsBridge ? {
|
|
336
|
+
typescriptServerPlugins: (origPkg.contributes?.typescriptServerPlugins?.length
|
|
337
|
+
? origPkg.contributes?.typescriptServerPlugins
|
|
338
|
+
: scanTypeScriptPlugins(input, result).map(p => ({
|
|
339
|
+
...p,
|
|
340
|
+
languages: p.languages.length ? p.languages : [configNamespace],
|
|
341
|
+
}))
|
|
342
|
+
) || [],
|
|
343
|
+
} : {}),
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Clean null fields
|
|
348
|
+
for (const key of ['configuration', 'commands'] as const) {
|
|
349
|
+
if (!pkg.contributes[key]) delete pkg.contributes[key]
|
|
350
|
+
}
|
|
351
|
+
if (Object.keys(pkg.contributes).length === 0) delete pkg.contributes
|
|
352
|
+
|
|
353
|
+
fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
354
|
+
|
|
355
|
+
// 10. Generate esbuild config
|
|
356
|
+
const externalMods = ['coc.nvim', ...serverModuleNames]
|
|
357
|
+
.map(n => n.startsWith('@') ? n.split('/').slice(0, 2).join('/') : n.split('/')[0])
|
|
358
|
+
.filter((v, i, a) => v && a.indexOf(v) === i)
|
|
359
|
+
const esbuildConfig = `\
|
|
360
|
+
import * as esbuild from 'esbuild'
|
|
361
|
+
|
|
362
|
+
const options = {
|
|
363
|
+
entryPoints: ['${escapeStr(esbuildEntry)}'],
|
|
364
|
+
bundle: true,
|
|
365
|
+
minify: false,
|
|
366
|
+
mainFields: ['module', 'main'],
|
|
367
|
+
external: [${externalMods.map(m => `'${escapeStr(m)}'`).join(', ')}],
|
|
368
|
+
platform: 'node',
|
|
369
|
+
target: 'node18',
|
|
370
|
+
outfile: 'lib/index.js',
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const result = await esbuild.build(options)
|
|
374
|
+
if (result.errors.length) {
|
|
375
|
+
console.error(result.errors)
|
|
376
|
+
process.exit(1)
|
|
377
|
+
}
|
|
378
|
+
`
|
|
379
|
+
fs.writeFileSync(path.join(output, 'esbuild.mjs'), esbuildConfig)
|
|
380
|
+
|
|
381
|
+
// 11. Generate report
|
|
382
|
+
console.log('\n=== Conversion Report ===')
|
|
383
|
+
console.log(` Plugin: ${pkg.name}`)
|
|
384
|
+
console.log(` Type: ${hasTsBridge ? 'ts-bridge' : 'pure-lsp'}`)
|
|
385
|
+
console.log(` Source files: ${copiedFiles.length + (hasTsBridge ? 0 : 0)}`)
|
|
386
|
+
|
|
387
|
+
if (hasTsBridge) {
|
|
388
|
+
console.log('\n ⚠ TS-bridge mode:')
|
|
389
|
+
console.log(' - Generated src/index.ts with tsserver/request bridge')
|
|
390
|
+
console.log(' - Package.json includes typescriptServerPlugins')
|
|
391
|
+
console.log(' - Requires modified coc-tsserver (PR #493)')
|
|
392
|
+
console.log(' - Install: cd ~/.config/coc/extensions && npm install ChuYanLon/coc-tsserver')
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log(`\n ${output}/`)
|
|
396
|
+
console.log(' ├── src/index.ts ← main entry')
|
|
397
|
+
console.log(' ├── package.json ← coc plugin config')
|
|
398
|
+
console.log(' ├── esbuild.mjs ← build config')
|
|
399
|
+
console.log(' └── src/*.ts ← converted source')
|
|
400
|
+
console.log('\n Next:')
|
|
401
|
+
console.log(` cd ${output}`)
|
|
402
|
+
console.log(' npm install')
|
|
403
|
+
console.log(' npm run build')
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Scan original source for TypeScript plugin names used by the extension.
|
|
408
|
+
* Checks: explicit imports, require() calls, package.json dependencies.
|
|
409
|
+
*/
|
|
410
|
+
function walkTsFiles(dir: string): string[] {
|
|
411
|
+
const files: string[] = []
|
|
412
|
+
try {
|
|
413
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
414
|
+
const full = path.join(dir, entry)
|
|
415
|
+
const stat = fs.statSync(full)
|
|
416
|
+
if (stat.isDirectory()) {
|
|
417
|
+
files.push(...walkTsFiles(full))
|
|
418
|
+
} else if (entry.endsWith('.ts') || entry.endsWith('.d.ts')) {
|
|
419
|
+
files.push(full)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch {}
|
|
423
|
+
return files
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function scanTypeScriptPlugins(input: string, _result: any): Array<{ name: string; languages: string[]; enableForWorkspaceTypeScriptVersions: boolean }> {
|
|
427
|
+
const plugins: Array<{ name: string; languages: string[]; enableForWorkspaceTypeScriptVersions: boolean }> = []
|
|
428
|
+
const scanDir = path.join(input, 'src')
|
|
429
|
+
if (!fs.existsSync(scanDir)) return plugins
|
|
430
|
+
|
|
431
|
+
// Collect all .ts file content (recursive)
|
|
432
|
+
const allContent = walkTsFiles(scanDir).map(f => fs.readFileSync(f, 'utf-8'))
|
|
433
|
+
const content = allContent.join('\n')
|
|
434
|
+
|
|
435
|
+
// Check for plugin names in require() / import statements
|
|
436
|
+
// Patterns like: @vue/typescript-plugin, @angular/language-service, etc.
|
|
437
|
+
const pluginRefs = content.matchAll(/['"](@[^'"]+\/(?:typescript-plugin|language-service)[^'"]*)['"]|['"](\w+(?:-typescript-plugin|-language-service))['"]/g)
|
|
438
|
+
for (const m of pluginRefs) {
|
|
439
|
+
const name = m[1] || m[2]
|
|
440
|
+
if (name && !plugins.some(p => p.name === name)) {
|
|
441
|
+
plugins.push({ name, languages: [], enableForWorkspaceTypeScriptVersions: true })
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check package.json for typescript plugin dependencies
|
|
446
|
+
const pkgPath = path.join(input, 'package.json')
|
|
447
|
+
if (fs.existsSync(pkgPath)) {
|
|
448
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
449
|
+
for (const dep of Object.keys(pkg.dependencies || {})) {
|
|
450
|
+
if (dep.includes('typescript-plugin') || dep.includes('language-service')) {
|
|
451
|
+
if (!plugins.some(p => p.name === dep)) {
|
|
452
|
+
plugins.push({ name: dep, languages: [], enableForWorkspaceTypeScriptVersions: true })
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return plugins
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Escape string for template literal */
|
|
462
|
+
function escapeStr(s: string): string {
|
|
463
|
+
return s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Clean up server path: relative → bare package name, subpath → base */
|
|
467
|
+
function sanitizeServerPath(p: string): string | null {
|
|
468
|
+
if (p.startsWith('../node_modules/')) p = p.replace(/^\.\.\/node_modules\//, '')
|
|
469
|
+
if (p.startsWith('./')) return null
|
|
470
|
+
if (p.startsWith('../')) return null
|
|
471
|
+
if (p.startsWith('@')) {
|
|
472
|
+
const parts = p.split('/')
|
|
473
|
+
if (parts.length > 2) return parts.slice(0, 2).join('/')
|
|
474
|
+
} else {
|
|
475
|
+
const parts = p.split('/')
|
|
476
|
+
if (parts.length > 1) return parts[0]
|
|
477
|
+
}
|
|
478
|
+
return p
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Detect language server module names from original source code.
|
|
483
|
+
* Returns an array of `require.resolve` argument strings (bare module names).
|
|
484
|
+
*/
|
|
485
|
+
function detectServerModules(input: string, _result: any): string[] {
|
|
486
|
+
const seen = new Set<string>()
|
|
487
|
+
const serverModules: string[] = []
|
|
488
|
+
const scanDir = path.join(input, 'src')
|
|
489
|
+
if (fs.existsSync(scanDir)) {
|
|
490
|
+
for (const file of walkTsFiles(scanDir)) {
|
|
491
|
+
const content = fs.readFileSync(file, 'utf-8')
|
|
492
|
+
for (const re of [/(?:require\s*(?:\.\s*resolve)?\s*\(|from\s+)['"]([^'"]+(?:language-server|server|Server|lsp)[^'"]*)['"]\s*\)?/g]) {
|
|
493
|
+
for (const m of content.matchAll(re)) {
|
|
494
|
+
const name = sanitizeServerPath(m[1])
|
|
495
|
+
if (name && !seen.has(name) && !name.includes('${')) { seen.add(name); serverModules.push(name) }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return serverModules
|
|
501
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge preset definitions.
|
|
3
|
+
* Each preset describes a notification → handler → response pattern
|
|
4
|
+
* that the converter uses to generate bridge code.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface BridgePreset {
|
|
8
|
+
/** Used to match presets in registry */
|
|
9
|
+
name: string
|
|
10
|
+
/** Notification to listen for from the language server */
|
|
11
|
+
notification: string
|
|
12
|
+
/** Notification to send back as response */
|
|
13
|
+
responseNotification?: string
|
|
14
|
+
/** Generated bridge code (template with placeholders) */
|
|
15
|
+
code: string
|
|
16
|
+
/** Required coc command (will be mentioned in report) */
|
|
17
|
+
requiresCommand?: string
|
|
18
|
+
/** Extra package.json contributions */
|
|
19
|
+
packageContributes?: Record<string, any>
|
|
20
|
+
/** Extra dependencies */
|
|
21
|
+
extraDeps?: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PRESETS: Record<string, BridgePreset> = {
|
|
25
|
+
'ts-bridge': {
|
|
26
|
+
name: 'ts-bridge',
|
|
27
|
+
notification: 'tsserver/request',
|
|
28
|
+
responseNotification: 'tsserver/response',
|
|
29
|
+
requiresCommand: 'typescript.tsserverRequest',
|
|
30
|
+
extraDeps: ['typescript'],
|
|
31
|
+
code: `\
|
|
32
|
+
// tsserver bridge: forward TypeScript requests from language server
|
|
33
|
+
client.onNotification('tsserver/request', async ([seq, command, args]: [number, string, any]) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await commands.executeCommand<any>('typescript.tsserverRequest', command, args, { isAsync: true, lowPriority: true })
|
|
36
|
+
client.sendNotification('tsserver/response', [seq, result?.body])
|
|
37
|
+
} catch { client.sendNotification('tsserver/response', [seq, undefined]) }
|
|
38
|
+
})`,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getPreset(name: string): BridgePreset | undefined {
|
|
43
|
+
return PRESETS[name]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getActivePresets(hasTsBridge: boolean, result: any): BridgePreset[] {
|
|
47
|
+
const presets: BridgePreset[] = []
|
|
48
|
+
if (hasTsBridge) {
|
|
49
|
+
const preset = getPreset('ts-bridge')
|
|
50
|
+
if (preset) presets.push(preset)
|
|
51
|
+
}
|
|
52
|
+
return presets
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function generateBridgeCode(presets: BridgePreset[]): string {
|
|
56
|
+
return presets.map(p => p.code).join('\n\n')
|
|
57
|
+
}
|