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.
@@ -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
- let hasTsBridge = false
39
- let hasDecoration = false
40
- let hasWebview = false
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: 'package.json', apis, actions: [] })
31
+ files.push({ path: relative, apis })
108
32
  }
109
33
  }
110
34
 
111
35
  return {
112
36
  files,
113
- hasTsBridge,
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
+ }