diff --git a/.gitignore b/.gitignore index 76add87..1ac4868 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -dist \ No newline at end of file +dist +*~ +*.kra diff --git a/package-lock.json b/package-lock.json index 64440a5..dfb9f5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "solid-js": "^1.8.11" }, "devDependencies": { + "@types/node": "^20.14.9", "eslint": "^8.57.0", "eslint-plugin-solid": "^0.14.0", "typescript": "^5.3.3", @@ -1281,6 +1282,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -3231,6 +3242,13 @@ } } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index 356990d..08e46b5 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "serve": "vite preview" }, "devDependencies": { + "@types/node": "^20.14.9", "eslint": "^8.57.0", "eslint-plugin-solid": "^0.14.0", "typescript": "^5.3.3", diff --git a/src/App.tsx b/src/App.tsx index c004a4f..ba2c178 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,13 @@ -import { createSignal, type Component, type JSX, For, createMemo, Show } from 'solid-js'; +import { For, Show, createMemo, createSignal, type Accessor, type Component, type JSX } from 'solid-js'; import styles from "./App.module.css"; -import { Feature, type FeatureProps } from './components/Feature'; import { ToggleButton } from './components/Button'; -import tools from './datapack/tools'; -import { PackOutput, Version, versions, type PackItem } from './datapack'; import Download from './components/Download'; +import { Feature, type FeatureProps } from './components/Feature'; +import { PackOutput, Version, makeModId, versions, type PackItem } from './datapack'; +import { turtleFlags } from './datapack/overlay'; +import tools from './datapack/tools'; const Section: Component<{ title: string, children?: JSX.Element }> = props =>
@@ -23,22 +24,29 @@ const FeatureSection: Component<{ title: string, features: FeatureProps[] }> = p ; -const createPackItem = (x: PackItem) => { +const createPackItem = (x: PackItem): FeatureProps => { const [checked, setChecked] = createSignal(false); return { ...x, checked, setChecked } } +const createPackItems = (x: PackItem[], version: Accessor): Accessor => { + const allTweaks = x.map(createPackItem); + return createMemo(() => allTweaks.filter(x => !x.enabled || x.enabled(version()))); +} + const App: Component = () => { const [packName, setPackName] = createSignal("Customisations for CC: Tweaked"); + const [packId, setPackId] = createSignal(""); const [mcVersion, setMcVersion] = createSignal(Version.MC_1_20_1); - const toolFeatures = tools.map(createPackItem); - - const allFeatures = [...toolFeatures]; + const enabledTools = createPackItems(tools, mcVersion); + const enabledTweaks = createPackItems([turtleFlags], mcVersion); + const allFeatures = () => [...enabledTools(), ...enabledTweaks()]; const createPack = createMemo(() => { - const pack = new PackOutput(mcVersion()); - for (const feature of allFeatures) { + const id = packId(); + const pack = new PackOutput(mcVersion(), packName(), id === "" ? undefined : id); + for (const feature of allFeatures()) { if (feature.checked()) feature.process(pack); } return pack; @@ -48,7 +56,19 @@ const App: Component = () => {
- setPackName(e.currentTarget.value)} /> + setPackName(e.currentTarget.value)} + /> + + + setPackId(e.currentTarget.value)} + /> Minecraft Version
@@ -58,10 +78,10 @@ const App: Component = () => {
- - + +
- +
; }; diff --git a/src/assets/ace_flag.png b/src/assets/ace_flag.png new file mode 100644 index 0000000..7cf78c8 Binary files /dev/null and b/src/assets/ace_flag.png differ diff --git a/src/assets/bisexual_flag.png b/src/assets/bisexual_flag.png new file mode 100644 index 0000000..81fb92e Binary files /dev/null and b/src/assets/bisexual_flag.png differ diff --git a/src/assets/flags.png b/src/assets/flags.png new file mode 100644 index 0000000..07766f7 Binary files /dev/null and b/src/assets/flags.png differ diff --git a/src/assets/nb_flag.png b/src/assets/nb_flag.png new file mode 100644 index 0000000..8dc99af Binary files /dev/null and b/src/assets/nb_flag.png differ diff --git a/src/assets/netherite_pickaxe.png b/src/assets/netherite_pickaxe.png index 36eb9b9..54e0c4f 100644 Binary files a/src/assets/netherite_pickaxe.png and b/src/assets/netherite_pickaxe.png differ diff --git a/src/components/Download.module.css b/src/components/Download.module.css index 899906a..cd12fb4 100644 --- a/src/components/Download.module.css +++ b/src/components/Download.module.css @@ -1,7 +1,7 @@ .downloadSplit { display: grid; grid-template-columns: 1fr min-content 1fr; - grid-template-rows: 1fr 1fr; + grid-template-rows: min-content; gap: 1rem 1rem; grid-template-areas: ". separator ." @@ -22,14 +22,34 @@ align-items: center; } -.downloadBar { +.downloadSeparator > .downloadBar { flex-basis: 50%; border-left: solid 1px var(--border-colour); height: 50%; } +.downloadSeparatorHorizontal { + text-transform: uppercase; + font-size: 0.8rem; + + display: flex; + gap: 0.5rem; + flex-direction: row; + align-items: center; +} + +.downloadSeparatorHorizontal > .downloadBar { + flex-basis: 50%; + border-top: solid 1px var(--border-colour); + height: 50%; +} + .downloadSummary { grid-row: 1; } -.downloadButtons { grid-row: 2; } + +.downloadButtons { + grid-row: 2; + align-content: center; +} .downloadSummary > * + * { margin: 0.5rem 0rem; } .downloadSummary h3 { font-size: 1.3rem; } diff --git a/src/components/Download.tsx b/src/components/Download.tsx index 1cbaa31..1e47045 100644 --- a/src/components/Download.tsx +++ b/src/components/Download.tsx @@ -1,37 +1,64 @@ -import type { Component } from "solid-js"; +import { Match, Show, Switch, type Component } from "solid-js"; import type { PackOutput } from "../datapack"; import { saveBlob } from "../utils"; import styles from "./Download.module.css"; import { Button } from "./Button"; -const Download: Component<{ name: string, pack: PackOutput }> = props => { - const packFileName = () => `${props.name.replace(/[^A-Za-z0-9_-]+/g, "-").toLowerCase()}` +const Download: Component<{ pack: PackOutput }> = props => { + const packFileName = () => `${props.pack.name.replace(/[^A-Za-z0-9_-]+/g, "-").toLowerCase()}` const createDatapack = () => { - props.pack.makeDataPack(props.name).generateAsync({ type: "blob" }) + props.pack.makeDataPack().generateAsync({ type: "blob" }) .then(x => saveBlob(`${packFileName()}.zip`, x)) .catch(e => console.error(e)); }; + const createResourcepack = () => { + props.pack.makeResourcePack().generateAsync({ type: "blob" }) + .then(x => saveBlob(`${packFileName()}-resources.zip`, x)) + .catch(e => console.error(e)); + }; + const createMod = () => { - props.pack.makeMod(props.name).generateAsync({ type: "blob" }) + props.pack.makeMod().generateAsync({ type: "blob" }) .then(x => saveBlob(`${packFileName()}.jar`, x)) .catch(e => console.error(e)); }; return
-

Download as Datapack

-

- Download as a datapack. This file should be saved to datapacks/{packFileName()}.zip in your world - folder. -

+ + +

Download as resource and datapack

+

+ Download a separate resource and datapack. The datapack should be saved to datapacks/{packFileName()}.zip in + your world folder, and the resource pack to the global resourcepacks folder. +

+
+ +

Download as datapack

+

+ Download as a datapack. This file should be saved to datapacks/{packFileName()}.zip in your world + folder. +

+
+
+ +
+
+

and

+
+
+ +
@@ -39,7 +66,7 @@ const Download: Component<{ name: string, pack: PackOutput }> = props => {
-

Download as Mod

+

Download as mod

Download as a mod. This file should be saved to mods/{packFileName()}.jar in your Minecraft folder.

diff --git a/src/components/Feature.module.css b/src/components/Feature.module.css index 0fe2f34..7e13321 100644 --- a/src/components/Feature.module.css +++ b/src/components/Feature.module.css @@ -24,6 +24,8 @@ drop-shadow(-2px -2px 1px var(--highlight-colour)); } +.featureLabel > span { text-align: center; } + .tooltip { width: max-content; max-width: 20rem; diff --git a/src/components/Feature.tsx b/src/components/Feature.tsx index 48af5fc..171b082 100644 --- a/src/components/Feature.tsx +++ b/src/components/Feature.tsx @@ -6,7 +6,7 @@ import type { PackItem } from '../datapack'; import { computePosition, flip, offset, shift } from '@floating-ui/dom'; -export type FeatureProps = Pick & { +export type FeatureProps = PackItem & { checked: Accessor, setChecked: Setter; }; diff --git a/src/datapack/index.ts b/src/datapack/index.ts index 3627f1b..eba5620 100644 --- a/src/datapack/index.ts +++ b/src/datapack/index.ts @@ -1,9 +1,10 @@ import JSZip from "jszip"; -import { prettyJson } from "../utils"; +import { Base64String, assertNever, prettyJson } from "../utils"; export enum Version { MC_1_20_1, MC_1_20_6, + MC_1_21, } type VersionInfo = Readonly<{ @@ -14,16 +15,33 @@ type VersionInfo = Readonly<{ }> export const versions: VersionInfo[] = [ + // See https://minecraft.wiki/w/Pack_format for versions. { version: Version.MC_1_20_1, label: "1.20.1", resourceVersion: 15, dataVersion: 15 }, { version: Version.MC_1_20_6, label: "1.20.6", resourceVersion: 32, dataVersion: 41 }, + { version: Version.MC_1_21, label: "1.21", resourceVersion: 34, dataVersion: 48 }, ] -const encode = (value: unknown): string | Blob => { +/** The contents of a file. */ +export type FileContents = string | Blob | Base64String; + +const encode = (value: unknown): FileContents => { if (typeof value === "string") return value; - if (value instanceof Blob) return value; + if (value instanceof Blob || value instanceof Base64String) return value; return prettyJson(value); } +const addFile = (zip: JSZip, path: string, contents: FileContents): void => { + if (typeof contents === "string") { + zip.file(path, contents); + } else if (contents instanceof Blob) { + zip.file(path, contents); + } else if (contents instanceof Base64String) { + zip.file(path, contents.contents, { base64: true }); + } else { + assertNever(contents); + } +}; + const newZip = (name: string, version: number): JSZip => { const zip = new JSZip(); zip.file("pack.mcmeta", prettyJson({ @@ -35,7 +53,7 @@ const newZip = (name: string, version: number): JSZip => { return zip; } -const makeModId = (name: string): string => name.toLowerCase() +export const makeModId = (name: string): string => name.toLowerCase() .replace(/^[^a-z]+/, "") .replaceAll(/[^a-z0-9_]+/g, "_") .substring(0, 60); @@ -43,11 +61,11 @@ const makeModId = (name: string): string => name.toLowerCase() /** * Create a fabric.mod.json file with no entrypoints for our datapack. */ -const makeFabricModJson = (name: string): string => prettyJson({ +const makeFabricModJson = (id: string, name: string): string => prettyJson({ schemaVersion: 1, - id: makeModId(name), + id, version: "1.0.0", - name: name, + name, license: "CC0-1.0", environment: "*", }); @@ -55,25 +73,30 @@ const makeFabricModJson = (name: string): string => prettyJson({ /** * Create a mods.toml file using the lowcode system (https://github.com/MinecraftForge/MinecraftForge/pull/8633). */ -const makeModsToml = (name: string): string => -`modLoader="lowcodefml" +const makeModsToml = (id: string, name: string): string => + `modLoader="lowcodefml" loaderVersion="[1,)" license="CC0-1.0" [[mods]] -modId="${makeModId(name)}" +modId="${id}" version="1.0.0" displayName=${prettyJson(name)}`; /** A builder for data and resource packs. */ export class PackOutput { - readonly #data = new Map(); - readonly #assets = new Map(); + readonly #data = new Map(); + readonly #assets = new Map(); readonly #translations = new Set(); + readonly #extraModels = new Set(); readonly version: Version; + readonly id: string; + readonly name: string; - public constructor(version: Version) { + public constructor(version: Version, name: string, id?: string) { this.version = version; + this.name = name; + this.id = id ?? makeModId(name); } /** Add a datapack entry. */ @@ -101,24 +124,49 @@ export class PackOutput { this.#translations.add(name); } + /** Add an extra model. */ + extraModel(name: string): void { + this.#extraModels.add(name); + } + /** Determine if the datapack has any files. */ hasData(): boolean { return this.#data.size > 0; } /** Determine if the resource pack has any files. */ - hasResources(): boolean { return this.#assets.size > 0 || this.#translations.size > 0; } + hasResources(): boolean { return this.#assets.size > 0 || this.#translations.size > 0 || this.#extraModels.size > 0; } + + private fillDataPack(zip: JSZip) { + for (const [path, contents] of this.#data.entries()) addFile(zip, path, contents); + } - makeDataPack(name: string): JSZip { - const zip = newZip(name, versions[this.version].dataVersion); - for (const [path, contents] of this.#data.entries()) zip.file<"string" | "blob">(path, contents); + private fillResourcePack(zip: JSZip) { + for (const [path, contents] of this.#assets.entries()) addFile(zip, path, contents); + + if (this.#extraModels.size > 0) { + zip.file(`assets/computercraft/extra_models.json`, prettyJson([...this.#extraModels])); + } + } + + makeDataPack(): JSZip { + const zip = newZip(this.name, versions[this.version].dataVersion); + this.fillDataPack(zip); return zip; } - makeMod(name: string): JSZip { - const zip = newZip(name, versions[this.version].dataVersion); - for (const [path, contents] of this.#data.entries()) zip.file<"string" | "blob">(path, contents); + makeResourcePack(): JSZip { + const zip = newZip(this.name, versions[this.version].resourceVersion); + this.fillResourcePack(zip); + return zip; + } + + makeMod(): JSZip { + const zip = newZip(this.name, versions[this.version].dataVersion); + zip.file("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n") + this.fillDataPack(zip); + this.fillResourcePack(zip); - zip.file("fabric.mod.json", makeFabricModJson(name)); - zip.file(this.version < Version.MC_1_20_6 ? "META-INF/mods.toml" : "META-INF/neoforge.mods.toml", makeModsToml(name)); + zip.file("fabric.mod.json", makeFabricModJson(this.id, this.name)); + zip.file(this.version < Version.MC_1_20_6 ? "META-INF/mods.toml" : "META-INF/neoforge.mods.toml", makeModsToml(this.id, this.name)); return zip; } } @@ -133,5 +181,9 @@ export type PackItem = { /** Alt text for the feature icon. */ iconAlt: string, + /** Whether this item is enabled for a specific version. */ + enabled?: (version: Version) => boolean; + + /** Process this datapack. */ process: (datapack: PackOutput) => void; }; diff --git a/src/datapack/overlay.ts b/src/datapack/overlay.ts new file mode 100644 index 0000000..4361cdf --- /dev/null +++ b/src/datapack/overlay.ts @@ -0,0 +1,148 @@ +import { Version, type FileContents, type PackItem, type PackOutput } from "."; +import { Base64String } from "../utils"; + +import aceFlag from "../assets/ace_flag.png?base64"; +import bisexualFlag from "../assets/bisexual_flag.png?base64"; +import icon from "../assets/flags.png"; +import nbFlag from "../assets/nb_flag.png?base64"; + +type Overlay = { + /// The id of this turtle overlay + id: string, + /** The height of the flag. */ + modelHeight: number, // TODO: A nicer interface for models and textures. + /** The contents of the texture. */ + texture: FileContents, + /** Whether to show the elf overlay. */ + showElfOverlay?: boolean, + /** Ingredients used to craft this overlay. */ + ingredients: Ingredient[] +}; + +type Ingredient = { tag: string } | { item: string }; + +const turtleFamilies = ["normal", "advanced"]; + +const makeModel = (texture: string, height: number): unknown => ({ + parent: "block/block", + textures: { + particle: texture, + texture: texture + }, + elements: [ + { + name: "Flag", + from: [1.5, 13.5, 10.5], + to: [2, 13.5 + (height / 2), 15.5], + rotation: { angle: 22.5, axis: "x", origin: [2, 11, 10.75] }, + faces: { + north: { uv: [0, 0, 1, height], texture: "#texture" }, + east: { uv: [0, 0, 7, height], texture: "#texture" }, + south: { uv: [0, 0, 1, height], texture: "#texture" }, + west: { uv: [0, 0, 7, height], texture: "#texture" }, + up: { uv: [10, 0, 11, height], texture: "#texture" }, + down: { uv: [8, 0, 9, height], texture: "#texture" } + } + }, + { + name: "Stick", + from: [1.5, 10.5, 10.5], + to: [2, 13.5, 11], + rotation: { angle: 22.5, axis: "x", origin: [2, 11, 10.75] }, + faces: { + north: { uv: [12, 0, 13, 6], texture: "#texture" }, + east: { uv: [13, 0, 14, 6], texture: "#texture" }, + south: { uv: [12, 0, 13, 6], texture: "#texture" }, + west: { uv: [13, 0, 14, 6], texture: "#texture" }, + up: { uv: [12, 6, 13, 7], texture: "#texture" }, + down: { uv: [13, 6, 14, 7], texture: "#texture" } + } + } + ] +}); + +const addOverlay = (output: PackOutput, { id, modelHeight, texture, showElfOverlay, ingredients }: Overlay) => { + const modelPath = `block/turtle_overlay_${id}`; + const modelId = `${output.id}:${modelPath}`; + + output.extraModel(modelId); + output.data(output.id, `computercraft/turtle_overlay/${id}.json`, { model: modelId, show_elf_overlay: showElfOverlay }); + output.resource(output.id, `models/${modelPath}.json`, makeModel(modelId, modelHeight)); + output.resource(output.id, `textures/${modelPath}.png`, texture); + + for (const family of turtleFamilies) { + output.data(output.id, `recipe/turtle_${family}_overlays/${id}.json`, { + type: "computercraft:transform_shapeless", + category: "redstone", + function: [ + { + type: "computercraft:copy_components", + exclude: ["computercraft:overlay"], + from: { item: `computercraft:turtle_${family}` } + } + ], + group: `computercraft:turtle_${family}_overlay`, + ingredients: [ + ...ingredients, + { item: `computercraft:turtle_${family}` } + ], + result: { + components: { "computercraft:overlay": `${output.id}:${id}` }, + count: 1, + id: `computercraft:turtle_${family}` + } + }) + } +} + +const makeOverlays = (overlays: Overlay[]) => (output: PackOutput): void => { + for (const overlay of overlays) addOverlay(output, overlay); +} + +export const turtleFlags: PackItem = { + name: "More Turtle Flags", + description: "Add extra flags for turtles to hold.", + icon, + iconAlt: "A Non-Binary and Bisexual flag crossed.", + enabled: version => version >= Version.MC_1_21, + process: makeOverlays([ + { + id: "ace_flag", + showElfOverlay: true, + modelHeight: 4, + texture: new Base64String(aceFlag), + ingredients: [ + { item: "minecraft:stick" }, + { item: "minecraft:black_dye" }, + { item: "minecraft:light_gray_dye" }, + { item: "minecraft:white_dye" }, + { item: "minecraft:purple_dye" }, + ], + }, + { + id: "bisexual_flag", + showElfOverlay: true, + modelHeight: 5, + texture: new Base64String(bisexualFlag), + ingredients: [ + { item: "minecraft:stick" }, + { item: "minecraft:purple_dye" }, + { item: "minecraft:magenta_dye" }, + { item: "minecraft:blue_dye" }, + ], + }, + { + id: "non_binary_flag", + showElfOverlay: true, + modelHeight: 4, + texture: new Base64String(nbFlag), + ingredients: [ + { item: "minecraft:stick" }, + { item: "minecraft:yellow_dye" }, + { item: "minecraft:white_dye" }, + { item: "minecraft:purple_dye" }, + { item: "minecraft:black_dye" }, + ], + }, + ]), +}; diff --git a/src/datapack/tools.ts b/src/datapack/tools.ts index d3307e3..ca6f0f0 100644 --- a/src/datapack/tools.ts +++ b/src/datapack/tools.ts @@ -83,7 +83,8 @@ const makeTool = (material: Material): PackItem => ({ break; case Version.MC_1_20_6: - // 1.20.5 uses computercraft/turtle_upgrade, and has the adjective as a JSON component. + case Version.MC_1_21: + // 1.20.5+ uses computercraft/turtle_upgrade, and has the adjective as a JSON component. pack.data("minecraft", `computercraft/turtle_upgrade/${name}.json`, { type: "computercraft:tool", adjective: { "translate": adjective }, diff --git a/src/index.css b/src/index.css index 0468bd5..16f910e 100644 --- a/src/index.css +++ b/src/index.css @@ -43,6 +43,14 @@ body { color: var(--text-colour); } h1 { font-size: 2.25rem; } h2 { font-size: 1.5rem; } +input[type=text] { + border: #aaa solid 1px; + outline: none; +} +input[type=text]:hover { border-color: #222; } +input[type=text]:focus { border-color: #0f0fd0; } +input[type=text]:invalid { border-color: red; } + #root { max-width: 56rem; margin: 5rem auto 1rem auto; diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..eea0f9b --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,4 @@ +declare module "*?base64" { + const src: string + export default src +} diff --git a/src/utils.ts b/src/utils.ts index 9e9d628..cb7017d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,3 +39,11 @@ export const prettyJson = (x: unknown): string => JSON.stringify(x, null, " "); export const assertNever = (_: never): never => { throw Error("Impossible: never"); } + +export class Base64String { + readonly contents: string; + + constructor(contents: string) { + this.contents = contents; + } +} diff --git a/tsconfig.json b/tsconfig.json index 1d154c3..9760998 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "jsx": "preserve", "jsxImportSource": "solid-js", - "types": ["vite/client"], + "types": ["vite/client", "node"], "noEmit": true, // Enforce stricter type-checking options diff --git a/vite.config.ts b/vite.config.ts index 5f393af..d99d4e4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,23 @@ -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; +import fs from "fs/promises"; import solidPlugin from "vite-plugin-solid"; +const b64Loader: Plugin = { + name: 'b64-loader', + transform: async (_code, id) => { + const [file, query] = id.split('?'); + if (query != "base64") return null; + + const data = await fs.readFile(file); + return `export default ${JSON.stringify(data.toString("base64"))};`; + } +}; + export default defineConfig({ base: "", plugins: [ solidPlugin(), + b64Loader, ], server: { port: 3000,