Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion package/ego-browser/src/help-runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { help, formatHelp } from "../dist/src/help-runtime.js";
import { help, formatHelp, __setDocsForTests } from "../dist/src/help-runtime.js";

// Regression test for GitHub issue #84: the runtime used to build its docs map
// by reading its own source, which produced an empty map whenever the SDK was
Expand Down Expand Up @@ -42,6 +42,41 @@ test("formatHelp renders the signature for an embedded doc", () => {
assert.ok(text.includes("click("), `expected signature in:\n${text}`);
});

test("help(name) falls back to the live function when embedded docs miss it", () => {
const live = async (selector, key = "Enter") => ({ selector, key });
const doc = help({ live }, "live");
assert.equal(typeof doc, "object");
assert.equal(doc.name, "live");
assert.ok(doc.signature.includes("live("));
assert.ok(doc.async);
assert.notEqual(doc, "Unknown helper: live");
});

test("help() lists live helpers when the embedded catalog is empty", () => {
__setDocsForTests("[]");
try {
const list = help({ click: () => {}, waitFor: async () => {} });
assert.ok(Array.isArray(list));
assert.equal(list.length, 2);
assert.ok(list.some((d) => d.name === "click"));
assert.ok(list.some((d) => d.name === "waitFor"));
} finally {
__setDocsForTests(null);
}
});

test("help(name) still reports unknown when the helper is not live", () => {
__setDocsForTests("[]");
try {
assert.equal(
help({}, "definitelyNotAHelper"),
"Unknown helper: definitelyNotAHelper",
);
} finally {
__setDocsForTests(null);
}
});

test("help works when the shipped bundle runs as an eval module", () => {
// The app executes the SDK from an in-memory string, so its import.meta.url
// ("file:///...[eval1]") is not a readable file — the exact condition that
Expand Down
87 changes: 72 additions & 15 deletions package/ego-browser/src/help-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,17 @@ export function help(
): HelperDoc | HelperDoc[] | string {
const docs = getDocsMap();
if (names.length === 0) {
const all = [...docs.values()].filter((d) => d.name in helpers);
return all;
const fromDocs = [...docs.values()].filter((d) => d.name in helpers);
if (fromDocs.length > 0) return fromDocs;
return listFallbackDocs(helpers);
}
if (names.length === 1) {
const doc = docs.get(names[0]);
if (!doc) return `Unknown helper: ${names[0]}`;
const name = names[0];
const doc = docs.get(name) || fallbackDoc(name, helpers[name]);
if (!doc) return `Unknown helper: ${name}`;
return doc;
}
return names.map(
(n) =>
docs.get(n) || {
name: n,
signature: n,
description: null,
params: [],
returns: null,
async: false,
},
);
return names.map((n) => docs.get(n) || fallbackDoc(n, helpers[n]) || emptyDoc(n));
}

export function formatHelp(doc: HelperDoc): string {
Expand Down Expand Up @@ -95,3 +87,68 @@ function parseEmbeddedDocs(raw: string): HelperDoc[] {
return [];
}
}

function listFallbackDocs(helpers: Record<string, unknown>): HelperDoc[] {
return Object.keys(helpers)
.filter((name) => name !== "help" && typeof helpers[name] === "function")
.sort()
.map((name) => fallbackDoc(name, helpers[name]))
.filter((doc): doc is HelperDoc => Boolean(doc));
}

function fallbackDoc(name: string, value: unknown): HelperDoc | null {
if (typeof value !== "function") return null;
const src = Function.prototype.toString.call(value);
const isAsync = /^\s*async\b/.test(src);
const paramMatch =
src.match(/^(?:async\s+)?(?:function[\s\w$]*)?\s*\(([^)]*)\)/) ||
src.match(/^(?:async\s*)?\(([^)]*)\)\s*=>/);
const rawParams = paramMatch?.[1]?.trim() ?? "";
const paramNames = rawParams
? rawParams
.split(",")
.map((part) => part.trim())
.filter(Boolean)
: [];
const params: ParamInfo[] = paramNames.map((part) => ({
name: part.replace(/^\.\.\./, "").replace(/\s*=[\s\S]*$/, "") || part,
type: null,
description: null,
optional: part.includes("=") || part.startsWith("..."),
rest: part.startsWith("..."),
default: null,
}));
const paramSig = paramNames.join(", ");
const returns = isAsync ? "Promise<...>" : null;
return {
name,
signature: `${name}(${paramSig})${returns ? ` → ${returns}` : ""}`,
description:
"Available helper. Embedded help docs were empty, so this signature was recovered from the live function.",
params,
returns,
async: isAsync,
};
}

function emptyDoc(name: string): HelperDoc {
return {
name,
signature: name,
description: null,
params: [],
returns: null,
async: false,
};
}

export function __setDocsForTests(raw: string | null): void {
if (raw === null) {
cache = null;
return;
}
cache = new Map();
for (const doc of parseEmbeddedDocs(raw)) {
cache.set(doc.name, doc);
}
}