Added cache check to WebGPU renderer's loadShader method. Un-commented entries into config.js. Inclusions are now explicit dynamic import lambdas, so the cache functions can detect and call them; however, webpack and rollup seem to use them differently.

This commit is contained in:
Rezmason
2025-05-20 07:57:27 -07:00
parent 24e939008e
commit f61a4e29c9
20 changed files with 268 additions and 160 deletions

View File

@@ -1,4 +1,19 @@
TODO:
Make sure component works right
bundled, of course
webpack?
Minimum react requirement?
Move off of regl
Unify implementations?
Responsive changes
Move start time to rain object
Matrix component should record, then overwrite it
Reshape all passes to react to config changes, ie. "configure"
main.js "formulate" --> "configure"
simple deltas only require updating the uniforms
return boolean of whether all deltas are simple
Resource changes are simple if they're cached and loaded, false otherwise
remake the pipeline if anything returns false
Create multiple distributions
core
One embedded MSDF, combined from the two main glyph sets and their configs
@@ -7,7 +22,6 @@ TODO:
and then one with built-in MSDF generation
(TTF + glyphString) --> MSDF
Is MSDF strictly necessary?
Move off of regl
Expanded configurability
Modify regl pass
async build(config, inputs)

View File

@@ -112,7 +112,9 @@ export const Matrix = memo((props) => {
const [rRenderer, setRenderer] = useState(null);
const [rRain, setRain] = useState(null);
const configProps = Object.fromEntries(Object.entries(rawConfigProps).filter(([_, value]) => value != null));
const configProps = Object.fromEntries(
Object.entries(rawConfigProps).filter(([_, value]) => value != null),
);
const supportsWebGPU = () => {
return (
@@ -155,7 +157,12 @@ export const Matrix = memo((props) => {
setCanvas(canvas);
const loadRain = async () => {
const renderer = await import(`./${useWebGPU ? "webgpu" : "regl"}/main.js`);
let renderer;
if (useWebGPU) {
renderer = await import("./webgpu/main.js");
} else {
renderer = await import("./regl/main.js");
}
setRenderer(renderer);
const rain = await renderer.init(canvas);
setRain(rain);

View File

@@ -2,7 +2,4 @@ import { Matrix } from "./Matrix";
import inclusions from "./inclusions";
import * as reglRenderer from "./regl/main";
import * as webgpuRenderer from "./webgpu/main";
globalThis.inclusions = inclusions;
globalThis.reglRenderer = reglRenderer;
globalThis.webgpuRenderer = webgpuRenderer;
globalThis.Matrix = Matrix;
export { inclusions, reglRenderer, webgpuRenderer, Matrix };

View File

@@ -1,9 +1,10 @@
export default async () => {
let glMatrix, createREGL;
let glMatrix, createREGL, inclusions;
try {
glMatrix = await import("gl-matrix");
createREGL = (await import("regl")).default;
inclusions = (await import("./inclusions.js")).default;
} catch {
const loadJS = (src) =>
new Promise((resolve, reject) => {
@@ -14,7 +15,8 @@ export default async () => {
await Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]);
glMatrix = globalThis.glMatrix;
createREGL = globalThis.createREGL;
inclusions = [];
}
return { glMatrix, createREGL };
return { glMatrix, createREGL, inclusions };
};

View File

@@ -1,75 +1,74 @@
import highPassFrag from "../shaders/glsl/bloomPass.highPass.frag.glsl";
import blurFrag from "../shaders/glsl/bloomPass.blur.frag.glsl";
import combineFrag from "../shaders/glsl/bloomPass.combine.frag.glsl";
import imagePassFrag from "../shaders/glsl/imagePass.frag.glsl";
import mirrorPassFrag from "../shaders/glsl/mirrorPass.frag.glsl";
import palettePassFrag from "../shaders/glsl/palettePass.frag.glsl";
import rainPassIntro from "../shaders/glsl/rainPass.intro.frag.glsl";
import rainPassRaindrop from "../shaders/glsl/rainPass.raindrop.frag.glsl";
import rainPassSymbol from "../shaders/glsl/rainPass.symbol.frag.glsl";
import rainPassEffect from "../shaders/glsl/rainPass.effect.frag.glsl";
import rainPassVert from "../shaders/glsl/rainPass.vert.glsl";
import rainPassFrag from "../shaders/glsl/rainPass.frag.glsl";
import stripePassFrag from "../shaders/glsl/stripePass.frag.glsl";
import msdfCoptic from "../assets/coptic_msdf.png";
import msdfGothic from "../assets/gothic_msdf.png";
import msdfMatrixCode from "../assets/matrixcode_msdf.png";
import msdfRes from "../assets/resurrections_msdf.png";
// import megacity from "../assets/megacity_msdf.png";
import msdfResGlint from "../assets/resurrections_glint_msdf.png";
// import msdfHuberfishA from "../assets/huberfish_a_msdf.png";
// import msdfHuberfishD from "../assets/huberfish_d_msdf.png";
// import msdfGtargTenretni from "../assets/gtarg_tenretniolleh_msdf.png";
// import msdfGtargAlienText from "../assets/gtarg_alientext_msdf.png";
// import msdfNeoMatrixology from "../assets/neomatrixology_msdf.png";
// import texSand from "../assets/sand.png";
// import texPixels from "../assets/pixel_grid.png";
import texMesh from "../assets/mesh.png";
import texMetal from "../assets/metal.png";
import bloomBlurShader from "../shaders/wgsl/bloomBlur.wgsl";
import bloomCombineShader from "../shaders/wgsl/bloomCombine.wgsl";
import endPassShader from "../shaders/wgsl/endPass.wgsl";
import imagePassShader from "../shaders/wgsl/imagePass.wgsl";
import mirrorPassShader from "../shaders/wgsl/mirrorPass.wgsl";
import palettePassShader from "../shaders/wgsl/palettePass.wgsl";
import rainPassShader from "../shaders/wgsl/rainPass.wgsl";
import stripePassShader from "../shaders/wgsl/stripePass.wgsl";
export default [
highPassFrag,
blurFrag,
combineFrag,
imagePassFrag,
mirrorPassFrag,
palettePassFrag,
rainPassIntro,
rainPassRaindrop,
rainPassSymbol,
rainPassEffect,
rainPassVert,
rainPassFrag,
stripePassFrag,
msdfCoptic,
msdfGothic,
msdfMatrixCode,
msdfRes,
// megacity,
msdfResGlint,
// msdfHuberfishA,
// msdfHuberfishD,
// msdfGtargTenretni,
// msdfGtargAlienText,
// msdfNeoMatrixology,
// texSand,
// texPixels,
texMesh,
texMetal,
bloomBlurShader,
bloomCombineShader,
endPassShader,
imagePassShader,
mirrorPassShader,
palettePassShader,
rainPassShader,
stripePassShader,
[
"import::shaders/glsl/bloomPass.highPass.frag.glsl",
() => import("../shaders/glsl/bloomPass.highPass.frag.glsl"),
],
[
"import::shaders/glsl/bloomPass.blur.frag.glsl",
() => import("../shaders/glsl/bloomPass.blur.frag.glsl"),
],
[
"import::shaders/glsl/bloomPass.combine.frag.glsl",
() => import("../shaders/glsl/bloomPass.combine.frag.glsl"),
],
["import::shaders/glsl/imagePass.frag.glsl", () => import("../shaders/glsl/imagePass.frag.glsl")],
[
"import::shaders/glsl/mirrorPass.frag.glsl",
() => import("../shaders/glsl/mirrorPass.frag.glsl"),
],
[
"import::shaders/glsl/palettePass.frag.glsl",
() => import("../shaders/glsl/palettePass.frag.glsl"),
],
[
"import::shaders/glsl/rainPass.intro.frag.glsl",
() => import("../shaders/glsl/rainPass.intro.frag.glsl"),
],
[
"import::shaders/glsl/rainPass.raindrop.frag.glsl",
() => import("../shaders/glsl/rainPass.raindrop.frag.glsl"),
],
[
"import::shaders/glsl/rainPass.symbol.frag.glsl",
() => import("../shaders/glsl/rainPass.symbol.frag.glsl"),
],
[
"import::shaders/glsl/rainPass.effect.frag.glsl",
() => import("../shaders/glsl/rainPass.effect.frag.glsl"),
],
["import::shaders/glsl/rainPass.vert.glsl", () => import("../shaders/glsl/rainPass.vert.glsl")],
["import::shaders/glsl/rainPass.frag.glsl", () => import("../shaders/glsl/rainPass.frag.glsl")],
[
"import::shaders/glsl/stripePass.frag.glsl",
() => import("../shaders/glsl/stripePass.frag.glsl"),
],
["import::assets/coptic_msdf.png", () => import("../assets/coptic_msdf.png")],
["import::assets/gothic_msdf.png", () => import("../assets/gothic_msdf.png")],
["import::assets/matrixcode_msdf.png", () => import("../assets/matrixcode_msdf.png")],
["import::assets/resurrections_msdf.png", () => import("../assets/resurrections_msdf.png")],
["import::assets/megacity_msdf.png", () => import("../assets/megacity_msdf.png")],
[
"import::assets/resurrections_glint_msdf.png",
() => import("../assets/resurrections_glint_msdf.png"),
],
["import::assets/huberfish_a_msdf.png", () => import("../assets/huberfish_a_msdf.png")],
["import::assets/huberfish_d_msdf.png", () => import("../assets/huberfish_d_msdf.png")],
[
"import::assets/gtarg_tenretniolleh_msdf.png",
() => import("../assets/gtarg_tenretniolleh_msdf.png"),
],
["import::assets/gtarg_alientext_msdf.png", () => import("../assets/gtarg_alientext_msdf.png")],
["import::assets/neomatrixology_msdf.png", () => import("../assets/neomatrixology_msdf.png")],
["import::assets/sand.png", () => import("../assets/sand.png")],
["import::assets/pixel_grid.png", () => import("../assets/pixel_grid.png")],
["import::assets/mesh.png", () => import("../assets/mesh.png")],
["import::assets/metal.png", () => import("../assets/metal.png")],
["import::shaders/wgsl/bloomBlur.wgsl", () => import("../shaders/wgsl/bloomBlur.wgsl")],
["import::shaders/wgsl/bloomCombine.wgsl", () => import("../shaders/wgsl/bloomCombine.wgsl")],
["import::shaders/wgsl/endPass.wgsl", () => import("../shaders/wgsl/endPass.wgsl")],
["import::shaders/wgsl/imagePass.wgsl", () => import("../shaders/wgsl/imagePass.wgsl")],
["import::shaders/wgsl/mirrorPass.wgsl", () => import("../shaders/wgsl/mirrorPass.wgsl")],
["import::shaders/wgsl/palettePass.wgsl", () => import("../shaders/wgsl/palettePass.wgsl")],
["import::shaders/wgsl/rainPass.wgsl", () => import("../shaders/wgsl/rainPass.wgsl")],
["import::shaders/wgsl/stripePass.wgsl", () => import("../shaders/wgsl/stripePass.wgsl")],
];

View File

@@ -4,7 +4,6 @@ import { createRoot } from "react-dom/client";
import { Matrix } from "./Matrix";
const root = createRoot(document.getElementById("root"));
let idx = 1;
const versions = [
"classic",
"3d",
@@ -17,24 +16,42 @@ const versions = [
"bugs",
"morpheus",
];
const effects = ["none", "plain", "palette", "stripes", "pride", "trans", "image", "mirror"];
const App = () => {
const [version, setVersion] = useState(versions[0]);
const [effect, setEffect] = useState("plain");
const [numColumns, setNumColumns] = useState(80);
const [cursorColor, setCursorColor] = useState(null);
const [backgroundColor, setBackgroundColor] = useState("0,0,0");
const [rendererType, setRendererType] = useState(null);
const [density, setDensity] = useState(2);
const [destroyed, setDestroyed] = useState(false);
const onButtonClick = () => {
const onVersionButtonClick = () => {
setVersion((s) => {
const newVersion = versions[idx];
idx = (idx + 1) % versions.length;
console.log(newVersion);
let index = versions.indexOf(version) + 1;
if (index === versions.length) {
index = 0;
}
const newVersion = versions[index];
console.log("version:", newVersion);
return newVersion;
});
setCursorColor(null);
setBackgroundColor(null);
};
const onEffectButtonClick = () => {
setEffect((s) => {
let index = effects.indexOf(effect) + 1;
if (index === effects.length) {
index = 0;
}
const newEffect = effects[index];
console.log("effect:", newEffect);
return newEffect;
});
setCursorColor(null);
setBackgroundColor(null);
};
const onRendererButtonClick = () => {
setRendererType(() => (rendererType === "webgpu" ? "regl" : "webgpu"));
};
@@ -45,7 +62,8 @@ const App = () => {
return (
<div>
<h1>Rain</h1>
<button onClick={onButtonClick}>Version: "{version}"</button>
<button onClick={onVersionButtonClick}>Version: "{version}"</button>
<button onClick={onEffectButtonClick}>Effect: "{effect}"</button>
<button onClick={onRendererButtonClick}>Renderer: {rendererType ?? "default (regl)"}</button>
<button onClick={onDestroyButtonClick}>Destroy</button>
<label htmlFor="cursor-color">Cursor color: </label>
@@ -86,6 +104,7 @@ const App = () => {
<Matrix
style={{ width: "80vw", height: "45vh" }}
version={version}
effect={effect}
numColumns={numColumns}
renderer={rendererType}
cursorColor={cursorColor}

View File

@@ -21,12 +21,13 @@ const effects = {
mirror: makeMirrorPass,
};
let createREGL, glMatrix;
let createREGL, glMatrix, inclusions;
export const init = async (canvas) => {
const libraries = await fetchLibraries();
createREGL = libraries.createREGL;
glMatrix = libraries.glMatrix;
inclusions = libraries.inclusions;
const resize = () => {
const devicePixelRatio = window.devicePixelRatio ?? 1;

View File

@@ -17,14 +17,12 @@ const fonts = {
glyphSequenceLength: 57,
glyphTextureGridSize: [8, 8],
},
/*
megacity: {
// The glyphs seen in the film trilogy
glyphMSDFURL: "assets/megacity_msdf.png",
glyphSequenceLength: 64,
glyphTextureGridSize: [8, 8],
},
*/
resurrections: {
// The glyphs seen in the film trilogy
glyphMSDFURL: "assets/resurrections_msdf.png",
@@ -32,7 +30,6 @@ const fonts = {
glyphSequenceLength: 135,
glyphTextureGridSize: [13, 12],
},
/*
huberfishA: {
glyphMSDFURL: "assets/huberfish_a_msdf.png",
glyphSequenceLength: 34,
@@ -58,12 +55,11 @@ const fonts = {
glyphSequenceLength: 12,
glyphTextureGridSize: [4, 4],
},
*/
};
const textureURLs = {
// sand: "assets/sand.png",
// pixels: "assets/pixel_grid.png",
sand: "assets/sand.png",
pixels: "assets/pixel_grid.png",
mesh: "assets/mesh.png",
metal: "assets/metal.png",
};
@@ -136,7 +132,6 @@ const defaults = {
const versions = {
classic: {},
/*
megacity: {
font: "megacity",
animationSpeed: 0.5,
@@ -155,7 +150,6 @@ const versions = {
cursorColor: hsl(0.167, 1, 0.75),
cursorIntensity: 2,
},
*/
operator: {
cursorColor: hsl(0.375, 1, 0.66),
cursorIntensity: 3,
@@ -268,7 +262,6 @@ const versions = {
raindropLength: 0.3,
density: 0.75,
},
/*
morpheus: {
font: "resurrections",
glintTexture: "mesh",
@@ -358,7 +351,6 @@ const versions = {
// { color: hsl(0.1, 1.0, 0.9), at: 1.0 },
],
},
*/
["3d"]: {
volumetric: true,
fallSpeed: 0.5,

View File

@@ -39,7 +39,7 @@ const makePyramidViews = (pyramid) => pyramid.map((tex) => tex.createView());
// The bloom pass is basically an added blur of the rain pass's high-pass output.
// The blur approximation is the sum of a pyramid of downscaled, blurred textures.
export default ({ config, device }) => {
export default ({ config, device, cache }) => {
const pyramidHeight = 4;
const bloomSize = config.bloomSize;
const bloomStrength = config.bloomStrength;
@@ -54,8 +54,8 @@ export default ({ config, device }) => {
}
const assets = [
loadShader(device, "shaders/wgsl/bloomBlur.wgsl"),
loadShader(device, "shaders/wgsl/bloomCombine.wgsl"),
loadShader(device, cache, "shaders/wgsl/bloomBlur.wgsl"),
loadShader(device, cache, "shaders/wgsl/bloomCombine.wgsl"),
];
const linearSampler = device.createSampler({

View File

@@ -5,7 +5,7 @@ import { loadShader, makeBindGroup, makePass } from "./utils.js";
const numVerticesPerQuad = 2 * 3;
export default ({ device, canvasFormat, canvasContext }) => {
export default ({ device, cache, canvasFormat, canvasContext }) => {
const nearestSampler = device.createSampler();
const renderPassConfig = {
@@ -21,7 +21,7 @@ export default ({ device, canvasFormat, canvasContext }) => {
let renderPipeline;
let renderBindGroup;
const assets = [loadShader(device, "shaders/wgsl/endPass.wgsl")];
const assets = [loadShader(device, cache, "shaders/wgsl/endPass.wgsl")];
const loaded = (async () => {
const [imageShader] = await Promise.all(assets);

View File

@@ -17,7 +17,7 @@ export default ({ config, cache, device }) => {
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
const assets = [
loadTexture(device, cache, bgURL),
loadShader(device, "shaders/wgsl/imagePass.wgsl"),
loadShader(device, cache, "shaders/wgsl/imagePass.wgsl"),
];
const linearSampler = device.createSampler({

View File

@@ -24,11 +24,12 @@ const effects = {
mirror: makeMirrorPass,
};
let glMatrix;
let glMatrix, inclusions;
export const init = async (canvas) => {
const libraries = await fetchLibraries();
glMatrix = libraries.glMatrix;
inclusions = libraries.inclusions;
const resize = () => {
const devicePixelRatio = window.devicePixelRatio ?? 1;

View File

@@ -24,8 +24,8 @@ window.onclick = (e) => {
touchesChanged = true;
};
export default ({ config, device, cameraTex, cameraAspectRatio, timeBuffer }) => {
const assets = [loadShader(device, "shaders/wgsl/mirrorPass.wgsl")];
export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffer }) => {
const assets = [loadShader(device, cache, "shaders/wgsl/mirrorPass.wgsl")];
const linearSampler = device.createSampler({
magFilter: "linear",

View File

@@ -73,7 +73,7 @@ const makePalette = (device, paletteUniforms, entries) => {
// won't persist across subsequent frames. This is a safe trick
// in screen space.
export default ({ config, device, timeBuffer }) => {
export default ({ config, device, cache, timeBuffer }) => {
const linearSampler = device.createSampler({
magFilter: "linear",
minFilter: "linear",
@@ -86,7 +86,7 @@ export default ({ config, device, timeBuffer }) => {
let output;
let screenSize;
const assets = [loadShader(device, "shaders/wgsl/palettePass.wgsl")];
const assets = [loadShader(device, cache, "shaders/wgsl/palettePass.wgsl")];
const loaded = (async () => {
const [paletteShader] = await Promise.all(assets);

View File

@@ -39,7 +39,7 @@ export default ({ config, glMatrix, cache, device, timeBuffer }) => {
loadTexture(device, cache, config.glintMSDFURL),
loadTexture(device, cache, config.baseTextureURL, false, true),
loadTexture(device, cache, config.glintTextureURL, false, true),
loadShader(device, "shaders/wgsl/rainPass.wgsl"),
loadShader(device, cache, "shaders/wgsl/rainPass.wgsl"),
];
// The volumetric mode multiplies the number of columns

View File

@@ -43,7 +43,7 @@ const numVerticesPerQuad = 2 * 3;
// won't persist across subsequent frames. This is a safe trick
// in screen space.
export default ({ config, device, timeBuffer }) => {
export default ({ config, device, cache, timeBuffer }) => {
// Expand and convert stripe colors into 1D texture data
const stripeColors =
"stripeColors" in config
@@ -68,7 +68,7 @@ export default ({ config, device, timeBuffer }) => {
let output;
let screenSize;
const assets = [loadShader(device, "shaders/wgsl/stripePass.wgsl")];
const assets = [loadShader(device, cache, "shaders/wgsl/stripePass.wgsl")];
const loaded = (async () => {
const [stripeShader] = await Promise.all(assets);

View File

@@ -62,7 +62,11 @@ const makeComputeTarget = (device, size, mipLevelCount = 1) =>
GPUTextureUsage.STORAGE_BINDING,
});
const loadShader = async (device, url) => {
const loadShader = async (device, cache, url) => {
const key = url;
if (cache.has(key)) {
return cache.get(key);
}
const response = await fetch(url);
const code = await response.text();
return {

View File

@@ -1,10 +1,21 @@
{
"name": "react-matrix-rain",
"version": "1.0.0",
"description": "web-based green code rain, made with love, for react",
"main": "dist/index.cjs.js",
"name": "digital-rain",
"version": "0.1.0",
"description": "web-based green code rain, made with love",
"type": "module",
"main": "./dist/digital-rain.cjs",
"module": "./dist/digital-rain.module.js",
"exports": {
".": {
"import": "./dist/digital-rain.module.js",
"require": "./dist/digital-rain.cjs"
}
},
"files": [
"/dist"
"/dist",
"LICENSE",
"package.json",
"README.md"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@@ -12,12 +23,35 @@
"start": "npm run format ; webpack serve --config ./webpack.config.js",
"build": "npm run format ; rollup -c"
},
"keywords": [],
"author": "",
"license": "ISC",
"keywords": [
"rain",
"matrix",
"javascript",
"webgl",
"webgl-computer-graphics",
"matrix-rain",
"matrix-digital-rain"
],
"author": {
"name": "Rezmason",
"url": "https://rezmason.net"
},
"contributors": [
{
"name": "nohren"
}
],
"homepage": "https://github.com/Rezmason/matrix",
"repository": {
"type": "git",
"url": "https://github.com/Rezmason/matrix"
},
"bugs": {
"url": "https://github.com/Rezmason/matrix/issues"
},
"license": "MIT",
"dependencies": {
"gl-matrix": "^3.4.3",
"holoplay-core": "^0.0.9",
"regl": "^2.1.0"
},
"devDependencies": {
@@ -46,5 +80,13 @@
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
}

View File

@@ -8,7 +8,8 @@ import terser from "@rollup/plugin-terser";
import { string } from "rollup-plugin-string";
import image from "@rollup/plugin-image";
export default {
export default [
{
input: "js/bundle-contents.js",
external: ["react", "react-dom"], // keep them out of your bundle
plugins: [
@@ -34,14 +35,38 @@ export default {
includeAssets: true,
}), // bundle-size treemap
],
output: [
{
output: {
inlineDynamicImports: true,
file: "dist/index.cjs.js",
file: "dist/digital-rain.cjs.js",
format: "cjs",
exports: "named",
sourcemap: false,
},
// { file: 'dist/index.esm.js', format: 'es' } // optional ESM build
},
{
input: "js/bundle-contents.js",
external: ["react", "react-dom"], // keep them out of your bundle
plugins: [
peerDepsExternal(), // auto-exclude peerDeps
nodeResolve(), // so Rollup can find deps in node_modules
string({ include: ["**/*.glsl"] }),
string({ include: ["**/*.wgsl"] }),
image({ include: ["**/*.png"], limit: 0 }),
babel({
exclude: "node_modules/**", // transpile JSX
babelHelpers: "bundled",
presets: ["@babel/preset-react", "@babel/preset-env"],
}),
commonjs(), // turn CJS deps into ES
],
};
output: [
{
inlineDynamicImports: true,
file: "dist/digital-rain.module.js",
format: "es",
exports: "named",
sourcemap: true,
},
],
},
];

View File

@@ -1,9 +1,14 @@
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
import webpack from "webpack";
import path from "path";
import HtmlWebpackPlugin from "html-webpack-plugin";
import CopyPlugin from "copy-webpack-plugin";
module.exports = {
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default {
mode: "development",
entry: path.resolve(__dirname, "./js/index.js"),
module: {