DataTable replaces the 20-line Table scaffold entirely: columns, sorting, filtering, pagination, selection, bulk actions, comparison layout, sticky first column, custom HTML cells, and a remote source driven by a `request` output rather than a function prop (props travel as HTML attributes, so a function arrives as its own source text). Toaster replaces the hand-rolled status div: tone icons, actions, hover pause/resume and a progress bar. Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing ever moved focus into the panel, so the @keydown handler on their root never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap and a body scroll lock now live in the reactive runtime, shared by both. ContextMenu placed pointer menus by subtracting a guessed 340x420 from the viewport, which pushed every menu that was not that size away from the pointer; it now positions at the pointer and lets the anchored clamp pull it back once it can be measured. The reactive runtime size budget moves 150k -> 175k to cover anchored overlays, dialog behaviour, the toaster and the DataTable client half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { dirname, join, relative, resolve } from "node:path";
|
|
import { buildRouter, createRouteManifest, nameRoutes } from "@wrnexus/router";
|
|
import { createPluginRunner, discoverPlugins } from "@wrnexus/plugin";
|
|
import { loadAppConfig } from "@wrnexus/styles";
|
|
import { uiComponentsDir } from "@wrnexus/ui/registry";
|
|
import { parse } from "@wrnexus/syntax";
|
|
|
|
export type InspectTarget =
|
|
"packages" | "plugins" | "routes" | "assets" | "runtimes" | "styles" | "migrations" | "bundle";
|
|
|
|
export function inspectComponent(appRoot: string, requestedName: string): unknown {
|
|
const root = resolve(appRoot);
|
|
const router = buildRouter(join(root, "app"), { componentDirs: [uiComponentsDir()] });
|
|
const component = router.components.find(
|
|
(candidate) => candidate.name.toLowerCase() === requestedName.toLowerCase(),
|
|
);
|
|
if (!component) throw new Error(`Component not found: ${requestedName}`);
|
|
const ast = parse(readFileSync(component.file, "utf8"));
|
|
return {
|
|
name: ast.name,
|
|
file: relative(root, component.file).replace(/\\/g, "/"),
|
|
props: ast.props.map(({ name, valueType, required, default: defaultValue }) => ({
|
|
name,
|
|
type: valueType ?? "unknown",
|
|
required,
|
|
...(defaultValue === undefined || defaultValue === "undefined"
|
|
? {}
|
|
: { default: defaultValue }),
|
|
})),
|
|
outputs: ast.outputs.map(({ name, payload }) => ({ name, payload: payload ?? null })),
|
|
functions: ast.runtimeFunctions.map(({ name, runtime, async, parameters, returnType }) => ({
|
|
name,
|
|
runtime,
|
|
async,
|
|
parameters,
|
|
returnType: returnType ?? "unknown",
|
|
})),
|
|
};
|
|
}
|
|
|
|
function json(path: string): Record<string, any> | null {
|
|
try {
|
|
return JSON.parse(readFileSync(path, "utf8")) as Record<string, any>;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
function workspaceRoot(start: string): string {
|
|
let current = resolve(start);
|
|
while (true) {
|
|
if (json(join(current, "package.json"))?.workspaces) return current;
|
|
const parent = dirname(current);
|
|
if (parent === current) return resolve(start);
|
|
current = parent;
|
|
}
|
|
}
|
|
function packageRows(root: string) {
|
|
const dirs = [join(root, "packages"), join(root, "services")];
|
|
const rows: Array<Record<string, unknown>> = [];
|
|
for (const dir of dirs) {
|
|
if (!existsSync(dir)) continue;
|
|
for (const name of readdirSync(dir)) {
|
|
const path = join(dir, name);
|
|
if (!statSync(path).isDirectory()) continue;
|
|
const pkg = json(join(path, "package.json"));
|
|
if (!pkg?.name) continue;
|
|
rows.push({
|
|
name: pkg.name,
|
|
version: pkg.version,
|
|
private: pkg.private === true,
|
|
path: relative(root, path).replace(/\\/g, "/"),
|
|
plugin: !!pkg.wrnexus?.plugin,
|
|
});
|
|
}
|
|
}
|
|
return rows.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
}
|
|
export async function inspectProject(appRoot: string, target: InspectTarget): Promise<unknown> {
|
|
const root = resolve(appRoot);
|
|
const workspace = workspaceRoot(root);
|
|
if (target === "packages") return packageRows(workspace);
|
|
if (target === "bundle") {
|
|
const report = join(root, "dist", "build-report.json");
|
|
if (!existsSync(report)) throw new Error("Run `wrnexus build` before inspecting the bundle.");
|
|
return json(report);
|
|
}
|
|
const config = await loadAppConfig(root);
|
|
const input = await discoverPlugins(root, config.plugins, {
|
|
includeDevDependencies: true,
|
|
strict: true,
|
|
});
|
|
const runner = createPluginRunner(input, {
|
|
root,
|
|
mode: "development",
|
|
command: "dev",
|
|
metadata: new Map(),
|
|
warn: () => {},
|
|
});
|
|
await runner.configure(config as Record<string, unknown>);
|
|
await runner.configResolved(config as Readonly<Record<string, unknown>>);
|
|
const contributions = await runner.contributions();
|
|
if (target === "plugins")
|
|
return runner.plugins.map((plugin) => ({
|
|
name: plugin.name,
|
|
version: plugin.version,
|
|
enforce: plugin.enforce ?? "normal",
|
|
}));
|
|
if (target === "assets")
|
|
return contributions.assets.map(({ id, publicPath, contentType, immutable }) => ({
|
|
id,
|
|
publicPath,
|
|
contentType,
|
|
immutable: immutable ?? false,
|
|
}));
|
|
if (target === "runtimes")
|
|
return contributions.clientRuntimes.map(
|
|
({ id, publicPath, type, load, singleton, bundle }) => ({
|
|
id,
|
|
publicPath,
|
|
type,
|
|
load,
|
|
singleton,
|
|
bundle,
|
|
}),
|
|
);
|
|
if (target === "styles")
|
|
return contributions.styles.map(({ id, entry, source, order }) => ({
|
|
id,
|
|
entry,
|
|
source,
|
|
order: order ?? "normal",
|
|
}));
|
|
if (target === "migrations")
|
|
return contributions.migrations.map(({ id, entry, source, database }) => ({
|
|
id,
|
|
entry,
|
|
inline: source !== undefined,
|
|
database: database ?? "default",
|
|
}));
|
|
const router = buildRouter(join(root, "app"), {
|
|
componentDirs: [uiComponentsDir(), ...contributions.componentDirs],
|
|
externalRoutes: contributions.routes,
|
|
middlewareFiles: contributions.middleware,
|
|
});
|
|
return {
|
|
pages: createRouteManifest(nameRoutes(router.pages)),
|
|
api: createRouteManifest(nameRoutes(router.api)),
|
|
realtime: createRouteManifest(nameRoutes(router.realtime)),
|
|
middleware: router.middlewareFiles.map((file) => relative(root, file).replace(/\\/g, "/")),
|
|
components: router.components.map((component) => ({
|
|
...component,
|
|
file: relative(root, component.file).replace(/\\/g, "/"),
|
|
})),
|
|
};
|
|
}
|
|
|
|
export async function runInspect(
|
|
appRoot: string,
|
|
targetArg?: string,
|
|
args: string[] = [],
|
|
): Promise<void> {
|
|
const target = (targetArg ?? "plugins") as InspectTarget;
|
|
if (
|
|
![
|
|
"packages",
|
|
"plugins",
|
|
"routes",
|
|
"assets",
|
|
"runtimes",
|
|
"styles",
|
|
"migrations",
|
|
"bundle",
|
|
].includes(target)
|
|
)
|
|
throw new Error(`Unknown inspect target: ${target}`);
|
|
const value = await inspectProject(appRoot, target);
|
|
if (args.includes("--json")) {
|
|
console.log(JSON.stringify(value, null, 2));
|
|
return;
|
|
}
|
|
console.log(`WRNexus ${target}\n`);
|
|
if (Array.isArray(value))
|
|
for (const row of value)
|
|
console.log(
|
|
` ${Object.entries(row as Record<string, unknown>)
|
|
.map(([key, item]) => `${key}=${String(item)}`)
|
|
.join(" ")}`,
|
|
);
|
|
else console.log(JSON.stringify(value, null, 2));
|
|
}
|
|
|
|
export function runInspectComponent(appRoot: string, name: string, args: string[] = []): void {
|
|
const value = inspectComponent(appRoot, name);
|
|
if (args.includes("--json")) console.log(JSON.stringify(value, null, 2));
|
|
else {
|
|
console.log(`WRNexus component ${name}\n`);
|
|
console.log(JSON.stringify(value, null, 2));
|
|
}
|
|
}
|