release: WRNexusJS 0.3.0

This commit is contained in:
2026-07-22 17:29:08 +05:30
parent 13dfa31d19
commit 07d8fb59d6
145 changed files with 9664 additions and 3881 deletions
+206
View File
@@ -0,0 +1,206 @@
import { parse, ParseError, type PageAst, type ViewNode } from "./parser.ts";
import {
WRN_DIAGNOSTIC_CODES,
WRN_HYDRATION_STRATEGIES,
WRN_RUNTIME_TARGETS,
type WrnHydrationStrategy,
type WrnRuntimeTarget,
} from "./spec.ts";
export type WrnDiagnosticSeverity = "error" | "warning" | "info";
export interface WrnSourcePosition {
offset: number;
line: number;
column: number;
}
export interface WrnDiagnostic {
code: string;
severity: WrnDiagnosticSeverity;
message: string;
hint?: string;
file?: string;
position?: WrnSourcePosition;
}
export interface DiagnoseOptions {
file?: string;
accessibility?: boolean;
}
export function positionAt(source: string, offset: number): WrnSourcePosition {
const safe = Math.max(0, Math.min(offset, source.length));
const before = source.slice(0, safe);
const lines = before.split(/\r?\n/);
return { offset: safe, line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
}
function offsetFromMessage(message: string): number | undefined {
const match = /offset\s+(\d+)/i.exec(message);
return match ? Number(match[1]) : undefined;
}
export function classifyParseError(message: string): string {
if (/Expected 'page', 'component', or 'layout'/.test(message)) return WRN_DIAGNOSTIC_CODES.root;
if (/Unknown (?:page|component|layout|ssr|client) member/.test(message)) {
return WRN_DIAGNOSTIC_CODES.member;
}
if (/prop initializer|Expected eq/.test(message)) return WRN_DIAGNOSTIC_CODES.propInitializer;
if (/State '.+' requires an initializer/.test(message)) {
return WRN_DIAGNOSTIC_CODES.stateInitializer;
}
if (/Cannot watch undeclared state/.test(message)) return WRN_DIAGNOSTIC_CODES.watchUndeclared;
return WRN_DIAGNOSTIC_CODES.parse;
}
export function diagnosticFromError(
source: string,
error: unknown,
options: DiagnoseOptions = {},
): WrnDiagnostic {
const message = error instanceof Error ? error.message : String(error);
const offset =
error instanceof ParseError && error.offset !== undefined
? error.offset
: offsetFromMessage(message);
return {
code: error instanceof ParseError ? error.code : classifyParseError(message),
severity: "error",
message,
file: options.file,
...(offset === undefined ? {} : { position: positionAt(source, offset) }),
};
}
function walk(nodes: ViewNode[], visit: (node: ViewNode) => void): void {
for (const node of nodes) {
visit(node);
if (node.type === "element") walk(node.children, visit);
else if (node.type === "each") {
walk(node.body, visit);
walk(node.empty, visit);
} else if (node.type === "if") {
for (const branch of node.branches) walk(branch.body, visit);
}
}
}
function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[] {
const diagnostics: WrnDiagnostic[] = [];
const seen = new Map<string, string>();
for (const [kind, declarations] of [
["prop", ast.props],
["state", ast.states],
["computed", ast.computed],
] as const) {
for (const declaration of declarations) {
const previous = seen.get(declaration.name);
if (previous) {
diagnostics.push({
code: WRN_DIAGNOSTIC_CODES.duplicateSymbol,
severity: "error",
message: `Duplicate symbol '${declaration.name}' (${previous} and ${kind}).`,
hint: "Rename one declaration so every prop, state, and computed value is unique.",
file: options.file,
});
} else {
seen.set(declaration.name, kind);
}
}
}
if (ast.hydrate && !isHydrationStrategy(ast.hydrate)) {
diagnostics.push({
code: WRN_DIAGNOSTIC_CODES.invalidHydration,
severity: "error",
message: `Unknown hydration strategy '${ast.hydrate}'.`,
hint: "Use load, idle, visible, interaction, none, or media:<query>.",
file: options.file,
});
}
if (ast.runtime && !isRuntimeTarget(ast.runtime)) {
diagnostics.push({
code: WRN_DIAGNOSTIC_CODES.invalidRuntime,
severity: "error",
message: `Unknown runtime target '${ast.runtime}'.`,
hint: "Use server, client, or universal.",
file: options.file,
});
}
let interactive = ast.states.length > 0 || ast.effects.length > 0 || ast.watches.length > 0;
walk(ast.view, (node) => {
if (node.type === "element" && node.attrs.some((attribute) => attribute.event))
interactive = true;
if (!options.accessibility || node.type !== "element") return;
const tag = node.tag.toLowerCase();
if (tag === "img" && !node.attrs.some((attribute) => attribute.name === "alt")) {
diagnostics.push({
code: WRN_DIAGNOSTIC_CODES.accessibility,
severity: "warning",
message: "Image is missing an alt attribute.",
hint: 'Add alt text, or alt="" for a decorative image.',
file: options.file,
});
}
});
if (ast.runtime === "server" && interactive) {
diagnostics.push({
code: WRN_DIAGNOSTIC_CODES.serverInteractive,
severity: "error",
message:
"A server-only WRN root cannot contain client state, effects, watches, or event handlers.",
hint: 'Use runtime = "universal" or remove interactive behavior.',
file: options.file,
});
}
return diagnostics;
}
export function diagnose(source: string, options: DiagnoseOptions = {}): WrnDiagnostic[] {
try {
return astDiagnostics(parse(source), options);
} catch (error) {
return [diagnosticFromError(source, error, options)];
}
}
export function assertValidAst(ast: PageAst, options: DiagnoseOptions = {}): void {
const errors = astDiagnostics(ast, options).filter(
(diagnostic) => diagnostic.severity === "error",
);
if (!errors.length) return;
const first = errors[0]!;
throw new ParseError(first.message, first.code);
}
export function isHydrationStrategy(value: string): value is WrnHydrationStrategy {
return (
(WRN_HYDRATION_STRATEGIES as readonly string[]).includes(value) ||
(value.startsWith("media:") && value.length > "media:".length)
);
}
export function isRuntimeTarget(value: string): value is WrnRuntimeTarget {
return (WRN_RUNTIME_TARGETS as readonly string[]).includes(value);
}
export function formatDiagnostic(source: string, diagnostic: WrnDiagnostic): string {
const location = diagnostic.position
? `${diagnostic.file ?? "<inline .wrn>"}:${diagnostic.position.line}:${diagnostic.position.column}`
: (diagnostic.file ?? "<inline .wrn>");
const lines = [
`${diagnostic.code} ${diagnostic.severity.toUpperCase()}`,
"",
diagnostic.message,
"",
location,
];
if (diagnostic.position) {
const sourceLine = source.split(/\r?\n/)[diagnostic.position.line - 1] ?? "";
lines.push("", sourceLine, `${" ".repeat(Math.max(0, diagnostic.position.column - 1))}^`);
}
if (diagnostic.hint) lines.push("", `Hint: ${diagnostic.hint}`);
return lines.join("\n");
}