Path: blob/main/extensions/copilot/script/build/extractChatLib.ts
13389 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { exec } from 'child_process';6import * as fs from 'fs';7import { glob } from 'glob';8import * as jsonc from 'jsonc-parser';9import * as path from 'path';10import { promisify } from 'util';1112const REPO_ROOT = path.join(__dirname, '..', '..');13const CHAT_LIB_DIR = path.join(REPO_ROOT, 'chat-lib');14const TARGET_DIR = path.join(CHAT_LIB_DIR, 'src');15const execAsync = promisify(exec);1617// Entry point - follow imports from the main chat-lib file18// Note: All *.ts files in src/lib/node/test/ are automatically included19const entryPoints = [20'src/lib/node/chatLibMain.ts',21'src/util/vs/base-common.d.ts',22'src/util/vs/vscode-globals-nls.d.ts',23'src/util/vs/vscode-globals-product.d.ts',24'src/util/common/globals.d.ts',25'src/util/common/test/shims/vscodeTypesShim.ts',26'src/platform/diff/common/diffWorker.ts',27'src/platform/tokenizer/node/tikTokenizerWorker.ts',28// For tests:29'src/platform/authentication/test/node/simulationTestCopilotTokenManager.ts',30'src/extension/completions-core/vscode-node/lib/src/test/textDocument.ts',31];3233interface FileInfo {34srcPath: string;35destPath: string;36relativePath: string;37dependencies: string[];38}3940class ChatLibExtractor {41private processedFiles = new Set<string>();42private allFiles = new Map<string, FileInfo>();43private pathMappings: Map<string, string> = new Map();4445async extract(): Promise<void> {46// Load path mappings from tsconfig.json47await this.loadPathMappings();48console.log('Starting chat-lib extraction...');4950// Clean target directory51await this.cleanTargetDir();5253// Process entry points and their dependencies54await this.processEntryPoints();5556// Copy all processed files57await this.copyFiles();5859// Use static module files60await this.generateModuleFiles();6162// Validate the module63await this.validateModule();6465// Compile TypeScript to validate66await this.compileTypeScript();6768console.log('Chat-lib extraction completed successfully!');69}7071private async loadPathMappings(): Promise<void> {72const tsconfigPath = path.join(REPO_ROOT, 'tsconfig.json');73const tsconfigContent = await fs.promises.readFile(tsconfigPath, 'utf-8');74const tsconfig = jsonc.parse(tsconfigContent);7576if (tsconfig.compilerOptions?.paths) {77for (const [alias, targets] of Object.entries(tsconfig.compilerOptions.paths)) {78// Skip the 'vscode' mapping as it's handled separately79if (alias === 'vscode') {80continue;81}8283// Handle path mappings like "#lib/*" -> ["./src/extension/completions-core/lib/src/*"]84// and "#types" -> ["./src/extension/completions-core/types/src"]85if (Array.isArray(targets) && targets.length > 0) {86const target = targets[0]; // Use the first target87// Remove leading './' and trailing '/*' if present88const cleanTarget = target.replace(/^\.\//, '').replace(/\/\*$/, '');89const cleanAlias = alias.replace(/\/\*$/, '');90this.pathMappings.set(cleanAlias, cleanTarget);91}92}93}9495console.log('Loaded path mappings:', Array.from(this.pathMappings.entries()));96}9798private async cleanTargetDir(): Promise<void> {99// Remove and recreate the src directory100if (fs.existsSync(TARGET_DIR)) {101await fs.promises.rm(TARGET_DIR, { recursive: true, force: true });102}103await fs.promises.mkdir(TARGET_DIR, { recursive: true });104}105106private async processEntryPoints(): Promise<void> {107console.log('Processing entry points and dependencies...');108109// Start with static entry points and dynamically add all test files110const testFiles = await glob('src/lib/vscode-node/test/*.ts', { cwd: REPO_ROOT });111const queue = [...entryPoints, ...testFiles];112113while (queue.length > 0) {114const filePath = queue.shift()!;115if (this.processedFiles.has(filePath)) {116continue;117}118119const fullPath = path.join(REPO_ROOT, filePath);120if (!fs.existsSync(fullPath)) {121console.warn(`Warning: File not found: ${filePath}`);122continue;123}124125const dependencies = await this.extractDependencies(fullPath);126const destPath = this.getDestinationPath(filePath);127128this.allFiles.set(filePath, {129srcPath: fullPath,130destPath,131relativePath: filePath,132dependencies133});134135this.processedFiles.add(filePath);136137// Add dependencies to queue138dependencies.forEach(dep => {139if (!this.processedFiles.has(dep)) {140queue.push(dep);141}142});143}144}145146private async extractDependencies(filePath: string): Promise<string[]> {147const content = await fs.promises.readFile(filePath, 'utf-8');148const dependencies: string[] = [];149150// Remove single-line comments and process line by line to avoid matching commented imports151// We need to be careful not to remove strings that contain '//'152const lines = content.split('\n');153const activeLines: string[] = [];154let inBlockComment = false;155156for (const line of lines) {157// Track block comments158if (line.trim().startsWith('/*')) {159// preserve pragmas in tsx files160if (!(filePath.endsWith('.tsx') && line.match(/\/\*\*\s+@jsxImportSource\s+\S+/))) {161inBlockComment = true;162}163}164if (inBlockComment) {165if (line.includes('*/')) {166inBlockComment = false;167}168continue;169}170171// Skip single-line comments172const trimmedLine = line.trim();173if (trimmedLine.startsWith('//')) {174continue;175}176177// For lines that might have inline comments, we need to preserve string content178// Remove comments that are not inside strings179let processedLine = line;180// Simple heuristic: if the line contains import/export, keep everything up to //181// that's outside of string literals182if (trimmedLine.includes('import') || trimmedLine.includes('export')) {183// Remove inline comments (this is a simple approach - could be improved)184const commentIndex = line.indexOf('//');185if (commentIndex !== -1) {186// Check if // is inside a string by counting quotes before it187const beforeComment = line.substring(0, commentIndex);188const singleQuotes = (beforeComment.match(/'/g) || []).length;189const doubleQuotes = (beforeComment.match(/"/g) || []).length;190// If even number of quotes, the comment is outside strings191if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0) {192processedLine = beforeComment;193}194}195}196197activeLines.push(processedLine);198}199200const activeContent = activeLines.join('\n');201202// Extract both import and export statements using regex203// Matches:204// - import ... from './path'205// - export ... from './path'206// - export { ... } from './path'207// Updated regex to match all relative imports (including multiple ../ segments)208const relativeImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"](\.\.?\/[^'"]*)['"]/g;209let match;210211while ((match = relativeImportRegex.exec(activeContent)) !== null) {212const importPath = match[1];213const resolvedPath = this.resolveImportPath(filePath, importPath);214215if (resolvedPath) {216dependencies.push(resolvedPath);217}218}219220// Also match path alias imports like: import ... from '#lib/...' or '#types'221// We need to resolve these to follow their dependencies222const aliasImportRegex = /(?:import(?:\s+type)?|export)\s+(?:(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?|\w+)\s+from\s+)?['"]([#][^'"]*)['"]/g;223224while ((match = aliasImportRegex.exec(activeContent)) !== null) {225const importPath = match[1];226const resolvedPath = this.resolvePathAlias(importPath);227228if (resolvedPath) {229dependencies.push(resolvedPath);230}231}232233// For tsx files process JSX imports as well234if (filePath.endsWith('.tsx')) {235const jsxRelativeImportRegex = /\/\*\*\s+@jsxImportSource\s+(\.\.?\/\S+)\s+\*\//g;236237while ((match = jsxRelativeImportRegex.exec(activeContent)) !== null) {238const importPath = match[1];239const resolvedPath = this.resolveImportPath(filePath, path.join(importPath, 'jsx-runtime'));240241if (resolvedPath) {242dependencies.push(resolvedPath);243}244}245}246247return dependencies;248}249250private resolvePathAlias(importPath: string): string | null {251// Handle path alias imports like '#lib/foo' or '#types'252// Find the matching alias by checking if the import starts with any registered alias253for (const [alias, targetPath] of this.pathMappings.entries()) {254if (importPath === alias) {255// Exact match for aliases without wildcards (e.g., '#types')256return this.resolveFileWithExtensions(path.join(REPO_ROOT, targetPath));257} else if (importPath.startsWith(alias + '/')) {258// Wildcard match for aliases with /* (e.g., '#lib/foo' matches '#lib')259const remainder = importPath.substring(alias.length + 1); // +1 to skip the '/'260const fullPath = path.join(REPO_ROOT, targetPath, remainder);261return this.resolveFileWithExtensions(fullPath);262}263}264265// If no alias matched, return null266console.warn(`Warning: Path alias not found for: ${importPath}`);267return null;268}269270private resolveFileWithExtensions(basePath: string): string | null {271// Try with .ts extension272if (fs.existsSync(basePath + '.ts')) {273return this.normalizePath(path.relative(REPO_ROOT, basePath + '.ts'));274}275276// Try with .tsx extension277if (fs.existsSync(basePath + '.tsx')) {278return this.normalizePath(path.relative(REPO_ROOT, basePath + '.tsx'));279}280281// Try with .d.ts extension282if (fs.existsSync(basePath + '.d.ts')) {283return this.normalizePath(path.relative(REPO_ROOT, basePath + '.d.ts'));284}285286// Try with index.ts287if (fs.existsSync(path.join(basePath, 'index.ts'))) {288return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.ts')));289}290291// Try with index.tsx292if (fs.existsSync(path.join(basePath, 'index.tsx'))) {293return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.tsx')));294}295296// Try with index.d.ts297if (fs.existsSync(path.join(basePath, 'index.d.ts'))) {298return this.normalizePath(path.relative(REPO_ROOT, path.join(basePath, 'index.d.ts')));299}300301// Try as-is302if (fs.existsSync(basePath)) {303return this.normalizePath(path.relative(REPO_ROOT, basePath));304}305306return null;307}308309private resolveImportPath(fromFile: string, importPath: string): string | null {310const fromDir = path.dirname(fromFile);311const resolved = path.resolve(fromDir, importPath);312313// If import path ends with .js, try replacing with .ts/.tsx first314if (importPath.endsWith('.js')) {315const baseResolved = resolved.slice(0, -3); // Remove .js316if (fs.existsSync(baseResolved + '.ts')) {317return this.normalizePath(path.relative(REPO_ROOT, baseResolved + '.ts'));318}319if (fs.existsSync(baseResolved + '.tsx')) {320return this.normalizePath(path.relative(REPO_ROOT, baseResolved + '.tsx'));321}322}323324// Try with .ts extension325if (fs.existsSync(resolved + '.ts')) {326return this.normalizePath(path.relative(REPO_ROOT, resolved + '.ts'));327}328329// Try with .tsx extension330if (fs.existsSync(resolved + '.tsx')) {331return this.normalizePath(path.relative(REPO_ROOT, resolved + '.tsx'));332}333334// Try with .d.ts extension335if (fs.existsSync(resolved + '.d.ts')) {336return this.normalizePath(path.relative(REPO_ROOT, resolved + '.d.ts'));337}338339// Try with index.ts340if (fs.existsSync(path.join(resolved, 'index.ts'))) {341return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.ts')));342}343344// Try with index.tsx345if (fs.existsSync(path.join(resolved, 'index.tsx'))) {346return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.tsx')));347}348349// Try with index.d.ts350if (fs.existsSync(path.join(resolved, 'index.d.ts'))) {351return this.normalizePath(path.relative(REPO_ROOT, path.join(resolved, 'index.d.ts')));352}353354// Try as-is355if (fs.existsSync(resolved)) {356return this.normalizePath(path.relative(REPO_ROOT, resolved));357}358359// If we get here, the file was not found - throw an error360throw new Error(`Import file not found: ${importPath} (resolved to ${resolved}) imported from ${fromFile}`);361}362363private normalizePath(filePath: string): string {364// Normalize path separators to forward slashes for consistency across platforms365return filePath.replace(/\\/g, '/');366}367368private getDestinationPath(filePath: string): string {369// Normalize the input path first, then convert src/... to _internal/...370const normalizedPath = this.normalizePath(filePath);371const relativePath = normalizedPath.replace(/^src\//, '_internal/');372return path.join(TARGET_DIR, relativePath);373}374375private async copyFiles(): Promise<void> {376console.log(`Copying ${this.allFiles.size} files...`);377378for (const [, fileInfo] of this.allFiles) {379// Skip the main entry point file since it becomes top-level main.ts380if (fileInfo.relativePath === 'src/lib/node/chatLibMain.ts') {381continue;382}383384await fs.promises.mkdir(path.dirname(fileInfo.destPath), { recursive: true }); // Read source file385const content = await fs.promises.readFile(fileInfo.srcPath, 'utf-8');386387// Transform content to replace vscode imports and fix relative paths388const transformedContent = this.transformFileContent(content, fileInfo.relativePath);389390// Write to destination391await fs.promises.writeFile(fileInfo.destPath, transformedContent);392}393}394395396397private transformFileContent(content: string, filePath: string): string {398let transformed = content;399400// Normalize path for consistent comparison across platforms401const normalizedFilePath = this.normalizePath(filePath);402403// Rewrite non-type imports of 'vscode' to use vscodeTypesShim404transformed = this.rewriteVscodeImports(transformed, normalizedFilePath);405406// Rewrite imports from local vscodeTypes to use vscodeTypesShim407transformed = this.rewriteVscodeTypesImports(transformed, normalizedFilePath);408409// Rewrite imports in test files: '../../node/chatLibMain' -> '../../../../main'410if (normalizedFilePath.startsWith('src/lib/vscode-node/test/')) {411transformed = transformed.replace(412/(from\s+['"])\.\.\/\.\.\/node\/chatLibMain(['"])/g,413'$1../../../../main$2'414);415}416417// Only rewrite relative imports for main.ts (chatLibMain.ts)418if (normalizedFilePath === 'src/lib/node/chatLibMain.ts') {419transformed = transformed.replace(420/import\s+([^'"]*)\s+from\s+['"](\.\/[^'"]*|\.\.\/[^'"]*)['"]/g,421(match, importClause, importPath) => {422const rewrittenPath = this.rewriteImportPath(filePath, importPath);423return `import ${importClause} from '${rewrittenPath}'`;424}425);426}427428return transformed;429}430431private rewriteVscodeImports(content: string, filePath: string): string {432// Don't rewrite vscode imports in the main vscodeTypes.ts file433if (filePath === 'src/vscodeTypes.ts') {434return content;435}436437// Pattern to match import statements from 'vscode'438// This regex captures:439// - import * as vscode from 'vscode'440// - import { Uri, window } from 'vscode'441// - import vscode from 'vscode'442// But NOT type-only imports like:443// - import type { Uri } from 'vscode'444// - import type * as vscode from 'vscode'445const vscodeImportRegex = /^(\s*import\s+)(?!type\s+)([^'"]*)\s+from\s+['"]vscode['"];?\s*$/gm;446447return content.replace(vscodeImportRegex, (match, importPrefix, importClause) => {448// Calculate the relative path to vscodeTypesShim based on the current file location449const shimPath = this.getVscodeTypesShimPath(filePath);450return `${importPrefix}${importClause.trim()} from '${shimPath}';`;451});452}453454private rewriteVscodeTypesImports(content: string, filePath: string): string {455// Don't rewrite vscodeTypes imports in the main vscodeTypes.ts file itself456if (filePath === 'src/vscodeTypes.ts') {457return content;458}459460// Don't rewrite in the vscodeTypesShim file itself to avoid circular imports461if (filePath === 'src/util/common/test/shims/vscodeTypesShim.ts') {462return content;463}464465// Pattern to match non-type imports from local vscodeTypes466// This regex captures imports like:467// - import { ChatErrorLevel } from '../../../vscodeTypes'468// - import * as vscodeTypes from '../../../vscodeTypes'469// But NOT type-only imports like:470// - import type { ChatErrorLevel } from '../../../vscodeTypes'471const vscodeTypesImportRegex = /^(\s*import\s+)(?!type\s+)([^'"]*)\s+from\s+['"]([^'"]*\/vscodeTypes)['"];?\s*$/gm;472473return content.replace(vscodeTypesImportRegex, (match, importPrefix, importClause, importPath) => {474// Calculate the relative path to vscodeTypesShim based on the current file location475const shimPath = this.getVscodeTypesShimPath(filePath);476return `${importPrefix}${importClause.trim()} from '${shimPath}';`;477});478}479480private getVscodeTypesShimPath(filePath: string): string {481// For main.ts (chatLibMain.ts), use the _internal structure482if (filePath === 'src/lib/node/chatLibMain.ts') {483return './_internal/util/common/test/shims/vscodeTypesShim';484}485486// For other files, calculate relative path from their location to the shim487// The target shim location will be: _internal/util/common/test/shims/vscodeTypesShim488// Files are placed in: _internal/<original_path_without_src>489490// Remove 'src/' prefix and calculate depth491const relativePath = filePath.replace(/^src\//, '');492const pathSegments = relativePath.split('/');493const depth = pathSegments.length - 1; // -1 because the last segment is the filename494495// Go up 'depth' levels, then down to the shim496const upLevels = '../'.repeat(depth);497return `${upLevels}util/common/test/shims/vscodeTypesShim`;498}499500private rewriteImportPath(fromFile: string, importPath: string): string {501// For main.ts, rewrite relative imports to use ./_internal structure502if (fromFile === 'src/lib/node/chatLibMain.ts') {503// Convert ../../extension/... to ./_internal/extension/...504// Convert ../../platform/... to ./_internal/platform/...505// Convert ../../util/... to ./_internal/util/...506return importPath.replace(/^\.\.\/\.\.\//, './_internal/');507}508509// For other files, don't change the import path510return importPath;511}512513private async generateModuleFiles(): Promise<void> {514console.log('Using static module files already present in chat-lib directory...');515516// Copy main.ts from src/lib/node/chatLibMain.ts517const mainTsPath = path.join(REPO_ROOT, 'src', 'lib', 'node', 'chatLibMain.ts');518const mainTsContent = await fs.promises.readFile(mainTsPath, 'utf-8');519const transformedMainTs = this.transformFileContent(mainTsContent, 'src/lib/node/chatLibMain.ts');520await fs.promises.writeFile(path.join(TARGET_DIR, 'main.ts'), transformedMainTs);521522// Copy root package.json to chat-lib/src523await this.copyRootPackageJson();524525// Copy all vscode.proposed.*.d.ts files526await this.copyVSCodeProposedTypes();527528// Copy all tiktoken files529await this.copyTikTokenFiles();530531// Copy test reply files532await this.copyTestReplyFiles();533534// Update chat-lib tsconfig.json with path mappings535await this.updateChatLibTsConfig();536}537538private async copyTestReplyFiles(): Promise<void> {539console.log('Copying test reply files...');540541// Find all .reply.txt files in src/lib/vscode-node/test/542const testDir = path.join(REPO_ROOT, 'src', 'lib', 'vscode-node', 'test');543const replyFiles = await glob('*.reply.txt', { cwd: testDir });544545for (const file of replyFiles) {546const srcPath = path.join(testDir, file);547const destPath = path.join(TARGET_DIR, '_internal', 'lib', 'vscode-node', 'test', file);548549await fs.promises.mkdir(path.dirname(destPath), { recursive: true });550await fs.promises.copyFile(srcPath, destPath);551}552553console.log(`Copied ${replyFiles.length} test reply files`);554}555556private async updateChatLibTsConfig(): Promise<void> {557console.log('Updating chat-lib tsconfig.json with path mappings...');558559const chatLibTsconfigPath = path.join(CHAT_LIB_DIR, 'tsconfig.json');560const tsconfigContent = await fs.promises.readFile(chatLibTsconfigPath, 'utf-8');561const tsconfig = jsonc.parse(tsconfigContent);562563// Ensure compilerOptions exists564if (!tsconfig.compilerOptions) {565tsconfig.compilerOptions = {};566}567568// Ensure paths exists569if (!tsconfig.compilerOptions.paths) {570tsconfig.compilerOptions.paths = {};571}572573// Read the root tsconfig once to check for wildcards574const rootTsconfigPath = path.join(REPO_ROOT, 'tsconfig.json');575const rootTsconfigContent = await fs.promises.readFile(rootTsconfigPath, 'utf-8');576const rootTsconfig = jsonc.parse(rootTsconfigContent);577578// Add path mappings from the root tsconfig, adjusted for chat-lib structure579// The files are in src/_internal/... structure580for (const [alias, targetPath] of this.pathMappings.entries()) {581// Convert from root paths like "src/extension/completions-core/lib/src"582// to chat-lib paths like "./src/_internal/extension/completions-core/lib/src"583// Remove the "src/" prefix from targetPath since it's already part of the _internal structure584const pathWithoutSrc = targetPath.replace(/^src\//, '');585const chatLibPath = `./src/_internal/${pathWithoutSrc}`;586587let aliasWithWildcard = alias;588let pathWithWildcard = chatLibPath;589590// Check if the original mapping had a wildcard591if (rootTsconfig.compilerOptions?.paths) {592for (const key of Object.keys(rootTsconfig.compilerOptions.paths)) {593const keyWithoutWildcard = key.replace(/\/\*$/, '');594if (keyWithoutWildcard === alias && key.endsWith('/*')) {595aliasWithWildcard = alias + '/*';596pathWithWildcard = chatLibPath + '/*';597break;598}599}600}601602tsconfig.compilerOptions.paths[aliasWithWildcard] = [pathWithWildcard];603}604605// Write the updated tsconfig back606await fs.promises.writeFile(607chatLibTsconfigPath,608JSON.stringify(tsconfig, null, '\t') + '\n'609);610611console.log('Chat-lib tsconfig.json updated with path mappings:', Object.keys(tsconfig.compilerOptions.paths));612}613614private async validateModule(): Promise<void> {615console.log('Validating module...');616617// Check if static files exist in chat-lib directory618const staticFiles = ['package.json', 'tsconfig.json', 'README.md', 'LICENSE.txt'];619for (const file of staticFiles) {620const filePath = path.join(CHAT_LIB_DIR, file);621if (!fs.existsSync(filePath)) {622throw new Error(`Required static file missing: ${file}`);623}624}625626// Check if main.ts exists in src directory627const mainTsPath = path.join(TARGET_DIR, 'main.ts');628if (!fs.existsSync(mainTsPath)) {629throw new Error(`Required file missing: src/main.ts`);630}631632console.log('Module validation passed!');633}634635private async copyVSCodeProposedTypes(): Promise<void> {636console.log('Copying vscode*.d.ts files referenced by vscode-api.d.ts...');637638const vscodeApiSrcPath = path.join(REPO_ROOT, 'src', 'extension', 'vscode-api.d.ts');639if (!fs.existsSync(vscodeApiSrcPath)) {640throw new Error(`vscode-api.d.ts not found at ${vscodeApiSrcPath}`);641}642643const content = await fs.promises.readFile(vscodeApiSrcPath, 'utf-8');644645// Parse all /// <reference path="..." /> directives646const refRegex = /\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g;647let match;648const referencedFiles: { refPath: string; fileName: string }[] = [];649650while ((match = refRegex.exec(content)) !== null) {651const refPath = match[1];652const fileName = path.basename(refPath);653referencedFiles.push({ refPath, fileName });654}655656// Copy each referenced .d.ts file from the vscode repo657const vscodeDtsDestDir = path.join(TARGET_DIR, '_internal', 'vscode-dts');658await fs.promises.mkdir(vscodeDtsDestDir, { recursive: true });659660for (const { refPath, fileName } of referencedFiles) {661// Resolve the reference path relative to the source file662const srcPath = path.resolve(path.dirname(vscodeApiSrcPath), refPath);663if (!fs.existsSync(srcPath)) {664console.warn(`Warning: Referenced file not found: ${srcPath}`);665continue;666}667668const destPath = path.join(vscodeDtsDestDir, fileName);669await fs.promises.copyFile(srcPath, destPath);670}671672// Copy vscode-api.d.ts itself, updating reference paths673const updatedContent = content.replace(674/\/\/\/\s*<reference\s+path="([^"]+)"\s*\/>/g,675(_match, refPath: string) => {676const fileName = path.basename(refPath);677return `/// <reference path="./vscode-dts/${fileName}" />`;678}679);680681const vscodeApiDestPath = path.join(TARGET_DIR, '_internal', 'vscode-api.d.ts');682await fs.promises.writeFile(vscodeApiDestPath, updatedContent);683684// Also copy thenable.d.ts which is referenced by vscode.d.ts685const vscodeRepoRoot = path.join(REPO_ROOT, '..', '..');686const thenableSrcPath = path.join(vscodeRepoRoot, 'src', 'typings', 'thenable.d.ts');687if (fs.existsSync(thenableSrcPath)) {688await fs.promises.copyFile(thenableSrcPath, path.join(vscodeDtsDestDir, 'thenable.d.ts'));689console.log('Copied thenable.d.ts');690} else {691console.warn(`Warning: thenable.d.ts not found at ${thenableSrcPath}`);692}693694console.log(`Copied vscode-api.d.ts and ${referencedFiles.length} referenced .d.ts files`);695}696697private async copyTikTokenFiles(): Promise<void> {698console.log('Copying tiktoken files...');699700// Find all .tiktoken files in src/platform/tokenizer/node/701const tokenizerDir = path.join(REPO_ROOT, 'src', 'platform', 'tokenizer', 'node');702const tikTokenFiles = await glob('*.tiktoken', { cwd: tokenizerDir });703704for (const file of tikTokenFiles) {705const srcPath = path.join(tokenizerDir, file);706const destPath = path.join(TARGET_DIR, '_internal', 'platform', 'tokenizer', 'node', file);707708await fs.promises.mkdir(path.dirname(destPath), { recursive: true });709await fs.promises.copyFile(srcPath, destPath);710}711712console.log(`Copied ${tikTokenFiles.length} tiktoken files`);713}714715private async copyRootPackageJson(): Promise<void> {716console.log('Copying root package.json to chat-lib/src...');717718const srcPath = path.join(REPO_ROOT, 'package.json');719const destPath = path.join(TARGET_DIR, 'package.json');720721await fs.promises.copyFile(srcPath, destPath);722console.log('Root package.json copied successfully!');723724// Update chat-lib package.json dependencies725await this.updateChatLibDependencies();726}727728private async updateChatLibDependencies(): Promise<void> {729console.log('Updating chat-lib package.json dependencies...');730731const rootPackageJsonPath = path.join(REPO_ROOT, 'package.json');732const chatLibPackageJsonPath = path.join(CHAT_LIB_DIR, 'package.json');733const rootPackageLockPath = path.join(REPO_ROOT, 'package-lock.json');734const chatLibPackageLockPath = path.join(CHAT_LIB_DIR, 'package-lock.json');735736// Read both package.json files737const rootPackageJson = JSON.parse(await fs.promises.readFile(rootPackageJsonPath, 'utf-8'));738const chatLibPackageJson = JSON.parse(await fs.promises.readFile(chatLibPackageJsonPath, 'utf-8'));739740// Combine all dependencies and devDependencies from root741const rootDependencies = {742...(rootPackageJson.dependencies || {}),743...(rootPackageJson.devDependencies || {})744};745746let updatedCount = 0;747let removedCount = 0;748const changes: string[] = [];749const updatedPackages = new Set<string>();750751// Update existing dependencies in chat-lib with versions from root752for (const depType of ['dependencies', 'devDependencies']) {753if (chatLibPackageJson[depType]) {754const dependencyNames = Object.keys(chatLibPackageJson[depType]);755756for (const depName of dependencyNames) {757if (rootDependencies[depName]) {758// Update version if it exists in root759const oldVersion = chatLibPackageJson[depType][depName];760const newVersion = rootDependencies[depName];761762if (oldVersion !== newVersion) {763chatLibPackageJson[depType][depName] = newVersion;764changes.push(` Updated ${depName}: ${oldVersion} → ${newVersion}`);765updatedCount++;766updatedPackages.add(depName);767}768} else {769// Remove dependency if it no longer exists in root770delete chatLibPackageJson[depType][depName];771changes.push(` Removed ${depName} (no longer in root package.json)`);772removedCount++;773}774}775776// Clean up empty dependency objects777if (Object.keys(chatLibPackageJson[depType]).length === 0) {778delete chatLibPackageJson[depType];779}780}781}782783// Write the updated chat-lib package.json784await fs.promises.writeFile(785chatLibPackageJsonPath,786JSON.stringify(chatLibPackageJson, null, '\t') + '\n'787);788789console.log(`Chat-lib dependencies updated: ${updatedCount} updated, ${removedCount} removed`);790if (changes.length > 0) {791console.log('Changes made:');792changes.forEach(change => console.log(change));793}794795// Update package-lock.json for changed dependencies and their transitive dependencies796if (updatedPackages.size > 0 && fs.existsSync(rootPackageLockPath) && fs.existsSync(chatLibPackageLockPath)) {797console.log('Updating chat-lib package-lock.json for changed dependencies...');798799const rootPackageLock = JSON.parse(await fs.promises.readFile(rootPackageLockPath, 'utf-8'));800const chatLibPackageLock = JSON.parse(await fs.promises.readFile(chatLibPackageLockPath, 'utf-8'));801802// Update the root package entry with new dependencies803if (chatLibPackageLock.packages && chatLibPackageLock.packages['']) {804chatLibPackageLock.packages[''].dependencies = chatLibPackageJson.dependencies || {};805chatLibPackageLock.packages[''].devDependencies = chatLibPackageJson.devDependencies || {};806}807808// Collect all packages to update (direct dependencies + their transitive dependencies)809const packagesToUpdate = new Set<string>();810const queue: string[] = [];811812// Start with updated packages813for (const pkgName of updatedPackages) {814const pkgPath = `node_modules/${pkgName}`;815queue.push(pkgPath);816packagesToUpdate.add(pkgPath);817}818819// Traverse dependency tree from root package-lock to find all transitive dependencies820while (queue.length > 0) {821const pkgPath = queue.shift()!;822const pkgInfo = rootPackageLock.packages?.[pkgPath];823824if (pkgInfo) {825// Collect all dependency types826const deps = {827...pkgInfo.dependencies,828...pkgInfo.optionalDependencies,829...pkgInfo.devDependencies830};831832for (const depName of Object.keys(deps)) {833// Handle nested dependencies834const nestedDepPath = `${pkgPath}/node_modules/${depName}`;835const topLevelDepPath = `node_modules/${depName}`;836837let actualDepPath: string | null = null;838if (rootPackageLock.packages[nestedDepPath]) {839actualDepPath = nestedDepPath;840} else if (rootPackageLock.packages[topLevelDepPath]) {841actualDepPath = topLevelDepPath;842} else {843// Walk up the parent chain844const pathParts = pkgPath.split('/node_modules/');845for (let i = pathParts.length - 1; i >= 0; i--) {846const parentPath = pathParts.slice(0, i).join('/node_modules/');847const candidatePath = parentPath ? `${parentPath}/node_modules/${depName}` : `node_modules/${depName}`;848if (rootPackageLock.packages[candidatePath]) {849actualDepPath = candidatePath;850break;851}852}853}854855if (actualDepPath && !packagesToUpdate.has(actualDepPath)) {856packagesToUpdate.add(actualDepPath);857queue.push(actualDepPath);858}859}860}861}862863// Update package entries in chat-lib lock file864let lockUpdatedCount = 0;865for (const pkgPath of packagesToUpdate) {866if (rootPackageLock.packages[pkgPath] && chatLibPackageLock.packages[pkgPath]) {867chatLibPackageLock.packages[pkgPath] = rootPackageLock.packages[pkgPath];868lockUpdatedCount++;869}870}871872// Write the updated chat-lib package-lock.json873await fs.promises.writeFile(874chatLibPackageLockPath,875JSON.stringify(chatLibPackageLock, null, '\t') + '\n'876);877878console.log(`Chat-lib package-lock.json updated: ${lockUpdatedCount} package entries updated`);879}880}881882private async compileTypeScript(): Promise<void> {883console.log('Compiling TypeScript to validate module...');884885try {886// Change to the chat-lib directory and run TypeScript compiler887const { stdout, stderr } = await execAsync('npx tsc --noEmit', {888cwd: CHAT_LIB_DIR,889timeout: 60000 // 60 second timeout890});891892if (stderr) {893console.warn('TypeScript compilation warnings:', stderr);894}895896console.log('TypeScript compilation successful!');897} catch (error: any) {898console.error('TypeScript compilation failed:', error.stdout || error.message);899throw new Error(`TypeScript compilation failed: ${error.stdout || error.message}`);900}901}902}903904// Main execution905async function main(): Promise<void> {906try {907const extractor = new ChatLibExtractor();908await extractor.extract();909} catch (error) {910console.error('Extraction failed:', error);911process.exit(1);912}913}914915if (require.main === module) {916main();917}918919920