release: WRNexusJS 0.8.3
Quality / quality (ubuntu-latest) (push) Failing after 12m9s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-03 19:47:30 +05:30
parent e8f630f12d
commit 4cebacadfe
156 changed files with 2608 additions and 473 deletions
+115 -17
View File
@@ -1,4 +1,10 @@
import { eraseFunctionTypes, type PageAst, type RuntimeFunctionDecl } from "@wrnexus/syntax";
import {
eraseFunctionTypes,
type PageAst,
type RuntimeFunctionDecl,
type StructuredImportDecl,
type ViewNode,
} from "@wrnexus/syntax";
const RESERVED_BINDINGS = new Set([
"await",
@@ -63,6 +69,111 @@ function safeIdentifier(name: string): boolean {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}
function identifierReferenced(source: string, name: string): boolean {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
}
function viewReferenceSource(nodes: ViewNode[]): string[] {
return nodes.flatMap((node): string[] => {
if (node.type === "text") return [node.value];
if (node.type === "each") {
return [
node.list,
node.key ?? "",
...viewReferenceSource(node.body),
...viewReferenceSource(node.empty),
];
}
if (node.type === "if") {
return node.branches.flatMap((branch) => [
branch.cond ?? "",
...viewReferenceSource(branch.body),
]);
}
return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
});
}
function browserReferenceSource(ast: PageAst, functions: RuntimeFunctionDecl[]): string {
return [
...functions.flatMap((fn) => [fn.source, fn.body]),
...ast.computed.map((entry) => entry.expr),
...ast.effects,
ast.lifecycle.mount ?? "",
ast.lifecycle.update ?? "",
ast.lifecycle.unmount ?? "",
...viewReferenceSource(ast.view),
].join("\n");
}
function renderSelectedImport(
entry: StructuredImportDecl,
source: string,
): { code: string; bindings: string[] } | null {
if (entry.typeOnly) return null;
const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
if (entry.source.endsWith(".wrn") && !isStore) return null;
const defaultImport =
entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport))
? entry.defaultImport
: undefined;
const namespaceImport =
entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport))
? entry.namespaceImport
: undefined;
const namedImports = entry.namedImports.filter(
(item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)),
);
const bindings = [
...(defaultImport ? [defaultImport] : []),
...(namespaceImport ? [namespaceImport] : []),
...namedImports.map((item) => item.local),
].filter(safeIdentifier);
const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
const sideEffectOnly =
!entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
if (!hasBindings && !sideEffectOnly) return null;
if (sideEffectOnly) return { code: entry.raw, bindings: [] };
const clauses: string[] = [];
if (defaultImport) clauses.push(defaultImport);
if (namespaceImport) clauses.push(`* as ${namespaceImport}`);
if (namedImports.length) {
clauses.push(
`{ ${namedImports
.map((item) =>
item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`,
)
.join(", ")} }`,
);
}
return {
code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
bindings,
};
}
function selectedBrowserImports(
ast: PageAst,
functions: RuntimeFunctionDecl[],
): { code: string; bindings: string[] }[] {
const referenceSource = browserReferenceSource(ast, functions);
return ast.structuredImports
.map((entry) => renderSelectedImport(entry, referenceSource))
.filter((entry): entry is { code: string; bindings: string[] } => entry !== null);
}
export function browserModuleRequired(ast: PageAst): boolean {
const functions = ast.runtimeFunctions.filter((fn) =>
["legacy", "client", "shared"].includes(fn.runtime),
);
return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
}
function functionEntry(
ast: PageAst,
fn: RuntimeFunctionDecl,
@@ -164,22 +275,9 @@ export function generateBrowserModule(ast: PageAst): string {
);
const functionNames = functions.map((fn) => fn.name);
const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
const storeImports = ast.structuredImports.filter(
(entry) =>
!entry.typeOnly && entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source),
);
const imports = ast.structuredImports
.filter((entry) => !entry.typeOnly)
.filter((entry) => !entry.source.endsWith(".wrn") || /(?:^|\/)stores?\//.test(entry.source))
.map((entry) => entry.raw)
.join("\n");
const importedBindings = storeImports
.flatMap((entry) => [
...(entry.defaultImport ? [entry.defaultImport] : []),
...(entry.namespaceImport ? [entry.namespaceImport] : []),
...entry.namedImports.map((item) => item.local),
])
.filter(safeIdentifier);
const selectedImports = selectedBrowserImports(ast, functions);
const imports = selectedImports.map((entry) => entry.code).join("\n");
const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
return `// generated WRNexusJS browser module for ${ast.name}
${imports}
export const __wrnexusClientFunctions = {