coc-vscode-loader 1.1.9 → 1.2.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/src/cli.ts +33 -2
- package/converter/src/convert.ts +294 -361
- package/converter/src/scanner.ts +5 -89
- package/converter/src/steps/bridge.ts +142 -0
- package/converter/src/steps/index.ts +29 -0
- package/converter/src/steps/language-client.ts +150 -0
- package/converter/src/steps/mark-unsupported.ts +81 -0
- package/converter/src/steps/source.ts +159 -0
- package/converter/src/transforms/class-to-factory.ts +1 -2
- package/converter/src/transforms/enum-offset.ts +0 -17
- package/converter/src/transforms/provider-register.ts +7 -1
- package/converter/src/transforms/strip-volar.ts +29 -0
- package/converter/src/types.ts +117 -0
- package/lib/index.js +40 -58
- package/package.json +1 -1
package/converter/src/convert.ts
CHANGED
|
@@ -2,40 +2,70 @@ import { Project } from 'ts-morph'
|
|
|
2
2
|
import * as fs from 'fs'
|
|
3
3
|
import * as path from 'path'
|
|
4
4
|
import { scan } from './scanner.js'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
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 {
|
|
5
|
+
import {
|
|
6
|
+
ConvertStep,
|
|
7
|
+
isLanguageClientStep,
|
|
8
|
+
StepResult,
|
|
9
|
+
} from './types.js'
|
|
10
|
+
import { executeStep, getRegisteredStepTypes } from './steps/index.js'
|
|
11
|
+
|
|
12
|
+
export interface ConvertOptions {
|
|
22
13
|
input: string
|
|
23
14
|
output: string
|
|
24
|
-
|
|
15
|
+
convert: ConvertStep[]
|
|
16
|
+
presets?: Record<string, any>
|
|
25
17
|
verbose?: boolean
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
interface MergedResult {
|
|
21
|
+
generatedFiles: Array<{ path: string; content: string }>
|
|
22
|
+
entryPoint?: string
|
|
23
|
+
keepDeps: Record<string, string>
|
|
24
|
+
activationEvents: string[]
|
|
25
|
+
serverBinary?: {
|
|
26
|
+
repo: string
|
|
27
|
+
asset: string
|
|
28
|
+
binaryPath?: string
|
|
29
|
+
args?: string[]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function convert(opts: ConvertOptions): Promise<void> {
|
|
34
|
+
const { input, output, convert: steps, verbose } = opts
|
|
30
35
|
|
|
31
36
|
if (!fs.existsSync(input)) {
|
|
32
37
|
console.error(`input not found: ${input}`)
|
|
33
38
|
process.exit(1)
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
if (!steps || steps.length === 0) {
|
|
42
|
+
console.error('no convert steps provided (use --convert)')
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. Validate
|
|
47
|
+
const knownTypes = getRegisteredStepTypes()
|
|
48
|
+
for (const s of steps) {
|
|
49
|
+
if (!knownTypes.includes(s.type)) {
|
|
50
|
+
console.error(`unknown step type: "${s.type}". Available: ${knownTypes.join(', ')}`)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
if (isLanguageClientStep(s) && s.server.kind === 'binary') {
|
|
54
|
+
if (s.server.binary?.asset) {
|
|
55
|
+
const vars = ['{{version}}', '{{platform}}', '{{arch}}', '{{rust-target}}']
|
|
56
|
+
for (const v of vars) {
|
|
57
|
+
if (s.server.binary.asset.includes(v)) break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Scan for summary (try src/ first, fall back to input root)
|
|
37
64
|
console.log('Scanning...')
|
|
38
|
-
|
|
65
|
+
let result = scan(path.join(input, 'src'))
|
|
66
|
+
if (result.files.length === 0) {
|
|
67
|
+
result = scan(input)
|
|
68
|
+
}
|
|
39
69
|
console.log(result.summary)
|
|
40
70
|
|
|
41
71
|
if (result.files.length === 0) {
|
|
@@ -43,285 +73,222 @@ export async function convert(opts: Options): Promise<void> {
|
|
|
43
73
|
return
|
|
44
74
|
}
|
|
45
75
|
|
|
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
76
|
// 3. Read original package.json
|
|
53
77
|
const origPkgPath = path.join(input, 'package.json')
|
|
54
78
|
const origPkg = fs.existsSync(origPkgPath)
|
|
55
79
|
? JSON.parse(fs.readFileSync(origPkgPath, 'utf-8'))
|
|
56
80
|
: {}
|
|
57
81
|
|
|
58
|
-
// 4.
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
}
|
|
82
|
+
// 4. Create output directory
|
|
83
|
+
if (fs.existsSync(output)) {
|
|
84
|
+
fs.rmSync(output, { recursive: true })
|
|
69
85
|
}
|
|
86
|
+
fs.mkdirSync(path.join(output, 'src'), { recursive: true })
|
|
70
87
|
|
|
71
|
-
// 5.
|
|
88
|
+
// 5. Project for transforms
|
|
72
89
|
const project = new Project()
|
|
73
|
-
const srcFiles = copiedFiles
|
|
74
|
-
.map(f => path.join(output, 'src', f))
|
|
75
|
-
.filter(f => fs.existsSync(f))
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
// 6. Execute steps
|
|
92
|
+
const ctx = { input, output, project, origPkg, verbose, presets: opts.presets }
|
|
93
|
+
const merged: MergedResult = {
|
|
94
|
+
generatedFiles: [],
|
|
95
|
+
entryPoint: undefined,
|
|
96
|
+
keepDeps: {},
|
|
97
|
+
activationEvents: [],
|
|
98
|
+
serverBinary: undefined,
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
const stepResults: StepResult[] = []
|
|
102
|
+
for (const step of steps) {
|
|
103
|
+
const stepVerbose = verbose || (step as any).verbose
|
|
104
|
+
if (stepVerbose) console.log(` step: ${step.type}`)
|
|
105
|
+
const stepCtx = { ...ctx, verbose: stepVerbose }
|
|
106
|
+
const stepResult: StepResult = executeStep(stepCtx, step)
|
|
107
|
+
stepResults.push(stepResult)
|
|
108
|
+
|
|
109
|
+
// Merge
|
|
110
|
+
merged.generatedFiles.push(...stepResult.generatedFiles)
|
|
111
|
+
merged.entryPoint = stepResult.entryPoint || merged.entryPoint
|
|
112
|
+
Object.assign(merged.keepDeps, stepResult.keepDeps)
|
|
113
|
+
merged.activationEvents.push(...stepResult.activationEvents)
|
|
114
|
+
if (stepResult.serverBinary) {
|
|
115
|
+
merged.serverBinary = stepResult.serverBinary
|
|
90
116
|
}
|
|
91
|
-
file.saveSync()
|
|
92
117
|
}
|
|
93
118
|
|
|
94
|
-
//
|
|
95
|
-
for (const
|
|
96
|
-
if (!
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
// 8. Apply code injections from steps (generic, no hardcoded plugin knowledge)
|
|
120
|
+
for (const stepResult of stepResults) {
|
|
121
|
+
if (!stepResult.codeInjections) continue
|
|
122
|
+
for (const inj of stepResult.codeInjections) {
|
|
123
|
+
const targetFile = merged.generatedFiles.find(f => f.path === inj.target)
|
|
124
|
+
if (!targetFile) continue
|
|
125
|
+
|
|
126
|
+
let content = targetFile.content
|
|
127
|
+
|
|
128
|
+
// Add import codes
|
|
129
|
+
if (inj.importCode) {
|
|
130
|
+
if (inj.importCode.endsWith(',')) {
|
|
131
|
+
// Import addition (adds a line to existing import block)
|
|
132
|
+
const marker = inj.insertBefore || "} from 'coc.nvim'"
|
|
133
|
+
if (!content.includes(inj.importCode.trim())) {
|
|
134
|
+
content = content.replace(marker, `${inj.importCode}\n${marker}`)
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Full import line
|
|
138
|
+
if (!content.includes(inj.importCode)) {
|
|
139
|
+
content = inj.importCode + '\n' + content
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
114
143
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
}
|
|
144
|
+
// Insert code before a marker
|
|
145
|
+
if (inj.insertBefore && inj.code) {
|
|
146
|
+
if (!content.includes(inj.code.trim())) {
|
|
147
|
+
content = content.replace(inj.insertBefore, inj.code + inj.insertBefore)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
136
150
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
}
|
|
151
|
+
// Insert code after a marker
|
|
152
|
+
if (inj.insertAfter && inj.code) {
|
|
153
|
+
if (!content.includes(inj.code.trim())) {
|
|
154
|
+
content = content.replace(inj.insertAfter, inj.insertAfter + inj.code)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
: origPkg.name || path.basename(input)
|
|
158
|
+
targetFile.content = content
|
|
159
|
+
if (verbose) console.log(` injected into ${inj.target}: ${(inj.importCode || inj.code).substring(0, 60)}...`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
159
162
|
|
|
160
|
-
//
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
// 9. Write generated files
|
|
164
|
+
for (const gf of merged.generatedFiles) {
|
|
165
|
+
const fp = path.join(output, gf.path)
|
|
166
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true })
|
|
167
|
+
fs.writeFileSync(fp, gf.content)
|
|
168
|
+
if (verbose) console.log(` wrote: ${gf.path}`)
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
171
|
+
// 10. Apply text replacements to all source files (getWordRangeAtPosition, .fileName)
|
|
172
|
+
{
|
|
173
|
+
const outputSrc = path.join(output, 'src')
|
|
174
|
+
if (fs.existsSync(outputSrc)) {
|
|
175
|
+
for (const f of fs.readdirSync(outputSrc, { recursive: true })) {
|
|
176
|
+
const fp = typeof f === 'string' ? path.join(outputSrc, f) : f
|
|
177
|
+
if (!fp.endsWith('.ts')) continue
|
|
178
|
+
let content = fs.readFileSync(fp, 'utf-8')
|
|
179
|
+
let changed = false
|
|
180
|
+
|
|
181
|
+
if (content.includes('document.getWordRangeAtPosition')) {
|
|
182
|
+
// const range = document.getWordRangeAtPosition(pos, regex)
|
|
183
|
+
content = content.replace(
|
|
184
|
+
/const range = document\.getWordRangeAtPosition\(([^,]+),\s*([^)]+)\)/g,
|
|
185
|
+
'const line = document.getText().split("\\n")[$1.line]; const pre = line.slice(0, $1.character); const m = pre.match($2); const range = m ? { start: { line: $1.line, character: $1.character - m[0].length }, end: { line: $1.line, character: $1.character } } : undefined'
|
|
186
|
+
)
|
|
187
|
+
// const range = document.getWordRangeAtPosition(pos)
|
|
188
|
+
content = content.replace(
|
|
189
|
+
/const range = document\.getWordRangeAtPosition\(([^)]+)\)/g,
|
|
190
|
+
'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 ? { start: { line: $1.line, character: $1.character - m[0].length }, end: { line: $1.line, character: $1.character } } : undefined'
|
|
191
|
+
)
|
|
192
|
+
// range = document.getWordRangeAtPosition(pos, regex) (without const)
|
|
193
|
+
content = content.replace(
|
|
194
|
+
/range = document\.getWordRangeAtPosition\(([^,]+),\s*([^)]+)\)/g,
|
|
195
|
+
'(() => { const ln = document.getText().split("\\n")[$1.line]; const pr = ln.slice(0, $1.character); const mt = pr.match($2); return mt ? { start: { line: $1.line, character: $1.character - mt[0].length }, end: { line: $1.line, character: $1.character } } : undefined; })()'
|
|
196
|
+
)
|
|
197
|
+
changed = true
|
|
190
198
|
}
|
|
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
199
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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'
|
|
200
|
+
if (content.includes('.fileName') || content.includes('.uri.fsPath')) {
|
|
201
|
+
// .fileName → .uri (coc's TextDocument#uri returns a string path, not a URI object)
|
|
202
|
+
content = content.replace(/(document|this\.document|textDocument|scope)\.fileName/g, '$1.uri')
|
|
203
|
+
// .uri.fsPath → .uri (coc's uri is already a string path, not a URI object)
|
|
204
|
+
content = content.replace(/\.uri\.fsPath/g, '.uri')
|
|
205
|
+
changed = true
|
|
206
|
+
}
|
|
218
207
|
|
|
219
|
-
|
|
220
|
-
|
|
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}
|
|
208
|
+
if (changed) fs.writeFileSync(fp, content)
|
|
209
|
+
}
|
|
234
210
|
}
|
|
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
211
|
}
|
|
264
|
-
|
|
265
|
-
|
|
212
|
+
|
|
213
|
+
// 12. Determine entry point
|
|
214
|
+
const hasLanguageClient = steps.some(s => s.type === 'language-client')
|
|
215
|
+
|
|
216
|
+
// If language-client step exists, it generates src/index.ts as entry.
|
|
217
|
+
// If source step provides an entry point, the generated index.ts imports it.
|
|
218
|
+
// For source-only plugins, entry is the source entry.
|
|
219
|
+
let esbuildEntry = 'src/index.ts'
|
|
220
|
+
if (!hasLanguageClient) {
|
|
221
|
+
esbuildEntry = merged.entryPoint
|
|
222
|
+
? `src/${merged.entryPoint.replace(/^src\//, '')}`
|
|
223
|
+
: 'src/extension.ts'
|
|
266
224
|
}
|
|
267
225
|
|
|
268
|
-
//
|
|
226
|
+
// 13. Generate package.json
|
|
227
|
+
const pluginName = origPkg.name || path.basename(input)
|
|
228
|
+
|
|
229
|
+
// Detect config namespace (from original contributes.properties or name)
|
|
230
|
+
const origProps = origPkg.contributes?.configuration?.properties || {}
|
|
231
|
+
const configNamespace = Object.keys(origProps).length > 0
|
|
232
|
+
? [...new Set(Object.keys(origProps).map((k: string) => k.split('.')[0]))][0]
|
|
233
|
+
: pluginName
|
|
234
|
+
|
|
235
|
+
// For bridge mode: detect TypeScript plugins from source + origPkg
|
|
236
|
+
const hasBridge = steps.some(s => s.type === 'bridge')
|
|
237
|
+
const tsPlugins = hasBridge ? scanTypeScriptPlugins(input, origPkg) : []
|
|
238
|
+
const description = origPkg.description || ''
|
|
239
|
+
|
|
240
|
+
const activationEvents = merged.activationEvents.length > 0
|
|
241
|
+
? merged.activationEvents
|
|
242
|
+
: ['onLanguage']
|
|
243
|
+
|
|
244
|
+
// Gather server deps: for module-kind language-client steps
|
|
269
245
|
const serverDeps: Record<string, string> = {}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
246
|
+
for (const s of steps) {
|
|
247
|
+
if (isLanguageClientStep(s) && s.server.kind === 'module') {
|
|
248
|
+
const pkg = s.server.package
|
|
249
|
+
const ver = origPkg.dependencies?.[pkg] || origPkg.devDependencies?.[pkg]
|
|
250
|
+
if (ver && ver.startsWith('workspace:')) {
|
|
251
|
+
serverDeps[pkg] = '*' // monorepo workspace → wildcard for npm
|
|
252
|
+
} else {
|
|
253
|
+
serverDeps[pkg] = ver || '*'
|
|
299
254
|
}
|
|
300
255
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Collect all runtime deps from origPkg (deps + devDeps, filtered)
|
|
259
|
+
const origDeps: Record<string, string> = {}
|
|
260
|
+
{
|
|
261
|
+
const allPkgDeps = { ...origPkg.dependencies, ...origPkg.devDependencies }
|
|
262
|
+
for (const [dep, ver] of Object.entries(allPkgDeps)) {
|
|
263
|
+
const v = ver as string
|
|
264
|
+
if (v.startsWith('workspace:')) continue
|
|
265
|
+
if (dep.startsWith('@types/')) continue
|
|
266
|
+
if (['typescript', 'mocha', 'c8', 'prettier', 'rollup', 'esbuild', '@vscode/'].some(p => dep.startsWith(p))) continue
|
|
267
|
+
origDeps[dep] = v
|
|
304
268
|
}
|
|
305
269
|
}
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
270
|
+
|
|
271
|
+
const deps: Record<string, string> = {
|
|
272
|
+
...serverDeps,
|
|
273
|
+
...origDeps,
|
|
274
|
+
...merged.keepDeps,
|
|
275
|
+
}
|
|
276
|
+
// Fix workspace: protocol deps (monorepo, invalid in npm) → wildcard
|
|
277
|
+
for (const k of Object.keys(deps)) {
|
|
278
|
+
if (typeof deps[k] === 'string' && deps[k].startsWith('workspace:')) deps[k] = '*'
|
|
279
|
+
}
|
|
280
|
+
|
|
312
281
|
const pkg = {
|
|
313
282
|
name: pluginName.startsWith('coc-') ? pluginName : `coc-${pluginName}`,
|
|
314
283
|
version: origPkg.version || '0.1.0',
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
},
|
|
284
|
+
description,
|
|
285
|
+
main: 'lib/index.js',
|
|
286
|
+
engines: { coc: '^0.0.82' },
|
|
287
|
+
activationEvents,
|
|
288
|
+
dependencies: deps,
|
|
318
289
|
devDependencies: {
|
|
319
290
|
esbuild: '^0.28.0',
|
|
320
291
|
},
|
|
321
|
-
description: description,
|
|
322
|
-
main: 'lib/index.js',
|
|
323
|
-
engines: { coc: '^0.0.82' },
|
|
324
|
-
activationEvents: [activationEvent],
|
|
325
292
|
contributes: {
|
|
326
293
|
configuration: origPkg.contributes?.configuration ? {
|
|
327
294
|
type: 'object',
|
|
@@ -332,39 +299,42 @@ ${bridgeCode ? bridgeCode + '\n\n' : ''}\
|
|
|
332
299
|
command: c.command,
|
|
333
300
|
title: c.title,
|
|
334
301
|
})) || undefined,
|
|
335
|
-
...(
|
|
336
|
-
typescriptServerPlugins: (
|
|
337
|
-
|
|
338
|
-
:
|
|
339
|
-
|
|
340
|
-
languages: p.languages.length ? p.languages : [configNamespace],
|
|
341
|
-
}))
|
|
342
|
-
) || [],
|
|
302
|
+
...(tsPlugins.length > 0 ? {
|
|
303
|
+
typescriptServerPlugins: tsPlugins.map(p => ({
|
|
304
|
+
...p,
|
|
305
|
+
languages: p.languages.length ? p.languages : [configNamespace],
|
|
306
|
+
})),
|
|
343
307
|
} : {}),
|
|
344
308
|
},
|
|
345
309
|
}
|
|
346
310
|
|
|
347
311
|
// Clean null fields
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (Object.keys(pkg.contributes).length === 0) delete pkg.contributes
|
|
312
|
+
if (!pkg.contributes?.configuration) delete (pkg.contributes as any).configuration
|
|
313
|
+
if (!pkg.contributes?.commands) delete (pkg.contributes as any).commands
|
|
314
|
+
if (Object.keys(pkg.contributes).length === 0) delete (pkg as any).contributes
|
|
352
315
|
|
|
353
316
|
fs.writeFileSync(path.join(output, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
354
317
|
|
|
355
|
-
//
|
|
356
|
-
const externalMods = [
|
|
357
|
-
|
|
358
|
-
.
|
|
318
|
+
// 14. Generate esbuild config — externalize all runtime deps
|
|
319
|
+
const externalMods = [
|
|
320
|
+
'coc.nvim',
|
|
321
|
+
...Object.keys(serverDeps),
|
|
322
|
+
...Object.keys(merged.keepDeps),
|
|
323
|
+
...Object.keys(origDeps),
|
|
324
|
+
].flatMap(n => {
|
|
325
|
+
if (n.startsWith('@')) return n.split('/').slice(0, 2).join('/')
|
|
326
|
+
return n.split('/')[0]
|
|
327
|
+
}).filter((v, i, a) => v && a.indexOf(v) === i)
|
|
328
|
+
|
|
359
329
|
const esbuildConfig = `\
|
|
360
330
|
import * as esbuild from 'esbuild'
|
|
361
331
|
|
|
362
332
|
const options = {
|
|
363
|
-
entryPoints: ['${
|
|
333
|
+
entryPoints: ['${esbuildEntry}'],
|
|
364
334
|
bundle: true,
|
|
365
335
|
minify: false,
|
|
366
336
|
mainFields: ['module', 'main'],
|
|
367
|
-
external: [${externalMods.map(m => `'${
|
|
337
|
+
external: [${externalMods.map(m => `'${m}'`).join(', ')}],
|
|
368
338
|
platform: 'node',
|
|
369
339
|
target: 'node18',
|
|
370
340
|
outfile: 'lib/index.js',
|
|
@@ -378,35 +348,40 @@ if (result.errors.length) {
|
|
|
378
348
|
`
|
|
379
349
|
fs.writeFileSync(path.join(output, 'esbuild.mjs'), esbuildConfig)
|
|
380
350
|
|
|
381
|
-
//
|
|
351
|
+
// 15. Write step metadata for pipeline
|
|
352
|
+
const meta = {
|
|
353
|
+
entryPoint: esbuildEntry,
|
|
354
|
+
activationEvents,
|
|
355
|
+
keepDeps: merged.keepDeps,
|
|
356
|
+
serverDeps,
|
|
357
|
+
serverBinary: merged.serverBinary,
|
|
358
|
+
hasLanguageClient,
|
|
359
|
+
}
|
|
360
|
+
fs.writeFileSync(path.join(output, 'coc-convert.json'), JSON.stringify(meta, null, 2))
|
|
361
|
+
|
|
362
|
+
// 16. Print report
|
|
382
363
|
console.log('\n=== Conversion Report ===')
|
|
383
364
|
console.log(` Plugin: ${pkg.name}`)
|
|
384
|
-
console.log(`
|
|
385
|
-
console.log(` Source files: ${
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
console.log(
|
|
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')
|
|
365
|
+
console.log(` Steps: ${steps.map(s => s.type).join(' → ')}`)
|
|
366
|
+
console.log(` Source files: ${result.files.length}`)
|
|
367
|
+
console.log(` Activation: ${activationEvents.join(', ')}`)
|
|
368
|
+
|
|
369
|
+
if (merged.serverBinary) {
|
|
370
|
+
console.log(` Server binary: ${merged.serverBinary.repo}`)
|
|
393
371
|
}
|
|
394
372
|
|
|
395
373
|
console.log(`\n ${output}/`)
|
|
396
|
-
console.log(' ├──
|
|
397
|
-
console.log(' ├──
|
|
398
|
-
console.log(' ├──
|
|
399
|
-
console.log('
|
|
374
|
+
console.log(' ├── package.json')
|
|
375
|
+
console.log(' ├── esbuild.mjs')
|
|
376
|
+
console.log(' ├── coc-convert.json')
|
|
377
|
+
if (hasLanguageClient) console.log(' ├── src/index.ts ← generated entry')
|
|
378
|
+
console.log(' ├── src/*.ts ← converted source')
|
|
400
379
|
console.log('\n Next:')
|
|
401
380
|
console.log(` cd ${output}`)
|
|
402
381
|
console.log(' npm install')
|
|
403
|
-
|
|
382
|
+
console.log(' npx tsx esbuild.mjs')
|
|
404
383
|
}
|
|
405
384
|
|
|
406
|
-
/**
|
|
407
|
-
* Scan original source for TypeScript plugin names used by the extension.
|
|
408
|
-
* Checks: explicit imports, require() calls, package.json dependencies.
|
|
409
|
-
*/
|
|
410
385
|
function walkTsFiles(dir: string): string[] {
|
|
411
386
|
const files: string[] = []
|
|
412
387
|
try {
|
|
@@ -414,6 +389,7 @@ function walkTsFiles(dir: string): string[] {
|
|
|
414
389
|
const full = path.join(dir, entry)
|
|
415
390
|
const stat = fs.statSync(full)
|
|
416
391
|
if (stat.isDirectory()) {
|
|
392
|
+
if (entry.startsWith('.') || entry === 'node_modules') continue
|
|
417
393
|
files.push(...walkTsFiles(full))
|
|
418
394
|
} else if (entry.endsWith('.ts') || entry.endsWith('.d.ts')) {
|
|
419
395
|
files.push(full)
|
|
@@ -423,79 +399,36 @@ function walkTsFiles(dir: string): string[] {
|
|
|
423
399
|
return files
|
|
424
400
|
}
|
|
425
401
|
|
|
426
|
-
function scanTypeScriptPlugins(input: string,
|
|
402
|
+
function scanTypeScriptPlugins(input: string, origPkg: Record<string, any>): Array<{ name: string; languages: string[]; enableForWorkspaceTypeScriptVersions: boolean }> {
|
|
403
|
+
// First check if origPkg already has typescriptServerPlugins configured
|
|
404
|
+
if (origPkg.contributes?.typescriptServerPlugins?.length) {
|
|
405
|
+
return origPkg.contributes.typescriptServerPlugins.map((p: any) => ({
|
|
406
|
+
name: p.name,
|
|
407
|
+
languages: p.languages || [],
|
|
408
|
+
enableForWorkspaceTypeScriptVersions: p.enableForWorkspaceTypeScriptVersions ?? true,
|
|
409
|
+
}))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Otherwise scan source files for plugin references
|
|
427
413
|
const plugins: Array<{ name: string; languages: string[]; enableForWorkspaceTypeScriptVersions: boolean }> = []
|
|
428
|
-
|
|
414
|
+
let scanDir = path.join(input, 'src')
|
|
415
|
+
if (!fs.existsSync(scanDir)) scanDir = input
|
|
429
416
|
if (!fs.existsSync(scanDir)) return plugins
|
|
430
417
|
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
const
|
|
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) {
|
|
418
|
+
const content = walkTsFiles(scanDir).map(f => fs.readFileSync(f, 'utf-8')).join('\n')
|
|
419
|
+
const refs = content.matchAll(/['"](@[^'"]+\/(?:typescript-plugin|language-service)[^'"]*)['"]|['"](\w+(?:-typescript-plugin|-language-service))['"]/g)
|
|
420
|
+
for (const m of refs) {
|
|
439
421
|
const name = m[1] || m[2]
|
|
440
422
|
if (name && !plugins.some(p => p.name === name)) {
|
|
441
423
|
plugins.push({ name, languages: [], enableForWorkspaceTypeScriptVersions: true })
|
|
442
424
|
}
|
|
443
425
|
}
|
|
444
426
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
}
|
|
427
|
+
for (const dep of Object.keys(origPkg.dependencies || {})) {
|
|
428
|
+
if ((dep.includes('typescript-plugin') || dep.includes('language-service')) && !plugins.some(p => p.name === dep)) {
|
|
429
|
+
plugins.push({ name: dep, languages: [], enableForWorkspaceTypeScriptVersions: true })
|
|
455
430
|
}
|
|
456
431
|
}
|
|
457
432
|
|
|
458
433
|
return plugins
|
|
459
434
|
}
|
|
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
|
-
}
|