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 = () => {
-
-
+
+
>;
};
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.
+
+
+
@@ -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,