jsmdcui 0.9.0 → 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.
@@ -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(),
@@ -78,3 +78,62 @@ test("--edit overrides the .md mdcui default with utf8", async () => {
78
78
  await rm(dir, { recursive: true, force: true });
79
79
  }
80
80
  });
81
+
82
+ test("--mdcui and --tui are equivalent to -encoding mdcui", async () => {
83
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-mdcui-"));
84
+ const markdownPath = join(dir, "sample.md");
85
+ await writeFile(markdownPath, "# Heading\n\n- one\n- two\n");
86
+
87
+ try {
88
+ const flag = Bun.spawnSync([bunBin, tui, "-cat", "--mdcui", markdownPath], {
89
+ cwd: dir,
90
+ stdout: "pipe",
91
+ stderr: "pipe",
92
+ });
93
+ const encoding = Bun.spawnSync([bunBin, tui, "-cat", "-encoding", "mdcui", markdownPath], {
94
+ cwd: dir,
95
+ stdout: "pipe",
96
+ stderr: "pipe",
97
+ });
98
+ const tuiFlag = Bun.spawnSync([bunBin, tui, "-cat", "--tui", markdownPath], {
99
+ cwd: dir,
100
+ stdout: "pipe",
101
+ stderr: "pipe",
102
+ });
103
+
104
+ expect(flag.exitCode).toBe(0);
105
+ expect(encoding.exitCode).toBe(0);
106
+ expect(tuiFlag.exitCode).toBe(0);
107
+ expect(flag.stdout.toString()).toBe(encoding.stdout.toString());
108
+ expect(tuiFlag.stdout.toString()).toBe(encoding.stdout.toString());
109
+ } finally {
110
+ await rm(dir, { recursive: true, force: true });
111
+ }
112
+ });
113
+
114
+ test("-filetype forces syntax highlighting independently of the filename", async () => {
115
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-cat-filetype-"));
116
+ const textPath = join(dir, "sample.txt");
117
+ await writeFile(textPath, "const answer = 42;\n");
118
+
119
+ try {
120
+ const automatic = Bun.spawnSync([bunBin, tui, "-cat", textPath], {
121
+ cwd: dir,
122
+ stdout: "pipe",
123
+ stderr: "pipe",
124
+ });
125
+ const javascript = Bun.spawnSync([bunBin, tui, "-cat", "-filetype", "javascript", textPath], {
126
+ cwd: dir,
127
+ stdout: "pipe",
128
+ stderr: "pipe",
129
+ });
130
+
131
+ expect(automatic.exitCode).toBe(0);
132
+ expect(javascript.exitCode).toBe(0);
133
+ expect(automatic.stdout.toString()).not.toContain("\x1b[");
134
+ expect(javascript.stdout.toString()).toContain("\x1b[");
135
+ expect(Bun.stripANSI(javascript.stdout.toString()).trimEnd()).toBe("const answer = 42;");
136
+ } finally {
137
+ await rm(dir, { recursive: true, force: true });
138
+ }
139
+ });
@@ -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,9 +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
+ import { readMarkdownInput } from "../runmd.mjs";
5
6
 
6
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");
7
10
  const bunBin = Bun.which("bun") || process.argv0;
8
11
 
