Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/script/build/extractChatLib.ts
13389 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { exec } from 'child_process';
7
import * as fs from 'fs';
8
import { glob } from 'glob';
9
import * as jsonc from 'jsonc-parser';
10
import * as path from 'path';
11
import { promisify } from 'util';
12
13
const REPO_ROOT = path.join(__dirname, '..', '..');
14
const CHAT_LIB_DIR = path.join(REPO_ROOT, 'chat-lib');
15
const TARGET_DIR = path.join(CHAT_LIB_DIR, 'src');
16
const execAsync = promisify(exec);
17
18
// Entry point - follow imports from the main chat-lib file
19
// Note: All *.ts files in src/lib/node/test/ are automatically included
20
const entryPoints = [
21
'src/lib/node/chatLibMain.ts',
22
'src/util/vs/base-common.d.ts',
23
'src/util/vs/vscode-globals-nls.d.ts',
24
'src/util/vs/vscode-globals-product.d.ts',
25
'src/util/common/globals.d.ts',
26
'src/util/common/test/shims/vscodeTypesShim.ts',
27
'src/platform/diff/common/diffWorker.ts',
28
'src/platform/tokenizer/node/tikTokenizerWorker.ts',
29
// For tests:
30
'src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts',
31
'src/extension/completions-core/vscode-node/lib/src/test/textDocument.ts',
32
];
33
34
interface FileInfo {
35
srcPath: string;
36
destPath: string;
37
relativePath: string;
38
dependencies: string[];
39
}
40
41
class ChatLibExtractor {
42
private processedFiles = new Set<string>();
43
private allFiles = new Map<string, FileInfo>();
44
private pathMappings: Map<string, string> = new Map();
45
46
async extract(): Promise<void> {
47
// Load path mappings from tsconfig.json
48
await this.loadPathMappings();
49
console.log('Starting chat-lib extraction...');
50
51
// Clean target directory
52
await this.cleanTargetDir();
53
54
// Process entry points and their dependencies
55
await this.processEntryPoints();
56
57
// Copy all processed files
58
await this.copyFiles();
59
60
// Use static module files
61
await this.generateModuleFiles();
62
63
// Validate the module
64
await this.validateModule();
65
66
// Compile TypeScript to validate
67
await this.compileTypeScript();
68
69
console.log('Chat-lib extraction completed successfully!');
70
}
71
72
private async loadPathMappings(): Promise<void> {
73
const tsconfigPath = path.join(REPO_ROOT, 'tsconfig.json');
74
const tsconfigContent = await fs.promises.readFile(tsconfigPath, 'utf-8');
75
const tsconfig = jsonc.parse(tsconfigContent);
76
77
if (tsconfig.compilerOptions?.paths) {
78
for (const [alias, targets] of Object.entries(tsconfig.compilerOptions.paths)) {
79
// Skip the 'vscode' mapping as it's handled separately
80
if (alias === 'vscode') {
81
continue;
82
}
83
84
// Handle path mappings like "#lib/*" -> ["./src/extension/completions-core/lib/src/*"]
85
// and "#types" -> ["./src/extension/completions-core/types/src"]
86
if (Array.isArray(targets) && targets.length > 0) {
87
const target = targets[0]; // Use the first target
88
// Remove leading './' and trailing '/*' if present
89
const cleanTarget = target.replace(/^\.\//, '').replace(/\/\*$/, '');
90
const cleanAlias = alias.replace(/\/\*$/, '');
91
this.pathMappings.set(cleanAlias, cleanTarget);
92
}
93
}
94
}
95
96
console.log('Loaded path mappings:', Array.from(this.pathMappings.entries()));
97
}
98
99
private async cleanTargetDir(): Promise<void> {
100
// Remove and recreate the src directory
101
if (fs.existsSync(TARGET_DIR)) {
102
await fs.promises.rm(TARGET_DIR, { recursive: true, force: true });
103
}
104
await fs.promises.mkdir(TARGET_DIR, { recursive: true });
105
}
106
107
private async processEntryPoints(): Promise<void> {
108
console.log('Processing entry points and dependencies...');
109
110
// Start with static entry points and dynamically add all test files
111
const testFiles = await glob('src/lib/vscode-node/test/*.ts', { cwd: REPO_ROOT });
112
const queue = [...entryPoints, ...testFiles];
113
114
while (queue.length > 0) {
115
const filePath = queue.shift()!;
116
if (this.processedFiles.has(filePath)) {
117
continue;
118
}
119
120
const fullPath = path.join(REPO_ROOT, filePath);
121
if (!fs.existsSync(fullPath)) {
122
console.warn(`Warning: File not found: ${filePath}`);
123
continue;
124
}
125
126
const dependencies = await this.extractDependencies(fullPath);
127
const destPath = this.getDestinationPath(filePath);
128
129
this.allFiles.set(filePath, {
130
srcPath: fullPath,
131
destPath,
132
relativePath: filePath,
133
dependencies
134
});
135
136
this.processedFiles.add(filePath);
137
138
// Add dependencies to queue
139
dependencies.forEach(dep => {
140
if (!this.processedFiles.has(dep)) {
141
queue.push(dep);
142
}
143
});
144
}
145
}
146
147
private async extractDependencies(filePath: string): Promise<string[]> {
148
const content = await fs.promises.readFile(filePath, 'utf-8');
149
const dependencies: string[] = [];
150
151
// Remove single-line comments and process line by line to avoid matching commented imports
152
// We need to be careful not to remove strings that contain '//'
153
const lines = content.split('\n');
154
const activeLines: string[] = [];
155
let inBlockComment = false;
156
157
for (const line of lines) {
158
// Track block comments
159
if (line.trim().startsWith('/*')) {
160
// preserve pragmas in tsx files
161
if (!(filePath.endsWith('.tsx') && line.match(/\/\*\*\s+@jsxImportSource\s+\S+/))) {
162
inBlockComment = true;
163
}
164
}
165
if (inBlockComment) {
166
if (line.includes('*/')) {
167
inBlockComment = false;
168
}
169
continue;
170
}
171
172
// Skip single-line comments
173
const trimmedLine = line.trim();
174
if (trimmedLine.startsWith('//')) {
175
continue;
176
}
177
178
// For lines that might have inline comments, we need to preserve string content
179
// Remove comments that are not inside strings
180
let processedLine = line;
181
// Simple heuristic: if the line contains import/export, keep everything up to //
182
// that's outside of string literals
183
if (trimmedLine.includes('import') || trimmedLine.includes('export')) {
184
// Remove inline comments (this is a simple approach - could be improved)
185
const commentIndex = line.indexOf('//');
186
if (commentIndex !== -1) {
187
// Check if // is inside a string by counting quotes before it
188
const beforeComment = line.substring(0, commentIndex);
189
const singleQuotes = (beforeComment.match(/'/g) || []).length;
190
const doubleQuotes = (beforeComment.match(/"/g) || []).length;
191
// If even number of quotes, the comment is outside strings
192
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {
193
processedLine = beforeComment;
194
}
195
}
196
}
197
198
activeLines.push(processedLine);
199
}
200
201
const activeContent = activeLines.join('\n');
202
203
// Extract both import and export statements using regex
204
// Matches:
205
// - import ... from './path'
206
// - export ... from './path'
207
// - export { ... } from './path'
208
// Updated regex to match all relative imports (including multiple ../ segments)
209
const relativeImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g;
210
let match;
211
212
while ((match = relativeImportRegex.exec(activeContent)) !== null) {
213
const importPath = match[1];
214
const resolvedPath = this.resolveImportPath(filePath, importPath);
215
216
if (resolvedPath) {
217
dependencies.push(resolvedPath);
218
}
219
}
220
221
// Also match path alias imports like: import ... from '#lib/...' or '#types'
222
// We need to resolve these to follow their dependencies
223
const aliasImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"]([#][^'"]*)['"]/g;
224
225
while ((match = aliasImportRegex.exec(activeContent)) !== null) {
226
const importPath = match[1];
227
const resolvedPath = this.resolvePathAlias(importPath);
228
229
if (resolvedPath) {
230
dependencies.push(resolvedPath);
231
}
232
}
233
234
// For tsx files process JSX imports as well
235
if (filePath.endsWith('.tsx')) {
236
const jsxRelativeImportRegex = /\/\*\*\s+@jsxImportSource\s+(\.\.?\/\S+)\s+\*\//g;
237
238
while ((match = jsxRelativeImportRegex.exec(activeContent)) !== null) {
239
const importPath = match[1];
240
const resolvedPath = this.resolveImportPath(filePath, path.join(importPath, 'jsx-runtime'));
241
242
if (resolvedPath) {
243
dependencies.push(resolvedPath);
244
}
245
}
246
}
247
248
return dependencies;
249
}
250
251
private resolvePathAlias(importPath: string): string | null {
252
// Handle path alias imports like '#lib/foo' or '#types'
253
// Find the matching alias by checking if the import starts with any registered alias
254
for (const [alias, targetPath] of this.pathMappings.entries()) {
255
if (importPath === alias) {
256
// Exact match for aliases without wildcards (e.g., '#types')
257
return this.resolveFileWithExtensions(path.join(REPO_ROOT, targetPath));
258
} else if (importPath.startsWith(alias + '/')) {
259
// Wildcard match for aliases with /* (e.g., '#lib/foo' matches '#lib')
260
const remainder = importPath.substring(alias.length + 1); // +1 to skip the '/'
261
const fullPath = path.join(REPO_ROOT, targetPath, remainder);
262
return this.resolveFileWithExtensions(fullPath);
263
}
264
}
265
266
// If no alias matched, return null
267
console.warn(`Warning: Path alias not found for: ${importPath}`);
268
return null;
269
}
270
271
private resolveFileWithExtensions(basePath: string): string | null {
272
// Try with .ts extension
273
if (fs.existsSync(basePath + '.ts')) {
274
return this.normalizePath(path.relative(REPO_ROOT, basePath + '.ts'));
275
}
276
277
// Try with .tsx extension
278
if (fs.existsSync(basePath + '.tsx')) {
279
return this.normalizePath(path.relative(REPO_ROOT, basePath + '.tsx'));
280
}
281
282
// Try with .d.ts extension
283
if (fs.existsSync(basePath + '.d.ts')) {
284
return this.normalizePath(path.relative(REPO_ROOT, basePath + '.d.ts'));
285
}
286
287
// Try with index.ts
288
if (fs.existsSync(path.join(basePath, 'index.ts'))) {
289
return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.ts')));
290
}
291
292
// Try with index.tsx
293
if (fs.existsSync(path.join(basePath, 'index.tsx'))) {
294
return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.tsx')));
295
}
296
297
// Try with index.d.ts
298
if (fs.existsSync(path.join(basePath, 'index.d.ts'))) {
299
return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.d.ts')));
300
}
301
302
// Try as-is
303
if (fs.existsSync(basePath)) {
304
return this.normalizePath(path.relative(REPO_ROOT, basePath));
305
}
306
307
return null;
308
}
309
310
private resolveImportPath(fromFile: string, importPath: string): string | null {
311
const fromDir = path.dirname(fromFile);
312
const resolved = path.resolve(fromDir, importPath);
313
314
// If import path ends with .js, try replacing with .ts/.tsx first
315
if (importPath.endsWith('.js')) {
316
const baseResolved = resolved.slice(0, -3); // Remove .js
317
if (fs.existsSync(baseResolved + '.ts')) {
318
return this.normalizePath(path.relative(REPO_ROOT, baseResolved + '.ts'));
319
}
320
if (fs.existsSync(baseResolved + '.tsx')) {
321
return this.normalizePath(path.relative(REPO_ROOT, baseResolved + '.tsx'));
322
}
323
}
324
325
// Try with .ts extension
326
if (fs.existsSync(resolved + '.ts')) {
327
return this.normalizePath(path.relative(REPO_ROOT, resolved + '.ts'));
328
}
329
330
// Try with .tsx extension
331
if (fs.existsSync(resolved + '.tsx')) {
332
return this.normalizePath(path.relative(REPO_ROOT, resolved + '.tsx'));
333
}
334
335
// Try with .d.ts extension
336
if (fs.existsSync(resolved + '.d.ts')) {
337
return this.normalizePath(path.relative(REPO_ROOT, resolved + '.d.ts'));
338
}
339
340
// Try with index.ts
341
if (fs.existsSync(path.join(resolved, 'index.ts'))) {
342
return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.ts')));
343
}
344
345
// Try with index.tsx
346
if (fs.existsSync(path.join(resolved, 'index.tsx'))) {
347
return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.tsx')));
348
}
349
350
// Try with index.d.ts
351
if (fs.existsSync(path.join(resolved, 'index.d.ts'))) {
352
return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.d.ts')));
353
}
354
355
// Try as-is
356
if (fs.existsSync(resolved)) {
357
return this.normalizePath(path.relative(REPO_ROOT, resolved));
358
}
359
360
// If we get here, the file was not found - throw an error
361
throw new Error(`Import file not found: ${importPath} (resolved to ${resolved}) imported from ${fromFile}`);
362
}
363
364
private normalizePath(filePath: string): string {
365
// Normalize path separators to forward slashes for consistency across platforms
366
return filePath.replace(/\\/g, '/');
367
}
368
369
private getDestinationPath(filePath: string): string {
370
// Normalize the input path first, then convert src/... to _internal/...
371
const normalizedPath = this.normalizePath(filePath);
372
const relativePath = normalizedPath.replace(/^src\//, '_internal/');
373
return path.join(TARGET_DIR, relativePath);
374
}
375
376
private async copyFiles(): Promise<void> {
377
console.log(`Copying ${this.allFiles.size} files...`);
378
379
for (const [, fileInfo] of this.allFiles) {
380
// Skip the main entry point file since it becomes top-level main.ts
381
if (fileInfo.relativePath === 'src/lib/node/chatLibMain.ts') {
382
continue;
383
}
384
385
await fs.promises.mkdir(path.dirname(fileInfo.destPath), { recursive: true }); // Read source file
386
const content = await fs.promises.readFile(fileInfo.srcPath, 'utf-8');
387
388
// Transform content to replace vscode imports and fix relative paths
389
const transformedContent = this.transformFileContent(content, fileInfo.relativePath);
390
391
// Write to destination
392
await fs.promises.writeFile(fileInfo.destPath, transformedContent);
393
}
394
}
395
396
397
398
private transformFileContent(content: string, filePath: string): string {
399
let transformed = content;
400
401
// Normalize path for consistent comparison across platforms
402
const normalizedFilePath = this.normalizePath(filePath);
403
404
// Rewrite non-type imports of 'vscode' to use vscodeTypesShim
405
transformed = this.rewriteVscodeImports(transformed, normalizedFilePath);
406
407
// Rewrite imports from local vscodeTypes to use vscodeTypesShim
408
transformed = this.rewriteVscodeTypesImports(transformed, normalizedFilePath);
409
410
// Rewrite imports in test files: '../../node/chatLibMain' -> '../../../../main'
411
if (normalizedFilePath.startsWith('src/lib/vscode-node/test/')) {
412
transformed = transformed.replace(
413
/(from\s+['"])\.\.\/\.\.\/node\/chatLibMain(['"])/g,
414
'$1../../../../main$2'
415
);
416
}
417
418
// Only rewrite relative imports for main.ts (chatLibMain.ts)
419
if (normalizedFilePath === 'src/lib/node/chatLibMain.ts') {
420
transformed = transformed.replace(
421
/import\s+([^'"]*)\s+from\s+['"](\.\/[^'"]*|\.\.\/[^'"]*)['"]/g,
422
(match, importClause, importPath) => {
423
const rewrittenPath = this.rewriteImportPath(filePath, importPath);
424
return `import ${importClause} from '${rewrittenPath}'`;
425
}
426
);
427
}
428
429
return transformed;
430
}
431
432
private rewriteVscodeImports(content: string, filePath: string): string {
433
// Don't rewrite vscode imports in the main vscodeTypes.ts file
434
if (filePath === 'src/vscodeTypes.ts') {
435
return content;
436
}
437
438
// Pattern to match import statements from 'vscode'
439
// This regex captures:
440
// - import * as vscode from 'vscode'
441
// - import { Uri, window } from 'vscode'
442
// - import vscode from 'vscode'
443
// But NOT type-only imports like:
444
// - import type { Uri } from 'vscode'
445
// - import type * as vscode from 'vscode'
446
const vscodeImportRegex = /^(\s*import\s+)(?!type\s+)([^'"]*)\s+from\s+['"]vscode['"];?\s*$/gm;
447
448
return content.replace(vscodeImportRegex, (match, importPrefix, importClause) => {
449
// Calculate the relative path to vscodeTypesShim based on the current file location
450
const shimPath = this.getVscodeTypesShimPath(filePath);
451
return `${importPrefix}${importClause.trim()} from '${shimPath}';`;
452
});
453
}
454
455
private rewriteVscodeTypesImports(content: string, filePath: string): string {
456
// Don't rewrite vscodeTypes imports in the main vscodeTypes.ts file itself
457
if (filePath === 'src/vscodeTypes.ts') {
458
return content;
459
}
460
461
// Don't rewrite in the vscodeTypesShim file itself to avoid circular imports
462
if (filePath === 'src/util/common/test/shims/vscodeTypesShim.ts') {
463
return content;
464
}
465
466
// Pattern to match non-type imports from local vscodeTypes
467
// This regex captures imports like:
468
// - import { ChatErrorLevel } from '../../../vscodeTypes'
469
// - import * as vscodeTypes from '../../../vscodeTypes'
470
// But NOT type-only imports like:
471
// - import type { ChatErrorLevel } from '../../../vscodeTypes'
472
const vscodeTypesImportRegex = /^(\s*import\s+)(?!type\s+)([^'"]*)\s+from\s+['"]([^'"]*\/vscodeTypes)['"];?\s*$/gm;
473
474
return content.replace(vscodeTypesImportRegex, (match, importPrefix, importClause, importPath) => {
475
// Calculate the relative path to vscodeTypesShim based on the current file location
476
const shimPath = this.getVscodeTypesShimPath(filePath);
477
return `${importPrefix}${importClause.trim()} from '${shimPath}';`;
478
});
479
}
480
481
private getVscodeTypesShimPath(filePath: string): string {
482
// For main.ts (chatLibMain.ts), use the _internal structure
483
if (filePath === 'src/lib/node/chatLibMain.ts') {
484
return './_internal/util/common/test/shims/vscodeTypesShim';
485
}
486
487
// For other files, calculate relative path from their location to the shim
488
// The target shim location will be: _internal/util/common/test/shims/vscodeTypesShim
489
// Files are placed in: _internal/<original_path_without_src>
490
491
// Remove 'src/' prefix and calculate depth
492
const relativePath = filePath.replace(/^src\//, '');
493
const pathSegments = relativePath.split('/');
494
const depth = pathSegments.length - 1; // -1 because the last segment is the filename
495
496
// Go up 'depth' levels, then down to the shim
497
const upLevels = '../'.repeat(depth);
498
return `${upLevels}util/common/test/shims/vscodeTypesShim`;
499
}
500
501
private rewriteImportPath(fromFile: string, importPath: string): string {
502
// For main.ts, rewrite relative imports to use ./_internal structure
503
if (fromFile === 'src/lib/node/chatLibMain.ts') {
504
// Convert ../../extension/... to ./_internal/extension/...
505
// Convert ../../platform/... to ./_internal/platform/...
506
// Convert ../../util/... to ./_internal/util/...
507
return importPath.replace(/^\.\.\/\.\.\//, './_internal/');
508
}
509
510
// For other files, don't change the import path
511
return importPath;
512
}
513
514
private async generateModuleFiles(): Promise<void> {
515
console.log('Using static module files already present in chat-lib directory...');
516
517
// Copy main.ts from src/lib/node/chatLibMain.ts
518
const mainTsPath = path.join(REPO_ROOT, 'src', 'lib', 'node', 'chatLibMain.ts');
519
const mainTsContent = await fs.promises.readFile(mainTsPath, 'utf-8');
520
const transformedMainTs = this.transformFileContent(mainTsContent, 'src/lib/node/chatLibMain.ts');
521
await fs.promises.writeFile(path.join(TARGET_DIR, 'main.ts'), transformedMainTs);
522
523
// Copy root package.json to chat-lib/src
524
await this.copyRootPackageJson();
525
526
// Copy all vscode.proposed.*.d.ts files
527
await this.copyVSCodeProposedTypes();
528
529
// Copy all tiktoken files
530
await this.copyTikTokenFiles();
531
532
// Copy test reply files
533
await this.copyTestReplyFiles();
534
535
// Update chat-lib tsconfig.json with path mappings
536
await this.updateChatLibTsConfig();
537
}
538
539
private async copyTestReplyFiles(): Promise<void> {
540
console.log('Copying test reply files...');
541
542
// Find all .reply.txt files in src/lib/vscode-node/test/
543
const testDir = path.join(REPO_ROOT, 'src', 'lib', 'vscode-node', 'test');
544
const replyFiles = await glob('*.reply.txt', { cwd: testDir });
545
546
for (const file of replyFiles) {
547
const srcPath = path.join(testDir, file);
548
const destPath = path.join(TARGET_DIR, '_internal', 'lib', 'vscode-node', 'test', file);
549
550
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
551
await fs.promises.copyFile(srcPath, destPath);
552
}
553
554
console.log(`Copied ${replyFiles.length} test reply files`);
555
}
556
557
private async updateChatLibTsConfig(): Promise<void> {
558
console.log('Updating chat-lib tsconfig.json with path mappings...');
559
560
const chatLibTsconfigPath = path.join(CHAT_LIB_DIR, 'tsconfig.json');
561
const tsconfigContent = await fs.promises.readFile(chatLibTsconfigPath, 'utf-8');
562
const tsconfig = jsonc.parse(tsconfigContent);
563
564
// Ensure compilerOptions exists
565
if (!tsconfig.compilerOptions) {
566
tsconfig.compilerOptions = {};
567
}
568
569
// Ensure paths exists
570
if (!tsconfig.compilerOptions.paths) {
571
tsconfig.compilerOptions.paths = {};
572
}
573
574
// Read the root tsconfig once to check for wildcards
575
const rootTsconfigPath = path.join(REPO_ROOT, 'tsconfig.json');
576
const rootTsconfigContent = await fs.promises.readFile(rootTsconfigPath, 'utf-8');
577
const rootTsconfig = jsonc.parse(rootTsconfigContent);
578
579
// Add path mappings from the root tsconfig, adjusted for chat-lib structure
580
// The files are in src/_internal/... structure
581
for (const [alias, targetPath] of this.pathMappings.entries()) {
582
// Convert from root paths like "src/extension/completions-core/lib/src"
583
// to chat-lib paths like "./src/_internal/extension/completions-core/lib/src"
584
// Remove the "src/" prefix from targetPath since it's already part of the _internal structure
585
const pathWithoutSrc = targetPath.replace(/^src\//, '');
586
const chatLibPath = `./src/_internal/${pathWithoutSrc}`;
587
588
let aliasWithWildcard = alias;
589
let pathWithWildcard = chatLibPath;
590
591
// Check if the original mapping had a wildcard
592
if (rootTsconfig.compilerOptions?.paths) {
593
for (const key of Object.keys(rootTsconfig.compilerOptions.paths)) {
594
const keyWithoutWildcard = key.replace(/\/\*$/, '');
595
if (keyWithoutWildcard === alias && key.endsWith('/*')) {
596
aliasWithWildcard = alias + '/*';
597
pathWithWildcard = chatLibPath + '/*';
598
break;
599
}
600
}
601
}
602
603
tsconfig.compilerOptions.paths[aliasWithWildcard] = [pathWithWildcard];
604
}
605
606
// Write the updated tsconfig back
607
await fs.promises.writeFile(
608
chatLibTsconfigPath,
609
JSON.stringify(tsconfig, null, '\t') + '\n'
610
);
611
612
console.log('Chat-lib tsconfig.json updated with path mappings:', Object.keys(tsconfig.compilerOptions.paths));
613
}
614
615
private async validateModule(): Promise<void> {
616
console.log('Validating module...');
617
618
// Check if static files exist in chat-lib directory
619
const staticFiles = ['package.json', 'tsconfig.json', 'README.md', 'LICENSE.txt'];
620
for (const file of staticFiles) {
621
const filePath = path.join(CHAT_LIB_DIR, file);
622
if (!fs.existsSync(filePath)) {
623
throw new Error(`Required static file missing: ${file}`);
624
}
625
}
626
627
// Check if main.ts exists in src directory
628
const mainTsPath = path.join(TARGET_DIR, 'main.ts');
629
if (!fs.existsSync(mainTsPath)) {
630
throw new Error(`Required file missing: src/main.ts`);
631
}
632
633
console.log('Module validation passed!');
634
}
635
636
private async copyVSCodeProposedTypes(): Promise<void> {
637
console.log('Copying vscode*.d.ts files referenced by vscode-api.d.ts...');
638
639
const vscodeApiSrcPath = path.join(REPO_ROOT, 'src', 'extension', 'vscode-api.d.ts');
640
if (!fs.existsSync(vscodeApiSrcPath)) {
641
throw new Error(`vscode-api.d.ts not found at ${vscodeApiSrcPath}`);
642
}
643
644
const content = await fs.promises.readFile(vscodeApiSrcPath, 'utf-8');
645
646
// Parse all /// <reference path="..." /> directives
647
const refRegex = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;
648
let match;
649
const referencedFiles: { refPath: string; fileName: string }[] = [];
650
651
while ((match = refRegex.exec(content)) !== null) {
652
const refPath = match[1];
653
const fileName = path.basename(refPath);
654
referencedFiles.push({ refPath, fileName });
655
}
656
657
// Copy each referenced .d.ts file from the vscode repo
658
const vscodeDtsDestDir = path.join(TARGET_DIR, '_internal', 'vscode-dts');
659
await fs.promises.mkdir(vscodeDtsDestDir, { recursive: true });
660
661
for (const { refPath, fileName } of referencedFiles) {
662
// Resolve the reference path relative to the source file
663
const srcPath = path.resolve(path.dirname(vscodeApiSrcPath), refPath);
664
if (!fs.existsSync(srcPath)) {
665
console.warn(`Warning: Referenced file not found: ${srcPath}`);
666
continue;
667
}
668
669
const destPath = path.join(vscodeDtsDestDir, fileName);
670
await fs.promises.copyFile(srcPath, destPath);
671
}
672
673
// Copy vscode-api.d.ts itself, updating reference paths
674
const updatedContent = content.replace(
675
/\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g,
676
(_match, refPath: string) => {
677
const fileName = path.basename(refPath);
678
return `/// <reference path="./vscode-dts/${fileName}" />`;
679
}
680
);
681
682
const vscodeApiDestPath = path.join(TARGET_DIR, '_internal', 'vscode-api.d.ts');
683
await fs.promises.writeFile(vscodeApiDestPath, updatedContent);
684
685
// Also copy thenable.d.ts which is referenced by vscode.d.ts
686
const vscodeRepoRoot = path.join(REPO_ROOT, '..', '..');
687
const thenableSrcPath = path.join(vscodeRepoRoot, 'src', 'typings', 'thenable.d.ts');
688
if (fs.existsSync(thenableSrcPath)) {
689
await fs.promises.copyFile(thenableSrcPath, path.join(vscodeDtsDestDir, 'thenable.d.ts'));
690
console.log('Copied thenable.d.ts');
691
} else {
692
console.warn(`Warning: thenable.d.ts not found at ${thenableSrcPath}`);
693
}
694
695
console.log(`Copied vscode-api.d.ts and ${referencedFiles.length} referenced .d.ts files`);
696
}
697
698
private async copyTikTokenFiles(): Promise<void> {
699
console.log('Copying tiktoken files...');
700
701
// Find all .tiktoken files in src/platform/tokenizer/node/
702
const tokenizerDir = path.join(REPO_ROOT, 'src', 'platform', 'tokenizer', 'node');
703
const tikTokenFiles = await glob('*.tiktoken', { cwd: tokenizerDir });
704
705
for (const file of tikTokenFiles) {
706
const srcPath = path.join(tokenizerDir, file);
707
const destPath = path.join(TARGET_DIR, '_internal', 'platform', 'tokenizer', 'node', file);
708
709
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
710
await fs.promises.copyFile(srcPath, destPath);
711
}
712
713
console.log(`Copied ${tikTokenFiles.length} tiktoken files`);
714
}
715
716
private async copyRootPackageJson(): Promise<void> {
717
console.log('Copying root package.json to chat-lib/src...');
718
719
const srcPath = path.join(REPO_ROOT, 'package.json');
720
const destPath = path.join(TARGET_DIR, 'package.json');
721
722
await fs.promises.copyFile(srcPath, destPath);
723
console.log('Root package.json copied successfully!');
724
725
// Update chat-lib package.json dependencies
726
await this.updateChatLibDependencies();
727
}
728
729
private async updateChatLibDependencies(): Promise<void> {
730
console.log('Updating chat-lib package.json dependencies...');
731
732
const rootPackageJsonPath = path.join(REPO_ROOT, 'package.json');
733
const chatLibPackageJsonPath = path.join(CHAT_LIB_DIR, 'package.json');
734
const rootPackageLockPath = path.join(REPO_ROOT, 'package-lock.json');
735
const chatLibPackageLockPath = path.join(CHAT_LIB_DIR, 'package-lock.json');
736
737
// Read both package.json files
738
const rootPackageJson = JSON.parse(await fs.promises.readFile(rootPackageJsonPath, 'utf-8'));
739
const chatLibPackageJson = JSON.parse(await fs.promises.readFile(chatLibPackageJsonPath, 'utf-8'));
740
741
// Combine all dependencies and devDependencies from root
742
const rootDependencies = {
743
...(rootPackageJson.dependencies || {}),
744
...(rootPackageJson.devDependencies || {})
745
};
746
747
let updatedCount = 0;
748
let removedCount = 0;
749
const changes: string[] = [];
750
const updatedPackages = new Set<string>();
751
752
// Update existing dependencies in chat-lib with versions from root
753
for (const depType of ['dependencies', 'devDependencies']) {
754
if (chatLibPackageJson[depType]) {
755
const dependencyNames = Object.keys(chatLibPackageJson[depType]);
756
757
for (const depName of dependencyNames) {
758
if (rootDependencies[depName]) {
759
// Update version if it exists in root
760
const oldVersion = chatLibPackageJson[depType][depName];
761
const newVersion = rootDependencies[depName];
762
763
if (oldVersion !== newVersion) {
764
chatLibPackageJson[depType][depName] = newVersion;
765
changes.push(` Updated ${depName}: ${oldVersion} → ${newVersion}`);
766
updatedCount++;
767
updatedPackages.add(depName);
768
}
769
} else {
770
// Remove dependency if it no longer exists in root
771
delete chatLibPackageJson[depType][depName];
772
changes.push(` Removed ${depName} (no longer in root package.json)`);
773
removedCount++;
774
}
775
}
776
777
// Clean up empty dependency objects
778
if (Object.keys(chatLibPackageJson[depType]).length === 0) {
779
delete chatLibPackageJson[depType];
780
}
781
}
782
}
783
784
// Write the updated chat-lib package.json
785
await fs.promises.writeFile(
786
chatLibPackageJsonPath,
787
JSON.stringify(chatLibPackageJson, null, '\t') + '\n'
788
);
789
790
console.log(`Chat-lib dependencies updated: ${updatedCount} updated, ${removedCount} removed`);
791
if (changes.length > 0) {
792
console.log('Changes made:');
793
changes.forEach(change => console.log(change));
794
}
795
796
// Update package-lock.json for changed dependencies and their transitive dependencies
797
if (updatedPackages.size > 0 && fs.existsSync(rootPackageLockPath) && fs.existsSync(chatLibPackageLockPath)) {
798
console.log('Updating chat-lib package-lock.json for changed dependencies...');
799
800
const rootPackageLock = JSON.parse(await fs.promises.readFile(rootPackageLockPath, 'utf-8'));
801
const chatLibPackageLock = JSON.parse(await fs.promises.readFile(chatLibPackageLockPath, 'utf-8'));
802
803
// Update the root package entry with new dependencies
804
if (chatLibPackageLock.packages && chatLibPackageLock.packages['']) {
805
chatLibPackageLock.packages[''].dependencies = chatLibPackageJson.dependencies || {};
806
chatLibPackageLock.packages[''].devDependencies = chatLibPackageJson.devDependencies || {};
807
}
808
809
// Collect all packages to update (direct dependencies + their transitive dependencies)
810
const packagesToUpdate = new Set<string>();
811
const queue: string[] = [];
812
813
// Start with updated packages
814
for (const pkgName of updatedPackages) {
815
const pkgPath = `node_modules/${pkgName}`;
816
queue.push(pkgPath);
817
packagesToUpdate.add(pkgPath);
818
}
819
820
// Traverse dependency tree from root package-lock to find all transitive dependencies
821
while (queue.length > 0) {
822
const pkgPath = queue.shift()!;
823
const pkgInfo = rootPackageLock.packages?.[pkgPath];
824
825
if (pkgInfo) {
826
// Collect all dependency types
827
const deps = {
828
...pkgInfo.dependencies,
829
...pkgInfo.optionalDependencies,
830
...pkgInfo.devDependencies
831
};
832
833
for (const depName of Object.keys(deps)) {
834
// Handle nested dependencies
835
const nestedDepPath = `${pkgPath}/node_modules/${depName}`;
836
const topLevelDepPath = `node_modules/${depName}`;
837
838
let actualDepPath: string | null = null;
839
if (rootPackageLock.packages[nestedDepPath]) {
840
actualDepPath = nestedDepPath;
841
} else if (rootPackageLock.packages[topLevelDepPath]) {
842
actualDepPath = topLevelDepPath;
843
} else {
844
// Walk up the parent chain
845
const pathParts = pkgPath.split('/node_modules/');
846
for (let i = pathParts.length - 1; i >= 0; i--) {
847
const parentPath = pathParts.slice(0, i).join('/node_modules/');
848
const candidatePath = parentPath ? `${parentPath}/node_modules/${depName}` : `node_modules/${depName}`;
849
if (rootPackageLock.packages[candidatePath]) {
850
actualDepPath = candidatePath;
851
break;
852
}
853
}
854
}
855
856
if (actualDepPath && !packagesToUpdate.has(actualDepPath)) {
857
packagesToUpdate.add(actualDepPath);
858
queue.push(actualDepPath);
859
}
860
}
861
}
862
}
863
864
// Update package entries in chat-lib lock file
865
let lockUpdatedCount = 0;
866
for (const pkgPath of packagesToUpdate) {
867
if (rootPackageLock.packages[pkgPath] && chatLibPackageLock.packages[pkgPath]) {
868
chatLibPackageLock.packages[pkgPath] = rootPackageLock.packages[pkgPath];
869
lockUpdatedCount++;
870
}
871
}
872
873
// Write the updated chat-lib package-lock.json
874
await fs.promises.writeFile(
875
chatLibPackageLockPath,
876
JSON.stringify(chatLibPackageLock, null, '\t') + '\n'
877
);
878
879
console.log(`Chat-lib package-lock.json updated: ${lockUpdatedCount} package entries updated`);
880
}
881
}
882
883
private async compileTypeScript(): Promise<void> {
884
console.log('Compiling TypeScript to validate module...');
885
886
try {
887
// Change to the chat-lib directory and run TypeScript compiler
888
const { stdout, stderr } = await execAsync('npx tsc --noEmit', {
889
cwd: CHAT_LIB_DIR,
890
timeout: 60000 // 60 second timeout
891
});
892
893
if (stderr) {
894
console.warn('TypeScript compilation warnings:', stderr);
895
}
896
897
console.log('TypeScript compilation successful!');
898
} catch (error: any) {
899
console.error('TypeScript compilation failed:', error.stdout || error.message);
900
throw new Error(`TypeScript compilation failed: ${error.stdout || error.message}`);
901
}
902
}
903
}
904
905
// Main execution
906
async function main(): Promise<void> {
907
try {
908
const extractor = new ChatLibExtractor();
909
await extractor.extract();
910
} catch (error) {
911
console.error('Extraction failed:', error);
912
process.exit(1);
913
}
914
}
915
916
if (require.main === module) {
917
main();
918
}
919
920