优秀的扩展不仅需要强大的功能,更需要稳定的质量和便捷的发布流程。本阶段将深入探讨扩展测试策略、性能优化、打包发布和持续集成,帮助你构建专业级的扩展开发和发布工作流。


🎯 学习目标

完成本阶段学习后,你将能够:

  • 🧪 全面测试策略:单元测试、集成测试、端到端测试、性能测试
  • 📦 打包优化技术:Webpack配置、代码分割、资源压缩、依赖管理
  • 🚀 发布流程管理:市场发布、版本控制、发布策略、回滚机制
  • 📊 质量监控:遥测数据、错误报告、性能监控、用户反馈
  • 🔄 持续集成:GitHub Actions、自动化测试、自动发布

预计学习时间:1-2周(每天1-2小时)


🧪 第一部分:全面测试策略

1.1 单元测试框架

测试环境配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// src/test/suite/index.ts
import * as path from 'path';
import * as Mocha from 'mocha';
import { glob } from 'glob';

export function run(): Promise<void> {
// 创建Mocha测试套件
const mocha = new Mocha({
ui: 'tdd',
color: true,
timeout: 10000
});

const testsRoot = path.resolve(__dirname, '..');

return new Promise((resolve, reject) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return reject(err);
}

// 添加测试文件到Mocha
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));

try {
// 运行测试
mocha.run(failures => {
if (failures > 0) {
reject(new Error(`${failures} tests failed.`));
} else {
resolve();
}
});
} catch (err) {
reject(err);
}
});
});
}

单元测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/test/suite/extension.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as myExtension from '../../extension';

suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('开始运行所有测试。');

test('Extension should be present', () => {
assert.ok(vscode.extensions.getExtension('publisher.my-extension'));
});

test('Should activate extension', async () => {
const ext = vscode.extensions.getExtension('publisher.my-extension');
await ext?.activate();
assert.ok(ext?.isActive);
});

test('Should register commands', async () => {
const commands = await vscode.commands.getCommands(true);
const myCommands = commands.filter(cmd => cmd.startsWith('myExtension.'));
assert.ok(myCommands.length > 0, '应该注册至少一个命令');
});

test('Configuration should be accessible', () => {
const config = vscode.workspace.getConfiguration('myExtension');
assert.ok(config !== undefined);
});
});

功能模块测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// src/test/suite/completionProvider.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';
import { MyLangCompletionProvider } from '../../completionProvider';

