jsmdcui 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -128,7 +128,7 @@ import { isHex3Encoding, isMdcuiEncoding } from "./runtime/encodings.js";
128
128
  import { createInterface } from "node:readline/promises";
129
129
 
130
130
  import pkg from "../package.json" with { type: "json" };
131
- import { REPO_ROOT,IS_COMPILED, buildExecutable,buildEarlyExit } from "../single-exe/compiled.js";
131
+ import { REPO_ROOT,IS_COMPILED, buildExecutable,buildEarlyExit,stringifyNonPrimitiveDefineValues } from "../single-exe/compiled.js";
132
132
 
133
133
 
134
134
  const __filename = fileURLToPath(import.meta.url);
@@ -186,6 +186,7 @@ const MDCUI_DEFAULT_DEMO_ENABLED = typeof MDCUI_DEFAULT_DEMO !== "undefined";
186
186
  const MDCUI_DEFAULT_DEMO_WUI_ENABLED = typeof MDCUI_DEFAULT_DEMO_WUI !== "undefined";
187
187
  const MDCUI_OVERWRITE_DEMO_ENABLED = typeof MDCUI_OVERWRITE_DEMO !== "undefined";
188
188
  let defaultEdit = false;
189
+ let noRuntimeArgs = false;
189
190
  const decoder = new TextDecoder();
190
191
  let _activeTtyStream = null; // set in App.start() for use by the global error handler
191
192
 
@@ -1173,9 +1174,15 @@ function parseArgs(argv) {
1173
1174
  }
1174
1175
  else if (arg.startsWith("--demo-")) {
1175
1176
  const name = arg.slice("--demo-".length);
1176
- flags.demo = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
1177
+ flags.demo = isValidDemoName(name)
1177
1178
  ? { option: arg, filename: `${name}.md`, asset: `demos/${name}.md` }
1178
- : { option: arg, error: "demo filename must contain only letters, numbers, dots, underscores, and hyphens" };
1179
+ : {
1180
+ option: arg,
1181
+ error:
1182
+ "demo filename must start with a Unicode letter or number "
1183
+ + "and contain only Unicode letters, numbers, dots, underscores, "
1184
+ + "or hyphens; whitespace is not allowed",
1185
+ };
1179
1186
  }
1180
1187
  else if (arg === "--allow-url") flags.allowUrl = true;
1181
1188
  else if (arg === "--kitty") flags.kittyMode = "extended";
@@ -1206,6 +1213,17 @@ function parseArgs(argv) {
1206
1213
  return { flags, files };
1207
1214
  }
1208
1215
 
