release: WRNexusJS 0.6.0

This commit is contained in:
2026-08-01 01:09:58 +05:30
parent 3e565e8d03
commit 687d345882
502 changed files with 33038 additions and 11358 deletions
+186
View File
@@ -22,6 +22,9 @@ export interface WrnDiagnostic {
hint?: string;
file?: string;
position?: WrnSourcePosition;
expected?: string;
received?: string;
related?: Array<{ file?: string; message: string; position?: WrnSourcePosition }>;
}
export interface DiagnoseOptions {
@@ -29,6 +32,95 @@ export interface DiagnoseOptions {
accessibility?: boolean;
}
function maskJavaScriptTrivia(source: string): string {
let result = "";
let index = 0;
let quote: "'" | '"' | "`" | null = null;
let lineComment = false;
let blockComment = false;
while (index < source.length) {
const char = source[index]!;
const next = source[index + 1];
if (lineComment) {
if (char === "\n") {
lineComment = false;
result += "\n";
} else result += " ";
index++;
continue;
}
if (blockComment) {
if (char === "*" && next === "/") {
result += " ";
index += 2;
blockComment = false;
} else {
result += char === "\n" ? "\n" : " ";
index++;
}
continue;
}
if (quote) {
if (char === "\\") {
result += " ";
index += Math.min(2, source.length - index);
} else if (char === quote) {
result += " ";
index++;
quote = null;
} else {
result += char === "\n" ? "\n" : " ";
index++;
}
continue;
}
if (char === "/" && next === "/") {
result += " ";
index += 2;
lineComment = true;
continue;
}
if (char === "/" && next === "*") {
result += " ";
index += 2;
blockComment = true;
continue;
}
if (char === "'" || char === '"' || char === "`") {
quote = char;
result += " ";
index++;
continue;
}
result += char;
index++;
}
return result;
}
export function containsReadonlyPropMutation(
body: string,
propName: string,
parameterNames: Set<string>,
): boolean {
const code = maskJavaScriptTrivia(body);
const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const operator = String.raw`(?:\+\+|--|(?:\*\*|&&|\|\||\?\?|[+\-*/%&|^])?=(?!=|>))`;
if (new RegExp(String.raw`\bprops\.${escaped}\s*${operator}`).test(code)) return true;
if (parameterNames.has(propName)) return false;
if (new RegExp(String.raw`\b(?:const|let|var)\s+${escaped}\b`).test(code)) return false;
return new RegExp(String.raw`(?:^|[^\w$.])${escaped}\s*${operator}`, "m").test(code);
}
export function positionAt(source: string, offset: number): WrnSourcePosition {
const safe = Math.max(0, Math.min(offset, source.length));
const before = source.slice(0, safe);
@@ -155,6 +247,100 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[]
file: options.file,
});
}
const outputs = new Set(ast.outputs.map((output) => output.name));
for (const fn of ast.runtimeFunctions) {
for (const call of fn.body.matchAll(/\boutput\.([A-Za-z_$][\w$]*)\s*\(/g)) {
const outputName = call[1]!;
if (fn.runtime === "server") {
diagnostics.push({
code: "WRN-OUTPUT-SERVER-CALL",
severity: "error",
message: `Server function '${fn.name}' cannot call output.${outputName}().`,
hint: "Return a typed value to the browser and call the output from a client function.",
file: options.file,
});
} else if (!outputs.has(outputName)) {
diagnostics.push({
code: "WRN-OUTPUT-UNKNOWN",
severity: "error",
message: `Unknown output '${outputName}' called from '${fn.name}'.`,
hint: `Declare ${outputName}(payload) inside outputs { ... }.`,
file: options.file,
});
}
}
if (fn.runtime === "client" && /\b(?:process|Bun|Deno|__dirname|require)\b/.test(fn.body)) {
diagnostics.push({
code: "WRN-CLIENT-SERVER-API",
severity: "error",
message: `Client function '${fn.name}' references a server-only API.`,
hint: "Move that operation into a server function and call it through server.name(...).",
file: options.file,
});
}
if (
fn.runtime === "server" &&
/\b(?:window|document|localStorage|sessionStorage|navigator)\b/.test(fn.body)
) {
diagnostics.push({
code: "WRN-SERVER-BROWSER-API",
severity: "error",
message: `Server function '${fn.name}' references a browser-only API.`,
hint: "Move that code into a client function.",
file: options.file,
});
}
const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
for (const prop of ast.props) {
if (containsReadonlyPropMutation(fn.body, prop.name, parameterNames)) {
diagnostics.push({
code: "WRN-PROP-READONLY",
severity: "error",
message: `Function '${fn.name}' attempts to mutate readonly prop '${prop.name}'.`,
hint: "Copy the prop into state before mutating it.",
file: options.file,
});
}
}
}
for (const state of ast.states) {
if (
state.runtime === "shared" &&
/^(?:new\s+(?:Map|Set|WeakMap|WeakSet)|(?:async\s+)?function\b|.*=>)/.test(state.expr.trim())
) {
diagnostics.push({
code: "WRN-STATE-NON-SERIALIZABLE",
severity: "error",
message: `Shared state '${state.name}' is not safely serializable.`,
hint: "Use JSON-compatible data or move the value into client/server state.",
file: options.file,
});
}
}
if (ast.persist) {
const stateNames = new Set(
ast.states.filter((state) => state.runtime !== "server").map((state) => state.name),
);
for (const name of ast.persist.include)
if (!stateNames.has(name))
diagnostics.push({
code: "WRN-PERSIST-UNKNOWN-FIELD",
severity: "error",
message: `Persist include references unknown or server-only state '${name}'.`,
hint: "Persist only declared shared/client state fields.",
file: options.file,
});
for (const name of ast.persist.include)
if (/token|password|secret|otp|api.?key/i.test(name))
diagnostics.push({
code: "WRN-PERSIST-SENSITIVE",
severity: "error",
message: `Sensitive field '${name}' cannot be persisted.`,
hint: "Remove secrets, tokens, passwords, OTPs, and API keys from persistence.",
file: options.file,
});
}
return diagnostics;
}