suite('Completion Provider Tests', () => {
let document: vscode.TextDocument;
let provider: MyLangCompletionProvider;

suiteSetup(async () => {
// 创建测试文档
document = await vscode.workspace.openTextDocument({
content: `function test() {
var myVariable = "hello";
console.log(myVariable);
}`,
language: 'mylang'
});

provider = new MyLangCompletionProvider();
});

test('Should provide keyword completions', async () => {
const position = new vscode.Position(1, 8); // var 后面
const completions = await provider.provideCompletionItems(
document,
position,
new vscode.CancellationTokenSource().token,
{ triggerKind: vscode.CompletionTriggerKind.Invoke }
);

assert.ok(Array.isArray(completions));
assert.ok(completions.length > 0);

const keywordCompletions = completions.filter(
item => item.kind === vscode.CompletionItemKind.Keyword
);
assert.ok(keywordCompletions.length > 0, '应该提供关键字补全');
});

test('Should provide variable completions', async () => {
const position = new vscode.Position(2, 20); // console.log( 后面
const completions = await provider.provideCompletionItems(
document,
position,
new vscode.CancellationTokenSource().token,
{ triggerKind: vscode.CompletionTriggerKind.Invoke }
);

const variableCompletions = completions.filter(
item => item.label === 'myVariable'
);
assert.ok(variableCompletions.length > 0, '应该提供变量补全');
});

test('Should resolve completion items', async () => {
const item = new vscode.CompletionItem('test', vscode.CompletionItemKind.Function);
item.data = 1;

const resolved = provider.resolveCompletionItem(
item,
new vscode.CancellationTokenSource().token
);

assert.ok(resolved);
if (resolved instanceof Promise) {
const result = await resolved;
assert.ok(result.detail, '应该提供详细信息');
}
});

suiteTeardown(async () => {
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
});

1.2 集成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// src/test/suite/integration.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as path from 'path';

suite('Integration Tests', () => {
let workspaceUri: vscode.Uri;

suiteSetup(async () => {
// 创建临时工作区
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
workspaceUri = workspaceFolder?.uri || vscode.Uri.file(path.join(__dirname, 'fixtures'));
});

test('Language features integration', async () => {
// 创建测试文件
const fileUri = vscode.Uri.joinPath(workspaceUri, 'test.ml');
const content = `function greet(name) {
return "Hello, " + name;
}

greet("World");`;

await vscode.workspace.fs.writeFile(fileUri, Buffer.from(content));
const document = await vscode.workspace.openTextDocument(fileUri);
await vscode.window.showTextDocument(document);

// 测试语法高亮
const tokens = await vscode.commands.executeCommand(
'vscode.provideDocumentSemanticTokens',
fileUri
) as vscode.SemanticTokens;

assert.ok(tokens, '应该提供语义tokens');
assert.ok(tokens.data.length > 0, 'tokens数据不应为空');

// 测试代码补全
const completions = await vscode.commands.executeCommand(
'vscode.executeCompletionItemProvider',
fileUri,
new vscode.Position(1, 12) // return 后面
) as vscode.CompletionList;

assert.ok(completions, '应该提供代码补全');
assert.ok(completions.items.length > 0, '补全项不应为空');

// 测试悬停信息
const hover = await vscode.commands.executeCommand(
'vscode.executeHoverProvider',
fileUri,
new vscode.Position(0, 9) // function关键字上
) as vscode.Hover[];

assert.ok(hover && hover.length > 0, '应该提供悬停信息');

// 清理
await vscode.workspace.fs.delete(fileUri);
});

test('Command execution integration', async () => {
// 测试命令注册和执行
const commands = await vscode.commands.getCommands(true);
const myCommands = commands.filter(cmd => cmd.startsWith('myExtension.'));

assert.ok(myCommands.length > 0, '应该注册扩展命令');

// 执行测试命令
try {
await vscode.commands.executeCommand('myExtension.helloWorld');
// 如果命令成功执行,应该没有异常
assert.ok(true, '命令执行成功');
} catch (error) {
assert.fail(`命令执行失败: ${error}`);
}
});

test('Configuration integration', async () => {
// 测试配置更改
const config = vscode.workspace.getConfiguration('myExtension');
const originalValue = config.get('enableFeature');

// 更改配置
await config.update('enableFeature', !originalValue, vscode.ConfigurationTarget.Workspace);

// 验证配置更改
const newValue = config.get('enableFeature');
assert.notStrictEqual(newValue, originalValue, '配置应该被更新');

// 恢复原始配置
await config.update('enableFeature', originalValue, vscode.ConfigurationTarget.Workspace);
});
});

1.3 端到端测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// src/test/suite/e2e.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as path from 'path';

suite('End-to-End Tests', () => {

test('Complete workflow: Create file → Edit → Save → Build', async () => {
// 1. 创建新文件
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
assert.ok(workspaceFolder, '需要打开工作区');

const fileUri = vscode.Uri.joinPath(workspaceFolder.uri, 'e2e-test.ml');
const initialContent = `// E2E测试文件
function main() {
console.log("Hello, E2E!");
}`;

await vscode.workspace.fs.writeFile(fileUri, Buffer.from(initialContent));

// 2. 打开文件
const document = await vscode.workspace.openTextDocument(fileUri);
const editor = await vscode.window.showTextDocument(document);

// 3. 编辑文件
const success = await editor.edit(editBuilder => {
editBuilder.insert(
new vscode.Position(3, 0),
'\nmain();\n'
);
});

assert.ok(success, '文件编辑应该成功');

// 4. 保存文件
await document.save();

// 5. 触发语言功能
// 测试诊断
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待诊断更新
const diagnostics = vscode.languages.getDiagnostics(fileUri);

// 6. 执行构建任务(如果有)
const tasks = await vscode.tasks.fetchTasks({ type: 'mylang' });
if (tasks.length > 0) {
const buildTask = tasks.find(task => task.name === 'build');
if (buildTask) {
const execution = await vscode.tasks.executeTask(buildTask);

// 等待任务完成
await new Promise<void>((resolve, reject) => {
const disposable = vscode.tasks.onDidEndTask(e => {
if (e.execution === execution) {
disposable.dispose();
resolve();
}
});

const errorDisposable = vscode.tasks.onDidEndTaskProcess(e => {
if (e.execution === execution && e.exitCode !== 0) {
errorDisposable.dispose();
reject(new Error(`任务失败,退出码: ${e.exitCode}`));
}
});
});
}
}

// 7. 清理
await vscode.workspace.fs.delete(fileUri);
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});

test('User interaction flow: QuickPick → Input → Action', async () => {
// 模拟用户交互流程

// 1. 执行显示QuickPick的命令
const quickPickPromise = vscode.commands.executeCommand('myExtension.showQuickPick');

// 2. 模拟用户选择(这在真实场景中需要用户交互)
// 在测试中,我们可以通过监听或mock来验证QuickPick是否显示

// 3. 验证命令执行
try {
await quickPickPromise;
assert.ok(true, 'QuickPick命令执行成功');
} catch (error) {
// 某些命令可能因为需要用户交互而超时,这是正常的
console.log('QuickPick命令需要用户交互,跳过测试');
}
});

test('Error handling workflow', async () => {
// 测试错误处理流程

// 1. 创建有语法错误的文件
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
assert.ok(workspaceFolder, '需要打开工作区');

const errorFileUri = vscode.Uri.joinPath(workspaceFolder.uri, 'error-test.ml');
const errorContent = `function broken() {
// 缺少闭合括号
console.log("这会导致语法错误"
}`;

await vscode.workspace.fs.writeFile(errorFileUri, Buffer.from(errorContent));
const document = await vscode.workspace.openTextDocument(errorFileUri);
await vscode.window.showTextDocument(document);

// 2. 等待诊断更新
await new Promise(resolve => setTimeout(resolve, 2000));

// 3. 验证错误诊断
const diagnostics = vscode.languages.getDiagnostics(errorFileUri);
assert.ok(diagnostics.length > 0, '应该检测到语法错误');

const errorDiagnostics = diagnostics.filter(
d => d.severity === vscode.DiagnosticSeverity.Error
);
assert.ok(errorDiagnostics.length > 0, '应该有错误级别的诊断');

// 4. 清理
await vscode.workspace.fs.delete(errorFileUri);
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
});

1.4 性能测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// src/test/suite/performance.test.ts
import * as assert from 'assert';
import * as vscode from 'vscode';

suite('Performance Tests', () => {

test('Completion provider performance', async () => {
// 创建大型测试文档
const largeContent = Array.from({ length: 1000 }, (_, i) =>
`var variable${i} = "value${i}";`
).join('\n');

const document = await vscode.workspace.openTextDocument({
content: largeContent,
language: 'mylang'
});

const provider = new (await import('../../completionProvider')).MyLangCompletionProvider();

// 测试补全性能
const startTime = Date.now();
const completions = await provider.provideCompletionItems(
document,
new vscode.Position(500, 10),
new vscode.CancellationTokenSource().token,
{ triggerKind: vscode.CompletionTriggerKind.Invoke }
);
const endTime = Date.now();

const duration = endTime - startTime;
console.log(`补全耗时: ${duration}ms`);

// 断言性能要求(500ms内完成)
assert.ok(duration < 500, `补全耗时 ${duration}ms 超过 500ms 限制`);
assert.ok(Array.isArray(completions), '应该返回补全数组');
});

test('Syntax highlighting performance', async () => {
// 创建复杂的语法测试文件
const complexContent = `
// 复杂语法结构
function complexFunction(param1, param2, param3) {
const obj = {
prop1: "string value",
prop2: 42,
prop3: [1, 2, 3, 4, 5],
method: function(x) {
return x * 2;
}
};

for (let i = 0; i < 100; i++) {
if (i % 2 === 0) {
console.log(\`Even: \${i}\`);
} else {
console.log(\`Odd: \${i}\`);
}
}

try {
return obj.method(param1 + param2 + param3);
} catch (error) {
throw new Error("计算错误: " + error.message);
}
}`.repeat(50); // 重复50次创建大文件

const document = await vscode.workspace.openTextDocument({
content: complexContent,
language: 'mylang'
});

// 测试语义tokens性能
const startTime = Date.now();
const tokens = await vscode.commands.executeCommand(
'vscode.provideDocumentSemanticTokens',
document.uri
);
const endTime = Date.now();

const duration = endTime - startTime;
console.log(`语义高亮耗时: ${duration}ms`);

// 断言性能要求(1秒内完成)
assert.ok(duration < 1000, `语义高亮耗时 ${duration}ms 超过 1000ms 限制`);
assert.ok(tokens, '应该返回语义tokens');
});

test('Memory usage monitoring', async () => {
// 监控内存使用
const initialMemory = process.memoryUsage();

// 执行一系列操作
const documents: vscode.TextDocument[] = [];

for (let i = 0; i < 10; i++) {
const content = `// 文件 ${i}\n`.repeat(100);
const doc = await vscode.workspace.openTextDocument({
content,
language: 'mylang'
});
documents.push(doc);
}

// 触发语言功能
for (const doc of documents) {
await vscode.commands.executeCommand(
'vscode.provideDocumentSemanticTokens',
doc.uri
);
}

// 强制垃圾回收
if (global.gc) {
global.gc();
}

const finalMemory = process.memoryUsage();
const memoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;

console.log(`内存增长: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB`);

// 断言内存使用合理(不超过50MB增长)
assert.ok(
memoryIncrease < 50 * 1024 * 1024,
`内存增长 ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB 超过 50MB 限制`
);
});
});

📦 第二部分:打包优化技术

2.1 Webpack配置优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// webpack.config.js
const path = require('path');

/**@type {import('webpack').Configuration}*/
const config = {
target: 'node', // VS Code扩展运行在Node.js环境

mode: 'none', // 留给VS Code决定如何优化

entry: './src/extension.ts', // 扩展入口点
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
},
externals: {
vscode: 'commonjs vscode' // VS Code API不应该被打包
},
resolve: {
extensions: ['.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log"
},
optimization: {
minimize: true,
minimizer: [
new (require('terser-webpack-plugin'))({
terserOptions: {
keep_fnames: true,
keep_classnames: true
}
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
plugins: [
new (require('webpack')).DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
})
]
};

module.exports = config;

生产环境优化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack.production.js
const { merge } = require('webpack-merge');
const common = require('./webpack.config.js');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimize: true,
sideEffects: false,
usedExports: true
},
plugins: [
// 分析打包结果
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
});

2.2 依赖管理和Tree Shaking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// package.json优化
{
"main": "./dist/extension.js",
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js",
"bundle-analysis": "webpack --config webpack.production.js --analyze"
},
"devDependencies": {
"@types/vscode": "^1.74.0",
"@types/node": "16.x",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-merge": "^5.8.0",
"terser-webpack-plugin": "^5.3.6",
"webpack-bundle-analyzer": "^4.7.0"
},
"dependencies": {
// 只包含运行时必需的依赖
}
}

代码分割策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/extension.ts
import * as vscode from 'vscode';

export async function activate(context: vscode.ExtensionContext) {
// 延迟加载非关键功能
const registerLanguageFeatures = async () => {
const { registerCompletionProvider } = await import('./providers/completionProvider');
const { registerHoverProvider } = await import('./providers/hoverProvider');
const { registerDiagnosticsProvider } = await import('./providers/diagnosticsProvider');

registerCompletionProvider(context);
registerHoverProvider(context);
registerDiagnosticsProvider(context);
};

// 注册基础命令(立即加载)
context.subscriptions.push(
vscode.commands.registerCommand('myExtension.helloWorld', () => {
vscode.window.showInformationMessage('Hello World!');
})
);

// 监听语言文件打开事件,然后加载语言功能
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument(async (document) => {
if (document.languageId === 'mylang') {
await registerLanguageFeatures();
}
})
);

// 如果当前已有MyLang文件打开,立即注册语言功能
const hasMyLangFiles = vscode.workspace.textDocuments.some(
doc => doc.languageId === 'mylang'
);

if (hasMyLangFiles) {
await registerLanguageFeatures();
}
}

2.3 资源优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/utils/resourceOptimizer.ts
export class ResourceOptimizer {

// 图片资源优化
static optimizeImages() {
// 使用WebP格式,提供fallback
const images = {
logo: {
webp: './images/logo.webp',
fallback: './images/logo.png'
}
};

return images;
}

// 字体资源优化
static optimizeFonts() {
// 只加载必要的字体子集
const fonts = {
icon: './fonts/codicons.woff2'
};

return fonts;
}

// 本地化资源按需加载
static async loadLocalization(locale: string) {
try {
const localization = await import(`../locales/${locale}.json`);
return localization.default;
} catch (error) {
// 回退到英语
const fallback = await import('../locales/en.json');
return fallback.default;
}
}
}

🚀 第三部分:发布流程管理

3.1 版本管理策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// package.json版本配置
{
"version": "1.2.3",
"engines": {
"vscode": "^1.74.0"
},
"galleryBanner": {
"color": "#C80000",
"theme": "dark"
},
"categories": [
"Programming Languages",
"Snippets",
"Debuggers"
],
"keywords": [
"mylang",
"programming",
"development",
"syntax highlighting"
],
"preview": false,
"repository": {
"type": "git",
"url": "https://github.com/username/my-extension.git"
},
"bugs": {
"url": "https://github.com/username/my-extension/issues"
},
"homepage": "https://github.com/username/my-extension#readme"
}

自动版本管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// scripts/version-bump.js
const fs = require('fs');
const path = require('path');

function bumpVersion(type = 'patch') {
const packagePath = path.join(__dirname, '../package.json');
const package = JSON.parse(fs.readFileSync(packagePath, 'utf8'));

const [major, minor, patch] = package.version.split('.').map(Number);

let newVersion;
switch (type) {
case 'major':
newVersion = `${major + 1}.0.0`;
break;
case 'minor':
newVersion = `${major}.${minor + 1}.0`;
break;
case 'patch':
default:
newVersion = `${major}.${minor}.${patch + 1}`;
break;
}

package.version = newVersion;

fs.writeFileSync(packagePath, JSON.stringify(package, null, 2));

// 更新CHANGELOG
updateChangelog(newVersion);

console.log(`版本更新为: ${newVersion}`);
return newVersion;
}

function updateChangelog(version) {
const changelogPath = path.join(__dirname, '../CHANGELOG.md');
const date = new Date().toISOString().split('T')[0];

let changelog = '';
if (fs.existsSync(changelogPath)) {
changelog = fs.readFileSync(changelogPath, 'utf8');
}

const newEntry = `## [${version}] - ${date}

### Added
-

### Changed
-

### Fixed
-

`;

const lines = changelog.split('\n');
const insertIndex = lines.findIndex(line => line.startsWith('## '));

if (insertIndex === -1) {
changelog = newEntry + changelog;
} else {
lines.splice(insertIndex, 0, newEntry);
changelog = lines.join('\n');
}

fs.writeFileSync(changelogPath, changelog);
}

// 命令行使用
const type = process.argv[2] || 'patch';
bumpVersion(type);

3.2 发布脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// scripts/publish.js
const { execSync } = require('child_process');
const fs = require('fs');

async function publish() {
try {
console.log('🚀 开始发布流程...');

// 1. 检查Git状态
checkGitStatus();

// 2. 运行测试
console.log('📋 运行测试...');
execSync('npm test', { stdio: 'inherit' });

// 3. 构建生产版本
console.log('📦 构建生产版本...');
execSync('npm run package', { stdio: 'inherit' });

// 4. 检查包大小
checkPackageSize();

// 5. 验证package.json
validatePackageJson();

// 6. 创建Git标签
const version = require('../package.json').version;
execSync(`git tag v${version}`, { stdio: 'inherit' });

// 7. 发布到VS Code市场
console.log('🚀 发布到VS Code市场...');
execSync('vsce publish', { stdio: 'inherit' });

// 8. 推送到Git仓库
console.log('📤 推送到Git仓库...');
execSync('git push origin main --tags', { stdio: 'inherit' });

console.log('✅ 发布成功!');

} catch (error) {
console.error('❌ 发布失败:', error.message);
process.exit(1);
}
}

function checkGitStatus() {
try {
const status = execSync('git status --porcelain', { encoding: 'utf8' });
if (status.trim()) {
throw new Error('Git工作目录不干净,请先提交所有更改');
}
} catch (error) {
throw new Error('检查Git状态失败: ' + error.message);
}
}

function checkPackageSize() {
const stats = execSync('vsce package --out temp.vsix', { encoding: 'utf8' });
const sizeMatch = stats.match(/(\d+\.\d+)MB/);

if (sizeMatch) {
const size = parseFloat(sizeMatch[1]);
console.log(`📦 包大小: ${size}MB`);

if (size > 10) {
console.warn(`⚠️ 包大小 ${size}MB 较大,建议优化`);
}
}

// 清理临时文件
if (fs.existsSync('temp.vsix')) {
fs.unlinkSync('temp.vsix');
}
}

function validatePackageJson() {
const package = require('../package.json');

const requiredFields = [
'name', 'version', 'description', 'publisher',
'engines', 'categories', 'main'
];

for (const field of requiredFields) {
if (!package[field]) {
throw new Error(`package.json缺少必需字段: ${field}`);
}
}

if (!package.repository || !package.repository.url) {
console.warn('⚠️ 建议添加repository字段');
}

if (!package.bugs || !package.bugs.url) {
console.warn('⚠️ 建议添加bugs字段');
}
}

publish();

3.3 CI/CD配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# .github/workflows/ci.yml
name: CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16.x, 18.x]

steps:
- uses: actions/checkout@v3

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run linter
run: npm run lint

- name: Compile TypeScript
run: npm run compile

- name: Run tests
uses: coactions/setup-xvfb@v1
with:
run: npm test

- name: Package extension
run: npm run package

- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: extension-${{ matrix.os }}-${{ matrix.node-version }}
path: dist/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# .github/workflows/release.yml
name: Release

on:
push:
tags:
- 'v*'

jobs:
release:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run tests
uses: coactions/setup-xvfb@v1
with:
run: npm test

- name: Package extension
run: npm run package

- name: Publish to VS Code Marketplace
run: npx vsce publish
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}

- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false

- name: Upload Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/extension.js
asset_name: extension.js
asset_content_type: application/javascript

📊 第四部分:质量监控

4.1 遥测数据收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// src/telemetry/telemetryReporter.ts
import * as vscode from 'vscode';

export class TelemetryReporter {
private static instance: TelemetryReporter;
private reporter?: any;

private constructor() {
this.initializeReporter();
}

static getInstance(): TelemetryReporter {
if (!TelemetryReporter.instance) {
TelemetryReporter.instance = new TelemetryReporter();
}
return TelemetryReporter.instance;
}

private async initializeReporter() {
try {
// 只在用户同意的情况下初始化遥测
const config = vscode.workspace.getConfiguration('telemetry');
const telemetryEnabled = config.get('enableTelemetry', true);

if (telemetryEnabled) {
const { default: Reporter } = await import('vscode-extension-telemetry');
this.reporter = new Reporter(
'your-extension-id',
'1.0.0',
'your-application-insights-key'
);
}
} catch (error) {
console.error('遥测初始化失败:', error);
}
}

// 记录功能使用
trackFeatureUsage(featureName: string, properties?: Record<string, string>) {
if (!this.reporter) return;

this.reporter.sendTelemetryEvent('featureUsage', {
feature: featureName,
timestamp: new Date().toISOString(),
...properties
});
}

// 记录错误
trackError(error: Error, context?: string) {
if (!this.reporter) return;

this.reporter.sendTelemetryErrorEvent('error', {
context: context || 'unknown',
errorMessage: error.message,
errorStack: error.stack || '',
timestamp: new Date().toISOString()
});
}

// 记录性能指标
trackPerformance(operation: string, duration: number, success: boolean) {
if (!this.reporter) return;

this.reporter.sendTelemetryEvent('performance', {
operation,
duration: duration.toString(),
success: success.toString(),
timestamp: new Date().toISOString()
});
}

// 记录用户配置
trackConfiguration(configChanges: Record<string, any>) {
if (!this.reporter) return;

const sanitizedConfig = this.sanitizeConfigData(configChanges);
this.reporter.sendTelemetryEvent('configuration', sanitizedConfig);
}

private sanitizeConfigData(config: Record<string, any>): Record<string, string> {
const sanitized: Record<string, string> = {};

for (const [key, value] of Object.entries(config)) {
// 不记录敏感信息
if (key.toLowerCase().includes('password') ||
key.toLowerCase().includes('token') ||
key.toLowerCase().includes('secret')) {
continue;
}

sanitized[key] = String(value);
}

return sanitized;
}

dispose() {
if (this.reporter) {
this.reporter.dispose();
}
}
}

