release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+111 -11
View File
@@ -32,6 +32,14 @@ export interface DiagnoseOptions {
accessibility?: boolean;
}
function stripAsciiControlAndSpace(value: string): string {
let result = "";
for (const character of value) {
if (character.charCodeAt(0) > 0x20) result += character;
}
return result;
}
function maskJavaScriptTrivia(source: string): string {
let result = "";
let index = 0;
@@ -222,19 +230,76 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[]
}
let interactive = ast.states.length > 0 || ast.effects.length > 0 || ast.watches.length > 0;
const urlAttributes = new Set([
"href",
"src",
"action",
"formaction",
"poster",
"cite",
"background",
"xlink:href",
]);
walk(ast.view, (node) => {
if (node.type === "element" && node.attrs.some((attribute) => attribute.event))
interactive = true;
if (!options.accessibility || node.type !== "element") return;
if (node.type !== "element") return;
if (node.attrs.some((attribute) => attribute.event)) interactive = true;
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,
});
for (const attribute of node.attrs) {
if (attribute.event || attribute.boolean || !urlAttributes.has(attribute.name.toLowerCase()))
continue;
if (attribute.value.includes("{")) continue;
const value = stripAsciiControlAndSpace(attribute.value.trim()).toLowerCase();
if (
/^(?:javascript|vbscript|file):/.test(value) ||
/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(value)
) {
diagnostics.push({
code: "WRN-SEC-UNSAFE-URL",
severity: "error",
message: `Unsafe URL protocol in ${attribute.name} on <${node.tag}>.`,
hint: "Use a relative URL, https:, mailto:, tel:, or a framework-validated URL helper.",
file: options.file,
});
}
}
if (tag === "a") {
const target = node.attrs.find((attribute) => attribute.name === "target")?.value;
const rel = node.attrs.find((attribute) => attribute.name === "rel")?.value ?? "";
if (target === "_blank" && !/\bnoopener\b/i.test(rel)) {
diagnostics.push({
code: "WRN-SEC-BLANK-REL",
severity: "warning",
message: "A target=_blank link should include rel=noopener.",
hint: 'Add rel="noopener noreferrer".',
file: options.file,
});
}
}
if (!options.accessibility) return;
if (tag === "img") {
if (!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,
});
}
const hasWidth = node.attrs.some((attribute) => attribute.name === "width");
const hasHeight = node.attrs.some((attribute) => attribute.name === "height");
if (!hasWidth || !hasHeight) {
diagnostics.push({
code: "WRN-PERF-IMAGE-DIMENSIONS",
severity: "warning",
message: "Image width and height are required to prevent layout shifts.",
hint: "Declare intrinsic width and height, or use @wrnexus/image.",
file: options.file,
});
}
}
});
if (ast.runtime === "server" && interactive) {
@@ -291,6 +356,29 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[]
file: options.file,
});
}
if (
fn.runtime !== "server" &&
/\b(?:eval\s*\(|new\s+Function\s*\(|document\.write\s*\(|\.innerHTML\s*=|\.outerHTML\s*=|insertAdjacentHTML\s*\()/.test(
fn.body,
)
) {
diagnostics.push({
code: "WRN-SEC-DOM-SINK",
severity: "error",
message: `Client function '${fn.name}' uses an unsafe dynamic-code or HTML sink.`,
hint: "Use compiled templates, textContent, typed outputs, or a reviewed TrustedHTML sanitizer.",
file: options.file,
});
}
if (fn.runtime !== "server" && /\b(?:setTimeout|setInterval)\s*\(\s*["'`]/.test(fn.body)) {
diagnostics.push({
code: "WRN-SEC-STRING-TIMER",
severity: "error",
message: `Client function '${fn.name}' passes a string to a timer.`,
hint: "Pass a function instead of executable text.",
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)) {
@@ -317,6 +405,18 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[]
file: options.file,
});
}
if (
state.runtime !== "server" &&
/\b(?:process\.env|Bun\.env|Deno\.env|ctx\.env|import\.meta\.env)\b/.test(state.expr)
) {
diagnostics.push({
code: "WRN-SEC-SERVER-SECRET-SOURCE",
severity: "error",
message: `Browser-visible state '${state.name}' reads from a server environment source.`,
hint: "Move environment-backed values into server state and return only an explicitly safe result.",
file: options.file,
});
}
}
if (ast.persist) {
const stateNames = new Set(