refactor: migrate vscode extension from CRA + webpack to Vite + esbuild

Replace react-scripts and webpack with Vite (webview) and esbuild
(extension host), removing ~1000 dependencies and all audit
vulnerabilities from the extension. Upgrade Mantine to v8.
This commit is contained in:
AykutSarac
2026-03-18 15:06:29 +03:00
parent 5a0cb57fff
commit 836394dcf0
28 changed files with 270 additions and 10923 deletions
+3 -7
View File
@@ -2,15 +2,11 @@
"version": "0.2.0",
"configurations": [
{
"name": "Run VSCode Extension (apps/vscode)",
"name": "Run VSCode Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode"
],
"outFiles": [
"${workspaceFolder}/apps/vscode/build/**/*.js"
],
"args": ["--extensionDevelopmentPath=${workspaceFolder}/apps/vscode"],
"outFiles": ["${workspaceFolder}/apps/vscode/build/**/*.js"],
"preLaunchTask": "build vscode extension"
}
]
-1
View File
@@ -1 +0,0 @@
PUBLIC_URL=./
-5
View File
@@ -1,5 +0,0 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"]
}
-21
View File
@@ -1,21 +0,0 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/build/**/*.js"
],
"preLaunchTask": "build extension"
}
]
}
-10
View File
@@ -1,10 +0,0 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"build": false
},
"search.exclude": {
"build": true
},
"typescript.tsc.autoDetect": "off"
}
-21
View File
@@ -1,21 +0,0 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"label": "build extension",
"type": "shell",
"command": "pnpm run build",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "watch extension",
"type": "shell",
"command": "pnpm run watch"
}
]
}
+5 -9
View File
@@ -3,18 +3,14 @@
.github/**
.turbo/**
node_modules/**
scripts/**
src/**
ext-src/**
.gitignore
.env
.prettierignore
.prettierrc
.pnpm-lock.yaml
webpack.config.js
tsconfig.extension.json
vsc-extension-quickstart.md
yarn.lock
index.html
vite.config.ts
esbuild.config.mjs
eslint.config.mjs
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
+24 -5
View File
@@ -16,18 +16,37 @@
The extension works **fully offline**. No data is sent to any server. All JSON parsing and visualization happens locally in your editor.
## Debugging
## Development
This extension lives in `apps/vscode` inside the [jsoncrack.com](https://github.com/AykutSarac/jsoncrack.com) monorepo.
**Prerequisites:** Node.js `>=24`, pnpm `>=10`
**Prerequisites:** Node.js `>=20`, pnpm `>=10`
**Stack:** Vite (webview) + esbuild (extension host) + React 19
```sh
# Install dependencies from repo root
pnpm install
# Watch mode — rebuilds on every change
pnpm dev:vscode
# Build the extension
cd apps/vscode
pnpm run build
```
Then press **F5** in VS Code to launch the Extension Development Host. Keep the watch process running for live iteration.
### Debugging
1. Open the **monorepo root** in VS Code.
2. Press **F5** to launch the "Run VSCode Extension" config — it builds and opens the Extension Development Host.
3. After making changes, press `Cmd+R` (macOS) / `Ctrl+R` (Windows/Linux) in the host window to reload.
### Scripts
| Script | Description |
|---|---|
| `build` | Production build (minified, no sourcemaps) |
| `build:dev` | Dev build (sourcemaps, no minification) |
| `watch` | Watch extension host (`ext-src/`) for changes |
| `watch:webview` | Watch webview (`src/`) for changes |
| `dev` | Start Vite dev server (standalone webview in browser) |
| `lint` | Run ESLint + Prettier check |
| `clean` | Remove `build/` directory |
+26
View File
@@ -0,0 +1,26 @@
import * as esbuild from "esbuild";
const isWatch = process.argv.includes("--watch");
const isProduction = process.argv.includes("--production");
/** @type {esbuild.BuildOptions} */
const config = {
entryPoints: ["ext-src/extension.ts"],
bundle: true,
outdir: "build",
external: ["vscode"],
format: "cjs",
platform: "node",
target: "node20",
sourcemap: !isProduction,
minify: isProduction,
logLevel: "info",
};
if (isWatch) {
const ctx = await esbuild.context(config);
await ctx.watch();
console.log("Watching for changes...");
} else {
await esbuild.build(config);
}
+1 -1
View File
@@ -50,5 +50,5 @@ export default defineConfig([
],
},
},
globalIgnores(["build/**", "public/**", "scripts/**", "webpack.config.js"]),
globalIgnores(["build/**"]),
]);
+9 -22
View File
@@ -1,9 +1,9 @@
import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
export function createWebviewPanel(context: vscode.ExtensionContext, title = "JSON Crack") {
const extPath = context.extensionPath;
const webviewDir = vscode.Uri.file(path.join(extPath, "build", "webview"));
const panel = vscode.window.createWebviewPanel(
"liveHTMLPreviewer",
@@ -12,29 +12,17 @@ export function createWebviewPanel(context: vscode.ExtensionContext, title = "JS
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(path.join(extPath, "build")),
vscode.Uri.file(path.join(extPath, "build", "static")),
vscode.Uri.file(path.join(extPath, "build", "static", "js")),
vscode.Uri.file(path.join(extPath, "build", "static", "css")),
vscode.Uri.file(path.join(extPath, "assets")),
],
localResourceRoots: [webviewDir, vscode.Uri.file(path.join(extPath, "assets"))],
}
);
panel.iconPath = vscode.Uri.file(path.join(extPath, "build", "assets", "favicon.ico"));
panel.iconPath = vscode.Uri.file(path.join(extPath, "assets", "jsoncrack.png"));
const manifest = JSON.parse(
fs.readFileSync(path.join(extPath, "build", "asset-manifest.json"), "utf-8")
const scriptUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extPath, "build", "webview", "index.js"))
);
const styleUri = panel.webview.asWebviewUri(
vscode.Uri.file(path.join(extPath, "build", "webview", "index.css"))
);
const mainScript = manifest.files["main.js"];
const mainStyle = manifest.files["main.css"];
const scriptPathOnDisk = vscode.Uri.file(path.join(extPath, "build", mainScript));
const stylePathOnDisk = vscode.Uri.file(path.join(extPath, "build", mainStyle));
const stylesMainUri = panel.webview.asWebviewUri(stylePathOnDisk);
const scriptUri = panel.webview.asWebviewUri(scriptPathOnDisk);
const nonce = getNonce();
const csp = [
@@ -49,9 +37,8 @@ export function createWebviewPanel(context: vscode.ExtensionContext, title = "JS
<html lang="en">
<head>
<meta charset="utf-8">
<base href="${panel.webview.asWebviewUri(vscode.Uri.file(path.join(extPath, "build")))}/">
<meta http-equiv="Content-Security-Policy" content="${csp}">
<link href="${stylesMainUri}" rel="stylesheet">
<link href="${styleUri}" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+19 -40
View File
@@ -29,7 +29,7 @@
"activationEvents": [
"workspaceContains:**/*.{json}"
],
"main": "./build/ext-src/extension.js",
"main": "./build/extension.js",
"contributes": {
"commands": [
{
@@ -77,65 +77,44 @@
},
"scripts": {
"vscode:prepublish": "pnpm run build",
"dev": "react-scripts start",
"start": "react-scripts start",
"compile": "webpack --mode development",
"watch": "webpack --mode development --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"analyze": "ANALYZE=true pnpm run build",
"dev": "vite",
"build": "vite build && node esbuild.config.mjs --production",
"build:dev": "vite build && node esbuild.config.mjs",
"watch": "node esbuild.config.mjs --watch",
"watch:webview": "vite build --watch",
"lint": "eslint src ext-src && prettier --check src ext-src",
"lint:fix": "eslint --fix src ext-src && prettier --write src ext-src",
"build": "node ./scripts/build-non-split.js && tsc -p tsconfig.extension.json",
"clean": "rm -rf build",
"watch-build": "nodemon --watch src --watch ext-src --watch scripts --ext js,tsx,ts --exec \"pnpm run build\"",
"eject": "react-scripts eject"
"clean": "rm -rf build"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@mantine/code-highlight": "^7.16.2",
"@mantine/core": "^7.16.2",
"@mantine/hooks": "^7.16.2",
"@eslint/js": "^10.0.0",
"@mantine/code-highlight": "^8.3.18",
"@mantine/core": "^8.3.18",
"@mantine/hooks": "^8.3.18",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "16.x",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/vscode": "^1.86.0",
"@types/webpack-env": "^1.18.5",
"@eslint/js": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"@vitejs/plugin-react": "^4.3.0",
"esbuild": "^0.25.0",
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-unused-imports": "^4.4.1",
"globals": "^17.0.0",
"typescript-eslint": "^8.57.0",
"nodemon": "^2.0.20",
"prettier": "^3.3.3",
"react-scripts": "^5.0.1",
"rewire": "^7.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.6.3",
"vsce": "^2.15.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4"
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.0",
"vite": "^6.0.0"
},
"dependencies": {
"jsoncrack-react": "workspace:*",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"react-dom": "19.2.4",
"shiki": "^3.22.0"
},
"repository": {
"type": "git",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env node
const path = require("path");
const rewire = require("rewire");
const defaults = rewire("react-scripts/scripts/build.js");
let config = defaults.__get__("config");
// Disable source maps
config.devtool = false;
// Disable code splitting
config.optimization.splitChunks = {
cacheGroups: {
default: false
}
};
config.optimization.runtimeChunk = false;
// Enable tree shaking
config.optimization.usedExports = true;
// Force a single React instance in the bundle. Without this, workspace-linked
// packages can resolve a different React version and break hooks at runtime.
config.resolve.alias = {
...(config.resolve.alias || {}),
react: path.resolve(__dirname, "../node_modules/react"),
"react-dom": path.resolve(__dirname, "../node_modules/react-dom")
};
config.resolve.plugins = (config.resolve.plugins || []).filter(
(plugin) => plugin.constructor.name !== "ModuleScopePlugin"
);
// Ensure production optimizations are enabled
config.mode = 'production';
config.optimization.minimize = true;
// Allow extensionless imports from ESM dependencies.
config.module.rules.push({
test: /\.m?js$/,
resolve: {
fullySpecified: false
}
});
+29 -19
View File
@@ -1,9 +1,17 @@
import { useCallback, useEffect, useState } from "react";
import { Anchor, Box, MantineProvider, Text } from "@mantine/core";
import { CodeHighlightAdapterProvider, createShikiAdapter } from "@mantine/code-highlight";
import type { NodeData } from "jsoncrack-react";
import { JSONCrack } from "jsoncrack-react";
import { NodeModal } from "./components/NodeModal";
async function loadShiki() {
const { createHighlighter } = await import("shiki");
return createHighlighter({ langs: ["json"], themes: [] });
}
const shikiAdapter = createShikiAdapter(loadShiki);
function getTheme() {
const theme = document.body.getAttribute("data-vscode-theme-kind");
if (theme?.includes("light")) return "light" as const;
@@ -43,25 +51,27 @@ const App: React.FC = () => {
return (
<MantineProvider forceColorScheme={theme}>
<Box h="100vh" w="100vw">
<JSONCrack json={json} theme={theme} showControls={false} onNodeClick={handleNodeClick} />
{selectedNode && (
<NodeModal opened={!!selectedNode} onClose={closeNodeModal} nodeData={selectedNode} />
)}
<Anchor
pos="fixed"
bottom={0}
left={0}
href="https://jsoncrack.com/editor?utm_source=vscode&utm_campaign=attribute"
target="_blank"
>
<Box px="12" py="4" bg="dark">
<Text fz="sm" c="white">
Powered by JSON Crack
</Text>
</Box>
</Anchor>
</Box>
<CodeHighlightAdapterProvider adapter={shikiAdapter}>
<Box h="100vh" w="100vw">
<JSONCrack json={json} theme={theme} showControls={false} onNodeClick={handleNodeClick} />
{selectedNode && (
<NodeModal opened={!!selectedNode} onClose={closeNodeModal} nodeData={selectedNode} />
)}
<Anchor
pos="fixed"
bottom={0}
left={0}
href="https://jsoncrack.com/editor?utm_source=vscode&utm_campaign=attribute"
target="_blank"
>
<Box px="12" py="4" bg="dark">
<Text fz="sm" c="white">
Powered by JSON Crack
</Text>
</Box>
</Anchor>
</Box>
</CodeHighlightAdapterProvider>
</MantineProvider>
);
};
-1
View File
@@ -1,4 +1,3 @@
import React from "react";
import type { ModalProps } from "@mantine/core";
import { Modal, Stack, Text, ScrollArea } from "@mantine/core";
import { CodeHighlight } from "@mantine/code-highlight";
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
-7
View File
@@ -1,7 +0,0 @@
/// <reference types="react-scripts" />
declare global {
interface Window {
acquireVsCodeApi?: () => any;
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "build",
"lib": ["es6", "dom"],
"sourceMap": true,
"rootDir": ".",
"strict": true
},
"include": ["ext-src"],
"exclude": ["node_modules", ".vscode-test"]
}
+8 -12
View File
@@ -1,22 +1,18 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "ES6",
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": false,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noEmit": true,
"noImplicitAny": false
},
"include": ["src"],
"include": ["src", "ext-src"],
"exclude": ["node_modules"]
}
+28
View File
@@ -0,0 +1,28 @@
import react from "@vitejs/plugin-react";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
plugins: [react()],
build: {
outDir: "build/webview",
emptyOutDir: true,
rollupOptions: {
input: "src/index.tsx",
output: {
entryFileNames: "index.js",
assetFileNames: "index[extname]",
inlineDynamicImports: true,
},
},
},
resolve: {
alias: {
react: path.resolve(__dirname, "node_modules/react"),
"react-dom": path.resolve(__dirname, "node_modules/react-dom"),
},
},
});
-48
View File
@@ -1,48 +0,0 @@
/** @typedef {import('webpack').Configuration} WebpackConfig **/
const webpack = require("webpack");
const path = require("path");
/** @type WebpackConfig */
const extensionConfig = {
target: "node",
mode: "none",
entry: "./ext-src/extension.ts",
output: {
path: path.resolve(__dirname, "build"),
filename: "extension.js",
libraryTarget: "commonjs2",
},
externals: {
vscode: "commonjs vscode",
},
module: {
rules: [{
test: /\.ts$/,
exclude: [/node_modules/],
use: [{
loader: 'ts-loader'
}]
}]
},
resolve: {
extensions: [".ts", ".js"],
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1, // disable chunks by default since web extensions must be a single bundle
}),
new webpack.ProvidePlugin({
process: "process/browser", // provide a shim for the global `process` variable
}),
],
devtool: "nosources-source-map",
performance: {
hints: false
},
infrastructureLogging: {
level: "log",
},
};
module.exports = extensionConfig;
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
-1
View File
@@ -1,4 +1,3 @@
import React from "react";
import type { AppProps } from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
+1 -1
View File
@@ -13,7 +13,7 @@
"scripts": {
"dev": "turbo run dev",
"dev:www": "turbo run dev --filter=www",
"dev:vscode": "turbo run watch-build --filter=vscode",
"dev:vscode": "turbo run watch --filter=vscode",
"build": "turbo run build",
"build:www": "turbo run build --filter=www",
"build:vscode": "turbo run build --filter=vscode",
+103 -10619
View File
File diff suppressed because it is too large Load Diff