记录一次VSCode插件开发的Monorepo搭建

为什么要改造成Monorepo?

在开发VSCode插件的过程中,随着项目规模的扩大和功能的增加,代码库变得越来越复杂。传统的单一仓库结构在这种情况下会面临以下挑战:

  1. 代码组织混乱:多个模块混杂在一起,难以维护和管理。
  2. 依赖管理困难:不同模块之间的依赖关系不清晰,容易导致版本冲突。
  3. 构建和测试复杂:每个模块的构建和测试流程独立,难以统一管理。
  4. 代码复用困难:跨模块的代码复用变得复杂,容易出现重复代码。

为了应对这些挑战,我们决定将项目改造成Monorepo架构。Monorepo允许我们在一个仓库中管理多个相关但独立的模块,带来了以下好处:

  1. 更好的代码组织:模块化管理,清晰的目录结构。
  2. 简化依赖管理:统一管理依赖,避免版本冲突。
  3. 统一的构建和测试流程:集中管理构建和测试,提高效率。
  4. 促进代码复用:跨模块共享代码,减少重复。

为什么选择pnpm workspace方案?

在众多Monorepo解决方案中,我们选择了pnpm workspace。这个选择基于以下几个原因:

  1. 高效的依赖管理:pnpm使用硬链接和符号链接来共享包,大大节省了磁盘空间和安装时间。
  2. 严格的依赖结构:pnpm默认创建非扁平的node_modules结构,有效防止依赖提升带来的潜在问题。
  3. 原生支持workspace:pnpm原生支持workspace,无需额外的工具就能管理多包项目。
  4. 快速且轻量:相比其他方案,pnpm的性能更优,且不需要复杂的配置。
  5. 良好的生态系统:pnpm与现有的npm生态完全兼容,无缝集成各种工具和脚本。
  6. 简单直观的命令: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/*' //子项目空间

packages下所有的文件夹都会是一个子项目的根目录

3、 创建子项目

在packages文件夹下创建多个子项目
文件结构如下:
packages/ //monorepo工程化子项目空间
├── extension/ //vscode开发工程
├── frontend/ //前端开发工程
└── nodemiddleware/ //node中间件,脚本等

4、tsconfig.json配置

  1. 根目录 tsconfig.json:
    在根目录创建一个基础的 tsconfig.json 文件,作为所有子项目的基础配置。
    使用 "extends" 字段让子项目继承根目录的配置。
  2. 子项目 tsconfig.json:
    每个子项目都应该有自己的 tsconfig.json,继承根目录的配置并根据需要进行覆盖。
    使用 "references" 字段来声明项目间的依赖关系。
  3. 路径别名:
    使用 "paths" 配置来设置路径别名,简化导入语句。
  4. 项目引用:
    利用 TypeScript 的项目引用功能(Project References)来优化构建过程和提高类型检查效率。
  5. 严格模式:
    建议开启严格模式 "strict": true,以获得更好的类型检查。
  6. 输出配置:
    根据项目需求配置 "outDir"、"rootDir" 等输出相关选项。
  7. 模块解析:
    设置 "moduleResolution": "node" 以使用 Node.js 风格的模块解析。
    兼容性:
    根据目标环境配置 "target" 和 "lib" 选项。
  8. 声明文件:
    配置 "declaration": true 以生成声明文件(.d.ts)。
  9. 增量编译:
    启用 "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配置

  1. 根目录 ESLint 配置:
    在根目录创建一个 .eslintrc.js 文件,作为基础配置。
    使用 root: true 标记根配置文件。
  2. 子项目 ESLint 配置:
    每个子项目可以有自己的 .eslintrc.js,继承根目录的配置并根据需要进行覆盖。
  3. 共享规则:
    在根目录定义共享的 ESLint 规则,确保整个项目代码风格一致。
  4. 插件和扩展:
    根据项目需求配置必要的 ESLint 插件和扩展,如 typescript-eslint、prettier 等。
  5. 忽略文件:
    在根目录创建 .eslintignore 文件,指定需要忽略的文件和目录。
  6. 与 TypeScript 集成:
    配置 ESLint 以正确处理 TypeScript 文件。
  7. 脚本配置:
    在 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'),
        },
      ],
    }),
  ],

这里有几个需要注意的事项:

  1. 环境变量: 当前环境变量通过.vscode中的文件配置
  2. webview的地址引用vite开发服务器的@vite/client 与/src/main.ts
  3. 因为本文重点介绍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文件以及插件文件。因此:

  1. 我们需要一个node脚本将dist中的目录复制到插件这个包中,才能进行打包
  2. 需要一个.vscodeignore文件用来标注那些文件不要被打包
  3. 需要一个.gitignore来标注复制来的这些文件不能上传到git服务器
  4. 脚本

在插件目录的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);
});
  1. .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插件中

  1. 添加..gitignore文件

    extension.js
    extension.js.map
    web/**/*
    
    

    上述文件不会被上传到git

  2. 配置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失效

安装依赖

  1. 在所有包中运行脚本:

    pnpm -r <command>
  2. 在特定包中运行脚本:

    pnpm --filter <package-name> <command>
  3. 添加依赖到特定包:

    pnpm add <package-name> --filter <target-package>
  4. 添加依赖到根目录:

    pnpm add <package-name> -w
  5. 删除依赖从特定包:

    pnpm remove <package-name> --filter <target-package>
  6. 更新所有包的依赖:

    
    pnpm update -r
  7. 清理所有包的 node_modules:

    pnpm -r exec -- rm -rf node_modules
  8. 列出工作空间中的所有包:

    pnpm ls -r
  9. 在所有包中运行自定义脚本:

    pnpm -r run <script-name>
  10. 安装所有依赖(包括工作空间包):

    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 扩展项目。主要特点如下:

  11. 使用 pnpm 作为包管理工具,支持工作空间功能。
  12. 项目分为三个主要部分:

    • extension:VS Code 扩展的核心代码,使用 TypeScript 开发。
    • frontend:扩展的 WebView 界面,使用 Vue.js 框架和 Vite 构建工具。
    • server:可能用于后端服务的代码。
  13. dist 目录包含了构建后的扩展文件和前端资源。
  14. 扩展部分使用 Webpack 进行构建,前端使用 Vite。

919101797
1 声望0 粉丝