diff --git a/.tool-versions b/.tool-versions index a348bd4b..3d9584e7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ crystal 1.14.0 -mint 0.19.0 +mint 0.20.0 nodejs 20.10.0 yarn 1.22.19 diff --git a/Makefile b/Makefile index 9627cbce..bc36cfcd 100644 --- a/Makefile +++ b/Makefile @@ -47,10 +47,15 @@ development-release: crystal build src/mint.cr -o mint-dev --static --no-debug --release mv ./mint-dev ~/.bin/ -src/assets/runtime.js: $(shell find runtime/src -type f) +src/assets/runtime.js: \ + $(shell find runtime/src -type f) \ + runtime/index.js cd runtime && make index -src/assets/runtime_test.js: $(shell find runtime/src -type f) +src/assets/runtime_test.js: \ + $(shell find runtime/src -type f) \ + runtime/index_testing.js \ + runtime/index.js cd runtime && make index_testing # This builds the binary and depends on files in some directories. diff --git a/core/source/Debug.mint b/core/source/Debug.mint index d2b73720..ebc47922 100644 --- a/core/source/Debug.mint +++ b/core/source/Debug.mint @@ -1,5 +1,17 @@ /* This module provides functions for debugging purposes. */ module Debug { + /* + Returns a nicely formatted version of the value. Values of Mint types + preserve their original name. + + Debug.inspect("Hello World!") -> "Hello World!" + Debug.inspect(Maybe.Nothing) -> Maybe.Nothing + Debug.inspect({ name: "Joe", age: 37 }) -> User { name: "Joe", age: 37 } + */ + fun inspect (value : a) : String { + `#{%inspect%}(#{value})` + } + /* Logs an arbitrary value to the windows console. diff --git a/runtime/index.js b/runtime/index.js index b0f1fe8e..32f269da 100644 --- a/runtime/index.js +++ b/runtime/index.js @@ -14,3 +14,4 @@ export * from "./src/program"; export * from "./src/portals"; export * from "./src/variant"; export * from "./src/styles"; +export * from "./src/debug"; diff --git a/runtime/src/debug.js b/runtime/src/debug.js new file mode 100644 index 00000000..83cc5bc2 --- /dev/null +++ b/runtime/src/debug.js @@ -0,0 +1,128 @@ +import indentString from "indent-string"; +import { isVnode } from './equality'; +import { Variant } from './variant'; +import { Name } from './symbols'; + +const render = (items, prefix, suffix, fn) => { + items = + items.map(fn); + + const newLines = + items.size > 3 || items.filter((item) => item.indexOf("\n") > 0).length; + + const joined = + items.join(newLines ? ",\n" : ", "); + + if (newLines) { + return `${prefix.trim()}\n${indentString(joined, 2)}\n${suffix.trim()}`; + } else { + return `${prefix}${joined}${suffix}`; + } +} + +const toString = (object) => { + if (object.type === "null") { + return "null"; + } else if (object.type === "undefined") { + return "undefined"; + } else if (object.type === "string") { + return `"${object.value}"`; + } else if (object.type === "number") { + return `${object.value}`; + } else if (object.type === "boolean") { + return `${object.value}`; + } else if (object.type === "element") { + return `<${object.value.toLowerCase()}>` + } else if (object.type === "variant") { + if (object.items) { + return render(object.items, `${object.value}(`, `)`, toString); + } else { + return object.value; + } + } else if (object.type === "array") { + return render(object.items, `[`, `]`, toString); + } else if (object.type === "object") { + return render(object.items, `{ `, ` }`, toString); + } else if (object.type === "record") { + return render(object.items, `${object.value} { `, ` }`, toString); + } else if (object.type === "unknown") { + return `{ ${object.value} }`; + } else if (object.type === "vnode") { + return `VNode`; + } else if (object.key) { + return `${object.key}: ${toString(object.value)}`; + } else if (object.value) { + return toString(object.value); + } +} + +const objectify = (value) => { + if (value === null) { + return { type: "null" }; + } else if (value === undefined) { + return { type: "undefined" }; + } else if (typeof value === "string") { + return { type: "string", value: value }; + } else if (typeof value === "number") { + return { type: "number", value: value.toString() }; + } else if (typeof value === "boolean") { + return { type: "boolean", value: value.toString() }; + } else if (value instanceof HTMLElement) { + return { type: "element", value: value.tagName }; + } else if (value instanceof Variant) { + const items = []; + + if (value.record) { + for (const key in value) { + if (key === "length" || key === "record" || key.startsWith("_")) { + continue; + }; + + items.push({ + value: objectify(value[key]), + key: key + }); + } + } else { + for (let i = 0; i < value.length; i++) { + items.push({ + value: objectify(value[`_${i}`]) + }); + }; + } + + if (items.length) { + return { type: "variant", value: value[Name], items: items }; + } else { + return { type: "variant", value: value[Name] }; + } + } else if (Array.isArray(value)) { + return { + items: value.map((item) => ({ value: objectify(item) })), + type: "array" + }; + } else if (isVnode(value)) { + return { type: "vnode" } + } else if (typeof value == "object") { + const items = []; + + for (const key in value) { + items.push({ + value: objectify(value[key]), + key: key + }); + }; + + if (Name in value) { + return { type: "record", value: value[Name], items: items }; + } else { + return { type: "object", items: items }; + } + } else { + return { type: "unknown", value: value.toString() }; + } +} + +export const inspect = (value) => { + return toString(objectify(value)) +} diff --git a/runtime/src/decoders.js b/runtime/src/decoders.js index 20fc5a51..7a371749 100644 --- a/runtime/src/decoders.js +++ b/runtime/src/decoders.js @@ -1,4 +1,5 @@ import indentString from "indent-string"; +import { Name } from "./symbols"; // Formats the given value as JSON with extra indentation. const format = (value) => { @@ -317,8 +318,8 @@ export const decodeMap = (decoder, ok, err) => (input) => { }; // Decodes a record, using the mappings. -export const decoder = (mappings, ok, err) => (input) => { - const object = {}; +export const decoder = (name, mappings, ok, err) => (input) => { + const object = {[Name]: name}; for (let key in mappings) { let decoder = mappings[key]; diff --git a/runtime/src/equality.js b/runtime/src/equality.js index 189a6728..efa5d24b 100644 --- a/runtime/src/equality.js +++ b/runtime/src/equality.js @@ -1,7 +1,7 @@ // This file contains code to have value equality instead of reference equality. // We use a `Symbol` to have a custom equality functions and then use these // functions when comparing two values. -export const Equals = Symbol("Equals"); +import { Equals } from './symbols'; /* v8 ignore next 3 */ if (typeof Node === "undefined") { @@ -123,7 +123,7 @@ Map.prototype[Equals] = function (other) { }; // If the object has a specific set of keys it's a Preact virtual DOM node. -const isVnode = (object) => +export const isVnode = (object) => object !== undefined && object !== null && typeof object == "object" && diff --git a/runtime/src/symbols.js b/runtime/src/symbols.js new file mode 100644 index 00000000..fd8cb2cf --- /dev/null +++ b/runtime/src/symbols.js @@ -0,0 +1,2 @@ +export const Equals = Symbol("Equals"); +export const Name = Symbol('Name'); diff --git a/runtime/src/utilities.js b/runtime/src/utilities.js index f12a641e..aa28f391 100644 --- a/runtime/src/utilities.js +++ b/runtime/src/utilities.js @@ -3,6 +3,7 @@ import { useEffect, useRef, useMemo } from "preact/hooks"; import { signal } from "@preact/signals"; import { compare } from "./equality"; +import { Name } from "./symbols"; // This finds the first element matching the key in a map ([[key, value]]). export const mapAccess = (map, key, just, nothing) => { @@ -87,6 +88,10 @@ export const access = (field) => (value) => value[field]; // Identity function, used in encoders. export const identity = (a) => a; +// Creates an instrumented object so we know which record it belongs to. +export const record = (name) => (value) => ({ [Name]: name, ...value}) + +// A component to lazy load another component. export class lazyComponent extends Component { async componentDidMount() { let x = await this.props.x(); @@ -102,8 +107,10 @@ export class lazyComponent extends Component { } } +// A higher order function to lazy load a module. export const lazy = (path) => async () => load(path) +// Loads load a module. export const load = async (path) => { const x = await import(path) return x.default diff --git a/runtime/src/variant.js b/runtime/src/variant.js index e0feb138..1a22d756 100644 --- a/runtime/src/variant.js +++ b/runtime/src/variant.js @@ -1,7 +1,8 @@ -import { Equals, compareObjects, compare } from "./equality"; +import { compareObjects, compare } from "./equality"; +import { Equals, Name } from "./symbols"; // The base class for variants. -class Variant { +export class Variant { [Equals](other) { if (!(other instanceof this.constructor)) { return false; @@ -27,10 +28,11 @@ class Variant { // Creates an type variant class, this is needed so we can do proper // comparisons and pattern matching / destructuring. -export const variant = (input) => { +export const variant = (input, name) => { return class extends Variant { constructor(...args) { super(); + this[Name] = name if (Array.isArray(input)) { this.length = input.length; this.record = true; diff --git a/runtime/tests/debug.test.js b/runtime/tests/debug.test.js new file mode 100644 index 00000000..8b28f67b --- /dev/null +++ b/runtime/tests/debug.test.js @@ -0,0 +1,77 @@ +import { variant, newVariant, inspect, record } from "../index_testing"; +import { expect, test, describe } from "vitest"; + +test("inspecting null", () => { + expect(inspect(null)).toBe("null"); +}); + +test("inspecting undefined", () => { + expect(inspect(undefined)).toBe("undefined"); +}); + +test("inspecting string", () => { + expect(inspect("Hello")).toBe(`"Hello"`); +}); + +test("inspecting number", () => { + expect(inspect(0)).toBe(`0`); +}); + +test("inspecting boolean", () => { + expect(inspect(true)).toBe(`true`); +}); + +test("inspecting boolean", () => { + expect(inspect({props: {}, type: {}, ref: {}, key: {},"__": {}})).toBe(`VNode`); +}); + +test("inspecting object", () => { + expect(inspect({ name: "Joe" })).toBe(`{ name: "Joe" }`); +}); + +test("inspecting element", () => { + expect(inspect(document.createElement("div"))).toBe(`