记录一次VSCode插件开发的Monorepo搭建
为什么要改造成Monorepo?
在开发VSCode插件的过程中,随着项目规模的扩大和功能的增加,代码库变得越来越复杂。传统的单一仓库结构在这种情况下会面临以下挑战:
- 代码组织混乱:多个模块混杂在一起,难以维护和管理。
- 依赖管理困难:不同模块之间的依赖关系不清晰,容易导致版本冲突。
- 构建和测试复杂:每个模块的构建和测试流程独立,难以统一管理。
- 代码复用困难:跨模块的代码复用变得复杂,容易出现重复代码。
为了应对这些挑战,我们决定将项目改造成Monorepo架构。Monorepo允许我们在一个仓库中管理多个相关但独立的模块,带来了以下好处:
- 更好的代码组织:模块化管理,清晰的目录结构。
- 简化依赖管理:统一管理依赖,避免版本冲突。
- 统一的构建和测试流程:集中管理构建和测试,提高效率。
- 促进代码复用:跨模块共享代码,减少重复。
为什么选择pnpm workspace方案?
在众多Monorepo解决方案中,我们选择了pnpm workspace。这个选择基于以下几个原因:
- 高效的依赖管理:pnpm使用硬链接和符号链接来共享包,大大节省了磁盘空间和安装时间。
- 严格的依赖结构:pnpm默认创建非扁平的node_modules结构,有效防止依赖提升带来的潜在问题。
- 原生支持workspace:pnpm原生支持workspace,无需额外的工具就能管理多包项目。
- 快速且轻量:相比其他方案,pnpm的性能更优,且不需要复杂的配置。
- 良好的生态系统:pnpm与现有的npm生态完全兼容,无缝集成各种工具和脚本。
- 简单直观的命令:pnpm提供了直观的命令来管理workspace中的包和依赖。
通过采用pnpm workspace,我们可以更有效地组织和管理VSCode插件项目的各个模块,提高开发效率,并为未来的扩展和维护奠定良好的基础。
创建workspace
在项目根目录使用 pnpm init 或者 npm init 创建package.json
1、 项目名称
根项目
{
"name": "vscode-design", // 项目名称
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm -r run dev",
"build": "pnpm -r run build",
"package": "pnpm -r run package",
"dev:parallel": "pnpm -r --parallel run dev"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}
子项目
路径:packages\extension\package.json
{
"name": "vscode-design-extension", //插件工程
"displayName": "vscode-design",
"$schema": "https://json.schemastore.org/jsconfig",
"description": "",
"version": "0.0.1",
"publisher": "design-plugin",
"engines": {
"vscode": "^1.80.0"
}
}
路径:packages\frontend\package.json
{
"name": "vscode-design-frontend", // 前端工程
"private": true,
"version": "0.0.0",
"type": "module"
}
2、初始化 pnpm-workspace.yaml
touch pnpm-workspace.yaml
packages:
- 'packages/*' //子项目空间
3、 创建子项目
在packages文件夹下创建多个子项目
文件结构如下:
packages/ //monorepo工程化子项目空间
├── extension/ //vscode开发工程
├── frontend/ //前端开发工程
└── nodemiddleware/ //node中间件,脚本等
4、tsconfig.json配置
- 根目录 tsconfig.json:
在根目录创建一个基础的 tsconfig.json 文件,作为所有子项目的基础配置。
使用 "extends" 字段让子项目继承根目录的配置。 - 子项目 tsconfig.json:
每个子项目都应该有自己的 tsconfig.json,继承根目录的配置并根据需要进行覆盖。
使用 "references" 字段来声明项目间的依赖关系。 - 路径别名:
使用 "paths" 配置来设置路径别名,简化导入语句。 - 项目引用:
利用 TypeScript 的项目引用功能(Project References)来优化构建过程和提高类型检查效率。 - 严格模式:
建议开启严格模式 "strict": true,以获得更好的类型检查。 - 输出配置:
根据项目需求配置 "outDir"、"rootDir" 等输出相关选项。 - 模块解析:
设置 "moduleResolution": "node" 以使用 Node.js 风格的模块解析。
兼容性:
根据目标环境配置 "target" 和 "lib" 选项。 - 声明文件:
配置 "declaration": true 以生成声明文件(.d.ts)。 - 增量编译:
启用 "incremental": true 以提高重复编译的速度。
根目录
{
"$schema": "https://json.schemastore.org/jsconfig",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "vue",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@vscode-design/*": ["packages/*/src"]
}
},
"exclude": ["node_modules", "dist"]
}
packages/frontend
{
"$schema": "https://json.schemastore.org/jsconfig",
"extends": ["@vue/tsconfig/tsconfig.dom.json", "../../tsconfig.json"],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/**/*.tsx", "src/**/*.jsx"],
"exclude": ["node_modules", "dist"],
// "exclude": ["src/**/*.d.ts", "src/**/*"],
"compilerOptions": {
"allowJs": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxImportSource": "vue",
"jsxFragmentFactory": "Fragment",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"types": ["vite/client"],
"module": "ESNext",
"moduleResolution": "node",
"baseUrl": ".",
"outDir": "./dist",
"rootDir": "./src",
"resolveJsonModule": true,
"lib": ["ESNext", "DOM"],
"ignoreDeprecations": "5.0",
"skipLibCheck": true,
"noEmit": false,
"emitDeclarationOnly": true,
"strict": true,
"isolatedModules": true,
"useDefineForClassFields": true,
"declaration": true,
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
packages/extension
{
"$schema": "https://json.schemastore.org/jsconfig",
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
5、eslint配置
- 根目录 ESLint 配置:
在根目录创建一个 .eslintrc.js 文件,作为基础配置。
使用 root: true 标记根配置文件。 - 子项目 ESLint 配置:
每个子项目可以有自己的 .eslintrc.js,继承根目录的配置并根据需要进行覆盖。 - 共享规则:
在根目录定义共享的 ESLint 规则,确保整个项目代码风格一致。 - 插件和扩展:
根据项目需求配置必要的 ESLint 插件和扩展,如 typescript-eslint、prettier 等。 - 忽略文件:
在根目录创建 .eslintignore 文件,指定需要忽略的文件和目录。 - 与 TypeScript 集成:
配置 ESLint 以正确处理 TypeScript 文件。 - 脚本配置:
在 package.json 中添加 lint 脚本,方便运行 ESLint。
项目根eslint
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"env": {
"node": true,
"es6": true
},
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "import",
"format": [ "camelCase", "PascalCase" ]
}
],
"@typescript-eslint/semi": "off"
},
"ignorePatterns": [
"out",
"dist",
"**/*.d.ts"
]
}
packages/frontend
module.exports = {
env: {
browser: true,
es2022: true
},
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'.eslintrc-auto-import.json',
'plugin:import/typescript',
'prettier',
'plugin:prettier/recommended',
'../../.eslintrc.json'
],
overrides: [
{
env: {
node: true
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script'
}
}
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
plugins: ['@typescript-eslint', 'import', 'node', 'vue', 'prettier'],
globals: {
document: true,
localStorage: true,
window: true
},
rules: {
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
'indent': 'off',
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
'no-undef': 0,
'vue/attribute-hyphenation': 0,
'no-console': 'off',
'vue/multi-word-component-names': 0,
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/html-self-closing': 0,
'comma-dangle': ['error', {
arrays: 'never',
objects: 'never',
imports: 'never',
exports: 'never',
functions: 'never'
}],
'@typescript-eslint/ban-types': 'off',
'import/no-unresolved': [
"error",
{
'ignore': ['^@/']
}
]
},
settings: {
'import/resolver': {
alias: {
map: [['@', './src']],
extensions: ['.js', '.vue', '.json', '.ts', '.tsx']
},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', 'vue']
}
}
}
}
packages/extension
{
"extends": [
"../../.eslintrc.json"
]
}
打包配置
因为vscode插件引入webview需要设置资源引用时最大权限文件夹,所以最好将web开发的产物与插件开发的产物打包到一个位置。最合适的位置莫过于项目root下的dist。因此需要配置打包文件的输出目录
针对开发环境配置
frontend (vite工程)
路径:package/frontend/vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'
import * as fs from 'fs'
import AutoImport from 'unplugin-auto-import/vite'
import progress from 'vite-plugin-progress'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'
import vuejsx from '@vitejs/plugin-vue-jsx'
import Inspector from 'vite-plugin-vue-inspector'
import dotenv from 'dotenv'
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer'
import { build } from 'vite'
export default defineConfig(({ mode }) => {
const env = dotenv.parse(
fs.readFileSync(path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || 'development'}`)),
)
const outDir = path.resolve(__dirname, '../../dist/web')
return {
define: {
'process.env.NODE_ENV': JSON.stringify(env),
},
plugins: [
vue(),
vuejsx(),
progress(),
...(mode === 'development' ? [Inspector()] : []),
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-import.d.ts',
eslintrc: {
// 已存在文件设置默认 false,需要更新时再打开,防止每次更新都重新生成
enabled: false,
// 生成文件地址和名称
filepath: './.eslintrc-auto-import.json',
globalsPropValue: true,
},
}),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false,
}),
],
}),
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz',
deleteOriginFile: false,
}),
ViteImageOptimizer({
png: {
quality: 70,
},
jpg: {
quality: 70,
},
webp: {
lossless: true,
},
}),
visualizer({
open: true, // 打包完成后自动打开浏览器查看报告
gzipSize: true, // 显示 gzip 压缩后的大小
brotliSize: true, // 显示 brotli 压缩后的大小
filename: 'dist/stats.html', // 生成的分析报告文件名
}),
{
name: 'generate-static-files',
apply: 'serve',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.url === '/generate-static') {
await build({
configFile: path.resolve(__dirname, 'vite.config.ts'),
mode: 'development',
build: {
outDir,
emptyOutDir: true,
},
})
res.end('Static files generated in dist directory')
} else {
next()
}
})
},
},
],
optimizeDeps: {
include: ['core-js', 'regenerator-runtime'],
},
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
},
},
build: {
outDir,
emptyOutDir: true,
sourcemap: 'inline',
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return 'vendor'
}
if (id.includes('src/components')) {
return 'components'
}
},
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
assetsInlineLimit: 4096,
chunkSizeWarningLimit: 1000,
minify: 'terser', // 压缩
terserOptions: {
// 压缩配置
mangle: true, // 混淆
compress: {
// drop_debugger: true, // 删除debugger
// drop_console: true, // 删除console
},
},
},
server: {
host: '0.0.0.0',
proxy: {
},
},
}
})
对于开发环境vscode的webview直接去vite服务器中引用js。但是vite配置需要开启允许外部访问
server: {
host: '0.0.0.0',
},
vscode插件的webview
路径:package/extension/src/extension.ts
vscode.commands.registerCommand('yourExtension.openWebview', async() => {
const panel = vscode.window.createWebviewPanel(
'yss-design',
'webapp',
vscode.ViewColumn.One,
{
retainContextWhenHidden: true, // 保证 Webview 所在页面进入后台时不被释放
enableScripts: true, // 运行 JS 执行
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri)]
}
);
console.log('Extension Path:', context.extensionPath);
panel.webview.html = await getWebviewContent(context, panel);
const webviewCommunication = new WebviewCommunication(panel)
}),
function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel) {
// const isDevelopment = context.extensionMode === vscode.ExtensionMode.Development;
// 需要一个预览环境,对应的web环境为生产。返回生成环境的html
const isDevelopment = process.env.NODE_ENV !== 'production';
if (isDevelopment) {
const localServerUrl = 'http://localhost:5173';
const localClientUrl = `${localServerUrl}/@vite/client`;
const localMainUrl = `${localServerUrl}/src/main.ts`;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发环境</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="${localClientUrl}"></script>
<script type="module" src="${localMainUrl}"></script>
</body>
</html>
`;
}
const extensionPath = context.extensionPath;
const htmlRoot = path.join(extensionPath, 'web');
const htmlIndex = path.join(htmlRoot, 'index.html');
const uris = panel?.webview.asWebviewUri(vscode.Uri.file(htmlIndex))
const html = fs.readFileSync(htmlIndex, 'utf8')?.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => {
const localPath = path.join(htmlRoot, $2);
const webviewUri = panel?.webview.asWebviewUri(vscode.Uri.file(localPath));
const replaceHref = $1 + webviewUri?.toString() + '"';
return replaceHref;
});
路径:package/extension/webpack.config.js
//@ts-check
'use strict';
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const extensionConfig = {
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, '../../dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
'extension.js', 'extension.js.map', '!package.json', // 添加这行以防止删除 package.json
],
}),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, './resources'),
to: path.resolve(__dirname, '../../dist/resources'),
},
{
from: path.resolve(__dirname, './package.json'),
to: path.resolve(__dirname, '../../dist/package.json'),
},
],
}),
],
externals: {
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
// modules added here also need to be added in the .vscodeignore file
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
};
module.exports = [extensionConfig];
这里特殊注意 添加了一个复制插件,和输出目录
输出目录:根目录的dist中
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, '../../dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
},
添加了一个copy-webpack-plugin插件将插件工程的package.json与静态资源复制到dist中。
Q: 为什么要复制package.json
A:因为package中包含了插件的信息,包括版本号,注册的命令等,入口文件等。vscode需要这些配置
const CopyWebpackPlugin = require('copy-webpack-plugin');
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, './resources'),
to: path.resolve(__dirname, '../../dist/resources'),
},
{
from: path.resolve(__dirname, './package.json'),
to: path.resolve(__dirname, '../../dist/package.json'),
},
],
}),
],
这里有几个需要注意的事项:
- 环境变量: 当前环境变量通过.vscode中的文件配置
- webview的地址引用vite开发服务器的@vite/client 与/src/main.ts
因为本文重点介绍pnpm workspace的改造插件工程生成请参考vscode官方文档。或者留言,有时间再给大家写一个插件工程的搭建
注意:
{ "name": "vscode-design-extension", "displayName": "vscode-design", "$schema": "https://json.schemastore.org/jsconfig", "description": "", "version": "0.0.1", "publisher": "design-plugin", "engines": { "vscode": "^1.80.0" }, "categories": [ "Other" ], "activationEvents": [], "main": "./extension.js", //插件入口 }
插件工程的package.json中 入口路径不是只想src下的extension.ts 而是当前路径下的extension.js 因为这个文件会被复制到dist中,到时候获取的是打包后的js文件。
配置脚本:dev
frontend 目录中的package.json中添加script
"scripts": { "dev": "cross-env NODE_ENV=development vite && eslint --config .eslintrc.cjs .", "lint": "eslint --ext .js,.vue,.ts,.tsx,.jsx src", "lint:fix": "eslint --ext .js,.vue,.ts,.tsx,.jsx --fix", "format": "prettier --write \"src/**/*.{js,ts,vue,jsx,tsx}\"" },
extension目录中也添加dev脚本
"scripts": {
"dev": "webpack --watch",
},
root目录添加 dev脚本(工程根目录)
"scripts": {
"dev": "pnpm -r run dev",
},
注意:
- 使用pnpm -r run dev过程中 -r 会将所有子工程的dev命令执行,本文只添加了两个子工程,如果还有其他工程自行添加,并且某个子工程没有添加dev脚本会执行报错。
- 开发环境目前没有解决引入静态文件的问题,当引入一个静态文件,因为不知道这个文件的地址所以webview的最大权限目录不能包含这个文件,导致在插件中不能展示。这个时候请直接访问前端环境即可。(后续解决会更新,或者希望有大佬能给个思路)
配置.vscode文件
针对预览环境
Q: 为什么要有预览环境而不是打包呢?
A: 因为开发服务器的一些接口引用等可能打包后没有使用vscode的协议进行转发导致不能获取到,所以需要一个web项目打包,但是插件开发的状态。来debug。
例如:
开发环境中web引入了某个静态资源,但是打包后这个资源没后包含在webview的最大权限文件夹内因此生成环境是没有权限拿到这个文件的。
vscode插件的webview
路径:package/extension/src/extension.ts
vscode.commands.registerCommand('yourExtension.openWebview', async() => {
const panel = vscode.window.createWebviewPanel(
'yss-design',
'webapp',
vscode.ViewColumn.One,
{
retainContextWhenHidden: true, // 保证 Webview 所在页面进入后台时不被释放
enableScripts: true, // 运行 JS 执行
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri)]
}
);
console.log('Extension Path:', context.extensionPath);
panel.webview.html = await getWebviewContent(context, panel);
const webviewCommunication = new WebviewCommunication(panel)
}),
function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel) {
// const isDevelopment = context.extensionMode === vscode.ExtensionMode.Development;
// 需要一个预览环境,对应的web环境为生产。返回生成环境的html
const isDevelopment = process.env.NODE_ENV !== 'production';
if (isDevelopment) {
const localServerUrl = 'http://localhost:5173';
const localClientUrl = `${localServerUrl}/@vite/client`;
const localMainUrl = `${localServerUrl}/src/main.ts`;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发环境</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="${localClientUrl}"></script>
<script type="module" src="${localMainUrl}"></script>
</body>
</html>
`;
}
const extensionPath = context.extensionPath;
const htmlRoot = path.join(extensionPath, 'web');
const htmlIndex = path.join(htmlRoot, 'index.html');
const uris = panel?.webview.asWebviewUri(vscode.Uri.file(htmlIndex))
const html = fs.readFileSync(htmlIndex, 'utf8')?.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => {
const localPath = path.join(htmlRoot, $2);
const webviewUri = panel?.webview.asWebviewUri(vscode.Uri.file(localPath));
const replaceHref = $1 + webviewUri?.toString() + '"';
return replaceHref;
});
注意:
预览/生产环境的js以及其他资源引入路径都要替换成vscode协议的接口
即:
const extensionPath = context.extensionPath; //运行时当前文件路径
const htmlRoot = path.join(extensionPath, 'web'); //web文件打包路径
const htmlIndex = path.join(htmlRoot, 'index.html'); // web的主页
const uris = panel?.webview.asWebviewUri(vscode.Uri.file(htmlIndex)) //转换为vscode协议
// 解析index.html将所有接口 地址替换为vscode协议的。最终返回新的html
const html = fs.readFileSync(htmlIndex, 'utf8')?.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => {
const localPath = path.join(htmlRoot, $2);
const webviewUri = panel?.webview.asWebviewUri(vscode.Uri.file(localPath));
const replaceHref = $1 + webviewUri?.toString() + '"';
return replaceHref;
});
vscode插件工程的webpack配置不需要变动。
web工程的vite.config.ts不需要变动。
配置脚本:preview
frontend 目录中的package.json中添加script
"scripts": {
"preview": "pnpm run build",
"dev": "cross-env NODE_ENV=development vite && eslint --config .eslintrc.cjs .",
"build": "cross-env NODE_ENV=production tsc --emitDeclarationOnly && vite build --emptyOutDir",
"lint": "eslint --ext .js,.vue,.ts,.tsx,.jsx src",
"lint:fix": "eslint --ext .js,.vue,.ts,.tsx,.jsx --fix",
"format": "prettier --write \"src/**/*.{js,ts,vue,jsx,tsx}\""
},
extension目录中也添加dev脚本
"scripts": {
"dev": "webpack --watch",
"preview": " webpack --watch",
},
root目录添加 dev脚本(工程根目录)
"scripts": {
"dev": "pnpm -r run dev",
"preview": "pnpm -r run preview",
},
针对打包环境
这个环境对于web工程来说没有任何改动,但是对于插件工程需要改动,因为,打包时最终在这个工程打包。
打包的内容:根目录的dist文件中的web文件以及插件文件。因此:
- 我们需要一个node脚本将dist中的目录复制到插件这个包中,才能进行打包
- 需要一个.vscodeignore文件用来标注那些文件不要被打包
- 需要一个.gitignore来标注复制来的这些文件不能上传到git服务器
- 脚本
在插件目录的src下面创建一个scripts文件夹然后创建一个prepare-package.js
const fs = require('fs');
const path = require('path');
const rootDir = path.resolve(__dirname, '../../../..');
const distDir = path.join(rootDir, 'dist');
const extensionDir = path.resolve(__dirname, '../..');
console.log('正在复制文件...');
console.log('从:', distDir);
console.log('到:', extensionDir);
// 这个函数实现了递归复制文件和目录的功能
function copyRecursiveSync(src, dest) {
// 检查源路径是否存在
const exists = fs.existsSync(src);
// 如果存在,获取源路径的文件状态信息
const stats = exists && fs.statSync(src);
// 判断源路径是否为目录
const isDirectory = exists && stats.isDirectory();
if (isDirectory) {
// 如果是目录,创建目标目录(如果不存在)
fs.mkdirSync(dest, { recursive: true });
// 读取源目录中的所有项目
fs.readdirSync(src).forEach(childItemName => {
// 对每个子项目递归调用复制函数
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
// 如果是文件,直接复制
console.log('复制文件:', src, '到', dest, '单文件');
fs.copyFileSync(src, dest);
}
}
// 复制 dist 目录内容到扩展目录
copyRecursiveSync(distDir, extensionDir);
// 确保 package.json 中的 main 字段指向正确的位置
const packageJsonPath = path.join(extensionDir, 'package.json');
console.log('packageJsonPath', packageJsonPath);
let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.main = './extension.js';
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log('文件复制完成');
console.log('package.json 更新完成');
// 列出扩展目录中的文件
console.log('扩展目录中的文件:');
fs.readdirSync(extensionDir).forEach(file => {
console.log(file);
});
- .vscodeignore
在插件根目录创建一个.vscodeignore文件
.vscode/**
.vscode-test-web/**
src/**
out/**
node_modules/**
dist/test/**
.gitignore
vsc-extension-quickstart.md
webpack.config.js
esbuild.js
.yarnrc
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
**/.vscode-test.*
webpack.*
.eslintignore
pnpm-lock.yaml
上述文件不会被打包到最终的vscode插件中
添加..gitignore文件
extension.js extension.js.map web/**/*
上述文件不会被上传到git
- 配置script
插件子工程的package.json配置
"scripts": {
"build": "webpack --mode production --devtool hidden-source-map",
"package": "pnpm run package-build",
"package-build": "webpack --mode production --devtool hidden-source-map",
"package-extension": "pnpm run package && node src/scripts/prepare-package.js && echo '准备运行 vsce 打包' && vsce package --no-dependencies"
},
配置项目根目录
"scripts": {
"package:build": "pnpm --filter vscode-design-extension run package-extension && mv packages/extension/*.vsix . ",
"package": " pnpm run build && pnpm run package:build"
}
注意:
vscode打包需要全局安装vsce
pnpm i vsce -g
或者将这个工具安装到开发环境中(这里装到了root,也可以选择装到插件工程的包中)
pnpm add @vscode/vsce -w
最后让我们执行打包吧(在项目根目录)
pnpm run package
配置.vscode文件
路径:.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension dev",
"type": "extensionHost",
"debugWebWorkerHost": true,
"request": "launch",
"args": [
// "--extensionDevelopmentPath=${workspaceFolder}/packages/extension"
"--extensionDevelopmentPath=${workspaceFolder}/dist"
// "--extensionDevelopmentPath=${workspaceFolder}/packages/extension"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
// "${workspaceFolder}/packages/extension/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Run Extension preview",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/dist"
// "--extensionDevelopmentPath=${workspaceFolder}/packages/extension"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: preview",
"env": {
"NODE_ENV": "production"
}
},
]
}
路径:.vscode/ tasks.json
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": "$ts-webpack-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "preview",
"label": "npm: preview", // 确保有这个 label
"problemMatcher": "$ts-webpack-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": "build"
},
]
}
这里对应了开发环境与预览环境,vscode点击调试按钮选择对应的环境按F5或者执行
路径 .vscode/settings
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"eslint.workingDirectories": [
".",
"./src/frontend",
"./src/backend"
]
}
eslint.workingDirectories 必须要配置, 这里为每个工程的路径。不配置可能会导致子工程的tsconfig失效
安装依赖
在所有包中运行脚本:
pnpm -r <command>
在特定包中运行脚本:
pnpm --filter <package-name> <command>
添加依赖到特定包:
pnpm add <package-name> --filter <target-package>
添加依赖到根目录:
pnpm add <package-name> -w
删除依赖从特定包:
pnpm remove <package-name> --filter <target-package>
更新所有包的依赖:
pnpm update -r
清理所有包的 node_modules:
pnpm -r exec -- rm -rf node_modules
列出工作空间中的所有包:
pnpm ls -r
在所有包中运行自定义脚本:
pnpm -r run <script-name>
安装所有依赖(包括工作空间包):
pnpm install
这些命令涵盖了 pnpm workspace 中最常见的操作。记住,-r 表示递归(所有包),--filter 用于指定特定包,-w 用于根工作空间。根据您的具体需求,可以组合使用这些命令来管理您的 monorepo 项目。
项目结构
├── CHANGELOG.md # 项目更新日志
├── dist # 构建输出目录
│ ├── extension.js # 编译后的扩展主文件
│ ├── extension.js.map # 源码映射文件
│ ├── package.json # 打包后的配置文件
│ ├── resources # 资源文件夹
│ │ └── icon.svg # 扩展图标
│ └── web # Web相关资源
│ ├── assets # 前端资源文件
│ │ ├── css # 样式文件
│ │ └── js # JavaScript文件
│ ├── index.html # 前端入口HTML
│ └── vite.svg # Vite logo
├── package.json # 项目主配置文件
├── packages # 多包结构目录
│ ├── extension # VS Code 扩展包
│ │ ├── src # 扩展源代码
│ │ │ ├── codeplugin # 代码插件相关
│ │ │ │ ├── designer.ts # 设计器相关代码
│ │ │ │ └── sidebar.ts # 侧边栏相关代码
│ │ │ ├── extension.ts # 扩展入口文件
│ │ │ ├── scripts # 脚本文件夹
│ │ │ ├── test # 测试文件夹
│ │ │ └── utils # 工具函数
│ │ ├── tsconfig.json # TypeScript配置
│ │ └── webpack.config.js # Webpack配置
│ ├── frontend # 前端项目
│ │ ├── src # 前端源代码
│ │ │ ├── App.vue # Vue主组件
│ │ │ ├── components # Vue组件目录
│ │ │ ├── main.ts # 前端入口文件
│ │ │ ├── utils # 工具函数
│ │ │ └── views # 视图组件
│ │ ├── tsconfig.json # TypeScript配置
│ │ └── vite.config.ts # Vite配置文件
│ └── server # 服务器端项目
│ ├── main.js # 服务器入口文件
│ ├── package.json # 服务器配置文件
│ └── src # 服务器源代码
├── pnpm-lock.yaml # pnpm 依赖锁定文件
├── pnpm-workspace.yaml # pnpm 工作空间配置
├── README.md # 项目说明文档
├── tsconfig.json # 根目录 TypeScript 配置
└── vscode-design-extension-0.0.1.vsix # VS Code 扩展安装包
这个项目结构展示了一个使用 monorepo 架构的 VS Code 扩展项目。主要特点如下:- 使用 pnpm 作为包管理工具,支持工作空间功能。
项目分为三个主要部分:
extension
:VS Code 扩展的核心代码,使用 TypeScript 开发。frontend
:扩展的 WebView 界面,使用 Vue.js 框架和 Vite 构建工具。server
:可能用于后端服务的代码。
dist
目录包含了构建后的扩展文件和前端资源。- 扩展部分使用 Webpack 进行构建,前端使用 Vite。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。