9
12
  test("--help describes the non-overwriting demo behavior", () => {
@@ -14,8 +17,8 @@ test("--help describes the non-overwriting demo behavior", () => {
14
17
 
15
18
  expect(result.exitCode).toBe(0);
16
19
  const output = result.stdout.toString();
17
- expect(output).toContain("use the existing ./testapp.md without overwriting it");
18
- expect(output).toContain("If ./testapp.md is missing, write the bundled demo there first");
20
+ expect(output).toContain("Execute an existing ./testapp.md");
21
+ expect(output).toContain("Or write the bundled demo if missing");
19
22
  expect(output).toContain("--demo-<filename>");
20
23
  expect(output).toContain("demos/<filename>.md");
21
24
  expect(output.match(/Open it in the TUI and write 5 generated files beside it/g)?.length).toBe(2);
@@ -39,6 +42,139 @@ test("--demo-list lists root and automatically discovered demos", () => {
39
42
  expect(output).toContain("--demo-imgtool --demo-image-processor");
40
43
  });
41
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
+
42
178
  test("--export-cdp-maze writes and overwrites the bundled solver", async () => {
43
179
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-export-cdp-maze-"));
44
180
  const outputPath = join(dir, "cdp-maze.js");
@@ -97,6 +233,44 @@ test("--demo preserves an existing testapp.md", async () => {
97
233
  }
98
234
  });
99
235
 
236
+ test("--overwrite-demo replaces an existing demo with the bundled copy", async () => {
237
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-overwrite-"));
238
+ const existing = "# Replace my demo\n";
239
+ try {
240
+ await writeFile(join(dir, "testapp.md"), existing);
241
+ const result = Bun.spawnSync(
242
+ [bunBin, tui, "--overwrite-demo", "--demo", "-cat", "-encoding", "utf8"],
243
+ {
244
+ cwd: dir,
245
+ stdout: "pipe",
246
+ stderr: "pipe",
247
+ },
248
+ );
249
+
250
+ expect(result.exitCode).toBe(0);
251
+ const written = await readFile(join(dir, "testapp.md"), "utf8");
252
+ expect(written).toContain("計算機");
253
+ expect(written).not.toBe(existing);
254
+ expect(Bun.stripANSI(result.stdout.toString())).toContain("計算機");
255
+ } finally {
256
+ await rm(dir, { recursive: true, force: true });
257
+ }
258
+ });
259
+
260
+ test("WUI demo loading can overwrite an existing testapp.md", async () => {
261
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-wui-demo-overwrite-"));
262
+ const demoPath = join(dir, "testapp.md");
263
+ try {
264
+ await writeFile(demoPath, "# Replace my WUI demo\n");
265
+ const source = await readMarkdownInput(demoPath, true);
266
+
267
+ expect(source).toContain("計算機");
268
+ expect(await readFile(demoPath, "utf8")).toBe(source);
269
+ } finally {
270
+ await rm(dir, { recursive: true, force: true });
271
+ }
272
+ });
273
+
100
274
  test("--demo-<filename> automatically loads a matching demos file", async () => {
101
275
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-demo-generic-"));
102
276
  try {
@@ -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
+ });
@@ -1,8 +1,8 @@
1
1
  import { afterEach, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { createWui } from "../runmd.mjs";
5
+ import { createWui, extractJs } from "../runmd.mjs";
6
6
 
7
7
  const temporaryDirectories = [];
8
8
 
@@ -33,3 +33,22 @@ test("createWui injects the image rule into an existing document head", async ()
33
33
  expect(html.indexOf("max-width: 100%")).toBeLessThan(html.indexOf("</head>"));
34
34
  expect(html.match(/document\.md\.front\.js/g)).toHaveLength(1);
35
35
  });
36
+
37
+ test("bundling mode creates a browser-only front entry", async () => {
38
+ const directory = mkdtempSync(join(tmpdir(), "jsmdcui-wui-bundling-"));
39
+ temporaryDirectories.push(directory);
40
+ const markdownPath = join(directory, "bundle.md");
41
+ const source = "```js front\nexport function pav() { return rpc }\n```";
42
+ const markdown = await extractJs(source, markdownPath, { bundling: true });
43
+ const html = await createWui(markdown, markdownPath, { bundling: true });
44
+ const frontSource = readFileSync(`${markdownPath}.tmpfs.js`, "utf8");
45
+ const installerSource = readFileSync(`${markdownPath}.tmpfi.js`, "utf8");
46
+
47
+ expect(frontSource).toContain("const rpc = wuiRpcClient");
48
+ expect(frontSource).not.toContain("globalThis.process");
49
+ expect(frontSource).not.toContain('import("./bundle.md.front.js")');
50
+ expect(installerSource).toContain('import * as frontMod from "./bundle.md.tmpfs.js"');
51
+ expect(installerSource).toContain("Object.assign(window, frontMod)");
52
+ expect(html).toContain('src="./bundle.md.tmpfi.js"');
53
+ expect(html).not.toContain("bundle.md.front.js");
54
+ });
package/tests/wui.test.js CHANGED
@@ -4,6 +4,9 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { readMarkdownInput, writeRuntimeFiles } from "../runmd.mjs";
6
6
 
7
+ const indexEntry = join(import.meta.dir, "..", "src", "index.js");
8
+ const bunBin = Bun.which("bun") || process.argv0;
9
+
7
10
  test("WUI writes the bundled testapp.md when it is missing", async () => {
8
11
  const dir = await mkdtemp(join(tmpdir(), "jsmdcui-wui-"));
9
12
  const mdpath = join(dir, "testapp.md");
@@ -43,3 +46,31 @@ test("generated WUI server falls back to a system port when 3000 is occupied", a
43
46
  await rm(dir, { recursive: true, force: true });
44
47
  }
45
48
  });
49
+
50
+ test("WUI reports an external server before starting it", async () => {
51
+ const dir = await mkdtemp(join(tmpdir(), "jsmdcui-wui-source-report-"));
52
+ const mdpath = join(dir, "external.md");
53
+ const preloadPath = join(dir, "mock-serve.mjs");
54
+ try {
55
+ await writeFile(mdpath, "# External WUI\n");
56
+ await writeFile(
57
+ preloadPath,
58
+ `Bun.serve = () => ({ port: 34567, stop() {} });\n`,
59
+ );
60
+ const result = Bun.spawnSync(
61
+ [bunBin, "--preload", preloadPath, indexEntry, "--wui", mdpath],
62
+ {
63
+ cwd: dir,
64
+ stdout: "pipe",
65
+ stderr: "pipe",
66
+ },
67
+ );
68
+
69
+ expect(result.exitCode).toBe(0);
70
+ const stderr = Bun.stripANSI(result.stderr.toString());
71
+ expect(stderr).toContain("[mdcui] Starting external WUI server:");
72
+ expect(stderr).toContain(`${mdpath}-server.js`);
73
+ } finally {
74
+ await rm(dir, { recursive: true, force: true });
75
+ }
76
+ });