From a331bca608bb198267cf30bfa1ee93d34d522fd0 Mon Sep 17 00:00:00 2001 From: ncpa0cpl Date: Fri, 13 Dec 2024 23:51:32 +0100 Subject: [PATCH] feat: custom console with pretty print and error stack trace source maps --- package.json | 9 +- runtime/esm/console.mjs | 644 +++++++++++++ runtime/esm/helpers/base64-vlq.mjs | 70 ++ runtime/esm/helpers/recursive-objects.mjs | 30 + runtime/esm/helpers/sourcemap-reader.mjs | 99 ++ scripts/build.cjs | 11 +- src/config/config-schema.ts | 1 + .../react-gtk/react-gtk-plugin.ts | 21 + src/index.ts | 9 + src/programs/base.ts | 11 +- src/programs/build-program.ts | 20 +- src/programs/bundle-program.ts | 10 +- src/programs/default-build-options.ts | 33 +- src/programs/start-program.ts | 12 +- src/runtime/console.ts | 879 ++++++++++++++++++ src/runtime/helpers/base64-vlq.ts | 100 ++ src/runtime/helpers/recursive-objects.ts | 39 + src/runtime/helpers/sourcemap-reader.ts | 153 +++ src/runtime/tsconfig.json | 7 + src/utils/get-runtime-init.ts | 99 ++ src/utils/read-config.ts | 4 + yarn.lock | 95 +- 22 files changed, 2285 insertions(+), 71 deletions(-) create mode 100644 runtime/esm/console.mjs create mode 100644 runtime/esm/helpers/base64-vlq.mjs create mode 100644 runtime/esm/helpers/recursive-objects.mjs create mode 100644 runtime/esm/helpers/sourcemap-reader.mjs create mode 100644 src/runtime/console.ts create mode 100644 src/runtime/helpers/base64-vlq.ts create mode 100644 src/runtime/helpers/recursive-objects.ts create mode 100644 src/runtime/helpers/sourcemap-reader.ts create mode 100644 src/runtime/tsconfig.json create mode 100644 src/utils/get-runtime-init.ts diff --git a/package.json b/package.json index 22ad38f..d830163 100644 --- a/package.json +++ b/package.json @@ -50,12 +50,16 @@ "email": "" }, "dependencies": { - "@reactgjs/renderer": "^0.0.1-beta.3", + "@reactgjs/renderer": "^0.0.1-beta.4", "buffer": "^6.0.3", "clify.js": "^1.0.0-beta.5", + "dedent": "^1.5.3", "dilswer": "2.1.1", "esbuild": "^0.24.0", "fs-gjs": "^1.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", "rimraf": "^4.4.1", "tar": "^6.2.0", "termx-markup": "~2.0.2", @@ -65,6 +69,9 @@ "@ncpa0cpl/nodepack": "^2.3.3", "@reactgjs/gest": "^0.6.3", "@swc/core": "^1.5.5", + "@types/lodash.clonedeep": "^4.5.9", + "@types/lodash.get": "^4.4.9", + "@types/lodash.set": "^4.3.9", "@types/node": "^20.12.12", "@types/tar": "^6.1.6", "dprint": "^0.45.1", diff --git a/runtime/esm/console.mjs b/runtime/esm/console.mjs new file mode 100644 index 0000000..85e6b05 --- /dev/null +++ b/runtime/esm/console.mjs @@ -0,0 +1,644 @@ +// src/runtime/console.ts +import GLib from "gi://GLib?version=2.0"; +import { SourceMapReader } from "./helpers/sourcemap-reader.mjs"; +var EOL = "\n"; +var COLOR = { + // info labels colors + Red: "\x1B[38;5;160m", + Blue: "\x1B[38;5;39m", + Purple: "\x1B[38;5;93m", + Yellow: "\x1B[38;5;220m", + Grey: "\x1B[38;5;247m", + HotOrange: "\x1B[38;5;202m", + // object formatting + BracketBlue: "\x1B[38;5;75m", + BracketGreen: "\x1B[38;5;77m", + BracketYellow: "\x1B[38;5;178m", + BracketMagenta: "\x1B[38;5;165m", + BracketGrey: "\x1B[38;5;251m", + Key: "\x1B[38;5;252m", + Reset: "\x1B[0m" +}; +function red(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Red}${text}${COLOR.Reset}`; +} +function blue(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Blue}${text}${COLOR.Reset}`; +} +function purple(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Purple}${text}${COLOR.Reset}`; +} +function yellow(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Yellow}${text}${COLOR.Reset}`; +} +function grey(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Grey}${text}${COLOR.Reset}`; +} +function hotOrange(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.HotOrange}${text}${COLOR.Reset}`; +} +function bracketBlue(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketBlue}${text}${COLOR.Reset}`; +} +function bracketGreen(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketGreen}${text}${COLOR.Reset}`; +} +function bracketYellow(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketYellow}${text}${COLOR.Reset}`; +} +function bracketMagenta(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketMagenta}${text}${COLOR.Reset}`; +} +function bracketGrey(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketGrey}${text}${COLOR.Reset}`; +} +function keyClr(text) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Key}${text}${COLOR.Reset}`; +} +var BRACKET_COLORS = [ + bracketBlue, + bracketYellow, + bracketMagenta, + bracketGreen, + bracketGrey +]; +function bracket(bracketChar, depth) { + depth -= 1; + const colorfn = BRACKET_COLORS[depth % BRACKET_COLORS.length]; + return colorfn(bracketChar); +} +var specifierTest = /%(d|i|s|f|o|O|c)/; +function makeIndent(indent, indentSize = 2) { + if (indent < 0) { + indent = 0; + } + return Array.from({ length: indent * indentSize }, () => " ").join(""); +} +function isTypedArray(value) { + return ArrayBuffer.isView(value); +} +function isArray(value) { + return Array.isArray(value); +} +function fmtError(err) { + const trace = StacktraceResolver.mapStackTrace(err.stack?.trim() ?? ""); + return `${err.name}: ${err.message}${EOL}${err.stack ? addIndent(trace, 2) : "No stack trace available"}`; +} +function fmtKey(key) { + switch (typeof key) { + case "bigint": + case "string": + case "number": + case "boolean": + return String(key); + case "function": + return `[Function ${key.name}]`; + case "symbol": + return `[Symbol ${key.toString()}]`; + case "undefined": + return "undefined"; + case "object": { + if (key === null) return "null"; + return "Object"; + } + } +} +function fmtMap(map, ctx) { + const indent = makeIndent(ctx.depth); + ctx.parentRefs.set(map, ctx.currentLocation); + let fmtd = `Map${bracket("<", ctx.depth)}${EOL}`; + for (let [key, value] of map) { + key = fmtKey(key); + const nextCtx = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}.${key}` + }; + const fmtv = fmt(value, nextCtx); + fmtd += `${indent}${keyClr(String(key))}: ${fmtv},${EOL}`; + } + fmtd += `${makeIndent(ctx.depth - 1)}${bracket(">", ctx.depth)}`; + ctx.parentRefs.delete(map); + return fmtd; +} +function fmtSet(set, ctx) { + const indent = makeIndent(ctx.depth); + ctx.parentRefs.set(set, ctx.currentLocation); + let fmtd = `Set${bracket("<", ctx.depth)}${EOL}`; + for (const value of set) { + const nextCtx = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}.` + }; + const fmtv = fmt(value, nextCtx); + fmtd += `${indent}${fmtv},${EOL}`; + } + fmtd += `${makeIndent(ctx.depth - 1)}${bracket(">", ctx.depth)}`; + ctx.parentRefs.delete(set); + return fmtd; +} +function fmtArray(arr, ctx) { + const indent = makeIndent(ctx.depth); + ctx.parentRefs.set(arr, ctx.currentLocation); + const entries = []; + for (let i = 0; i < arr.length; i++) { + const value = arr[i]; + const nextCtx = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}[${i}]` + }; + const fmtv = fmt(value, nextCtx); + entries.push(fmtv); + } + let fmtd = bracket("[", ctx.depth); + const totalEntriesLen = entries.reduce((sum, e) => sum + e.length, 0); + if (totalEntriesLen < 28) { + fmtd += `${entries.join(", ")}${bracket("]", ctx.depth)}`; + } else { + fmtd += `${EOL}${entries.map((e) => `${indent}${e}`).join(`,${EOL}`)},${EOL}${makeIndent(ctx.depth - 1)}${bracket("]", ctx.depth)}`; + } + ctx.parentRefs.delete(arr); + return fmtd; +} +function fmtTypedArray(arr, ctx) { + const typedArrayName = arr.constructor.name; + return `${typedArrayName} ${fmtArray(arr, ctx)}`; +} +function fmtPlainObject(obj, ctx) { + const indent = makeIndent(ctx.depth); + ctx.parentRefs.set(obj, ctx.currentLocation); + let fmtd = `${bracket("{", ctx.depth)}${EOL}`; + const keys = Object.keys(obj); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = obj[key]; + const nextCtx = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}.${key}` + }; + const fmtv = fmt(value, nextCtx); + fmtd += `${indent}${keyClr(String(key))}: ${fmtv},${EOL}`; + } + fmtd += `${makeIndent(ctx.depth - 1)}${bracket("}", ctx.depth)}`; + ctx.parentRefs.delete(obj); + return fmtd; +} +function fmtObject(obj, ctx) { + if (ctx.parentRefs.has(obj)) { + const ref = ctx.parentRefs.get(obj); + return `## Recursive reference [${ref}] ##`; + } + if ("toConsolePrint" in obj && typeof obj.toConsolePrint === "function") { + const objStr = obj.toConsolePrint(); + if (typeof objStr === "string") { + return addIndent(objStr, ctx.depth, 1); + } + } + if (obj instanceof Error) { + return fmtError(obj); + } + if (obj instanceof GLib.Error) { + return fmtError(obj, ctx); + } + if (obj instanceof Map) { + return fmtMap(obj, ctx); + } + if (obj instanceof Set) { + return fmtSet(obj, ctx); + } + if (isTypedArray(obj)) { + return fmtTypedArray(obj, ctx); + } + if (isArray(obj)) { + return fmtArray(obj, ctx); + } + return fmtPlainObject(obj, ctx); +} +function fmt(item, ctx = { depth: 1, parentRefs: /* @__PURE__ */ new Map(), currentLocation: "" }) { + switch (typeof item) { + case "bigint": + case "string": + case "number": + case "boolean": + return String(item); + case "function": + return `[Function ${item.name}]`; + case "symbol": + return `[Symbol ${item.toString()}]`; + case "undefined": + return "undefined"; + case "object": { + if (item === null) return "null"; + return fmtObject(item, ctx); + } + } +} +function fmtArgs(args) { + args = args.slice(); + for (let i = 0; i < args.length; i++) { + const value = args[i]; + if (value instanceof Error) { + args[i] = fmtError(value); + } else if (typeof value === "object" && value !== null) { + args[i] = fmt(value); + } + } + return args; +} +function hasFormatSpecifiers(str) { + return specifierTest.test(str); +} +function formatOptimally(item) { + if (item instanceof Error || item instanceof GLib.Error) { + return `${item.toString()}${item.stack ? EOL : ""}${item.stack?.split(EOL).map((line) => line.padStart(2, " ")).join(EOL)}`; + } + if (typeof item === "object" && item !== null) { + if (item.constructor?.name !== "Object") + return `${item.constructor?.name} ${fmt(item)}`; + else if (String(item) === "GIRepositoryNamespace") + return `[${String(item)} ${item.__name__}]`; + } + return fmt(item); +} +function addIndent(text, indent, startFromLine = 0) { + const identStr = makeIndent(indent, 1); + const lines = text.split(EOL); + for (let i = startFromLine; i < lines.length; i++) { + lines[i] = identStr + lines[i]; + } + return lines.join(EOL); +} +var NUM_REGEX = /^-?\d+(\.\d+)?$/; +function isNumber(value) { + return typeof value === "number" || typeof value === "bigint" || typeof value === "string" && NUM_REGEX.test(value); +} +function addLogPrefix(loglevel, message) { + switch (loglevel) { + case "log" /* Log */: + return `[${grey("LOG")}] ${message}`; + case "warn" /* Warn */: + return `[${yellow("WARN")}] ${message}`; + case "error" /* Error */: + return `[${red("ERROR")}] ${message}`; + case "assert" /* Assert */: + return `[${hotOrange("ASSERT")}] ${message}`; + case "debug" /* Debug */: + return `[${purple("DEBUG")}] ${message}`; + case "info" /* Info */: + return `[${blue("INFO")}] ${message}`; + case "trace" /* Trace */: + return `[${grey("TRACE")}] ${message}`; + case "group" /* Group */: + case "groupCollapsed" /* GroupCollapsed */: + return `[${grey("GROUP")}] ${message}`; + case "count" /* Count */: + case "countReset" /* CountReset */: + return `[${grey("COUNT")}] ${message}`; + case "timeLog" /* TimeLog */: + case "timeEnd" /* TimeEnd */: + return `[${grey("TIME")}] ${message}`; + default: + return message; + } +} +var ConsoleUtils = class { + static indent = 0; + static counters = /* @__PURE__ */ new Map(); + static timers = /* @__PURE__ */ new Map(); + static pretty = true; + static incrementCounter(label) { + const count = this.counters.get(label) ?? 0; + const newCount = count + 1; + this.counters.set(label, newCount); + return newCount; + } + static resetCounter(label) { + this.counters.delete(label); + } + static clearIndent() { + this.indent = 0; + } + static enterGroup() { + this.indent++; + } + static leaveGroup() { + this.indent = Math.max(0, this.indent - 1); + } + static startTimer(label, time) { + this.timers.set(label, time); + } + static getTimer(label) { + return this.timers.get(label); + } + static endTimer(label) { + const startTime = this.timers.get(label); + this.timers.delete(label); + return startTime; + } + static logger(logLevel, args) { + if (args.length === 0) { + this.printer(logLevel, []); + return; + } + if (args.length === 1) { + this.printer(logLevel, fmtArgs(args)); + return void 0; + } + const [first, ...rest] = args; + if (typeof first !== "string" || !hasFormatSpecifiers(first)) { + this.printer(logLevel, fmtArgs(args)); + return void 0; + } + this.printer(logLevel, this.formatter([first, ...rest])); + return void 0; + } + static formatter(args) { + if (args.length === 1) return args; + let target = String(args[0]); + const current = args[1]; + const specifierIndex = specifierTest.exec(target)?.index; + const specifier = target.slice(specifierIndex, specifierIndex + 2); + let converted = null; + switch (specifier) { + case "%s": + converted = String(current); + break; + case "%d": + case "%i": + if (!isNumber(current)) { + converted = 0; + } else { + converted = Number(current).toFixed(0); + } + break; + case "%f": + if (!isNumber(current)) { + converted = 0; + } else { + converted = String(Number(current)); + } + break; + case "%o": + converted = formatOptimally(current); + break; + case "%O": + converted = fmt(current); + break; + case "%c": + converted = ""; + break; + } + if (converted !== null) { + target = target.slice(0, specifierIndex) + converted + target.slice(specifierIndex + 2); + } + if (!hasFormatSpecifiers(target)) + return [target, ...fmtArgs(args.slice(2))]; + const result = [target, ...args.slice(2)]; + if (result.length === 1) return result; + return this.formatter(result); + } + static printer(logLevel, args, options = {}) { + let severity; + switch (logLevel) { + case "log" /* Log */: + case "dir" /* Dir */: + case "dirxml" /* Dirxml */: + case "trace" /* Trace */: + case "group" /* Group */: + case "groupCollapsed" /* GroupCollapsed */: + case "timeLog" /* TimeLog */: + case "timeEnd" /* TimeEnd */: + severity = GLib.LogLevelFlags.LEVEL_MESSAGE; + break; + case "debug" /* Debug */: + severity = GLib.LogLevelFlags.LEVEL_DEBUG; + break; + case "count" /* Count */: + case "info" /* Info */: + severity = GLib.LogLevelFlags.LEVEL_INFO; + break; + case "warn" /* Warn */: + case "countReset" /* CountReset */: + case "reportWarning" /* ReportWarning */: + severity = GLib.LogLevelFlags.LEVEL_WARNING; + break; + case "error" /* Error */: + case "assert" /* Assert */: + severity = GLib.LogLevelFlags.LEVEL_CRITICAL; + break; + default: + severity = GLib.LogLevelFlags.LEVEL_MESSAGE; + } + const output = args.map((a) => { + if (a === null) return "null"; + else if (typeof a === "object") return formatOptimally(a); + else if (typeof a === "undefined") return "undefined"; + else if (typeof a === "bigint") return `${a}n`; + else return String(a); + }).join(" "); + let formattedOutput = output; + const extraFields = {}; + let stackTrace = options?.stackTrace; + let stackTraceLines = null; + if (!stackTrace && (logLevel === "trace" || severity <= GLib.LogLevelFlags.LEVEL_WARNING)) { + stackTrace = new Error().stack; + if (stackTrace) { + const currentFile = stackTrace.match(/^[^@]*@(.*):\d+:\d+$/m)?.at(1); + if (currentFile) { + const index = stackTrace.lastIndexOf(currentFile) + currentFile.length; + stackTraceLines = stackTrace.substring(index).split(EOL); + stackTraceLines.shift(); + } + } + } + if (stackTraceLines == null) { + stackTraceLines = []; + } + if (logLevel === "trace" /* Trace */) { + if (stackTraceLines.length) { + formattedOutput += `${EOL}${addIndent(stackTraceLines.join(EOL), this.indent)}`; + } else { + formattedOutput += `${EOL}${addIndent("No stack trace available", this.indent)}`; + } + } + if (stackTraceLines.length) { + const [stackLine] = stackTraceLines; + const match = stackLine?.match(/^([^@]*)@(.*):(\d+):\d+$/); + if (match) { + const [_, func, file, line] = match; + if (func) extraFields.CODE_FUNC = func; + if (file) extraFields.CODE_FILE = file; + if (line) extraFields.CODE_LINE = line; + } + } + const logContent = addLogPrefix(logLevel, formattedOutput); + print(logContent); + } +}; +var Console = { + get [Symbol.toStringTag]() { + return "console"; + }, + assert(condition, ...data) { + if (condition) return; + const message = "Assertion failed"; + if (data.length === 0) data.push(message); + if (typeof data[0] !== "string") { + data.unshift(message); + } else { + const first = data.shift(); + data.unshift(`${message}: ${first}`); + } + ConsoleUtils.logger("assert" /* Assert */, data); + }, + clear() { + ConsoleUtils.clearIndent(); + imports.gi.GjsPrivate.clear_terminal(); + }, + debug(...data) { + ConsoleUtils.logger("debug" /* Debug */, data); + }, + error(...data) { + ConsoleUtils.logger("error" /* Error */, data); + }, + info(...data) { + ConsoleUtils.logger("info" /* Info */, data); + }, + log(...data) { + ConsoleUtils.logger("log" /* Log */, data); + }, + table(tabularData, _properties) { + this.log(tabularData); + }, + trace(...data) { + if (data.length === 0) data = ["Trace"]; + ConsoleUtils.logger("trace" /* Trace */, data); + }, + warn(...data) { + ConsoleUtils.logger("warn" /* Warn */, data); + }, + dir(item, options) { + const object = fmt(item); + ConsoleUtils.printer("dir" /* Dir */, [object], options); + }, + dirxml(...data) { + this.log(...data); + }, + count(label) { + const count = ConsoleUtils.incrementCounter(label); + const msg = `${label}: ${count}`; + ConsoleUtils.logger("count" /* Count */, [msg]); + }, + countReset(label) { + ConsoleUtils.resetCounter(label); + }, + group(...data) { + ConsoleUtils.enterGroup(); + ConsoleUtils.logger("group" /* Group */, data); + }, + groupCollapsed(...data) { + this.group(...data); + }, + groupEnd() { + ConsoleUtils.leaveGroup(); + }, + time(label) { + const ts = imports.gi.GLib.get_monotonic_time(); + ConsoleUtils.startTimer(label, ts); + }, + timeLog(label, ...data) { + const startTime = ConsoleUtils.getTimer(label); + if (startTime == null) { + ConsoleUtils.logger("warn" /* Warn */, [`Timer \u201C${label}\u201D doesn\u2019t exist.`]); + return; + } + const ts = imports.gi.GLib.get_monotonic_time(); + const durationMs = (ts - startTime) / 1e3; + const msg = `${label}: ${durationMs.toFixed(3)} ms`; + data.unshift(msg); + ConsoleUtils.printer("timeLog" /* TimeLog */, data); + }, + timeEnd(label) { + const startTime = ConsoleUtils.endTimer(label); + if (startTime == null) { + ConsoleUtils.logger("warn" /* Warn */, [`Timer \u201C${label}\u201D doesn\u2019t exist.`]); + return; + } + const ts = imports.gi.GLib.get_monotonic_time(); + const durationMs = (ts - startTime) / 1e3; + const msg = `${label}: ${durationMs.toFixed(3)} ms`; + ConsoleUtils.printer("timeEnd" /* TimeEnd */, [msg]); + }, + profile() { + }, + profileEnd() { + }, + timeStamp() { + }, + setPretty(pretty) { + ConsoleUtils.pretty = !!pretty; + } +}; +var StacktraceResolver = class _StacktraceResolver { + static sourcmapReader; + static map; + static { + import(`${imports.package.moduledir}/main.js.map`).then((main) => { + const map = JSON.parse(main.map); + _StacktraceResolver.map = map; + _StacktraceResolver.sourcmapReader = new SourceMapReader(map, map.root); + }).catch((error) => { + }); + } + static mapStackTrace(stack) { + if (!_StacktraceResolver.sourcmapReader) { + return stack; + } + const lines = stack.split(EOL); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = line.match(/main.js:\d+:\d+$/); + if (match) { + const lineCol = line.split("/main.js:").pop(); + const [lineNo, colNo] = lineCol.split(":"); + const l = Number(lineNo) - this.map.rowOffset; + const c = Number(colNo) - this.map.colOffset; + const org = _StacktraceResolver.sourcmapReader.getOriginalPosition(l, c); + if (org && org.file) { + if (org.file.startsWith(_StacktraceResolver.map.wd)) { + org.file = "./" + org.file.substring(_StacktraceResolver.map.wd.length + 1); + } + if (org.symbolName == null) { + if (line.match(/^[\w\d]+@/)) { + org.symbolName = line.split("@")[0]; + } + } + if (org.symbolName) { + lines[i] = `${org.symbolName} at ${org.file}:${org.line}:${org.column}`; + } else { + lines[i] = `at ${org.file}:${org.line}:${org.column}`; + } + } + } + } + return lines.join(EOL); + } +}; +export { + Console as __console_proxy +}; diff --git a/runtime/esm/helpers/base64-vlq.mjs b/runtime/esm/helpers/base64-vlq.mjs new file mode 100644 index 0000000..5857033 --- /dev/null +++ b/runtime/esm/helpers/base64-vlq.mjs @@ -0,0 +1,70 @@ +// src/runtime/helpers/base64-vlq.ts +var Base64VLQ = class { + charToInteger = /* @__PURE__ */ new Map(); + integerToChar = /* @__PURE__ */ new Map(); + constructor() { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach((char, i) => { + this.charToInteger.set(char, i); + this.integerToChar.set(i, char); + }); + } + decode(string) { + const result = []; + let shift = 0; + let value = 0; + for (let i = 0; i < string.length; i += 1) { + const char = string[i]; + let integer = this.charToInteger.get(char); + if (integer === void 0) { + throw new Error(`Invalid character (${string[i]})`); + } + const has_continuation_bit = integer & 32; + integer &= 31; + value += integer << shift; + if (has_continuation_bit) { + shift += 5; + } else { + const should_negate = value & 1; + value >>>= 1; + if (should_negate) { + result.push(value === 0 ? -2147483648 : -value); + } else { + result.push(value); + } + value = shift = 0; + } + } + return result; + } + encode(value) { + if (typeof value === "number") { + return this.encodeInteger(value); + } + let result = ""; + for (let i = 0; i < value.length; i += 1) { + const char = value[i]; + result += this.encodeInteger(char); + } + return result; + } + encodeInteger(num) { + let result = ""; + if (num < 0) { + num = -num << 1 | 1; + } else { + num <<= 1; + } + do { + let clamped = num & 31; + num >>>= 5; + if (num > 0) { + clamped |= 32; + } + result += this.integerToChar.get(clamped); + } while (num > 0); + return result; + } +}; +export { + Base64VLQ +}; diff --git a/runtime/esm/helpers/recursive-objects.mjs b/runtime/esm/helpers/recursive-objects.mjs new file mode 100644 index 0000000..58fe99e --- /dev/null +++ b/runtime/esm/helpers/recursive-objects.mjs @@ -0,0 +1,30 @@ +// src/runtime/helpers/recursive-objects.ts +var copyWithoutRecursiveRefs = (references, obj) => { + references.push(obj); + let cleanObject = {}; + Object.keys(obj).forEach((key) => { + var value = obj[key]; + if (value && typeof value === "object") { + if (references.indexOf(value) < 0) { + references.push(value); + cleanObject[key] = copyWithoutRecursiveRefs(references, value); + references.pop(); + } else { + cleanObject[key] = "###_Recursive Reference_###"; + } + } else if (typeof value !== "function") { + cleanObject[key] = value; + } + }); + const proto = Object.getPrototypeOf(obj); + if (proto) { + Object.setPrototypeOf(cleanObject, proto); + } + return cleanObject; +}; +function removeRecursiveRefs(obj) { + return copyWithoutRecursiveRefs([], obj); +} +export { + removeRecursiveRefs +}; diff --git a/runtime/esm/helpers/sourcemap-reader.mjs b/runtime/esm/helpers/sourcemap-reader.mjs new file mode 100644 index 0000000..988fe13 --- /dev/null +++ b/runtime/esm/helpers/sourcemap-reader.mjs @@ -0,0 +1,99 @@ +// src/runtime/helpers/sourcemap-reader.ts +import Fs from "fs-gjs"; +import path from "path-gjsify"; +import { Base64VLQ } from "./base64-vlq.mjs"; +var SourceMapReader = class _SourceMapReader { + constructor(map, mapFilepath) { + this.map = map; + this.mapFilepath = mapFilepath; + } + static async newFromMapFile(mapFilepath) { + try { + const fileContent = await Fs.readTextFile(mapFilepath); + const map = JSON.parse(fileContent); + return new _SourceMapReader(map, mapFilepath); + } catch (err) { + return void 0; + } + } + converter = new Base64VLQ(); + getFile(file) { + if (file === void 0) return void 0; + const rel = this.map.sources[file]; + if (!rel) return void 0; + return path.join(path.dirname(this.mapFilepath), rel); + } + getLineN(text, n) { + let line = 0; + let lineStart = 0; + while (line !== n) { + lineStart = text.indexOf("\n", lineStart) + 1; + line++; + } + if (line > 0 && lineStart === 0) { + return ""; + } + let lineEnd = text.indexOf("\n", lineStart + 1); + if (lineEnd === -1) { + lineEnd = text.length; + } + return text.slice(lineStart, lineEnd); + } + getOriginalPosition(outLine, outColumn) { + outLine -= 1; + outColumn -= 1; + const vlqs = this.map.mappings.split(";").map((line) => line.split(",")); + const state = [0, 0, 0, 0, 0]; + if (vlqs.length <= outLine) return null; + for (let index = 0; index < vlqs.length; index++) { + const line = vlqs[index]; + state[0] = 0; + let prevSegment = [0]; + for (let i = 0; i < line.length; i++) { + const segment = line[i]; + if (!segment) continue; + const decodedSegment = this.converter.decode(segment); + const prevState = state.slice(); + state[0] += decodedSegment[0]; + if (decodedSegment.length > 1) { + state[1] += decodedSegment[1] ?? 0; + state[2] += decodedSegment[2] ?? 0; + state[3] += decodedSegment[3] ?? 0; + state[4] += decodedSegment[4] ?? 0; + if (index === outLine) { + const currentOutCol = state[0]; + const prevOutCol = prevState[0]; + if (currentOutCol === outColumn) { + return { + file: this.getFile(state[1]), + line: state[2] + 1, + column: state[3] + 1, + symbolName: decodedSegment[4] != null ? this.map.names[state[4]] : void 0 + }; + } else if (outColumn >= prevOutCol && outColumn <= currentOutCol) { + return { + file: this.getFile(state[1]), + line: state[2] + 1, + column: prevState[3] + 1, + symbolName: prevSegment[4] != null ? this.map.names[prevState[4]] : void 0 + }; + } + } + } + prevSegment = decodedSegment; + } + if (index === outLine) { + return { + file: this.getFile(state[1]), + line: state[2] + 1, + // back to 1 based + column: 1 + }; + } + } + return null; + } +}; +export { + SourceMapReader +}; diff --git a/scripts/build.cjs b/scripts/build.cjs index e5a345e..ede9845 100644 --- a/scripts/build.cjs +++ b/scripts/build.cjs @@ -16,7 +16,7 @@ async function main() { tsConfig: p("tsconfig.json"), formats: ["cjs", "esm", "legacy"], declarations: true, - exclude: [/\/polyfills\//], + exclude: [/\/polyfills\//, /\/runtime\//], isomorphicImports: { "./config/eval-js-config/eval-js-config.ts": { js: "./config/eval-js-config/eval-js-config.cjs.ts", @@ -39,6 +39,15 @@ async function main() { formats: ["esm"], exclude: [/\.d\.ts$/, /index.ts/, /\.json$/], }), + // Build polyfill packages + await build({ + target: "ESNext", + srcDir: p("src/runtime"), + outDir: p("runtime"), + tsConfig: p("tsconfig.json"), + formats: ["esm"], + exclude: [/\.d\.ts$/, /index.ts/, /\.json$/], + }), ]); const { ConfigSchema } = require(p("dist/cjs/config/config-schema.cjs")); diff --git a/src/config/config-schema.ts b/src/config/config-schema.ts index 63723ea..dfa94cd 100644 --- a/src/config/config-schema.ts +++ b/src/config/config-schema.ts @@ -105,6 +105,7 @@ export const ConfigSchema = DataType.RecordOf({ return typeof v === "function"; }).setExtra({ typeDef: "(buildDir: string) => any" }), ), + sourcemap: OptionalField(DataType.Boolean), }); ConfigSchema.setTitle("Config"); diff --git a/src/esbuild-plugins/react-gtk/react-gtk-plugin.ts b/src/esbuild-plugins/react-gtk/react-gtk-plugin.ts index 420e23b..4fa77cd 100644 --- a/src/esbuild-plugins/react-gtk/react-gtk-plugin.ts +++ b/src/esbuild-plugins/react-gtk/react-gtk-plugin.ts @@ -221,7 +221,28 @@ ${leftPad(bundle, 2, " ")} `, ].join("\n"), ); + + if (program.config.sourcemap) { + const mapJson = await fs.readFile( + build.initialOptions.outfile! + ".map", + "utf8", + ); + const map = JSON.parse(mapJson); + delete map.sourcesContent; + map.rowOffset = countLines(imports.join("\n")) + 3; + map.colOffset = 2; + map.root = build.initialOptions.outfile!; + map.wd = program.cwd; + await fs.writeFile( + build.initialOptions.outfile! + ".map", + `export const map = ${JSON.stringify(JSON.stringify(map))};`, + ); + } }); }, }; }; + +function countLines(str: string) { + return str.split("\n").length; +} diff --git a/src/index.ts b/src/index.ts index 479e642..179005d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,12 @@ /// export type { Config as BuildConfig } from "./config/config-type"; + +declare global { + interface Console { + /** + * When disabled the output will not be prettified with ANSI colors. + */ + setPretty(pretty: boolean): void; + } +} diff --git a/src/programs/base.ts b/src/programs/base.ts index a135116..58f5723 100644 --- a/src/programs/base.ts +++ b/src/programs/base.ts @@ -11,8 +11,7 @@ import { validateAppName } from "../utils/validate-app-name"; import { validatePrefix } from "../utils/validate-prefix"; export type DeepReadonly = { - readonly [P in keyof T]: T[P] extends object ? DeepReadonly - : T[P]; + readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; }; const WatchOpt = defineOption({ @@ -103,12 +102,18 @@ export abstract class Program { /** @internal */ abstract main(program: T): any; + protected afterBuild?(): void; + /** @internal */ async run() { try { this.config = await readConfig(this); this.populateDefaultEnvVars(); - return await this.main(this); + const result = await this.main(this); + if (this.afterBuild) { + await this.afterBuild(); + } + return result; } catch (e) { handleProgramError(e); } finally { diff --git a/src/programs/build-program.ts b/src/programs/build-program.ts index 3620881..dd4b31d 100644 --- a/src/programs/build-program.ts +++ b/src/programs/build-program.ts @@ -24,6 +24,7 @@ import { AppResources } from "../utils/app-resources"; import { Command } from "../utils/command"; import { getPlugins } from "../utils/get-plugins"; import { getGlobalPolyfills } from "../utils/get-polyfills"; +import { getRuntimeInit } from "../utils/get-runtime-init"; import { pascalToKebab } from "../utils/pascal-to-kebab"; import { Program } from "./base"; import { createBuildOptions } from "./default-build-options"; @@ -166,6 +167,7 @@ export class BuildProgram extends Program { const gresource = getGResourceXml({ appID: context.appID, + files: this.config.sourcemap ? ["main.js.map"] : [], }); const srcMesonBuild = getSrcMesonBuild(); @@ -227,6 +229,15 @@ export class BuildProgram extends Program { return context; } + protected async afterBuild() { + if (this.config.sourcemap && !this.watchMode) { + const buildDirPath = path.resolve(this.cwd, this.config.outDir, ".build"); + const mapFilePath = path.resolve(buildDirPath, "src", "main.js.map"); + const mapContent = await fs.readFile(mapFilePath, "utf-8"); + await fs.writeFile(mapFilePath, `export const map = ${mapContent};`); + } + } + /** * @internal */ @@ -241,13 +252,18 @@ export class BuildProgram extends Program { if (existsSync(buildDirPath)) await rimraf(buildDirPath, {}); const polyfills = await getGlobalPolyfills(this); + const initScript = await getRuntimeInit(); await this.esbuildCtx.init( createBuildOptions({ - banner: { js: polyfills.bundle }, + banner: { js: `${polyfills.bundle}\n${initScript.bundle}` }, entryPoints: [path.resolve(this.cwd, this.config.entrypoint)], outfile: path.resolve(buildDirPath, "src", "main.js"), - plugins: getPlugins(this, { giRequirements: polyfills.requirements }), + plugins: getPlugins(this, { + giRequirements: polyfills.requirements.concat( + initScript.requirements, + ), + }), minify: this.config.minify ?? (this.isDev ? false : true), treeShaking: this.config.treeShake ?? (this.isDev ? false : true), }), diff --git a/src/programs/bundle-program.ts b/src/programs/bundle-program.ts index 021302b..dc27afa 100644 --- a/src/programs/bundle-program.ts +++ b/src/programs/bundle-program.ts @@ -2,6 +2,7 @@ import path from "path"; import { html, Output } from "termx-markup"; import { getPlugins } from "../utils/get-plugins"; import { getGlobalPolyfills } from "../utils/get-polyfills"; +import { getRuntimeInit } from "../utils/get-runtime-init"; import { Program } from "./base"; import { createBuildOptions } from "./default-build-options"; @@ -25,13 +26,18 @@ export class BundleProgram extends Program { } const polyfills = await getGlobalPolyfills(this); + const initScript = await getRuntimeInit(); await this.esbuildCtx.init( createBuildOptions({ - banner: { js: polyfills.bundle }, + banner: { js: `${polyfills.bundle}\n${initScript.bundle}` }, entryPoints: [path.resolve(this.cwd, this.config.entrypoint)], outfile: path.resolve(this.cwd, this.config.outDir, "index.js"), - plugins: getPlugins(this, { giRequirements: polyfills.requirements }), + plugins: getPlugins(this, { + giRequirements: polyfills.requirements.concat( + initScript.requirements, + ), + }), minify: this.config.minify ?? (this.isDev ? false : true), treeShaking: this.config.treeShake ?? (this.isDev ? false : true), }), diff --git a/src/programs/default-build-options.ts b/src/programs/default-build-options.ts index 7cdbf3b..140e8ba 100644 --- a/src/programs/default-build-options.ts +++ b/src/programs/default-build-options.ts @@ -1,40 +1,11 @@ import type { BuildOptions } from "esbuild"; -const consoleLogReplacement = /** - * Js - */ - ` -const __console_proxy = { - log: console.log?.bind(console), - info: console.info?.bind(console), - warn: console.warn?.bind(console), - error: console.error?.bind(console), - debug: console.debug?.bind(console), - group: console.group?.bind(console), - groupEnd: console.groupEnd?.bind(console), - groupCollapsed: console.groupCollapsed?.bind(console), - table: console.table?.bind(console), - dir: console.dir?.bind(console), - dirxml: console.dirxml?.bind(console), - trace: console.trace?.bind(console), - clear: console.clear?.bind(console), - count: console.count?.bind(console), - countReset: console.countReset?.bind(console), - assert: console.assert?.bind(console), - profile: console.profile?.bind(console), - profileEnd: console.profileEnd?.bind(console) -}; -`; - const defaultBuildOptions = { target: "es2022", format: "esm", jsx: "transform", keepNames: true, bundle: true, - banner: { - js: consoleLogReplacement, - }, define: { console: "__console_proxy", }, @@ -48,6 +19,7 @@ export const createBuildOptions = ( & Partial>, ): BuildOptions => { return { + sourcemap: "external", ...defaultBuildOptions, ...options, define: { @@ -56,8 +28,7 @@ export const createBuildOptions = ( }, banner: { ...options.banner, - js: defaultBuildOptions.banner.js - + (options.banner?.js ? "\n" + options.banner.js : ""), + js: options.banner?.js ? "\n" + options.banner.js : "", }, }; }; diff --git a/src/programs/start-program.ts b/src/programs/start-program.ts index 3adf5c3..8659210 100644 --- a/src/programs/start-program.ts +++ b/src/programs/start-program.ts @@ -8,6 +8,7 @@ import { Command } from "../utils/command"; import type { AdditionalPlugins } from "../utils/get-plugins"; import { getPlugins } from "../utils/get-plugins"; import { getGlobalPolyfills } from "../utils/get-polyfills"; +import { getRuntimeInit } from "../utils/get-runtime-init"; import { BuildProgram } from "./build-program"; import { createBuildOptions } from "./default-build-options"; @@ -64,13 +65,18 @@ export class StartProgram extends BuildProgram { if (existsSync(buildDirPath)) await rimraf(buildDirPath, {}); const polyfills = await getGlobalPolyfills(this); + const initScript = await getRuntimeInit(); await this.esbuildCtx.init( createBuildOptions({ - banner: { js: polyfills.bundle }, + banner: { js: `${polyfills.bundle}\n${initScript.bundle}` }, entryPoints: [path.resolve(this.cwd, this.config.entrypoint)], outfile: path.resolve(buildDirPath, "src", "main.js"), - plugins: getPlugins(this, { giRequirements: polyfills.requirements }), + plugins: getPlugins(this, { + giRequirements: polyfills.requirements.concat( + initScript.requirements, + ), + }), minify: this.config.minify ?? (this.isDev ? false : true), treeShaking: this.config.treeShake ?? (this.isDev ? false : true), }), @@ -78,5 +84,7 @@ export class StartProgram extends BuildProgram { ); await this.esbuildCtx.start(); + + Output.print(html` Build completed. `); } } diff --git a/src/runtime/console.ts b/src/runtime/console.ts new file mode 100644 index 0000000..ed0b1c3 --- /dev/null +++ b/src/runtime/console.ts @@ -0,0 +1,879 @@ +import GLib from "gi://GLib?version=2.0"; +import { SourceMap, SourceMapReader } from "./helpers/sourcemap-reader"; + +const EOL = "\n"; + +const COLOR = { + // info labels colors + Red: "\u001b[38;5;160m", + Blue: "\u001b[38;5;39m", + Purple: "\u001b[38;5;93m", + Yellow: "\u001b[38;5;220m", + Grey: "\u001b[38;5;247m", + HotOrange: "\u001b[38;5;202m", + + // object formatting + BracketBlue: "\u001b[38;5;75m", + BracketGreen: "\u001b[38;5;77m", + BracketYellow: "\u001b[38;5;178m", + BracketMagenta: "\u001b[38;5;165m", + BracketGrey: "\u001b[38;5;251m", + Key: "\u001b[38;5;252m", + + Reset: "\u001b[0m", +}; + +function red(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Red}${text}${COLOR.Reset}`; +} + +function blue(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Blue}${text}${COLOR.Reset}`; +} + +function purple(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Purple}${text}${COLOR.Reset}`; +} + +function yellow(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Yellow}${text}${COLOR.Reset}`; +} + +function grey(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Grey}${text}${COLOR.Reset}`; +} + +function hotOrange(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.HotOrange}${text}${COLOR.Reset}`; +} + +function bracketBlue(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketBlue}${text}${COLOR.Reset}`; +} + +function bracketGreen(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketGreen}${text}${COLOR.Reset}`; +} + +function bracketYellow(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketYellow}${text}${COLOR.Reset}`; +} + +function bracketMagenta(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketMagenta}${text}${COLOR.Reset}`; +} + +function bracketGrey(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.BracketGrey}${text}${COLOR.Reset}`; +} + +function keyClr(text: string) { + if (!ConsoleUtils.pretty) return text; + return `${COLOR.Key}${text}${COLOR.Reset}`; +} + +const BRACKET_COLORS = [ + bracketBlue, + bracketYellow, + bracketMagenta, + bracketGreen, + bracketGrey, +]; + +function bracket(bracketChar: string, depth: number) { + depth -= 1; + const colorfn = BRACKET_COLORS[depth % BRACKET_COLORS.length]!; + return colorfn(bracketChar); +} + +/** + * A simple regex to capture formatting specifiers + */ +const specifierTest = /%(d|i|s|f|o|O|c)/; + +function makeIndent(indent: number, indentSize = 2) { + if (indent < 0) { + indent = 0; + } + return Array.from({ length: indent * indentSize }, () => " ").join(""); +} + +type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array; + +function isTypedArray(value: unknown): value is TypedArray { + return ArrayBuffer.isView(value); +} + +function isArray(value: unknown): value is unknown[] { + return Array.isArray(value); +} + +type FmtContext = { + depth: number; + parentRefs: Map; + currentLocation: string; +}; + +function fmtError(err: Error | GLib.Error): string { + const trace = StacktraceResolver.mapStackTrace(err.stack?.trim() ?? ""); + return `${err.name}: ${err.message}${EOL}${ + err.stack ? addIndent(trace, 2) : "No stack trace available" + }`; +} + +function fmtKey(key: unknown): string { + switch (typeof key) { + case "bigint": + case "string": + case "number": + case "boolean": + return String(key); + case "function": + return `[Function ${key.name}]`; + case "symbol": + return `[Symbol ${key.toString()}]`; + case "undefined": + return "undefined"; + case "object": { + if (key === null) return "null"; + return "Object"; + } + } +} + +function fmtMap(map: Map, ctx: FmtContext): string { + const indent = makeIndent(ctx.depth); + + ctx.parentRefs.set(map, ctx.currentLocation); + + let fmtd = `Map${bracket("<", ctx.depth)}${EOL}`; + for (let [key, value] of map) { + key = fmtKey(key); + const nextCtx: FmtContext = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}.${key}`, + }; + const fmtv = fmt(value, nextCtx); + fmtd += `${indent}${keyClr(String(key))}: ${fmtv},${EOL}`; + } + fmtd += `${makeIndent(ctx.depth - 1)}${bracket(">", ctx.depth)}`; + + ctx.parentRefs.delete(map); + + return fmtd; +} + +function fmtSet(set: Set, ctx: FmtContext): string { + const indent = makeIndent(ctx.depth); + + ctx.parentRefs.set(set, ctx.currentLocation); + + let fmtd = `Set${bracket("<", ctx.depth)}${EOL}`; + for (const value of set) { + const nextCtx: FmtContext = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}.`, + }; + const fmtv = fmt(value, nextCtx); + fmtd += `${indent}${fmtv},${EOL}`; + } + fmtd += `${makeIndent(ctx.depth - 1)}${bracket(">", ctx.depth)}`; + + ctx.parentRefs.delete(set); + + return fmtd; +} + +function fmtArray(arr: Array | TypedArray, ctx: FmtContext): string { + const indent = makeIndent(ctx.depth); + + ctx.parentRefs.set(arr, ctx.currentLocation); + + const entries: string[] = []; + for (let i = 0; i < arr.length; i++) { + const value = arr[i]; + const nextCtx: FmtContext = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}[${i}]`, + }; + const fmtv = fmt(value, nextCtx); + entries.push(fmtv); + } + let fmtd = bracket("[", ctx.depth); + const totalEntriesLen = entries.reduce((sum, e) => sum + e.length, 0); + if (totalEntriesLen < 28) { + fmtd += `${entries.join(", ")}${bracket("]", ctx.depth)}`; + } else { + fmtd += `${EOL}${ + entries.map((e) => `${indent}${e}`).join(`,${EOL}`) + },${EOL}${makeIndent(ctx.depth - 1)}${bracket("]", ctx.depth)}`; + } + + ctx.parentRefs.delete(arr); + + return fmtd; +} + +function fmtTypedArray(arr: TypedArray, ctx: FmtContext): string { + const typedArrayName = arr.constructor.name; + return `${typedArrayName} ${fmtArray(arr, ctx)}`; +} + +function fmtPlainObject(obj: Record, ctx: FmtContext): string { + const indent = makeIndent(ctx.depth); + + ctx.parentRefs.set(obj, ctx.currentLocation); + + let fmtd = `${bracket("{", ctx.depth)}${EOL}`; + const keys = Object.keys(obj); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]!; + const value = obj[key]; + const nextCtx: FmtContext = { + parentRefs: ctx.parentRefs, + depth: ctx.depth + 1, + currentLocation: `${ctx.currentLocation}.${key}`, + }; + const fmtv = fmt(value, nextCtx); + fmtd += `${indent}${keyClr(String(key))}: ${fmtv},${EOL}`; + } + + fmtd += `${makeIndent(ctx.depth - 1)}${bracket("}", ctx.depth)}`; + + ctx.parentRefs.delete(obj); + + return fmtd; +} + +function fmtObject(obj: object, ctx: FmtContext): string { + if (ctx.parentRefs.has(obj)) { + const ref = ctx.parentRefs.get(obj); + return `## Recursive reference [${ref}] ##`; + } + + if ("toConsolePrint" in obj && typeof obj.toConsolePrint === "function") { + const objStr = obj.toConsolePrint(); + if (typeof objStr === "string") { + return addIndent(objStr, ctx.depth, 1); + } + } + + if (obj instanceof Error) { + return fmtError(obj); + } + if (obj instanceof GLib.Error) { + return fmtError(obj); + } + if (obj instanceof Map) { + return fmtMap(obj, ctx); + } + if (obj instanceof Set) { + return fmtSet(obj, ctx); + } + if (isTypedArray(obj)) { + return fmtTypedArray(obj, ctx); + } + if (isArray(obj)) { + return fmtArray(obj, ctx); + } + return fmtPlainObject(obj as any, ctx); +} + +function fmt( + item: unknown, + ctx: FmtContext = { depth: 1, parentRefs: new Map(), currentLocation: "" }, +): string { + switch (typeof item) { + case "bigint": + case "string": + case "number": + case "boolean": + return String(item); + case "function": + return `[Function ${item.name}]`; + case "symbol": + return `[Symbol ${item.toString()}]`; + case "undefined": + return "undefined"; + case "object": { + if (item === null) return "null"; + return fmtObject(item, ctx); + } + } +} + +function fmtArgs(args: unknown[]) { + args = args.slice(); + for (let i = 0; i < args.length; i++) { + const value = args[i]; + if (value instanceof Error) { + args[i] = fmtError(value); + } else if (typeof value === "object" && value !== null) { + args[i] = fmt(value); + } + } + return args; +} + +/** + * @param {string} str a string to check for format specifiers like %s or %i + * @returns {boolean} + */ +function hasFormatSpecifiers(str: string) { + return specifierTest.test(str); +} + +/** + * @param {any} item an item to format + * @returns {string} + */ +function formatOptimally(item: unknown) { + // Handle optimal error formatting. + if (item instanceof Error || item instanceof GLib.Error) { + return `${item.toString()}${item.stack ? EOL : ""}${ + item.stack + ?.split(EOL) + // Pad each stacktrace line. + .map((line) => line.padStart(2, " ")) + .join(EOL) + }`; + } + + if (typeof item === "object" && item !== null) { + if (item.constructor?.name !== "Object") { + return `${item.constructor?.name} ${fmt(item)}`; + } else if (String(item) === "GIRepositoryNamespace") { + // @ts-expect-error + return `[${String(item)} ${item.__name__}]`; + } + } + return fmt(item); +} + +function addIndent(text: string, indent: number, startFromLine = 0) { + const identStr = makeIndent(indent, 1); + const lines = text.split(EOL); + for (let i = startFromLine; i < lines.length; i++) { + lines[i] = identStr + lines[i]; + } + return lines.join(EOL); +} + +const NUM_REGEX = /^-?\d+(\.\d+)?$/; +function isNumber(value: unknown) { + return ( + typeof value === "number" + || typeof value === "bigint" + || (typeof value === "string" && NUM_REGEX.test(value)) + ); +} + +enum LogLevel { + Log = "log", + Dir = "dir", + Dirxml = "dirxml", + Trace = "trace", + Group = "group", + GroupCollapsed = "groupCollapsed", + TimeLog = "timeLog", + TimeEnd = "timeEnd", + Debug = "debug", + Count = "count", + CountReset = "countReset", + Info = "info", + Warn = "warn", + ReportWarning = "reportWarning", + Error = "error", + Assert = "assert", +} + +function addLogPrefix(loglevel: LogLevel, message: string) { + switch (loglevel) { + case LogLevel.Log: + return `[${grey("LOG")}] ${message}`; + case LogLevel.Warn: + return `[${yellow("WARN")}] ${message}`; + case LogLevel.Error: + return `[${red("ERROR")}] ${message}`; + case LogLevel.Assert: + return `[${hotOrange("ASSERT")}] ${message}`; + case LogLevel.Debug: + return `[${purple("DEBUG")}] ${message}`; + case LogLevel.Info: + return `[${blue("INFO")}] ${message}`; + case LogLevel.Trace: + return `[${grey("TRACE")}] ${message}`; + case LogLevel.Group: + case LogLevel.GroupCollapsed: + return `[${grey("GROUP")}] ${message}`; + case LogLevel.Count: + case LogLevel.CountReset: + return `[${grey("COUNT")}] ${message}`; + case LogLevel.TimeLog: + case LogLevel.TimeEnd: + return `[${grey("TIME")}] ${message}`; + default: + return message; + } +} + +type PrinterOptions = { + stackTrace?: string; + fields?: Record; +}; + +class ConsoleUtils { + private static indent = 0; + private static counters = new Map(); + private static timers = new Map(); + static pretty = true; + + static incrementCounter(label: string) { + const count = this.counters.get(label) ?? 0; + const newCount = count + 1; + this.counters.set(label, newCount); + return newCount; + } + + static resetCounter(label: string) { + this.counters.delete(label); + } + + static clearIndent() { + this.indent = 0; + } + + static enterGroup() { + this.indent++; + } + + static leaveGroup() { + this.indent = Math.max(0, this.indent - 1); + } + + static startTimer(label: unknown, time: number) { + this.timers.set(label, time); + } + + static getTimer(label: unknown) { + return this.timers.get(label); + } + + static endTimer(label?: unknown) { + const startTime = this.timers.get(label); + this.timers.delete(label); + return startTime; + } + + static logger(logLevel: LogLevel, args: unknown[]) { + if (args.length === 0) { + this.printer(logLevel, []); + return; + } + + if (args.length === 1) { + this.printer(logLevel, fmtArgs(args)); + return undefined; + } + + const [first, ...rest] = args; + + // If first does not contain any format specifiers, don't call Formatter + if (typeof first !== "string" || !hasFormatSpecifiers(first)) { + this.printer(logLevel, fmtArgs(args)); + return undefined; + } + + // Otherwise, perform print the result of Formatter. + this.printer(logLevel, this.formatter([first, ...rest])); + + return undefined; + } + + static formatter(args: unknown[]): unknown[] { + if (args.length === 1) return args; + + // The initial formatting string is the first arg + let target = String(args[0]); + + const current = args[1]; + + // Find the index of the first format specifier. + const specifierIndex = specifierTest.exec(target)?.index!; + const specifier = target.slice(specifierIndex, specifierIndex + 2); + let converted = null; + switch (specifier) { + case "%s": + converted = String(current); + break; + case "%d": + case "%i": + if (!isNumber(current)) { + converted = 0; + } else { + converted = Number(current).toFixed(0); + } + break; + case "%f": + if (!isNumber(current)) { + converted = 0; + } else { + converted = String(Number(current)); + } + break; + case "%o": + converted = formatOptimally(current); + break; + case "%O": + converted = fmt(current); + break; + case "%c": + converted = ""; + break; + } + // If any of the previous steps set converted, replace the specifier in + // target with the converted value. + if (converted !== null) { + target = target.slice(0, specifierIndex) + + converted + + target.slice(specifierIndex + 2); + } + + if (!hasFormatSpecifiers(target)) { + return [target, ...fmtArgs(args.slice(2))]; + } + + const result = [target, ...args.slice(2)]; + + if (result.length === 1) return result; + + return this.formatter(result); + } + + static printer( + logLevel: LogLevel, + args: unknown[], + options: PrinterOptions = {}, + ) { + let severity; + + switch (logLevel) { + case LogLevel.Log: + case LogLevel.Dir: + case LogLevel.Dirxml: + case LogLevel.Trace: + case LogLevel.Group: + case LogLevel.GroupCollapsed: + case LogLevel.TimeLog: + case LogLevel.TimeEnd: + severity = GLib.LogLevelFlags.LEVEL_MESSAGE; + break; + case LogLevel.Debug: + severity = GLib.LogLevelFlags.LEVEL_DEBUG; + break; + case LogLevel.Count: + case LogLevel.Info: + severity = GLib.LogLevelFlags.LEVEL_INFO; + break; + case LogLevel.Warn: + case LogLevel.CountReset: + case LogLevel.ReportWarning: + severity = GLib.LogLevelFlags.LEVEL_WARNING; + break; + case LogLevel.Error: + case LogLevel.Assert: + severity = GLib.LogLevelFlags.LEVEL_CRITICAL; + break; + default: + severity = GLib.LogLevelFlags.LEVEL_MESSAGE; + } + + const output = args + .map((a) => { + if (a === null) return "null"; + else if (typeof a === "object") return formatOptimally(a); + else if (typeof a === "undefined") return "undefined"; + else if (typeof a === "bigint") return `${a}n`; + else return String(a); + }) + .join(" "); + + let formattedOutput = output; // addIndent(, this.indent); + const extraFields: { + CODE_FUNC?: string; + CODE_FILE?: string; + CODE_LINE?: string; + } = {}; + + let stackTrace = options?.stackTrace; + let stackTraceLines: string[] | null = null; + if ( + !stackTrace + && (logLevel === "trace" || severity <= GLib.LogLevelFlags.LEVEL_WARNING) + ) { + stackTrace = new Error().stack; + if (stackTrace) { + const currentFile = stackTrace.match(/^[^@]*@(.*):\d+:\d+$/m)?.at(1); + if (currentFile) { + const index = stackTrace.lastIndexOf(currentFile) + + currentFile.length; + + stackTraceLines = stackTrace.substring(index).split(EOL); + // Remove the remainder of the first line + stackTraceLines.shift(); + } + } + } + + if (stackTraceLines == null) { + stackTraceLines = []; + } + + if (logLevel === LogLevel.Trace) { + if (stackTraceLines.length) { + formattedOutput += `${EOL}${ + addIndent(stackTraceLines.join(EOL), this.indent) + }`; + } else { + formattedOutput += `${EOL}${ + addIndent("No stack trace available", this.indent) + }`; + } + } + + if (stackTraceLines.length) { + const [stackLine] = stackTraceLines; + const match = stackLine?.match(/^([^@]*)@(.*):(\d+):\d+$/); + + if (match) { + const [_, func, file, line] = match; + + if (func) extraFields.CODE_FUNC = func; + if (file) extraFields.CODE_FILE = file; + if (line) extraFields.CODE_LINE = line; + } + } + + const logContent = addLogPrefix(logLevel, formattedOutput); + print(logContent); + } +} + +/** + * Implementation of the WHATWG Console object. + */ +const Console = { + get [Symbol.toStringTag]() { + return "console"; + }, + + assert(condition: unknown, ...data: unknown[]) { + if (condition) return; + + const message = "Assertion failed"; + + if (data.length === 0) data.push(message); + + if (typeof data[0] !== "string") { + data.unshift(message); + } else { + const first = data.shift(); + data.unshift(`${message}: ${first}`); + } + ConsoleUtils.logger(LogLevel.Assert, data); + }, + + clear() { + ConsoleUtils.clearIndent(); + imports.gi.GjsPrivate.clear_terminal(); + }, + + debug(...data: unknown[]) { + ConsoleUtils.logger(LogLevel.Debug, data); + }, + + error(...data: unknown[]) { + ConsoleUtils.logger(LogLevel.Error, data); + }, + + info(...data: unknown[]) { + ConsoleUtils.logger(LogLevel.Info, data); + }, + + log(...data: unknown[]) { + ConsoleUtils.logger(LogLevel.Log, data); + }, + + table(tabularData: unknown, _properties: unknown) { + this.log(tabularData); + }, + + trace(...data: unknown[]) { + if (data.length === 0) data = ["Trace"]; + + ConsoleUtils.logger(LogLevel.Trace, data); + }, + + warn(...data: unknown[]) { + ConsoleUtils.logger(LogLevel.Warn, data); + }, + + dir(item: unknown, options: never) { + const object = fmt(item); + ConsoleUtils.printer(LogLevel.Dir, [object], options); + }, + + dirxml(...data: unknown[]) { + this.log(...data); + }, + + count(label: string) { + const count = ConsoleUtils.incrementCounter(label); + const msg = `${label}: ${count}`; + ConsoleUtils.logger(LogLevel.Count, [msg]); + }, + + countReset(label: string) { + ConsoleUtils.resetCounter(label); + }, + + group(...data: unknown[]) { + ConsoleUtils.enterGroup(); + ConsoleUtils.logger(LogLevel.Group, data); + }, + + groupCollapsed(...data: unknown[]) { + this.group(...data); + }, + + groupEnd() { + ConsoleUtils.leaveGroup(); + }, + + time(label: unknown) { + const ts = imports.gi.GLib.get_monotonic_time(); + ConsoleUtils.startTimer(label, ts); + }, + + timeLog(label: unknown, ...data: unknown[]) { + const startTime = ConsoleUtils.getTimer(label); + if (startTime == null) { + ConsoleUtils.logger(LogLevel.Warn, [`Timer “${label}” doesn’t exist.`]); + return; + } + const ts = imports.gi.GLib.get_monotonic_time(); + const durationMs = (ts - startTime) / 1000; + const msg = `${label}: ${durationMs.toFixed(3)} ms`; + data.unshift(msg); + + ConsoleUtils.printer(LogLevel.TimeLog, data); + }, + + timeEnd(label: unknown) { + const startTime = ConsoleUtils.endTimer(label); + if (startTime == null) { + ConsoleUtils.logger(LogLevel.Warn, [`Timer “${label}” doesn’t exist.`]); + return; + } + const ts = imports.gi.GLib.get_monotonic_time(); + const durationMs = (ts - startTime) / 1000; + const msg = `${label}: ${durationMs.toFixed(3)} ms`; + ConsoleUtils.printer(LogLevel.TimeEnd, [msg]); + }, + + profile() {}, + + profileEnd() {}, + + timeStamp() {}, + + setPretty(pretty: boolean) { + ConsoleUtils.pretty = !!pretty; + }, +}; + +type AppSourceMaps = SourceMap & { + rowOffset: number; + colOffset: number; + root: string; + wd: string; +}; + +class StacktraceResolver { + static sourcmapReader: SourceMapReader; + static map: AppSourceMaps; + + static { + import(`${imports.package.moduledir}/main.js.map`) + .then((main: { map: string }) => { + const map = JSON.parse(main.map) as AppSourceMaps; + StacktraceResolver.map = map; + StacktraceResolver.sourcmapReader = new SourceMapReader(map, map.root); + }) + .catch((error) => {}); + } + + static mapStackTrace(stack: string) { + if (!StacktraceResolver.sourcmapReader) { + return stack; + } + + const lines = stack.split(EOL); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]!; + const match = line.match(/main.js:\d+:\d+$/); + if (match) { + const lineCol = line.split("/main.js:").pop()!; + const [lineNo, colNo] = lineCol.split(":") as [string, string]; + const l = Number(lineNo) - this.map.rowOffset; + const c = Number(colNo) - this.map.colOffset; + const org = StacktraceResolver.sourcmapReader.getOriginalPosition(l, c); + if (org && org.file) { + if (org.file.startsWith(StacktraceResolver.map.wd)) { + org.file = "./" + + org.file.substring(StacktraceResolver.map.wd.length + 1); + } + + if (org.symbolName == null) { + if (line.match(/^[\w\d]+@/)) { + org.symbolName = line.split("@")[0]; + } + } + + if (org.symbolName) { + lines[i] = + `${org.symbolName} at ${org.file}:${org.line}:${org.column}`; + } else { + lines[i] = `at ${org.file}:${org.line}:${org.column}`; + } + } + } + } + return lines.join(EOL); + } +} + +export { Console as __console_proxy }; diff --git a/src/runtime/helpers/base64-vlq.ts b/src/runtime/helpers/base64-vlq.ts new file mode 100644 index 0000000..df9e0e5 --- /dev/null +++ b/src/runtime/helpers/base64-vlq.ts @@ -0,0 +1,100 @@ +export type Segment = + | [ + outColumn: number, + file: number, + line: number, + column: number, + nameIndex: number, + ] + | [outColumn: number, file: number, line: number, column: number] + | [outColumn: number]; + +export class Base64VLQ { + charToInteger: Map = new Map(); + integerToChar: Map = new Map(); + + constructor() { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" + .split("") + .forEach((char, i) => { + this.charToInteger.set(char, i); + this.integerToChar.set(i, char); + }); + } + + decode(string: string): Segment { + const result: number[] = []; + + let shift = 0; + let value = 0; + + for (let i = 0; i < string.length; i += 1) { + const char = string[i]!; + let integer = this.charToInteger.get(char); + + if (integer === undefined) { + throw new Error(`Invalid character (${string[i]})`); + } + + const has_continuation_bit = integer & 32; + + integer &= 31; + value += integer << shift; + + if (has_continuation_bit) { + shift += 5; + } else { + const should_negate = value & 1; + value >>>= 1; + + if (should_negate) { + result.push(value === 0 ? -0x80000000 : -value); + } else { + result.push(value); + } + + // reset + value = shift = 0; + } + } + + return result as Segment; + } + + encode(value: number | number[]) { + if (typeof value === "number") { + return this.encodeInteger(value); + } + + let result = ""; + for (let i = 0; i < value.length; i += 1) { + const char = value[i]!; + result += this.encodeInteger(char); + } + + return result; + } + + encodeInteger(num: number) { + let result = ""; + + if (num < 0) { + num = (-num << 1) | 1; + } else { + num <<= 1; + } + + do { + let clamped = num & 31; + num >>>= 5; + + if (num > 0) { + clamped |= 32; + } + + result += this.integerToChar.get(clamped)!; + } while (num > 0); + + return result; + } +} diff --git a/src/runtime/helpers/recursive-objects.ts b/src/runtime/helpers/recursive-objects.ts new file mode 100644 index 0000000..1146f65 --- /dev/null +++ b/src/runtime/helpers/recursive-objects.ts @@ -0,0 +1,39 @@ +/** + * Creates a copy of object without Circular References which breaks + * JSON.stringify function. + */ +const copyWithoutRecursiveRefs = >( + references: object[], + obj: T, +): object => { + references.push(obj); + let cleanObject: Record = {}; + + Object.keys(obj).forEach((key) => { + var value = obj[key]; + if (value && typeof value === "object") { + if (references.indexOf(value) < 0) { + references.push(value); + cleanObject[key] = copyWithoutRecursiveRefs(references, value); + references.pop(); + } else { + cleanObject[key] = "###_Recursive Reference_###"; + } + } else if (typeof value !== "function") { + cleanObject[key] = value; + } + }); + + const proto = Object.getPrototypeOf(obj); + if (proto) { + Object.setPrototypeOf(cleanObject, proto); + } + + return cleanObject; +}; + +export function removeRecursiveRefs>( + obj: T, +): object { + return copyWithoutRecursiveRefs([], obj); +} diff --git a/src/runtime/helpers/sourcemap-reader.ts b/src/runtime/helpers/sourcemap-reader.ts new file mode 100644 index 0000000..c48db22 --- /dev/null +++ b/src/runtime/helpers/sourcemap-reader.ts @@ -0,0 +1,153 @@ +import Fs from "fs-gjs"; +import path from "path-gjsify"; +import type { Segment } from "./base64-vlq"; +import { Base64VLQ } from "./base64-vlq"; + +export type SourceMap = { + version: number; + sources: string[]; + sourcesContent: string[]; + mappings: string; + names: string[]; +}; + +export type FileLocation = { + file: string | undefined; + line: number; + column: number; + symbolName?: string | undefined; +}; + +type MapState = [ + outColumn: number, + file: number, + line: number, + column: number, + nameIndex: number, +]; + +export class SourceMapReader { + static async newFromMapFile(mapFilepath: string) { + try { + const fileContent = await Fs.readTextFile(mapFilepath); + const map = JSON.parse(fileContent); + return new SourceMapReader(map, mapFilepath); + } catch (err) { + return undefined; + } + } + + private converter = new Base64VLQ(); + + constructor( + private map: SourceMap, + private mapFilepath: string, + ) {} + + protected getFile(file?: number) { + if (file === undefined) return undefined; + + const rel = this.map.sources[file]; + + if (!rel) return undefined; + + return path.join(path.dirname(this.mapFilepath), rel); + } + + protected getLineN(text: string, n: number) { + let line = 0; + let lineStart = 0; + + while (line !== n) { + lineStart = text.indexOf("\n", lineStart) + 1; + line++; + } + + if (line > 0 && lineStart === 0) { + return ""; + } + + let lineEnd = text.indexOf("\n", lineStart + 1); + + if (lineEnd === -1) { + lineEnd = text.length; + } + + return text.slice(lineStart, lineEnd); + } + + getOriginalPosition(outLine: number, outColumn: number): FileLocation | null { + // SourceMap is 0 based, error stack is 1 based + outLine -= 1; + outColumn -= 1; + + const vlqs = this.map.mappings.split(";").map((line) => line.split(",")); + + const state: MapState = [0, 0, 0, 0, 0]; + + if (vlqs.length <= outLine) return null; + + for (let index = 0; index < vlqs.length; index++) { + const line = vlqs[index]!; + state[0] = 0; + + let prevSegment: Segment = [0]; + + for (let i = 0; i < line.length; i++) { + const segment = line[i]; + if (!segment) continue; + const decodedSegment = this.converter.decode(segment); + + const prevState = state.slice() as MapState; + + state[0] += decodedSegment[0]; + + if (decodedSegment.length > 1) { + state[1] += decodedSegment[1] ?? 0; + state[2] += decodedSegment[2] ?? 0; + state[3] += decodedSegment[3] ?? 0; + state[4] += decodedSegment[4] ?? 0; + + if (index === outLine) { + const currentOutCol = state[0]; + const prevOutCol = prevState[0]; + + if (currentOutCol === outColumn) { + return { + file: this.getFile(state[1]), + line: state[2] + 1, + column: state[3] + 1, + symbolName: + decodedSegment[4] != null + ? this.map.names[state[4]] + : undefined, + }; + } else if (outColumn >= prevOutCol && outColumn <= currentOutCol) { + return { + file: this.getFile(state[1]), + line: state[2] + 1, + column: prevState[3] + 1, + symbolName: + prevSegment[4] != null + ? this.map.names[prevState[4]] + : undefined, + }; + } + } + } + + prevSegment = decodedSegment; + } + + if (index === outLine) { + return { + file: this.getFile(state[1]), + line: state[2] + 1, // back to 1 based + column: 1, + }; + } + } + + return null; + } +} diff --git a/src/runtime/tsconfig.json b/src/runtime/tsconfig.json new file mode 100644 index 0000000..857d141 --- /dev/null +++ b/src/runtime/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "declaration": false + } +} diff --git a/src/utils/get-runtime-init.ts b/src/utils/get-runtime-init.ts new file mode 100644 index 0000000..23bcfe6 --- /dev/null +++ b/src/utils/get-runtime-init.ts @@ -0,0 +1,99 @@ +import dedent from "dedent"; +import esbuild from "esbuild"; +import fs from "fs"; +import path from "path"; +import { getDirPath } from "../get-dirpath/get-dirpath"; + +const ALLOWED_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs"]; + +export function getRuntimeInit() { + const rootPath = getDirPath(); + const files = fs.readdirSync(path.join(rootPath, "runtime/esm")); + const modules: string[] = []; + for (const filename of files) { + const ext = path.extname(filename); + if (ALLOWED_EXTENSIONS.includes(ext)) { + modules.push("./runtime/esm/" + filename); + } + } + return buildInitBundle(modules); +} + +async function buildInitBundle(scriptsFilepaths: string[]) { + const rootPath = getDirPath(); + const modules = scriptsFilepaths.map((p, i) => ({ + path: p, + name: `runtime_${i}`, + })); + const index = + `${ + modules.map((m) => /* js */ `import * as ${m.name} from "${m.path}";`) + .join("\n") + }`.trim() + + dedent` + const modules = [${modules.map((m) => m.name + ",")}]; + for (const module of modules) { + const entries = Object.entries(module); + for (const [key, value] of entries) { + if (key in globalThis) { + continue; + } + Object.defineProperty(globalThis, key, { + value: value, + }); + } + } + `; + + const requirements: [string, string | undefined][] = []; + + const result = await esbuild.build({ + stdin: { + contents: index, + loader: "ts", + resolveDir: rootPath, + sourcefile: "runtime.ts", + }, + bundle: true, + write: false, + format: "iife", + target: "esnext", + plugins: [ + { + name: "replace-gi-imports", + setup(build) { + build.onResolve({ filter: /^gi:\/\/.+/ }, (args) => { + return { + namespace: "gi", + path: args.path.replace(/^gi:/, ""), + }; + }); + + build.onLoad({ namespace: "gi", filter: /.*/ }, (args) => { + const name = args.path.replace(/^\/\//, "").replace(/\?.+/, ""); + const version = args.path.indexOf("?") !== -1 + ? args.path.slice( + args.path.indexOf("?") + "version=".length + 1, + ) + : undefined; + requirements.push([name, version]); + return { + contents: /* js */ `export default ${name};`, + }; + }); + }, + }, + ], + }); + + if (result.errors.length > 0) { + throw new Error(result.errors[0]!.text); + } + + const [out] = result.outputFiles; + + return { + bundle: out!.text, + requirements, + }; +} diff --git a/src/utils/read-config.ts b/src/utils/read-config.ts index cf9002f..994b062 100644 --- a/src/utils/read-config.ts +++ b/src/utils/read-config.ts @@ -16,5 +16,9 @@ export const readConfig = async (program: Program) => { mode: program.isDev ? "development" : "production", }); + if (config.sourcemap == null) { + config.sourcemap = true; + } + return config; }; diff --git a/yarn.lock b/yarn.lock index d214081..838d57f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -280,13 +280,14 @@ gjs-multiprocess "0.0.5" path-gjsify "^1.0.0" -"@reactgjs/renderer@^0.0.1-beta.3": - version "0.0.1-beta.3" - resolved "https://registry.yarnpkg.com/@reactgjs/renderer/-/renderer-0.0.1-beta.3.tgz#c78e79c25964c5a5685c7e53110dcf7b51ca471f" - integrity sha512-uK4Z4d/jzpziZF9zhk6fABgqm6pGCejMTfixHQC+dejKkke1xs3AnVlt1V1OHLAs/FjeeIBGdINfSNznzKyBzw== +"@reactgjs/renderer@^0.0.1-beta.4": + version "0.0.1-beta.4" + resolved "https://registry.yarnpkg.com/@reactgjs/renderer/-/renderer-0.0.1-beta.4.tgz#40b904cc6aec176220642608fa51ce4ed7d68264" + integrity sha512-ACUpy5LtQMv2TEuOB6Yb+nSicTX+yR/nYTiY/pFxoAxLBBb0EDhf9IUf3Ti+krxhOpkZACerwI3R0gECwi7xyQ== dependencies: dilswer "^2.1.1" - react-reconciler "^0.29.0" + react "^19.0.0" + react-reconciler "^0.31.0" "@swc/core-darwin-arm64@1.5.5": version "1.5.5" @@ -411,6 +412,32 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/lodash.clonedeep@^4.5.9": + version "4.5.9" + resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz#ea48276c7cc18d080e00bb56cf965bcceb3f0fc1" + integrity sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q== + dependencies: + "@types/lodash" "*" + +"@types/lodash.get@^4.4.9": + version "4.4.9" + resolved "https://registry.yarnpkg.com/@types/lodash.get/-/lodash.get-4.4.9.tgz#6390714bf688321d9a445cbc8e90220635649713" + integrity sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA== + dependencies: + "@types/lodash" "*" + +"@types/lodash.set@^4.3.9": + version "4.3.9" + resolved "https://registry.yarnpkg.com/@types/lodash.set/-/lodash.set-4.3.9.tgz#55d95bce407b42c6655f29b2d0811fd428e698f0" + integrity sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" + integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== + "@types/node@*": version "20.12.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.11.tgz#c4ef00d3507000d17690643278a60dc55a9dc9be" @@ -683,6 +710,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +dedent@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -1265,11 +1297,6 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - json-to-ts@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/json-to-ts/-/json-to-ts-1.7.0.tgz#896a4eaa3d1ed3fef0c9e73ba98b3e8b25c1e02d" @@ -1279,23 +1306,31 @@ json-to-ts@^1.7.0: hash.js "^1.0.3" pluralize "^3.1.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.mergewith@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg== + lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - lru-cache@^10.2.0: version "10.2.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" @@ -1516,13 +1551,17 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-reconciler@^0.29.0: - version "0.29.2" - resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.29.2.tgz#8ecfafca63549a4f4f3e4c1e049dd5ad9ac3a54f" - integrity sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg== +react-reconciler@^0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.31.0.tgz#6b7390fe8fab59210daf523d7400943973de1458" + integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" + scheduler "^0.25.0" + +react@^19.0.0: + version "19.0.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" + integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== readdirp@~3.6.0: version "3.6.0" @@ -1590,12 +1629,10 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" +scheduler@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" + integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== set-function-length@^1.2.1: version "1.2.2"