Files
WRNexusJS/packages/syntax/src/v060.ts
T
2026-08-01 01:09:58 +05:30

461 lines
14 KiB
TypeScript

import { Lexer, LexError } from "./tokenizer.ts";
export type FunctionRuntime = "legacy" | "client" | "server" | "shared";
export type StateRuntime = "shared" | "client" | "server";
export type StoreKind = "global" | "page";
export interface FunctionParameterDecl {
name: string;
optional: boolean;
valueType?: string;
default?: string;
}
export interface RuntimeFunctionDecl {
name: string;
runtime: FunctionRuntime;
async: boolean;
parameters: FunctionParameterDecl[];
returnType?: string;
body: string;
source: string;
}
export interface OutputDecl {
name: string;
payload?: {
name: string;
valueType: string;
optional: boolean;
};
}
export interface StructuredImportDecl {
source: string;
typeOnly: boolean;
defaultImport?: string;
namespaceImport?: string;
namedImports: Array<{ imported: string; local: string; typeOnly: boolean }>;
raw: string;
}
export interface PersistDecl {
storage: "memory" | "session" | "local";
include: string[];
version: number;
migrations?: string;
validation?: string;
}
export interface StoreLifecycleDecl {
serverInit?: string;
clientInit?: string;
hydrate?: string;
dispose?: string;
}
function splitTopLevel(input: string, separator = ","): string[] {
const parts: string[] = [];
let start = 0;
let quote: string | null = null;
let angle = 0;
let square = 0;
let brace = 0;
let paren = 0;
for (let i = 0; i < input.length; i++) {
const c = input[i]!;
if (quote) {
if (c === "\\") i++;
else if (c === quote) quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
continue;
}
if (c === "<") angle++;
else if (c === ">" && angle > 0) angle--;
else if (c === "[") square++;
else if (c === "]" && square > 0) square--;
else if (c === "{") brace++;
else if (c === "}" && brace > 0) brace--;
else if (c === "(") paren++;
else if (c === ")" && paren > 0) paren--;
else if (c === separator && angle === 0 && square === 0 && brace === 0 && paren === 0) {
parts.push(input.slice(start, i).trim());
start = i + 1;
}
}
const tail = input.slice(start).trim();
if (tail) parts.push(tail);
return parts;
}
function findTopLevelChar(input: string, wanted: string): number {
let quote: string | null = null;
let angle = 0;
let square = 0;
let brace = 0;
let paren = 0;
for (let i = 0; i < input.length; i++) {
const c = input[i]!;
if (quote) {
if (c === "\\") i++;
else if (c === quote) quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
continue;
}
if (c === "<") angle++;
else if (c === ">" && angle > 0) angle--;
else if (c === "[") square++;
else if (c === "]" && square > 0) square--;
else if (c === "{") brace++;
else if (c === "}" && brace > 0) brace--;
else if (c === "(") paren++;
else if (c === ")" && paren > 0) paren--;
if (c === wanted && angle === 0 && square === 0 && brace === 0 && paren === 0) return i;
}
return -1;
}
function parseParameters(source: string): FunctionParameterDecl[] {
return splitTopLevel(source)
.filter(Boolean)
.map((entry) => {
const eq = findTopLevelChar(entry, "=");
const declaration = (eq >= 0 ? entry.slice(0, eq) : entry).trim();
const defaultValue = eq >= 0 ? entry.slice(eq + 1).trim() : undefined;
const colon = findTopLevelChar(declaration, ":");
const rawName = (colon >= 0 ? declaration.slice(0, colon) : declaration).trim();
const optional = rawName.endsWith("?");
const name = optional ? rawName.slice(0, -1).trim() : rawName;
const valueType = colon >= 0 ? declaration.slice(colon + 1).trim() : undefined;
return {
name,
optional,
...(valueType ? { valueType } : {}),
...(defaultValue ? { default: defaultValue } : {}),
};
});
}
function skipTrivia(source: string, start: number): number {
let i = start;
while (i < source.length) {
if (/\s/.test(source[i]!)) {
i++;
continue;
}
if (source.startsWith("//", i)) {
const end = source.indexOf("\n", i + 2);
i = end < 0 ? source.length : end + 1;
continue;
}
if (source.startsWith("/*", i)) {
const end = source.indexOf("*/", i + 2);
i = end < 0 ? source.length : end + 2;
continue;
}
break;
}
return i;
}
function readWord(source: string, start: number): { word: string; end: number } | null {
const match = /^[A-Za-z_$][\w$]*/.exec(source.slice(start));
return match ? { word: match[0], end: start + match[0].length } : null;
}
function readBalanced(
source: string,
start: number,
open: string,
close: string,
): { inner: string; end: number } {
if (source[start] !== open) throw new Error(`Expected '${open}' at offset ${start}`);
let depth = 0;
let quote: string | null = null;
for (let i = start; i < source.length; i++) {
const c = source[i]!;
if (quote) {
if (c === "\\") i++;
else if (c === quote) quote = null;
continue;
}
if (c === '"' || c === "'" || c === "`") {
quote = c;
continue;
}
if (c === open) depth++;
else if (c === close && --depth === 0) return { inner: source.slice(start + 1, i), end: i + 1 };
}
throw new Error(`Unbalanced '${open}${close}' starting at offset ${start}`);
}
export function parseRuntimeFunctions(source: string): RuntimeFunctionDecl[] {
const declarations: RuntimeFunctionDecl[] = [];
let i = 0;
while (i < source.length) {
i = skipTrivia(source, i);
const start = i;
let token = readWord(source, i);
if (!token) {
i++;
continue;
}
let runtime: FunctionRuntime = "legacy";
if (["client", "server", "shared"].includes(token.word)) {
runtime = token.word as FunctionRuntime;
i = skipTrivia(source, token.end);
token = readWord(source, i);
if (!token) continue;
}
let isAsync = false;
if (token.word === "async") {
isAsync = true;
i = skipTrivia(source, token.end);
token = readWord(source, i);
if (!token) continue;
}
if (token.word !== "function") {
i = token.end;
continue;
}
i = skipTrivia(source, token.end);
const nameToken = readWord(source, i);
if (!nameToken) throw new Error(`Expected function name at offset ${i}`);
const name = nameToken.word;
i = skipTrivia(source, nameToken.end);
const params = readBalanced(source, i, "(", ")");
i = skipTrivia(source, params.end);
let returnType: string | undefined;
if (source[i] === ":") {
i++;
const typeStart = i;
let quote: string | null = null;
let angle = 0;
let square = 0;
let paren = 0;
while (i < source.length) {
const c = source[i]!;
if (quote) {
if (c === "\\") i++;
else if (c === quote) quote = null;
i++;
continue;
}
if (c === '"' || c === "'" || c === "`") quote = c;
else if (c === "<") angle++;
else if (c === ">" && angle > 0) angle--;
else if (c === "[") square++;
else if (c === "]" && square > 0) square--;
else if (c === "(") paren++;
else if (c === ")" && paren > 0) paren--;
else if (c === "{" && angle === 0 && square === 0 && paren === 0) break;
i++;
}
returnType = source.slice(typeStart, i).trim();
}
i = skipTrivia(source, i);
const body = readBalanced(source, i, "{", "}");
i = body.end;
declarations.push({
name,
runtime,
async: isAsync,
parameters: parseParameters(params.inner),
...(returnType ? { returnType } : {}),
body: body.inner,
source: source.slice(start, body.end).trim(),
});
}
return declarations;
}
export function stripRuntimeFunctionModifiers(source: string, include: FunctionRuntime[]): string {
const allowed = new Set(include);
return parseRuntimeFunctions(source)
.filter((entry) => allowed.has(entry.runtime))
.map((entry) => {
const params = entry.parameters
.map(
(param) =>
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : ""}${param.default ? ` = ${param.default}` : ""}`,
)
.join(", ");
return `${entry.async ? "async " : ""}function ${entry.name}(${params})${entry.returnType ? `: ${entry.returnType}` : ""} {${entry.body}}`;
})
.join("\n\n");
}
export function parseOutputs(source: string): OutputDecl[] {
const out: OutputDecl[] = [];
let i = 0;
while (i < source.length) {
i = skipTrivia(source, i);
if (i >= source.length) break;
const nameToken = readWord(source, i);
if (!nameToken) throw new Error(`Expected output name at offset ${i}`);
i = skipTrivia(source, nameToken.end);
const args = readBalanced(source, i, "(", ")");
i = args.end;
const parameters = parseParameters(args.inner);
if (parameters.length > 1)
throw new Error(`Output '${nameToken.word}' accepts zero or one payload`);
const payload = parameters[0];
if (payload && !payload.valueType)
throw new Error(`Output '${nameToken.word}' payload requires a type`);
out.push({
name: nameToken.word,
...(payload
? {
payload: {
name: payload.name,
valueType: payload.valueType!,
optional: payload.optional,
},
}
: {}),
});
}
return out;
}
export function parseStructuredImports(imports: string[]): StructuredImportDecl[] {
return imports.map((raw) => {
const sourceMatch = /\sfrom\s+["']([^"']+)["']|^import\s+["']([^"']+)["']/.exec(raw);
const source = sourceMatch?.[1] ?? sourceMatch?.[2] ?? "";
const typeOnly = /^import\s+type\b/.test(raw);
const clause = raw
.replace(/^import\s+(?:type\s+)?/, "")
.replace(/\s+from\s+["'][^"']+["']\s*;?$/, "")
.trim();
const declaration: StructuredImportDecl = { source, typeOnly, namedImports: [], raw };
if (!clause || clause.startsWith('"') || clause.startsWith("'")) return declaration;
if (clause.startsWith("*")) {
declaration.namespaceImport = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause)?.[1];
return declaration;
}
let rest = clause;
if (!rest.startsWith("{")) {
const comma = findTopLevelChar(rest, ",");
declaration.defaultImport = (comma < 0 ? rest : rest.slice(0, comma)).trim();
rest = comma < 0 ? "" : rest.slice(comma + 1).trim();
}
const named = /^\{([\s\S]*)\}$/.exec(rest)?.[1];
if (named !== undefined) {
declaration.namedImports = splitTopLevel(named).map((item) => {
const localTypeOnly = /^type\s+/.test(item);
const cleaned = item.replace(/^type\s+/, "").trim();
const [imported, local] = cleaned.split(/\s+as\s+/);
return {
imported: imported!.trim(),
local: (local ?? imported)!.trim(),
typeOnly: typeOnly || localTypeOnly,
};
});
}
return declaration;
});
}
export function parseStateDeclarations(
source: string,
runtime: StateRuntime,
): Array<{ name: string; valueType?: string; expr: string; runtime: StateRuntime }> {
const lx = new Lexer(source);
const out: Array<{ name: string; valueType?: string; expr: string; runtime: StateRuntime }> = [];
while (lx.peek().type !== "eof") {
const nameToken = lx.next();
if (nameToken.type !== "ident")
throw new LexError(`Expected a state name at offset ${nameToken.pos}`);
let valueType: string | undefined;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault)
throw new LexError(`State '${nameToken.value}' requires an initializer`);
} else {
const eq = lx.next();
if (eq.type !== "eq")
throw new LexError(`Expected '=' after state '${nameToken.value}' at offset ${eq.pos}`);
}
out.push({
name: nameToken.value,
...(valueType ? { valueType } : {}),
expr: lx.readPropInitializer(),
runtime,
});
}
return out;
}
export function parseComputedDeclarations(
source: string,
): Array<{ name: string; valueType?: string; expr: string }> {
const lx = new Lexer(source);
const out: Array<{ name: string; valueType?: string; expr: string }> = [];
while (lx.peek().type !== "eof") {
const nameToken = lx.next();
if (nameToken.type !== "ident")
throw new LexError(`Expected a computed name at offset ${nameToken.pos}`);
let valueType: string | undefined;
if (lx.peek().type === "colon") {
lx.next();
const annotation = lx.readTypeAnnotation();
valueType = annotation.type;
if (!annotation.hasDefault)
throw new LexError(`Computed '${nameToken.value}' requires an expression`);
} else {
const eq = lx.next();
if (eq.type !== "eq")
throw new LexError(`Expected '=' after computed '${nameToken.value}' at offset ${eq.pos}`);
}
out.push({
name: nameToken.value,
...(valueType ? { valueType } : {}),
expr: lx.readPropInitializer(),
});
}
return out;
}
function nestedBlock(source: string, name: string): string | undefined {
const match = new RegExp(`\\b${name}\\s*\\{`).exec(source);
if (!match) return undefined;
const brace = source.indexOf("{", match.index);
return readBalanced(source, brace, "{", "}").inner.trim() || undefined;
}
export function parsePersist(source: string): PersistDecl {
const storage = /\bstorage\s*=\s*["'](memory|session|local)["']/.exec(source)?.[1] as
PersistDecl["storage"] | undefined;
const includeRaw = /\binclude\s*=\s*\[([\s\S]*?)\]/.exec(source)?.[1] ?? "";
const include = Array.from(includeRaw.matchAll(/["']([^"']+)["']/g), (match) => match[1]!);
const version = Number(/\bversion\s*=\s*(\d+)/.exec(source)?.[1] ?? "1");
const migrations = nestedBlock(source, "migrations");
const validation = nestedBlock(source, "validate");
return {
storage: storage ?? "memory",
include,
version,
...(migrations ? { migrations } : {}),
...(validation ? { validation } : {}),
};
}
export function parseStoreLifecycle(source: string): StoreLifecycleDecl {
const out: StoreLifecycleDecl = {};
for (const hook of ["serverInit", "clientInit", "hydrate", "dispose"] as const) {
const start = new RegExp(`\\b${hook}\\s*\\{`).exec(source);
if (!start) continue;
const brace = source.indexOf("{", start.index);
out[hook] = readBalanced(source, brace, "{", "}").inner;
}
return out;
}