@czhlin/vite-plugin-vscode
Version:
Use vue/react to develop 'vscode extension webview', supporting esm/cjs
478 lines (464 loc) • 18.1 kB
JavaScript
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 };