// 使用示例
export function trackCommandExecution(commandId: string) {
const telemetry = TelemetryReporter.getInstance();
const startTime = Date.now();

return {
success: () => {
const duration = Date.now() - startTime;
telemetry.trackPerformance(commandId, duration, true);
telemetry.trackFeatureUsage('command', { commandId });
},
error: (error: Error) => {
const duration = Date.now() - startTime;
telemetry.trackPerformance(commandId, duration, false);
telemetry.trackError(error, `command:${commandId}`);
}
};
}

4.2 错误报告和诊断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// src/diagnostics/errorHandler.ts
export class ErrorHandler {
private static errorQueue: ErrorReport[] = [];
private static maxQueueSize = 100;

static handleError(error: Error, context: ErrorContext): void {
const errorReport: ErrorReport = {
id: this.generateErrorId(),
timestamp: new Date().toISOString(),
error: {
name: error.name,
message: error.message,
stack: error.stack
},
context,
environment: this.getEnvironmentInfo(),
userActions: this.getUserActions()
};

// 添加到错误队列
this.addToQueue(errorReport);

// 记录遥测
TelemetryReporter.getInstance().trackError(error, context.operation);

// 显示用户友好的错误信息
this.showUserError(errorReport);

// 严重错误自动上报
if (context.severity === 'critical') {
this.reportErrorImmediately(errorReport);
}
}

private static addToQueue(errorReport: ErrorReport): void {
this.errorQueue.push(errorReport);

if (this.errorQueue.length > this.maxQueueSize) {
this.errorQueue.shift(); // 移除最旧的错误
}
}

private static getEnvironmentInfo(): EnvironmentInfo {
return {
vscodeVersion: vscode.version,
extensionVersion: require('../../package.json').version,
platform: process.platform,
nodeVersion: process.version,
language: vscode.env.language,
remoteName: vscode.env.remoteName
};
}

private static getUserActions(): UserAction[] {
// 这里可以记录用户最近的操作序列
return [];
}

private static showUserError(errorReport: ErrorReport): void {
const message = this.createUserFriendlyMessage(errorReport);

switch (errorReport.context.severity) {
case 'critical':
vscode.window.showErrorMessage(message, '报告问题').then(action => {
if (action === '报告问题') {
this.openIssueReporter(errorReport);
}
});
break;

case 'warning':
vscode.window.showWarningMessage(message);
break;

default:
vscode.window.showInformationMessage(message);
break;
}
}

private static createUserFriendlyMessage(errorReport: ErrorReport): string {
const operation = errorReport.context.operation;

// 根据错误类型提供友好的错误消息
const errorMappings: Record<string, string> = {
'ENOENT': '找不到指定的文件或目录',
'EACCES': '权限不足,无法访问文件',
'TIMEOUT': '操作超时,请检查网络连接',
'PARSE_ERROR': '文件格式错误,请检查语法'
};

const friendlyMessage = errorMappings[errorReport.error.name] ||
errorReport.error.message;

return `执行 ${operation} 时发生错误:${friendlyMessage}`;
}

private static async openIssueReporter(errorReport: ErrorReport): Promise<void> {
const issueBody = this.generateIssueBody(errorReport);
const issueUrl = `https://github.com/your-username/your-extension/issues/new?body=${encodeURIComponent(issueBody)}`;

await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
}

private static generateIssueBody(errorReport: ErrorReport): string {
return `## 错误报告

**错误ID:** ${errorReport.id}
**时间:** ${errorReport.timestamp}
**操作:** ${errorReport.context.operation}

### 错误信息
\`\`\`
${errorReport.error.name}: ${errorReport.error.message}
\`\`\`

### 环境信息
- VS Code版本: ${errorReport.environment.vscodeVersion}
- 扩展版本: ${errorReport.environment.extensionVersion}
- 平台: ${errorReport.environment.platform}
- Node.js版本: ${errorReport.environment.nodeVersion}

### 堆栈跟踪
\`\`\`
${errorReport.error.stack}
\`\`\`

### 重现步骤
1.
2.
3.

### 预期行为

### 实际行为
`;
}

private static generateErrorId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

private static async reportErrorImmediately(errorReport: ErrorReport): Promise<void> {
// 实现自动错误上报逻辑
console.log('Critical error reported:', errorReport.id);
}
}

