UNPKG

@czhlin/vite-plugin-vscode

Version:

Use vue/react to develop 'vscode extension webview', supporting esm/cjs

478 lines (464 loc) 18.1 kB
import path from "node:path"; import { cwd } from "node:process"; import merge from "lodash.merge"; import { build } from "tsdown"; import Logger from "@tomjs/logger"; import { fileURLToPath } from "node:url"; import fs from "node:fs"; import { emptyDirSync, readFileSync, readJsonSync } from "@tomjs/node"; import { parse } from "node-html-parser"; //#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion //#region src/constants.ts const PLUGIN_NAME = "@czhlin:vscode"; const PACKAGE_NAME = "@czhlin/vite-plugin-vscode"; const WEBVIEW_METHOD_NAME = "__getWebviewHtml__"; /** * @see https://github.com/vitejs/vite/blob/v4.0.1/packages/vite/src/node/constants.ts#L137-L147 */ const loopbackHosts = new Set([ "localhost", "127.0.0.1", "::1", "0000:0000:0000:0000:0000:0000:0000:0001" ]); const wildcardHosts = new Set([ "0.0.0.0", "::", "0000:0000:0000:0000:0000:0000:0000:0000" ]); //#endregion //#region src/logger.ts function createLogger() { return new Logger({ prefix: `[${PLUGIN_NAME}]`, time: true }); } //#endregion //#region node_modules/.pnpm/tsdown@0.12.9_typescript@5._8653d812bb279c16714774e8101ee61c/node_modules/tsdown/esm-shims.js const getFilename = () => fileURLToPath(import.meta.url); const getDirname = () => path.dirname(getFilename()); const __dirname = /* @__PURE__ */ getDirname(); //#endregion //#region src/utils.ts function resolveHostname(hostname) { return loopbackHosts.has(hostname) || wildcardHosts.has(hostname) ? "localhost" : hostname; } /** * @see https://github.com/jonschlinkert/normalize-path/blob/master/index.js#L8 */ function normalizePath(path$1, stripTrailing) { if (typeof path$1 !== "string") throw new TypeError("expected path to be a string"); if (path$1 === "\\" || path$1 === "/") return "/"; const len = path$1.length; if (len <= 1) return path$1; let prefix = ""; if (len > 4 && path$1[3] === "\\") { const ch = path$1[2]; if ((ch === "?" || ch === ".") && path$1.slice(0, 2) === "\\\\") { path$1 = path$1.slice(2); prefix = "//"; } } const segs = path$1.split(/[/\\]+/); if (stripTrailing !== false && segs[segs.length - 1] === "") segs.pop(); return prefix + segs.join("/"); } function resolveServerUrl(server) { const addressInfo = server.httpServer.address(); const isAddressInfo = (x) => x === null || x === void 0 ? void 0 : x.address; if (isAddressInfo(addressInfo)) { const { address, port } = addressInfo; const hostname = resolveHostname(address); const options = server.config.server; const protocol = options.https ? "https" : "http"; const devBase = server.config.base; const path$1 = typeof options.open === "string" ? options.open : devBase; const url = path$1.startsWith("http") ? path$1 : `${protocol}://${hostname}:${port}${path$1}`; return url; } } function getPkg() { const pkgFile = path.resolve(process.cwd(), "package.json"); if (!fs.existsSync(pkgFile)) throw new Error("Main file is not specified, and no package.json found"); const pkg = readJsonSync(pkgFile); if (!pkg.main) throw new Error("Main file is not specified, please check package.json"); return pkg; } const isDev$1 = process.env.NODE_ENV === "development"; function getDevWebviewClient() { return readFileSync(path.join(__dirname, "client.iife.js")); } //#endregion //#region src/webview-helper.ts var WebviewHelper = class { static getCsp(webview) { return (webview === null || webview === void 0 ? void 0 : webview.csp) || `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src {{cspSource}} 'unsafe-inline'; script-src 'nonce-{{nonce}}' 'unsafe-eval';">`; } static getUuidCode() { return ` function uuid() { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 32; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } `; } static getWebviewHtmlCode(uuidCode, cacheCode) { return ` import { Uri } from 'vscode'; ${uuidCode} ${cacheCode} export default function getWebviewHtml(options){ const { webview, context, inputName, injectCode } = options || {}; const nonce = uuid(); const baseUri = webview.asWebviewUri(Uri.joinPath(context.extensionUri, (process.env.VITE_WEBVIEW_DIST || 'dist'))); let html = htmlCode[inputName || 'index'] || ''; if (injectCode) { html = html.replace('<head>', '<head>'+ injectCode); } return html.replaceAll('{{cspSource}}', webview.cspSource).replaceAll('{{nonce}}', nonce).replaceAll('{{baseUri}}', baseUri); } `; } static handleHtmlCode(html, webview) { const root = parse(html); const head = root.querySelector("head"); if (!head) root === null || root === void 0 || root.insertAdjacentHTML("beforeend", "<head></head>"); const csp = this.getCsp(webview); head.insertAdjacentHTML("afterbegin", csp); if (csp && csp.includes("{{nonce}}")) { const tags = { script: "src", link: "href" }; Object.keys(tags).forEach((tag) => { const elements = root.querySelectorAll(tag); elements.forEach((element) => { const attr = element.getAttribute(tags[tag]); if (attr) element.setAttribute(tags[tag], `{{baseUri}}${attr}`); element.setAttribute("nonce", "{{nonce}}"); }); }); } return root.removeWhitespace().toString(); } static getCacheCode(cache, webview) { return `const htmlCode = { ${Object.keys(cache).map((s) => `'${s}': \`${this.handleHtmlCode(cache[s], webview)}\`,`).join("\n")} };`; } static getInjectCode(cache, webview) { return this.getWebviewHtmlCode(this.getUuidCode(), this.getCacheCode(cache, webview)); } static writeCode(code) { const prodCachePkgName = `${PACKAGE_NAME}-inject`; const prodCacheFolder = path.join(cwd(), "node_modules", prodCachePkgName); emptyDirSync(prodCacheFolder); const destFile = path.join(prodCacheFolder, "index.js"); fs.writeFileSync(destFile, code, { encoding: "utf8" }); return normalizePath(destFile); } }; //#endregion //#region node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/typeof.js var require_typeof = __commonJS({ "node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/typeof.js"(exports, module) { function _typeof$2(o) { "@babel/helpers - typeof"; return module.exports = _typeof$2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o$1) { return typeof o$1; } : function(o$1) { return o$1 && "function" == typeof Symbol && o$1.constructor === Symbol && o$1 !== Symbol.prototype ? "symbol" : typeof o$1; }, module.exports.__esModule = true, module.exports["default"] = module.exports, _typeof$2(o); } module.exports = _typeof$2, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/toPrimitive.js var require_toPrimitive = __commonJS({ "node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/toPrimitive.js"(exports, module) { var _typeof$1 = require_typeof()["default"]; function toPrimitive$1(t, r) { if ("object" != _typeof$1(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof$1(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } module.exports = toPrimitive$1, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/toPropertyKey.js var require_toPropertyKey = __commonJS({ "node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/toPropertyKey.js"(exports, module) { var _typeof = require_typeof()["default"]; var toPrimitive = require_toPrimitive(); function toPropertyKey$1(t) { var i = toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } module.exports = toPropertyKey$1, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/defineProperty.js var require_defineProperty = __commonJS({ "node_modules/.pnpm/@oxc-project+runtime@0.77.2/node_modules/@oxc-project/runtime/src/helpers/defineProperty.js"(exports, module) { var toPropertyKey = require_toPropertyKey(); function _defineProperty$1(e, r, t) { return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } module.exports = _defineProperty$1, module.exports.__esModule = true, module.exports["default"] = module.exports; } }); //#endregion //#region src/vscode-plugin.ts var import_defineProperty = __toESM(require_defineProperty()); const isDev = process.env.NODE_ENV === "development"; const logger = createLogger(); var VscodePlugin = class { constructor(options) { (0, import_defineProperty.default)(this, "options", void 0); (0, import_defineProperty.default)(this, "resolvedConfig", null); (0, import_defineProperty.default)(this, "prodHtmlCache", {}); (0, import_defineProperty.default)(this, "devWebviewClient", ""); this.options = this.mergeOptions(options); if (this.options.webview) this.devWebviewClient = getDevWebviewClient(); } mergeOptions(options) { const pkg = getPkg(); const format = pkg.type === "module" ? "esm" : "cjs"; const opts = merge({ webview: true, recommended: true, debug: false, extension: { entry: "extension/index.ts", outDir: "dist-extension", target: format === "esm" ? ["node20"] : ["es2019", "node14"], format, shims: true, clean: true, dts: false, treeshake: !isDev, outExtensions() { return { js: ".js" }; }, external: ["vscode"] } }, options); const opt = opts.extension || {}; ["entry", "format"].forEach((prop) => { const value = opt[prop]; if (!Array.isArray(value) && value) opt[prop] = [value]; }); if (isDev) opt.sourcemap = opt.sourcemap ?? true; else opt.minify ??= true; if (typeof opt.external !== "function") opt.external = ["vscode"].concat(opt.external ?? []); else { const fn = opt.external; opt.external = function(id, parentId, isResolved) { return id === "vscode" || fn.call(this, id, parentId, isResolved); }; } if (!opt.skipNodeModulesBundle) opt.noExternal = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.peerDependencies || {})); opts.extension = opt; if (opts.webview !== false) { let name = WEBVIEW_METHOD_NAME; if (typeof opts.webview === "string") name = opts.webview ?? WEBVIEW_METHOD_NAME; opts.webview = Object.assign({ name }, opts.webview); } return opts; } build({ env, webviewPath, onSuccess, watch }) { var _this$options; const { onSuccess: _onSuccess,...tsdownOptions } = this.options.extension || {}; const webview = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.webview; const injectPlugin = [{ name: `${PLUGIN_NAME}:inject`, transform(code) { if (code.includes(`${webview.name}(`)) return `import ${webview.name} from '${webviewPath}';\n${code}`; return code; } }]; return build(merge(tsdownOptions, { watch, env, silent: true, plugins: webview ? injectPlugin : [], async onSuccess() { if (typeof _onSuccess === "function") await _onSuccess(); onSuccess(); } })); } handleConfig(config) { var _config$build, _config$build2, _config$build3, _config$build4; let outDir = (config === null || config === void 0 || (_config$build = config.build) === null || _config$build === void 0 ? void 0 : _config$build.outDir) || "dist"; this.options.extension ??= {}; if (this.options.recommended) { this.options.extension.outDir = path.resolve(outDir, "extension"); outDir = path.resolve(outDir, "webview"); } const assetsDir = (config === null || config === void 0 || (_config$build2 = config.build) === null || _config$build2 === void 0 ? void 0 : _config$build2.assetsDir) || "assets"; const output = { chunkFileNames: `${assetsDir}/[name].js`, entryFileNames: `${assetsDir}/[name].js`, assetFileNames: `${assetsDir}/[name].[ext]` }; let rollupOutput = (config === null || config === void 0 || (_config$build3 = config.build) === null || _config$build3 === void 0 || (_config$build3 = _config$build3.rollupOptions) === null || _config$build3 === void 0 ? void 0 : _config$build3.output) ?? {}; if (Array.isArray(rollupOutput)) rollupOutput.map((s) => Object.assign(s, output)); else rollupOutput = Object.assign({}, rollupOutput, output); return { build: { outDir, sourcemap: isDev ? true : config === null || config === void 0 || (_config$build4 = config.build) === null || _config$build4 === void 0 ? void 0 : _config$build4.sourcemap, rollupOptions: { output: rollupOutput } } }; } configResolved(config) { this.resolvedConfig = config; } configureServer(server) { var _server$httpServer; if (!server || !server.httpServer) return; (_server$httpServer = server.httpServer) === null || _server$httpServer === void 0 || _server$httpServer.once("listening", async () => { const env = { NODE_ENV: server.config.mode || "development", VITE_DEV_SERVER_URL: resolveServerUrl(server) }; let buildCount = 0; logger.info("extension build start"); await this.build({ env, webviewPath: `${PACKAGE_NAME}/webview`, watch: true, onSuccess() { logger.info(buildCount > 0 ? "extension rebuild success" : "extension build success"); buildCount++; } }); }); } transformIndexHtml({ html, ctx, mode }) { if (!this.options.webview) return html; if (mode === "build") { var _ctx$chunk; this.prodHtmlCache[(_ctx$chunk = ctx.chunk) === null || _ctx$chunk === void 0 ? void 0 : _ctx$chunk.name] = html; return html; } if (this.options.devtools ?? true) { var _this$resolvedConfig, _this$resolvedConfig2; let port; if ((_this$resolvedConfig = this.resolvedConfig) === null || _this$resolvedConfig === void 0 ? void 0 : _this$resolvedConfig.plugins.find((s) => ["vite:react-refresh", "vite:react-swc"].includes(s.name))) port = 8097; else if ((_this$resolvedConfig2 = this.resolvedConfig) === null || _this$resolvedConfig2 === void 0 ? void 0 : _this$resolvedConfig2.plugins.find((s) => ["vite:vue", "vite:vue2"].includes(s.name))) port = 8098; if (port) html = html.replace(/<head>/i, `<head><script src="http://localhost:${port}"><\/script>`); else logger.warn("Only support react-devtools and vue-devtools!"); } return html.replace(/<head>/i, `<head><script>${this.devWebviewClient}<\/script>`); } configBundle() { var _this$resolvedConfig3, _this$resolvedConfig4; let webviewPath = ""; const webview = this.options.webview; if (webview) { const injectCode = WebviewHelper.getInjectCode(this.prodHtmlCache, webview); webviewPath = WebviewHelper.writeCode(injectCode); } let outDir = ((_this$resolvedConfig3 = this.resolvedConfig) === null || _this$resolvedConfig3 === void 0 ? void 0 : _this$resolvedConfig3.build.outDir.replace(cwd(), "").replaceAll("\\", "/")) ?? ""; if (outDir.startsWith("/")) outDir = outDir.substring(1); const env = { NODE_ENV: ((_this$resolvedConfig4 = this.resolvedConfig) === null || _this$resolvedConfig4 === void 0 ? void 0 : _this$resolvedConfig4.mode) || "production", VITE_WEBVIEW_DIST: outDir }; logger.info("extension build start"); this.build({ env, webviewPath, onSuccess() { logger.info("extension build success"); } }); } }; //#endregion //#region src/index.ts function useVSCodePlugin(options) { const vscodePlugin = new VscodePlugin(options); return [{ name: PLUGIN_NAME, apply: "serve", config(config) { return vscodePlugin.handleConfig(config); }, configResolved(config) { vscodePlugin.configResolved(config); }, configureServer(server) { vscodePlugin.configureServer(server); }, transformIndexHtml(html, ctx) { return vscodePlugin.transformIndexHtml({ html, ctx, mode: "dev" }); } }, { name: PLUGIN_NAME, apply: "build", enforce: "post", config(config) { return vscodePlugin.handleConfig(config); }, configResolved(config) { vscodePlugin.configResolved(config); }, transformIndexHtml(html, ctx) { return vscodePlugin.transformIndexHtml({ html, ctx, mode: "build" }); }, closeBundle() { vscodePlugin.configBundle(); } }]; } var src_default = useVSCodePlugin; //#endregion export { src_default as default };