1216
+ function isValidDemoName(name) {
1217
+ return /^[\p{L}\p{N}][\p{L}\p{N}\p{M}._-]*$/u.test(String(name));
1218
+ }
1219
+
1220
+ function demoNameFromMarkdownBasename(filename) {
1221
+ const value = String(filename);
1222
+ if (!value.endsWith(".md")) return "";
1223
+ const name = value.slice(0, -".md".length);
1224
+ return isValidDemoName(name) ? name : "";
1225
+ }
1226
+
1209
1227
  function usage() {
1210
1228
  return [
1211
1229
  `Usage:
@@ -1337,7 +1355,7 @@ Experimental single-exe:
1337
1355
  Build a Bun single-file executable for target & exit
1338
1356
 
1339
1357
  [BUN_ARGS...]
1340
- --define MDCUI_XXX=true
1358
+ --define MDCUI_XXX=1
1341
1359
  More info in --readme
1342
1360
  https://bun.com/docs/bundler/executables
1343
1361
  `
@@ -1400,6 +1418,7 @@ class BufferModel {
1400
1418
  this._mdcuiFenceEvents = isMdcuiEncoding(this.encoding) ? fenceEventMap(sourceText ?? tuiSourceText ?? text) : new Map();
1401
1419
  this._mdcuiImages = isMdcuiEncoding(this.encoding) ? (mdcuiImages ?? []) : [];
1402
1420
  this._mdcuiRenderWidth = Math.trunc(Number(mdcuiRenderWidth) || 0);
1421
+ this._useBundledMdcuiModules = false;
1403
1422
  this.cursor = { x: 0, y: 0 };
1404
1423
  this.scroll = { x: 0, y: 0, row: 0 };
1405
1424
  this._modified = false;
@@ -4540,7 +4559,10 @@ class App {
4540
4559
  ) return false;
4541
4560
 
4542
4561
  const frontPath = `${buf.path}.front.js`;
4543
- if (!existsSync(frontPath)) return false;
4562
+ const useBundledMdcuiModules = Boolean(
4563
+ global.MDCUI_MAIN && buf._useBundledMdcuiModules,
4564
+ );
4565
+ if (!useBundledMdcuiModules && !existsSync(frontPath)) return false;
4544
4566
  try {
4545
4567
  const selection = globalThis.$?.(`${declaration.tag}#${id}`);
4546
4568
  const target = {
@@ -4555,8 +4577,12 @@ class App {
4555
4577
  });
4556
4578
  const event = tuiKeyEventForFront(inputEvent, target, eventName);
4557
4579
  const [{ evalFront }, frontMod] = await Promise.all([
4558
- import("./cui/rpc.mjs"),
4559
- import(localModuleUrl(frontPath)),
4580
+ useBundledMdcuiModules
4581
+ ? import(global.MDCUI_MAIN + "-rpc.js")
4582
+ : import("./cui/rpc.mjs"),
4583
+ useBundledMdcuiModules
4584
+ ? import(global.MDCUI_MAIN + ".front.js")
4585
+ : import(localModuleUrl(frontPath)),
4560
4586
  ]);
4561
4587
  const result = await evalFront(frontMod, code, { event });
4562
4588
  if (result && typeof result === "object" && result.ok === false) {
@@ -4591,9 +4617,16 @@ class App {
4591
4617
  { // defaultCallback
4592
4618
  if (payload.link && /^javascript:/i.test(payload.link) && buf?.path && !isHttpUrl(buf.path)) {
4593
4619
  try {
4620
+ const useBundledMdcuiModules = Boolean(
4621
+ global.MDCUI_MAIN && buf._useBundledMdcuiModules,
4622
+ );
4594
4623
  const [{ evalFront }, frontMod] = await Promise.all([
4595
- import("./cui/rpc.mjs"),
4596
- import(localModuleUrl(`${buf.path}.front.js`)),
4624
+ useBundledMdcuiModules
4625
+ ? import(global.MDCUI_MAIN + "-rpc.js")
4626
+ : import("./cui/rpc.mjs"),
4627
+ useBundledMdcuiModules
4628
+ ? import(global.MDCUI_MAIN + ".front.js")
4629
+ : import(localModuleUrl(`${buf.path}.front.js`)),
4597
4630
  ]);
4598
4631
  const result = await evalFront(frontMod, payload.link);
4599
4632
  if (result && typeof result === "object" && result.ok === false) {
@@ -5481,10 +5514,18 @@ class App {
5481
5514
  const frontPath = buffer.path && !isHttpUrl(buffer.path)
5482
5515
  ? `${buffer.path}.front.js`
5483
5516
  : "";
5484
- if (!frontPath || !existsSync(frontPath)) return;
5517
+ const useBundledMdcuiModules = Boolean(
5518
+ global.MDCUI_MAIN && buffer._useBundledMdcuiModules,
5519
+ );
5520
+ if (
5521
+ !useBundledMdcuiModules
5522
+ && (!frontPath || !existsSync(frontPath))
5523
+ ) return;
5485
5524
 
5486
5525
  try {
5487
- const frontMod = await import(localModuleUrl(frontPath));
5526
+ const frontMod = useBundledMdcuiModules
5527
+ ? await import(global.MDCUI_MAIN + ".front.js")
5528
+ : await import(localModuleUrl(frontPath));
5488
5529
  if (typeof frontMod.onMdcuiExit !== "function") return;
5489
5530
  await frontMod.onMdcuiExit({
5490
5531
  reason,
@@ -7021,6 +7062,11 @@ function makeBufferAdapter(buffer) {
7021
7062
  get Name() { return buffer.Name ?? buffer.name ?? ""; },
7022
7063
  get Modified() { return buffer.modified ?? false; },
7023
7064
  get Settings() { return buffer.Settings; },
7065
+ get MdcuiModuleSource() {
7066
+ return global.MDCUI_MAIN && buffer._useBundledMdcuiModules
7067
+ ? "embedded"
7068
+ : "external";
7069
+ },
7024
7070
  Line: (...args) => buffer.Line(Number(lastArg(args))),
7025
7071
  Insert: (...args) => insertAtLoc(buffer, decodeLoc(args.at(-2)), String(args.at(-1))),
7026
7072
  Replace: (...args) => replaceAtLocs(buffer, decodeLoc(args.at(-3)), decodeLoc(args.at(-2)), String(args.at(-1))),
@@ -8137,11 +8183,11 @@ function printDemoList() {
8137
8183
  }
8138
8184
 
8139
8185
  async function prepareDemo(flags, rawFiles) {
8140
- if (!flags.demo) return true;
8186
+ if (!flags.demo) return { ok: true };
8141
8187
  if (flags.demo.error) {
8142
8188
  console.error(`Invalid demo option ${flags.demo.option}: ${flags.demo.error}`);
8143
8189
  process.exitCode = 2;
8144
- return false;
8190
+ return { ok: false };
8145
8191
  }
8146
8192
  let demoSource;
8147
8193
  try {
@@ -8149,29 +8195,169 @@ async function prepareDemo(flags, rawFiles) {
8149
8195
  } catch {
8150
8196
  console.error(`Unknown demo ${flags.demo.option}: ${flags.demo.asset} was not found`);
8151
8197
  process.exitCode = 2;
8152
- return false;
8198
+ return { ok: false };
8153
8199
  }
8200
+
8154
8201
  const demoPath = resolve(flags.demo.filename);
8155
- if (flags.overwriteDemo || !(await Bun.file(demoPath).exists())) {
8202
+ const demoFile = Bun.file(demoPath);
8203
+ const demoExists = await demoFile.exists();
8204
+ const writeBundledContent = flags.overwriteDemo || !demoExists;
8205
+ const bundledByteLength = Buffer.byteLength(demoSource);
8206
+ const demoByteLength = writeBundledContent
8207
+ ? bundledByteLength
8208
+ : demoFile.size;
8209
+ const configuredMainBase = String(
8210
+ global.MDCUI_MAIN_BASE
8211
+ || (global.MDCUI_MAIN ? basename(String(global.MDCUI_MAIN)) : ""),
8212
+ );
8213
+ const isConfiguredMainDemo = Boolean(
8214
+ global.MDCUI_MAIN
8215
+ && configuredMainBase
8216
+ && basename(demoPath) === configuredMainBase,
8217
+ );
8218
+
8219
+ if (writeBundledContent) {
8156
8220
  await Bun.write(demoPath, demoSource);
8221
+ } else if (isConfiguredMainDemo && demoByteLength !== bundledByteLength) {
8222
+ console.error(
8223
+ `[mdcui] ${demoPath} differs from the bundled demo `
8224
+ + `(${demoByteLength} bytes vs ${bundledByteLength} bytes). `
8225
+ + "The edited demo may not work correctly if it imports companion modules "
8226
+ + "instead of keeping all application code in the Markdown file.",
8227
+ );
8157
8228
  }
8229
+
8158
8230
  rawFiles.splice(0, rawFiles.length, demoPath);
8159
- return true;
8231
+ return {
8232
+ ok: true,
8233
+ path: demoPath,
8234
+ useBundledModules: isConfiguredMainDemo
8235
+ && demoByteLength === bundledByteLength,
8236
+ };
8237
+ }
8238
+
8239
+ function stringDefineFromArgv(argv, name) {
8240
+ const prefix = `${name}=`;
8241
+ for (let i = 0; i < argv.length; i++) {
8242
+ let definition = "";
8243
+ if (argv[i] === "--define") definition = argv[i + 1] ?? "";
8244
+ else if (argv[i].startsWith("--define="))
8245
+ definition = argv[i].slice("--define=".length);
8246
+ if (!definition.startsWith(prefix)) continue;
8247
+ try {
8248
+ const value = JSON.parse(definition.slice(prefix.length));
8249
+ return typeof value === "string" ? value : "";
8250
+ } catch {
8251
+ return "";
8252
+ }
8253
+ }
8254
+ return "";
8160
8255
  }
8161
8256
 
8162
8257
  async function main() {
8163
8258
  addCheckpoint("Argument Parsing");
8164
-
8259
+
8260
+ stringifyNonPrimitiveDefineValues(process.argv, "global.MDCUI_MAIN");
8261
+ const argvMdcuiMain = stringDefineFromArgv(
8262
+ process.argv,
8263
+ "global.MDCUI_MAIN",
8264
+ );
8265
+ let sourceMdcuiMain = "";
8266
+ if (IS_COMPILED) {
8267
+ sourceMdcuiMain = "";
8268
+ } else {
8269
+ sourceMdcuiMain = String(argvMdcuiMain || global.MDCUI_MAIN || "");
8270
+ }
8271
+
8272
+ if (sourceMdcuiMain) {
8273
+ const mdcuiMain = resolve(sourceMdcuiMain);
8274
+ const mdcuiMainBase = basename(mdcuiMain);
8275
+ const mdcuiMainDemoName = demoNameFromMarkdownBasename(mdcuiMainBase);
8276
+ if (!mdcuiMainDemoName) {
8277
+ throw new Error(
8278
+ "MDCUI_MAIN basename must end with lowercase .md, start with a "
8279
+ + "Unicode letter or number, and contain no whitespace; only Unicode "
8280
+ + "letters, numbers, dots, underscores, and hyphens are allowed",
8281
+ );
8282
+ }
8283
+ const runmd = await import("../runmd.mjs");
8284
+ const mdcuiMainFile = Bun.file(mdcuiMain);
8285
+ if (!(await mdcuiMainFile.exists())) {
8286
+ throw new Error(`MDCUI_MAIN file not found: ${mdcuiMain}`);
8287
+ }
8288
+ let markdown = await mdcuiMainFile.text();
8289
+ const demoPath = join(REPO_ROOT, "demos", mdcuiMainBase);
8290
+ if (resolve(mdcuiMain) !== resolve(demoPath)) {
8291
+ await Bun.write(demoPath, markdown);
8292
+ }
8293
+ markdown = await runmd.extractJs(markdown, mdcuiMain, {
8294
+ bundling: true,
8295
+ });
8296
+ await runmd.createWui(markdown, mdcuiMain, {
8297
+ bundling: true,
8298
+ });
8299
+
8300
+ if (
8301
+ process.argv.includes("--build-for")
8302
+ || process.argv.includes("--build-exe")
8303
+ ) {
8304
+ if (argvMdcuiMain) {
8305
+ const normalizedDefine =
8306
+ `global.MDCUI_MAIN=${JSON.stringify(mdcuiMain)}`;
8307
+ for (let i = 0; i < process.argv.length; i++) {
8308
+ if (
8309
+ process.argv[i] === "--define"
8310
+ && process.argv[i + 1]?.startsWith("global.MDCUI_MAIN=")
8311
+ ) {
8312
+ process.argv[i + 1] = normalizedDefine;
8313
+ } else if (
8314
+ process.argv[i].startsWith("--define=global.MDCUI_MAIN=")
8315
+ ) {
8316
+ process.argv[i] = `--define=${normalizedDefine}`;
8317
+ }
8318
+ }
8319
+ } else {
8320
+ process.argv.push(
8321
+ "--define",
8322
+ `global.MDCUI_MAIN=${JSON.stringify(mdcuiMain)}`,
8323
+ );
8324
+ }
8325
+ process.argv.push(
8326
+ "--define",
8327
+ `global.MDCUI_MAIN_BASE=${JSON.stringify(mdcuiMainBase)}`,
8328
+ );
8329
+ }
8330
+ }
8331
+
8165
8332
  await buildEarlyExit(null,DEFAULT_BUILD_OUTFILE)
8166
8333
  defaultEdit = await Bun.file(join(REPO_ROOT, "src", "MDCUI_DEFAULT_EDIT")).exists();
8167
8334
 
8168
8335
  const runtimeArgs = process.argv.slice(2);
8169
- const noRuntimeArgs = runtimeArgs.length === 0;
8336
+ noRuntimeArgs = runtimeArgs.length === 0;
8170
8337
  if (MDCUI_DEFAULT_EDIT_ENABLED) runtimeArgs.unshift("--edit");
8171
8338
  if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_ENABLED) runtimeArgs.push("--demo");
8172
8339
  if (noRuntimeArgs && MDCUI_DEFAULT_DEMO_WUI_ENABLED) runtimeArgs.push("--wui");
8340
+ if (
8341
+ noRuntimeArgs
8342
+ && (global.MDCUI_MAIN || global.MDCUI_MAIN_BASE)
8343
+ ) {
8344
+ const mdcuiMainBase = String(
8345
+ global.MDCUI_MAIN_BASE || basename(String(global.MDCUI_MAIN)),
8346
+ );
8347
+ const demoName = demoNameFromMarkdownBasename(mdcuiMainBase);
8348
+ if (!demoName) {
8349
+ console.error(
8350
+ "Invalid MDCUI_MAIN_BASE: expected a lowercase .md basename with "
8351
+ + "Unicode letters or numbers and no whitespace",
8352
+ );
8353
+ process.exitCode = 2;
8354
+ return;
8355
+ }
8356
+ runtimeArgs.push(`--demo-${demoName}`);
8357
+ }
8173
8358
  if (MDCUI_OVERWRITE_DEMO_ENABLED) runtimeArgs.push("--overwrite-demo");
8174
8359
  const { flags, files: rawFiles } = parseArgs(runtimeArgs);
8360
+ let demoPreparation = null;
8175
8361
  kittyImageMode = flags.kittyMode;
8176
8362
  allowRemoteKittyImages = flags.allowUrl;
8177
8363
 
@@ -8195,6 +8381,14 @@ async function main() {
8195
8381
  console.log(" MDCUI_DEFAULT_DEMO:", MDCUI_DEFAULT_DEMO_ENABLED ? "enabled" : "disabled");
8196
8382
  console.log(" MDCUI_DEFAULT_DEMO_WUI:", MDCUI_DEFAULT_DEMO_WUI_ENABLED ? "enabled" : "disabled");
8197
8383
  console.log(" MDCUI_OVERWRITE_DEMO:", MDCUI_OVERWRITE_DEMO_ENABLED ? "enabled" : "disabled");
8384
+ console.log(
8385
+ " global.MDCUI_MAIN:",
8386
+ String(global.MDCUI_MAIN || "") || "not set",
8387
+ );
8388
+ console.log(
8389
+ " global.MDCUI_MAIN_BASE:",
8390
+ String(global.MDCUI_MAIN_BASE || "") || "not set",
8391
+ );
8198
8392
  const clipboard = new ClipboardManager();
8199
8393
  let osc52Available = false;
8200
8394
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -8210,12 +8404,14 @@ async function main() {
8210
8404
  return;
8211
8405
  }
8212
8406
  if (flags.wui) {
8213
- if (!(await prepareDemo(flags, rawFiles))) return;
8407
+ demoPreparation = await prepareDemo(flags, rawFiles);
8408
+ if (!demoPreparation.ok) return;
8214
8409
  const runmd = await import("../runmd.mjs");
8215
8410
  await runmd.main(30, {
8216
8411
  mdpath: rawFiles[0] ?? null,
8217
8412
  overwriteDemo: flags.overwriteDemo && rawFiles.length === 0,
8218
8413
  printUi: flags.printUi,
8414
+ useBundledMdcuiServer: demoPreparation.useBundledModules,
8219
8415
  });
8220
8416
  return;
8221
8417
  }
@@ -8263,7 +8459,8 @@ async function main() {
8263
8459
  return;
8264
8460
  }
8265
8461
  if (flags.demo) {
8266
- if (!(await prepareDemo(flags, rawFiles))) return;
8462
+ demoPreparation = await prepareDemo(flags, rawFiles);
8463
+ if (!demoPreparation.ok) return;
8267
8464
  }
8268
8465
  if (flags.options) {
8269
8466
  for (const [key, value] of Object.entries(defaultAllSettings()).sort(([a], [b]) => a.localeCompare(b))) {
@@ -8411,6 +8608,16 @@ async function main() {
8411
8608
  const buffers = await loadBuffers(files.map((file) =>
8412
8609
  isHttpUrl(file) ? file : resolve(file)
8413
8610
  ), command);
8611
+ if (demoPreparation?.useBundledModules) {
8612
+ const bundledDemoBuffer = buffers.find((buffer) =>
8613
+ buffer.path
8614
+ && !isHttpUrl(buffer.path)
8615
+ && resolve(buffer.path) === demoPreparation.path
8616
+ );
8617
+ if (bundledDemoBuffer) {
8618
+ bundledDemoBuffer._useBundledMdcuiModules = true;
8619
+ }
8620
+ }
8414
8621
 
8415
8622
  let historyPromise = Promise.resolve();
8416
8623
  if (config.getGlobalOption("savehistory") !== false) {
@@ -1622,6 +1622,11 @@ function _makeBufAPI(buffer) {
1622
1622
  get Type() { return buffer.Type; },
1623
1623
  get Settings() { return buffer.Settings; },
1624
1624
  get Modified() { return buffer.modified; },
1625
+ get MdcuiModuleSource() {
1626
+ return global.MDCUI_MAIN && buffer._useBundledMdcuiModules
1627
+ ? "embedded"
1628
+ : "external";
1629
+ },
1625
1630
 
1626
1631
  Line: (n) => buffer.Line(n),
1627
1632
  LinesNum: () => buffer.LinesNum(),
@@ -1,8 +1,75 @@
1
1
  import { expect, test } from "bun:test";
2
- import { isCompiledBinary as isSingleExeCompiled } from "../single-exe/compiled.js";
2
+ import {
3
+ isCompiledBinary as isSingleExeCompiled,
4
+ stringifyNonPrimitiveDefineValues,
5
+ } from "../single-exe/compiled.js";
3
6
 
4
7
  test("single-exe recognizes Bun compiled virtual paths", () => {
5
8
  expect(isSingleExeCompiled(["bun", "/$bunfs/root/app.js"])).toBe(true);
6
9
  expect(isSingleExeCompiled(["bun", "B:/~BUN/root/app.js"])).toBe(true);
7
10
  expect(isSingleExeCompiled(["bun", "/project/src/index.js"])).toBe(false);
8
11
  });
12
+
13
+ test("non-primitive MDCUI_MAIN define values become strings", () => {
14
+ const split = [
15
+ "bun",
16
+ "src/index.js",
17
+ "--build-exe",
18
+ "--define",
19
+ "global.MDCUI_MAIN=../中文=工具.md",
20
+ ];
21
+ expect(
22
+ stringifyNonPrimitiveDefineValues(split, "global.MDCUI_MAIN"),
23
+ ).toBe(split);
24
+ expect(split.at(-1)).toBe(
25
+ 'global.MDCUI_MAIN="../中文=工具.md"',
26
+ );
27
+
28
+ const inline = [
29
+ "--define=global.MDCUI_MAIN=../m.md",
30
+ "--define",
31
+ "OTHER=../other.md",
32
+ "--define",
33
+ "global.MDCUI_MAIN_BASE=m.md",
34
+ ];
35
+ stringifyNonPrimitiveDefineValues(inline, "global.MDCUI_MAIN");
36
+ expect(inline).toEqual([
37
+ '--define=global.MDCUI_MAIN="../m.md"',
38
+ "--define",
39
+ "OTHER=../other.md",
40
+ "--define",
41
+ "global.MDCUI_MAIN_BASE=m.md",
42
+ ]);
43
+
44
+ for (const source of ["", "[]", "{}", "makePath()"]) {
45
+ const argv = ["--define", `global.MDCUI_MAIN=${source}`];
46
+ stringifyNonPrimitiveDefineValues(argv, "global.MDCUI_MAIN");
47
+ expect(argv[1]).toBe(
48
+ `global.MDCUI_MAIN=${JSON.stringify(source)}`,
49
+ );
50
+ }
51
+ });
52
+
53
+ test("primitive MDCUI_MAIN define values keep their type", () => {
54
+ const cases = new Map([
55
+ ['"../m.md"', '"../m.md"'],
56
+ ["'../m.md'", '"../m.md"'],
57
+ ["true", "true"],
58
+ ["false", "false"],
59
+ ["null", "null"],
60
+ ["undefined", "undefined"],
61
+ ["0", "0"],
62
+ ["-1.5", "-1.5"],
63
+ ["NaN", "NaN"],
64
+ ["Infinity", "Infinity"],
65
+ ["0xff", "0xff"],
66
+ ["0b10", "0b10"],
67
+ ["1_000", "1_000"],
68
+ ]);
69
+
70
+ for (const [source, normalized] of cases) {
71
+ const argv = ["--define", `global.MDCUI_MAIN=${source}`];
72
+ stringifyNonPrimitiveDefineValues(argv, "global.MDCUI_MAIN");
73
+ expect(argv[1]).toBe(`global.MDCUI_MAIN=${normalized}`);
74
+ }
75
+ });
@@ -1,10 +1,12 @@
1
1
  import { expect, test } from "bun:test";
2
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { readMarkdownInput } from "../runmd.mjs";
6
6
 
7
7
  const tui = join(import.meta.dir, "..", "tui");
8
+ const indexEntry = join(import.meta.dir, "..", "src", "index.js");
9
+ const demosDirectory = join(import.meta.dir, "..", "demos");
8
10
  const bunBin = Bun.which("bun") || process.argv0;
9
11
 
10
12
  test("--help describes the non-overwriting demo behavior", () => {
@@ -40,6 +42,139 @@ test("--demo-list lists root and automatically discovered demos", () => {
40
42
  expect(output).toContain("--demo-imgtool --demo-image-processor");
41
43
  });
42
44
 
45
+ test("--version lists every MDCUI build define", () => {
46
+ const result = Bun.spawnSync([bunBin, tui, "--version"], {
47
+ stdout: "pipe",
48
+ stderr: "pipe",
49
+ });
50
+
51
+ expect(result.exitCode).toBe(0);
52
+ const output = result.stdout.toString();
53
+ expect(output).toContain("MDCUI_DEFAULT_EDIT:");
54
+ expect(output).toContain("MDCUI_DEFAULT_DEMO:");
55
+ expect(output).toContain("MDCUI_DEFAULT_DEMO_WUI:");
56
+ expect(output).toContain("MDCUI_OVERWRITE_DEMO:");
57
+ expect(output).toContain("global.MDCUI_MAIN:");
58
+ expect(output).toContain("global.MDCUI_MAIN_BASE:");
59
+
60
+ const presenceResult = Bun.spawnSync(
61
+ [
62
+ bunBin,
63
+ "--define",
64
+ "MDCUI_DEFAULT_DEMO_WUI=0",
65
+ indexEntry,
66
+ "--version",
67
+ ],
68
+ {
69
+ stdout: "pipe",
70
+ stderr: "pipe",
71
+ },
72
+ );
73
+ expect(presenceResult.exitCode).toBe(0);
74
+ expect(presenceResult.stdout.toString()).toContain(
75
+ "MDCUI_DEFAULT_DEMO_WUI: enabled",
76
+ );
77
+ });
78
+
79
+ test("--demo names allow Chinese but reject whitespace", () => {
80
+ const unicodeResult = Bun.spawnSync(
81
+ [bunBin, tui, "--demo-不存在的程式", "-cat", "-encoding", "utf8"],
82
+ {
83
+ stdout: "pipe",
84
+ stderr: "pipe",
85
+ },
86
+ );
87
+ expect(unicodeResult.exitCode).toBe(2);
88
+ expect(unicodeResult.stderr.toString()).toContain(
89
+ "Unknown demo --demo-不存在的程式",
90
+ );
91
+ expect(unicodeResult.stderr.toString()).not.toContain("Invalid demo option");
92
+
93
+ const whitespaceResult = Bun.spawnSync(
94
+ [bunBin, tui, "--demo-中文 程式", "-cat", "-encoding", "utf8"],
95
+ {
96
+ stdout: "pipe",
97
+ stderr: "pipe",
98
+ },
99
+ );
100
+ expect(whitespaceResult.exitCode).toBe(2);
101
+ expect(whitespaceResult.stderr.toString()).toContain("Invalid demo option");
102
+ expect(whitespaceResult.stderr.toString()).toContain(
103
+ "whitespace is not allowed",
104
+ );
105
+ });
106
+
107
+ test("Unicode MDCUI_MAIN overwrite starts the embedded WUI server", async () => {
108
+ const directory = await mkdtemp(join(tmpdir(), "jsmdcui-main-unicode-"));
109
+ const sourceDirectory = join(directory, "source");
110
+ const workDirectory = join(directory, "work");
111
+ const demoName = `中文工具-${crypto.randomUUID()}`;
112
+ const markdownName = `${demoName}.md`;
113
+ const sourcePath = join(sourceDirectory, markdownName);
114
+ const workPath = join(workDirectory, markdownName);
115
+ const bundledDemoPath = join(demosDirectory, markdownName);
116
+ const preloadPath = join(directory, "mock-serve.mjs");
117
+ const markdown = `# 中文工具
118
+
119
+ \`\`\`js front
120
+ export async function run() {
121
+ return await rpc.answer();
122
+ }
123
+ \`\`\`
124
+
125
+ \`\`\`js back
126
+ import { helper } from "./helper.js";
127
+ export function answer() {
128
+ return helper();
129
+ }
130
+ \`\`\`
131
+ `;
132
+
133
+ try {
134
+ await mkdir(sourceDirectory);
135
+ await mkdir(workDirectory);
136
+ await writeFile(sourcePath, markdown);
137
+ await writeFile(
138
+ join(sourceDirectory, "helper.js"),
139
+ 'export function helper() { return "ok"; }\n',
140
+ );
141
+ await writeFile(workPath, "# stale file with a different byte length\n");
142
+ await writeFile(
143
+ preloadPath,
144
+ `Bun.serve = () => ({ port: 34567, stop() {} });\n`,
145
+ );
146
+
147
+ const result = Bun.spawnSync(
148
+ [
149
+ bunBin,
150
+ "--preload",
151
+ preloadPath,
152
+ "--define",
153
+ `global.MDCUI_MAIN=${JSON.stringify(sourcePath)}`,
154
+ indexEntry,
155
+ "--wui",
156
+ `--demo-${demoName}`,
157
+ "--overwrite-demo",
158
+ ],
159
+ {
160
+ cwd: workDirectory,
161
+ stdout: "pipe",
162
+ stderr: "pipe",
163
+ },
164
+ );
165
+
166
+ expect(result.exitCode).toBe(0);
167
+ expect(await readFile(workPath, "utf8")).toBe(markdown);
168
+ const stderr = Bun.stripANSI(result.stderr.toString());
169
+ expect(stderr).toContain("[mdcui] Starting embedded WUI server:");
170
+ expect(stderr).toContain(sourcePath);
171
+ expect(stderr).not.toContain("Cannot find module");
172
+ } finally {
173
+ await rm(bundledDemoPath, { force: true });
174
+ await rm(directory, { recursive: true, force: true });
175
+ }
176
+ });
177
+
43
178
  test("--export-cdp-maze writes and overwrites the bundled solver", async () => {
44
179
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-export-cdp-maze-"));
45
180
  const outputPath = join(dir, "cdp-maze.js");
@@ -44,3 +44,33 @@ test("micro prompt APIs delegate and return synchronously", () => {
44
44
  else globalThis.$ = previousSelector;
45
45
  }
46
46
  });
47
+
48
+ test("current buffer exposes its actual MDCUI module source", () => {
49
+ const previousMicro = globalThis.micro;
50
+ const previousSelector = globalThis.$;
51
+ const previousMain = global.MDCUI_MAIN;
52
+ const buffer = {
53
+ path: "/tmp/app.md",
54
+ _useBundledMdcuiModules: true,
55
+ };
56
+ const app = { buffer };
57
+
58
+ try {
59
+ global.MDCUI_MAIN = "/build/app.md";
60
+ const micro = buildMicroGlobal({ _app: app, _ctx: null, on() {} });
61
+
62
+ expect(micro.CurPane().Buf.MdcuiModuleSource).toBe("embedded");
63
+ buffer._useBundledMdcuiModules = false;
64
+ expect(micro.CurPane().Buf.MdcuiModuleSource).toBe("external");
65
+ delete global.MDCUI_MAIN;
66
+ buffer._useBundledMdcuiModules = true;
67
+ expect(micro.CurPane().Buf.MdcuiModuleSource).toBe("external");
68
+ } finally {
69
+ if (previousMain === undefined) delete global.MDCUI_MAIN;
70
+ else global.MDCUI_MAIN = previousMain;
71
+ if (previousMicro === undefined) delete globalThis.micro;
72
+ else globalThis.micro = previousMicro;
73
+ if (previousSelector === undefined) delete globalThis.$;
74
+ else globalThis.$ = previousSelector;
75
+ }
76
+ });