interface ErrorReport {
id: string;
timestamp: string;
error: {
name: string;
message: string;
stack?: string;
};
context: ErrorContext;
environment: EnvironmentInfo;
userActions: UserAction[];
}

interface ErrorContext {
operation: string;
severity: 'info' | 'warning' | 'error' | 'critical';
userId?: string;
sessionId?: string;
}

interface EnvironmentInfo {
vscodeVersion: string;
extensionVersion: string;
platform: string;
nodeVersion: string;
language: string;
remoteName?: string;
}

interface UserAction {
action: string;
timestamp: string;
context?: any;
}

📚 本阶段总结

通过本阶段学习,你已经掌握了:

全面测试策略:单元测试、集成测试、端到端测试、性能测试
打包优化技术:Webpack配置、代码分割、资源压缩、依赖管理
发布流程管理:版本控制、自动化发布、CI/CD集成
质量监控体系:遥测数据、错误报告、性能监控、用户反馈
专业开发流程:代码质量、发布策略、持续改进

核心技术回顾

  • 测试金字塔:单元测试为基础,集成测试为保障,E2E测试为验证
  • 打包优化:代码分割、Tree Shaking、资源压缩
  • CI/CD流程:自动化测试、自动化发布、质量门禁
  • 监控体系:遥测数据收集、错误追踪、性能监控

🚀 下一步学习方向

在最后一个阶段(实战项目),我们将学习:

  • 🛠️ 综合项目实战:端到端扩展开发项目
  • 🎯 最佳实践总结:架构设计、代码组织、性能优化
  • 📈 扩展推广:用户获取、社区建设、持续运营
  • 🔄 维护和迭代:版本管理、功能迭代、用户支持

继续学习之前,建议:

  1. 为现有扩展添加完整的测试覆盖
  2. 实践CI/CD流程的搭建
  3. 集成遥测和错误报告系统
  4. 优化扩展的打包和性能

相关资源

恭喜你完成了VS Code扩展开发的测试与发布阶段!现在你已经具备了专业级扩展开发的完整技能栈。