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/scanner.ts
CHANGED
|
@@ -3,122 +3,38 @@ import * as path from 'path'
|
|
|
3
3
|
|
|
4
4
|
export interface ScanResult {
|
|
5
5
|
files: ScannedFile[]
|
|
6
|
-
hasTsBridge: boolean
|
|
7
|
-
hasDecoration: boolean
|
|
8
|
-
hasWebview: boolean
|
|
9
6
|
summary: string
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
export interface ScannedFile {
|
|
13
10
|
path: string
|
|
14
11
|
apis: string[]
|
|
15
|
-
actions: string[]
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
const UNSUPPORTED_PATTERNS = [
|
|
19
|
-
{ pattern: 'createTextEditorDecorationType', action: 'mark-unsupported', label: 'decoration API' },
|
|
20
|
-
{ pattern: 'setDecorations', action: 'mark-unsupported', label: 'decoration API' },
|
|
21
|
-
{ pattern: 'createWebviewPanel', action: 'mark-unsupported', label: 'webview API' },
|
|
22
|
-
{ pattern: 'registerTreeDataProvider', action: 'mark-unsupported', label: 'tree data provider' },
|
|
23
|
-
{ pattern: 'window.showInputBox', action: 'needs-rewrite', label: 'use requestInput instead' },
|
|
24
|
-
{ pattern: 'env.openExternal', action: 'mark-unsupported', label: 'no equivalent' },
|
|
25
|
-
{ pattern: 'showOpenDialog', action: 'mark-unsupported', label: 'no equivalent' },
|
|
26
|
-
{ pattern: 'showSaveDialog', action: 'mark-unsupported', label: 'no equivalent' },
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
const TS_BRIDGE_PATTERNS = [
|
|
30
|
-
'tsserver/request',
|
|
31
|
-
'tsserver/response',
|
|
32
|
-
'_vue:',
|
|
33
|
-
'typescript.tsserverRequest',
|
|
34
|
-
]
|
|
35
|
-
|
|
36
14
|
export function scan(dir: string): ScanResult {
|
|
37
15
|
const files: ScannedFile[] = []
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
return { files, summary: 'no source directory found' }
|
|
18
|
+
}
|
|
42
19
|
const tsFiles = walk(dir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
|
|
43
20
|
|
|
44
21
|
for (const filePath of tsFiles) {
|
|
45
22
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
46
23
|
const apis: string[] = []
|
|
47
|
-
const actions: string[] = []
|
|
48
24
|
const relative = path.relative(dir, filePath)
|
|
49
25
|
|
|
50
|
-
// Check for vscode imports
|
|
51
26
|
if (content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")')) {
|
|
52
27
|
apis.push('vscode')
|
|
53
28
|
}
|
|
54
29
|
|
|
55
|
-
// Check for unsupported patterns
|
|
56
|
-
for (const { pattern, action, label } of UNSUPPORTED_PATTERNS) {
|
|
57
|
-
if (content.includes(pattern)) {
|
|
58
|
-
apis.push(label)
|
|
59
|
-
actions.push(action)
|
|
60
|
-
if (action === 'mark-unsupported') {
|
|
61
|
-
if (label.includes('decoration')) hasDecoration = true
|
|
62
|
-
if (label.includes('webview')) hasWebview = true
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Check for TS bridge
|
|
68
|
-
for (const pattern of TS_BRIDGE_PATTERNS) {
|
|
69
|
-
if (content.includes(pattern)) {
|
|
70
|
-
hasTsBridge = true
|
|
71
|
-
apis.push('tsserver bridge')
|
|
72
|
-
break
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Check for LanguageClient
|
|
77
|
-
if (content.includes('LanguageClient')) {
|
|
78
|
-
apis.push('LanguageClient')
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check for typescriptServerPlugins in package.json
|
|
82
|
-
if (relative === 'package.json' || filePath.endsWith('package.json')) {
|
|
83
|
-
if (content.includes('typescriptServerPlugins')) {
|
|
84
|
-
hasTsBridge = true
|
|
85
|
-
apis.push('typescriptServerPlugins')
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (apis.length > 0) {
|
|
90
|
-
files.push({ path: relative, apis, actions })
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Read package.json
|
|
95
|
-
const pkgPath = path.join(dir, 'package.json')
|
|
96
|
-
if (fs.existsSync(pkgPath)) {
|
|
97
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
98
|
-
const apis: string[] = []
|
|
99
|
-
if (pkg.contributes?.typescriptServerPlugins) {
|
|
100
|
-
hasTsBridge = true
|
|
101
|
-
apis.push('typescriptServerPlugins')
|
|
102
|
-
}
|
|
103
|
-
if (pkg.activationEvents) {
|
|
104
|
-
apis.push(`activationEvents: ${pkg.activationEvents.length}`)
|
|
105
|
-
}
|
|
106
30
|
if (apis.length > 0) {
|
|
107
|
-
files.push({ path:
|
|
31
|
+
files.push({ path: relative, apis })
|
|
108
32
|
}
|
|
109
33
|
}
|
|
110
34
|
|
|
111
35
|
return {
|
|
112
36
|
files,
|
|
113
|
-
|
|
114
|
-
hasDecoration,
|
|
115
|
-
hasWebview,
|
|
116
|
-
summary: [
|
|
117
|
-
`found ${files.length} files with vscode API`,
|
|
118
|
-
hasTsBridge ? ', ts-bridge detected' : '',
|
|
119
|
-
hasDecoration ? ', decoration API (marked)' : '',
|
|
120
|
-
hasWebview ? ', webview API (marked)' : '',
|
|
121
|
-
].join(''),
|
|
37
|
+
summary: `found ${files.length} files with vscode API`,
|
|
122
38
|
}
|
|
123
39
|
}
|
|
124
40
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { StepGenerator, StepContext, BridgeStep, StepResult } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe, audited bridge code generators.
|
|
5
|
+
* Registry presets can only reference these types — no arbitrary code execution.
|
|
6
|
+
*/
|
|
7
|
+
interface BridgeTemplateResult {
|
|
8
|
+
code: string
|
|
9
|
+
injectExts: string[]
|
|
10
|
+
injectSvcs: string[]
|
|
11
|
+
callAfter: string | null
|
|
12
|
+
extraDeps: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BRIDGE_TEMPLATES: Record<string, (opts: any) => BridgeTemplateResult> = {
|
|
16
|
+
'tsserver-forward': (opts) => {
|
|
17
|
+
const command = opts.command || 'typescript.tsserverRequest'
|
|
18
|
+
return {
|
|
19
|
+
code: `\
|
|
20
|
+
client.onNotification('tsserver/request', async ([seq, command, args]: [number, string, any]) => {
|
|
21
|
+
try {
|
|
22
|
+
const result = await commands.executeCommand<any>('${command}', command, args, { isAsync: true, lowPriority: true })
|
|
23
|
+
client.sendNotification('tsserver/response', [seq, result?.body])
|
|
24
|
+
} catch { client.sendNotification('tsserver/response', [seq, undefined]) }
|
|
25
|
+
})`,
|
|
26
|
+
injectExts: opts.extensions || [],
|
|
27
|
+
injectSvcs: opts.services || [],
|
|
28
|
+
callAfter: 'registerBridge(context, client)',
|
|
29
|
+
extraDeps: ['typescript'],
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getRegisteredBridgeTypes(): string[] {
|
|
35
|
+
return Object.keys(BRIDGE_TEMPLATES)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const bridgeGenerator: StepGenerator = {
|
|
39
|
+
type: 'bridge',
|
|
40
|
+
|
|
41
|
+
generate(ctx: StepContext, step: any): StepResult {
|
|
42
|
+
const bs = step as BridgeStep
|
|
43
|
+
|
|
44
|
+
// Resolve preset config from registry
|
|
45
|
+
let type: string
|
|
46
|
+
let opts: Record<string, any>
|
|
47
|
+
if (bs.preset) {
|
|
48
|
+
const presetDef = ctx.presets?.[bs.preset]
|
|
49
|
+
if (!presetDef) {
|
|
50
|
+
throw new Error(`Unknown bridge preset: "${bs.preset}". Check presets.json in registry.`)
|
|
51
|
+
}
|
|
52
|
+
type = presetDef.type || ''
|
|
53
|
+
opts = { ...presetDef.options, ...(bs.options || {}) }
|
|
54
|
+
} else {
|
|
55
|
+
type = ''
|
|
56
|
+
opts = bs.options || {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Look up safe template
|
|
60
|
+
const template = BRIDGE_TEMPLATES[type]
|
|
61
|
+
if (!template) {
|
|
62
|
+
throw new Error(`Unknown bridge type: "${type}". Available types: ${Object.keys(BRIDGE_TEMPLATES).join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const generated = template(opts)
|
|
66
|
+
let code = generated.code
|
|
67
|
+
|
|
68
|
+
if (bs.verbose) {
|
|
69
|
+
code = `\
|
|
70
|
+
console.log('[bridge] registerBridge called')
|
|
71
|
+
client.onReady().then(() => console.log('[bridge] client ready')).catch(e => console.log('[bridge] client error:', e.message))
|
|
72
|
+
${code}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate the bridge module
|
|
76
|
+
const moduleContent = `\
|
|
77
|
+
import { commands, ExtensionContext } from 'coc.nvim'
|
|
78
|
+
|
|
79
|
+
export function registerBridge(context: ExtensionContext, client: any): void {
|
|
80
|
+
${code}
|
|
81
|
+
}
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
// Build code injections
|
|
85
|
+
const codeInjections: StepResult['codeInjections'] = []
|
|
86
|
+
const extIds = generated.injectExts || []
|
|
87
|
+
const svcIds = generated.injectSvcs || []
|
|
88
|
+
const callAfter = generated.callAfter
|
|
89
|
+
|
|
90
|
+
if (callAfter) {
|
|
91
|
+
codeInjections.push({
|
|
92
|
+
target: 'src/index.ts',
|
|
93
|
+
importCode: `import { registerBridge } from './bridge'`,
|
|
94
|
+
insertBefore: ` } catch (e: any) {`,
|
|
95
|
+
code: ` ${callAfter}\n`,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (extIds.length || svcIds.length) {
|
|
100
|
+
let activationCode = ''
|
|
101
|
+
for (const extId of extIds) {
|
|
102
|
+
const varName = extId.replace(/[^a-z0-9]/gi, '_')
|
|
103
|
+
activationCode += `\
|
|
104
|
+
const ${varName} = extensions.all.find(e => e.id === '${extId}')
|
|
105
|
+
if (${varName} && !${varName}.isActive) { await ${varName}.activate() }
|
|
106
|
+
`
|
|
107
|
+
}
|
|
108
|
+
for (const svc of svcIds) {
|
|
109
|
+
const varName = svc.replace(/[^a-z0-9]/gi, '_') + 'Svc'
|
|
110
|
+
activationCode += `\
|
|
111
|
+
const ${varName} = services.getService('${svc}')
|
|
112
|
+
if (${varName}) { await ${varName}.start() }
|
|
113
|
+
`
|
|
114
|
+
}
|
|
115
|
+
if (activationCode) {
|
|
116
|
+
codeInjections.push({
|
|
117
|
+
target: 'src/index.ts',
|
|
118
|
+
insertAfter: 'try {\n',
|
|
119
|
+
code: activationCode,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
codeInjections.push({
|
|
123
|
+
target: 'src/index.ts',
|
|
124
|
+
importCode: ` extensions,`,
|
|
125
|
+
insertBefore: `} from 'coc.nvim'`,
|
|
126
|
+
code: '',
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result: StepResult = {
|
|
131
|
+
generatedFiles: [{ path: 'src/bridge.ts', content: moduleContent }],
|
|
132
|
+
entryPoint: undefined,
|
|
133
|
+
keepDeps: Object.fromEntries((generated.extraDeps || []).map((d: string) => {
|
|
134
|
+
const ver = ctx.origPkg.dependencies?.[d] || ctx.origPkg.devDependencies?.[d]
|
|
135
|
+
return [d, ver || '*']
|
|
136
|
+
})),
|
|
137
|
+
activationEvents: [],
|
|
138
|
+
}
|
|
139
|
+
if (codeInjections.length) result.codeInjections = codeInjections
|
|
140
|
+
return result
|
|
141
|
+
},
|
|
142
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StepGenerator, StepContext, ConvertStep, StepResult } from '../types.js'
|
|
2
|
+
import { languageClientGenerator } from './language-client.js'
|
|
3
|
+
import { sourceGenerator } from './source.js'
|
|
4
|
+
import { bridgeGenerator } from './bridge.js'
|
|
5
|
+
import { markUnsupportedGenerator } from './mark-unsupported.js'
|
|
6
|
+
|
|
7
|
+
const REGISTRY: Record<string, StepGenerator> = {}
|
|
8
|
+
|
|
9
|
+
export function registerGenerator(g: StepGenerator): void {
|
|
10
|
+
REGISTRY[g.type] = g
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getRegisteredStepTypes(): string[] {
|
|
14
|
+
return Object.keys(REGISTRY)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function executeStep(ctx: StepContext, step: ConvertStep): StepResult {
|
|
18
|
+
const gen = REGISTRY[step.type]
|
|
19
|
+
if (!gen) {
|
|
20
|
+
throw new Error(`Unknown step type: "${step.type}". Available: ${Object.keys(REGISTRY).join(', ')}`)
|
|
21
|
+
}
|
|
22
|
+
return gen.generate(ctx, step)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Register built-in generators
|
|
26
|
+
registerGenerator(languageClientGenerator)
|
|
27
|
+
registerGenerator(sourceGenerator)
|
|
28
|
+
registerGenerator(bridgeGenerator)
|
|
29
|
+
registerGenerator(markUnsupportedGenerator)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { StepGenerator, StepContext, LanguageClientStep, StepResult } from '../types.js'
|
|
2
|
+
|
|
3
|
+
function escapeStr(s: string): string {
|
|
4
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/`/g, '\\`').replace(/\$/g, '\\$')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const languageClientGenerator: StepGenerator = {
|
|
8
|
+
type: 'language-client',
|
|
9
|
+
|
|
10
|
+
generate(ctx: StepContext, step: any): StepResult {
|
|
11
|
+
const ls = step as LanguageClientStep
|
|
12
|
+
const id = ls.id || (ctx.origPkg.name || 'language-client')
|
|
13
|
+
const transport = ls.transport || (ls.server.kind === 'binary' ? 'stdio' : 'ipc')
|
|
14
|
+
const transportExpr = transport === 'stdio' ? 'TransportKind.stdio' : 'TransportKind.ipc'
|
|
15
|
+
const languages = ls.languages
|
|
16
|
+
const multiRoot = ls.multiRoot ?? false
|
|
17
|
+
const pluginName = ctx.origPkg.name || 'plugin'
|
|
18
|
+
const description = ctx.origPkg.description || pluginName
|
|
19
|
+
|
|
20
|
+
let serverPathCode: string
|
|
21
|
+
let serverOptionsCode: string
|
|
22
|
+
let binaryDownloaded = false
|
|
23
|
+
|
|
24
|
+
if (ls.server.kind === 'binary') {
|
|
25
|
+
const pkg = ls.server.package
|
|
26
|
+
const binary = ls.server.binary
|
|
27
|
+
const args = ls.server.args || []
|
|
28
|
+
const argsStr = args.length ? `[${args.map(a => `'${escapeStr(a)}'`).join(', ')}]` : '[]'
|
|
29
|
+
|
|
30
|
+
serverPathCode = `\
|
|
31
|
+
let serverPath: string | undefined
|
|
32
|
+
try {
|
|
33
|
+
serverPath = require.resolve('${escapeStr(pkg)}')
|
|
34
|
+
} catch {}
|
|
35
|
+
if (!serverPath) {
|
|
36
|
+
serverPath = require('path').join(__dirname, '..', 'server', '${escapeStr(binary.binaryPath || pkg)}')
|
|
37
|
+
}`
|
|
38
|
+
serverOptionsCode = `{ command: serverPath, args: ${argsStr} }`
|
|
39
|
+
binaryDownloaded = true
|
|
40
|
+
} else {
|
|
41
|
+
const pkg = ls.server.package
|
|
42
|
+
const entry = ls.server.entry || 'main'
|
|
43
|
+
|
|
44
|
+
// Same resolution as old converter: resolve main entry first, then walk for bin
|
|
45
|
+
serverPathCode = `\
|
|
46
|
+
let serverPath: string | undefined
|
|
47
|
+
try {
|
|
48
|
+
serverPath = require.resolve('${escapeStr(pkg)}')
|
|
49
|
+
} catch {}
|
|
50
|
+
try {
|
|
51
|
+
// Walk up from the resolved main entry to find the package's package.json
|
|
52
|
+
// We can't use require.resolve('pkg/package.json') because exports field may block it
|
|
53
|
+
let _dir = require('path').dirname(require.resolve('${escapeStr(pkg)}'));
|
|
54
|
+
while (_dir !== require('path').dirname(_dir)) {
|
|
55
|
+
const _pkgPath = require('path').join(_dir, 'package.json');
|
|
56
|
+
if (require('fs').existsSync(_pkgPath)) {
|
|
57
|
+
const _pkg = JSON.parse(require('fs').readFileSync(_pkgPath, 'utf-8'));
|
|
58
|
+
if (_pkg.bin) {
|
|
59
|
+
const _entry = typeof _pkg.bin === 'string' ? _pkg.bin : Object.values(_pkg.bin)[0];
|
|
60
|
+
serverPath = require('path').join(_dir, _entry);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
_dir = require('path').dirname(_dir);
|
|
65
|
+
}
|
|
66
|
+
} catch {}`
|
|
67
|
+
// Use full require.resolve path (including bin walking) if available, else fallback to simple main entry
|
|
68
|
+
serverOptionsCode = `{ module: serverPath || require.resolve('${escapeStr(pkg)}'), transport: ${transportExpr} }`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const docSelectorCode = `[${languages.map(l => `{ scheme: 'file', language: '${l}' }`).join(', ')}]`
|
|
72
|
+
const code = `\
|
|
73
|
+
import {
|
|
74
|
+
LanguageClient,
|
|
75
|
+
TransportKind,
|
|
76
|
+
services,
|
|
77
|
+
workspace,
|
|
78
|
+
window,
|
|
79
|
+
commands,
|
|
80
|
+
ExtensionContext,
|
|
81
|
+
} from 'coc.nvim'
|
|
82
|
+
import * as path from 'path'
|
|
83
|
+
|
|
84
|
+
export async function activate(context: ExtensionContext): Promise<void> {
|
|
85
|
+
try {
|
|
86
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] activate() called')\n` : ''}${serverPathCode}
|
|
87
|
+
if (!serverPath) {
|
|
88
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] serverPath undefined')\n` : ''}\
|
|
89
|
+
window.showErrorMessage('Cannot find language server.')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] serverPath =', serverPath)\n` : ''}\
|
|
93
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] creating LanguageClient')\n` : ''}\
|
|
94
|
+
const createClient = () => {
|
|
95
|
+
const c = new LanguageClient(
|
|
96
|
+
'${escapeStr(id)}',
|
|
97
|
+
'${escapeStr(description)}',
|
|
98
|
+
${serverOptionsCode},
|
|
99
|
+
{
|
|
100
|
+
documentSelector: ${docSelectorCode},
|
|
101
|
+
outputChannelName: '${escapeStr(description)}',
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
context.subscriptions.push({ dispose: () => c.stop() })
|
|
105
|
+
context.subscriptions.push(services.registerLanguageClient(c))
|
|
106
|
+
return c
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] registering LanguageClient')\n` : ''}\
|
|
110
|
+
let client: LanguageClient
|
|
111
|
+
if (${multiRoot ? 'workspace.workspaceFolders && workspace.workspaceFolders.length > 1' : 'false'}) {
|
|
112
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] multiRoot mode')\n` : ''}\
|
|
113
|
+
for (const folder of workspace.workspaceFolders) {
|
|
114
|
+
client = createClient()
|
|
115
|
+
client.start()
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
client = createClient()
|
|
119
|
+
${ls.verbose ? ` console.log('[${escapeStr(id)}] starting client')\n` : ''}\
|
|
120
|
+
client.start()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
context.subscriptions.push(
|
|
124
|
+
commands.registerCommand('${escapeStr(pluginName)}.restart', async () => {
|
|
125
|
+
await client.stop()
|
|
126
|
+
await client.start()
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
} catch (e: any) {
|
|
130
|
+
window.showErrorMessage('${escapeStr(pluginName)} error: ' + (e.message || String(e)))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
const activationEvents = languages.map(l => `onLanguage:${l}`)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
generatedFiles: [{ path: 'src/index.ts', content: code }],
|
|
139
|
+
entryPoint: 'src/index.ts',
|
|
140
|
+
keepDeps: {},
|
|
141
|
+
activationEvents,
|
|
142
|
+
serverBinary: ls.server.kind === 'binary' ? {
|
|
143
|
+
repo: ls.server.binary.repo,
|
|
144
|
+
asset: ls.server.binary.asset,
|
|
145
|
+
binaryPath: ls.server.binary.binaryPath,
|
|
146
|
+
args: ls.server.args,
|
|
147
|
+
} : undefined,
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { StepGenerator, StepContext, MarkUnsupportedStep, StepResult } from '../types.js'
|
|
4
|
+
|
|
5
|
+
const FEATURE_WARNINGS: Record<string, string> = {
|
|
6
|
+
'decoration': 'Decoration API is not supported in coc.nvim',
|
|
7
|
+
'webview': 'Webview API is not supported in coc.nvim',
|
|
8
|
+
'tree-data-provider': 'Tree data provider is not supported in coc.nvim',
|
|
9
|
+
'open-external': 'env.openExternal has no equivalent in coc.nvim',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FEATURE_PATTERNS: Record<string, RegExp[]> = {
|
|
13
|
+
'decoration': [/createTextEditorDecorationType/g, /setDecorations/g],
|
|
14
|
+
'webview': [/createWebviewPanel/g],
|
|
15
|
+
'tree-data-provider': [/registerTreeDataProvider/g],
|
|
16
|
+
'open-external': [/env\.openExternal/g],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function walkFiles(dir: string): string[] {
|
|
20
|
+
const files: string[] = []
|
|
21
|
+
try {
|
|
22
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const p = path.join(dir, entry.name)
|
|
24
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
25
|
+
files.push(...walkFiles(p))
|
|
26
|
+
} else if (entry.isFile()) {
|
|
27
|
+
files.push(p)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
return files
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const markUnsupportedGenerator: StepGenerator = {
|
|
35
|
+
type: 'mark-unsupported',
|
|
36
|
+
|
|
37
|
+
generate(ctx: StepContext, step: any): StepResult {
|
|
38
|
+
const ms = step as MarkUnsupportedStep
|
|
39
|
+
const { output, verbose } = ctx
|
|
40
|
+
const srcDir = path.join(output, 'src')
|
|
41
|
+
|
|
42
|
+
const results: Array<{ path: string; content: string }> = []
|
|
43
|
+
const appliedFeatures: string[] = []
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(srcDir)) return { generatedFiles: [], keepDeps: {}, activationEvents: [] }
|
|
46
|
+
|
|
47
|
+
for (const feature of ms.features) {
|
|
48
|
+
const warning = FEATURE_WARNINGS[feature]
|
|
49
|
+
const patterns = FEATURE_PATTERNS[feature]
|
|
50
|
+
if (!warning || !patterns) {
|
|
51
|
+
if (verbose) console.warn(` unknown feature: ${feature}`)
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const fp of walkFiles(srcDir)) {
|
|
56
|
+
if (!fp.endsWith('.ts')) continue
|
|
57
|
+
let content = fs.readFileSync(fp, 'utf-8')
|
|
58
|
+
let changed = false
|
|
59
|
+
|
|
60
|
+
for (const re of patterns) {
|
|
61
|
+
content = content.replace(re, (match) => {
|
|
62
|
+
changed = true
|
|
63
|
+
return `/* [converter] TODO: ${warning} — removed: ${match} */ void 0`
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (changed) {
|
|
68
|
+
fs.writeFileSync(fp, content)
|
|
69
|
+
if (verbose) console.log(` mark-unsupported(${feature}): ${path.relative(output, fp)}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
appliedFeatures.push(feature)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
generatedFiles: results,
|
|
77
|
+
keepDeps: {},
|
|
78
|
+
activationEvents: [],
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { StepGenerator, StepContext, SourceStep, StepResult } from '../types.js'
|
|
4
|
+
import { transformImportMapping } from '../transforms/import-mapping.js'
|
|
5
|
+
import { transformClassToFactory } from '../transforms/class-to-factory.js'
|
|
6
|
+
import { transformProviderRegister } from '../transforms/provider-register.js'
|
|
7
|
+
import { transformEnumOffset } from '../transforms/enum-offset.js'
|
|
8
|
+
import { transformStripVolar } from '../transforms/strip-volar.js'
|
|
9
|
+
|
|
10
|
+
const TRANSFORM_MAP: Record<string, (ctx: any) => void> = {
|
|
11
|
+
'import-mapping': transformImportMapping,
|
|
12
|
+
'class-to-factory': transformClassToFactory,
|
|
13
|
+
'provider-register': transformProviderRegister,
|
|
14
|
+
'enum-offset': transformEnumOffset,
|
|
15
|
+
'strip-volar': transformStripVolar,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function walkFiles(dir: string): string[] {
|
|
19
|
+
const files: string[] = []
|
|
20
|
+
try {
|
|
21
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
const p = path.join(dir, entry.name)
|
|
23
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
24
|
+
files.push(...walkFiles(p))
|
|
25
|
+
} else if (entry.isFile()) {
|
|
26
|
+
files.push(p)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
return files
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const sourceGenerator: StepGenerator = {
|
|
34
|
+
type: 'source',
|
|
35
|
+
|
|
36
|
+
generate(ctx: StepContext, step: any): StepResult {
|
|
37
|
+
const ss = step as SourceStep
|
|
38
|
+
const { input, output, project, verbose } = ctx
|
|
39
|
+
const outputsDir = path.join(output, 'src')
|
|
40
|
+
fs.mkdirSync(outputsDir, { recursive: true })
|
|
41
|
+
|
|
42
|
+
// Copy ALL .ts/.tsx files from source directory (try src/ first, fall back to input root)
|
|
43
|
+
let srcDir = path.join(input, 'src')
|
|
44
|
+
if (!fs.existsSync(srcDir)) {
|
|
45
|
+
srcDir = input
|
|
46
|
+
}
|
|
47
|
+
const hasStripVolar = ss.transforms.includes('strip-volar')
|
|
48
|
+
const allFiles: Array<{ src: string; rel: string }> = []
|
|
49
|
+
const vscodeFiles: string[] = []
|
|
50
|
+
|
|
51
|
+
for (const f of walkFiles(srcDir)) {
|
|
52
|
+
const rel = path.relative(srcDir, f)
|
|
53
|
+
if (!rel.endsWith('.ts') && !rel.endsWith('.tsx')) continue
|
|
54
|
+
|
|
55
|
+
// Skip framework files that are replaced by generated code
|
|
56
|
+
if (hasStripVolar) {
|
|
57
|
+
const content = fs.readFileSync(f, 'utf-8')
|
|
58
|
+
if (content.includes('@volar/vscode') || content.includes('reactive-vscode')) continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
allFiles.push({ src: f, rel })
|
|
62
|
+
|
|
63
|
+
const content = fs.readFileSync(f, 'utf-8')
|
|
64
|
+
if (content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")')) {
|
|
65
|
+
vscodeFiles.push(rel)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Copy all files to output
|
|
70
|
+
for (const { src, rel } of allFiles) {
|
|
71
|
+
const dest = path.join(outputsDir, rel)
|
|
72
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
73
|
+
fs.copyFileSync(src, dest)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (verbose) {
|
|
77
|
+
console.log(` source: copied ${allFiles.length} files (${vscodeFiles.length} with vscode imports)`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Apply transforms via ts-morph (only to files with vscode imports)
|
|
81
|
+
for (const rel of vscodeFiles) {
|
|
82
|
+
const fp = path.join(outputsDir, rel)
|
|
83
|
+
if (!fs.existsSync(fp)) continue
|
|
84
|
+
try { project.addSourceFileAtPath(fp) } catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const sf of project.getSourceFiles()) {
|
|
88
|
+
const relPath = path.relative(outputsDir, sf.getFilePath())
|
|
89
|
+
if (!vscodeFiles.some(f => sf.getFilePath().endsWith(f))) continue
|
|
90
|
+
|
|
91
|
+
for (const t of ss.transforms) {
|
|
92
|
+
const fn = TRANSFORM_MAP[t]
|
|
93
|
+
if (!fn) {
|
|
94
|
+
if (verbose) console.warn(` unknown transform: ${t}`)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
fn({ file: sf, project })
|
|
99
|
+
if (verbose) console.log(` ${t}: ${relPath}`)
|
|
100
|
+
} catch (e: any) {
|
|
101
|
+
if (verbose) console.warn(` ${t} error on ${relPath}: ${e.message}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
sf.saveSync()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Resolve keepDeps from origPkg (with workspace root fallback)
|
|
108
|
+
const keepDeps: Record<string, string> = {}
|
|
109
|
+
if (ss.keepDeps) {
|
|
110
|
+
if (Array.isArray(ss.keepDeps)) {
|
|
111
|
+
for (const dep of ss.keepDeps) {
|
|
112
|
+
const ver = resolveDepVersion(ctx.origPkg, dep, input)
|
|
113
|
+
if (ver) {
|
|
114
|
+
keepDeps[dep] = ver
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error(`keepDeps: cannot find version for "${dep}" in source package.json, devDependencies, or workspace root. Use object syntax in registry to specify version manually.`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
Object.assign(keepDeps, ss.keepDeps)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
generatedFiles: [],
|
|
126
|
+
entryPoint: ss.entry,
|
|
127
|
+
keepDeps,
|
|
128
|
+
activationEvents: ss.activationEvents || [],
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveDepVersion(pkg: Record<string, any>, name: string, inputDir?: string): string | undefined {
|
|
134
|
+
// 1. Check source package's dependencies
|
|
135
|
+
if (pkg.dependencies?.[name]) return pkg.dependencies[name]
|
|
136
|
+
// 2. Check source package's devDependencies
|
|
137
|
+
if (pkg.devDependencies?.[name]) return pkg.devDependencies[name]
|
|
138
|
+
// 3. Walk up for workspace root
|
|
139
|
+
if (inputDir) {
|
|
140
|
+
let dir = inputDir
|
|
141
|
+
const fs = require('fs') as typeof import('fs')
|
|
142
|
+
const path = require('path') as typeof import('path')
|
|
143
|
+
while (dir !== path.dirname(dir)) {
|
|
144
|
+
dir = path.dirname(dir)
|
|
145
|
+
const wsPkgPath = path.join(dir, 'package.json')
|
|
146
|
+
if (fs.existsSync(wsPkgPath)) {
|
|
147
|
+
try {
|
|
148
|
+
const wsPkg = JSON.parse(fs.readFileSync(wsPkgPath, 'utf-8'))
|
|
149
|
+
if (wsPkg.dependencies?.[name]) return wsPkg.dependencies[name]
|
|
150
|
+
if (wsPkg.devDependencies?.[name]) return wsPkg.devDependencies[name]
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
// Stop at filesystem root
|
|
154
|
+
if (dir === path.dirname(dir)) break
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// 4. Not found
|
|
158
|
+
return undefined
|
|
159
|
+
}
|