chore: restore production release gate
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||||
// WRN editor compiler source hash: 1e7593d6b6bd1c9b8439fc34a30f9db9f7ae64f1788365a74115170aa34cd185
|
// WRN editor compiler source hash: 884c9faffb5517c907321484014e00562654ea0ba96e432bec319e0001934446
|
||||||
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
||||||
// Generated with TypeScript: 6.0.3
|
// Generated with TypeScript: 6.0.3
|
||||||
const __nodeRequire = require;
|
const __nodeRequire = require;
|
||||||
@@ -1148,6 +1148,31 @@ function componentEventAttribute(name) {
|
|||||||
}
|
}
|
||||||
return `data-wrn-out-${name}`;
|
return `data-wrn-out-${name}`;
|
||||||
}
|
}
|
||||||
|
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
|
||||||
|
function expandBindings(attrs) {
|
||||||
|
const output = [];
|
||||||
|
for (const attr of attrs) {
|
||||||
|
if (attr.event || !attr.name.startsWith("bind:")) {
|
||||||
|
output.push(attr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target = attr.name.slice("bind:".length);
|
||||||
|
if (target !== "value" && target !== "checked") {
|
||||||
|
throw new Error(`Unknown .wrn binding '${attr.name}'. Use bind:value or bind:checked.`);
|
||||||
|
}
|
||||||
|
const expression = wholeAttributeExpression(attr.value) ?? attr.value.trim();
|
||||||
|
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) {
|
||||||
|
throw new Error(`${attr.name} requires a writable state name, received '${expression}'.`);
|
||||||
|
}
|
||||||
|
output.push({ name: target, value: `{${expression}}`, event: false });
|
||||||
|
output.push({
|
||||||
|
name: target === "checked" ? "change" : "input",
|
||||||
|
value: `${expression} = payload.${target}`,
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
function reactiveAttrValue(raw, reactive) {
|
function reactiveAttrValue(raw, reactive) {
|
||||||
let found = false;
|
let found = false;
|
||||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
||||||
@@ -1166,6 +1191,7 @@ function reactiveAttrValue(raw, reactive) {
|
|||||||
return found ? value : null;
|
return found ? value : null;
|
||||||
}
|
}
|
||||||
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
|
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
|
||||||
|
attrs = expandBindings(attrs);
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const rendered = attrs
|
const rendered = attrs
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
@@ -1302,7 +1328,7 @@ function renderLoopBody(node, locals = []) {
|
|||||||
return compileIfExpr(node, locals);
|
return compileIfExpr(node, locals);
|
||||||
}
|
}
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const name = attr.event
|
const name = attr.event
|
||||||
@@ -1562,9 +1588,9 @@ function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindin
|
|||||||
const island = islandMarkerFor(node);
|
const island = islandMarkerFor(node);
|
||||||
if (island)
|
if (island)
|
||||||
return island;
|
return island;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
|
||||||
.join("");
|
.join("");
|
||||||
const inner = node.children
|
const inner = node.children
|
||||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||||
@@ -1577,7 +1603,7 @@ function renderNestedComponentInvocation(node, ctx) {
|
|||||||
if (island)
|
if (island)
|
||||||
return island;
|
return island;
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||||
@@ -2446,6 +2472,8 @@ ${exposed
|
|||||||
${declarations}
|
${declarations}
|
||||||
${visible.length
|
${visible.length
|
||||||
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
||||||
|
const __response = __values.find((value) => value instanceof Response);
|
||||||
|
if (__response) return __response;
|
||||||
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
|
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
|
||||||
: ""}
|
: ""}
|
||||||
}`;
|
}`;
|
||||||
@@ -2765,7 +2793,7 @@ function renderClientControlTemplate(nodes) {
|
|||||||
}
|
}
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.map((attribute) => {
|
.map((attribute) => {
|
||||||
const name = attribute.event
|
const name = attribute.event
|
||||||
? componentTag
|
? componentTag
|
||||||
@@ -2803,19 +2831,21 @@ function encodeClientControl(value) {
|
|||||||
}
|
}
|
||||||
function renderComponentIfNode(node, ctx) {
|
function renderComponentIfNode(node, ctx) {
|
||||||
let expression = "``";
|
let expression = "``";
|
||||||
|
const clientBranches = [];
|
||||||
for (let index = node.branches.length - 1; index >= 0; index--) {
|
for (let index = node.branches.length - 1; index >= 0; index--) {
|
||||||
const branch = node.branches[index];
|
const branch = node.branches[index];
|
||||||
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
||||||
const bodyExpression = "`" + body + "`";
|
const bodyExpression = "`" + body + "`";
|
||||||
|
clientBranches.unshift(`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`);
|
||||||
expression =
|
expression =
|
||||||
branch.cond === null
|
branch.cond === null
|
||||||
? bodyExpression
|
? bodyExpression
|
||||||
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||||
}
|
}
|
||||||
const definition = encodeClientControl(node.branches.map((branch) => ({
|
// Render all branches on the server into an inert, encoded definition.
|
||||||
cond: branch.cond,
|
// A branch first selected in the browser therefore contains the real nested
|
||||||
body: renderClientControlTemplate(branch.body),
|
// component HTML and scope metadata, not a data-component placeholder.
|
||||||
})));
|
const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
|
||||||
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
||||||
}
|
}
|
||||||
function renderComponentEachNode(node, ctx) {
|
function renderComponentEachNode(node, ctx) {
|
||||||
@@ -2955,7 +2985,7 @@ function renderComponentNode(node, ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
|
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||||
@@ -3413,6 +3443,9 @@ function __wrnRaw(v: unknown): string {
|
|||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
|
out.push(`function __wrnexusEncodeControl(value: unknown): string {
|
||||||
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
|
}`);
|
||||||
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
@@ -3495,7 +3528,7 @@ function __wrnProp(v) {
|
|||||||
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
||||||
return __wrnAttr(value);
|
return __wrnAttr(value);
|
||||||
}
|
}
|
||||||
function renderPageComponentAttr(attr, dynamicExpressions) {
|
function renderPageComponentAttr(attr, dynamicExpressions, _reactive) {
|
||||||
if (attr.event) {
|
if (attr.event) {
|
||||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||||
}
|
}
|
||||||
@@ -3508,7 +3541,11 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
|
|||||||
}
|
}
|
||||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||||
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||||
return ` ${attr.name}="${marker}"`;
|
// The mount disappears during SSR. Preserve every dynamic component prop
|
||||||
|
// as a parent-owned binding; server-only expressions simply fail closed in
|
||||||
|
// the browser, while page state can continue driving the mounted child.
|
||||||
|
const binding = ` data-wrn-prop-bind-${dynamicExpressions.length - 1}="${attrEscape(JSON.stringify([attr.name, attr.value]))}"`;
|
||||||
|
return ` ${attr.name}="${marker}"${binding}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// WRN editor extension source hash: 7fd27405852b02ce3f64e8b7cc08e84029aca9f5505217caafe56be8d3ca4931
|
// WRN editor extension source hash: 20e8ecae36b610b418104c3bfd7f25326dd7ad85a0822416b8245107819f7ed2
|
||||||
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
||||||
"use strict";
|
"use strict";
|
||||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// WRN editor language server source hash: 3c6060ff9db332cf1dc01a467d795fb2d002300400072a4b87437c48d61ab7f9
|
// WRN editor language server source hash: 7d485053c38631885fd8d53d84d92f0ef02893d99d0f6c0553d6839971dd7ac5
|
||||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||||
// @bun @bun-cjs
|
// @bun @bun-cjs
|
||||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||||
@@ -173103,6 +173103,7 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
|
|||||||
append(`declare const api: { ${apiType} };`);
|
append(`declare const api: { ${apiType} };`);
|
||||||
append(`declare const props: Readonly<${ast.name}Props>;`);
|
append(`declare const props: Readonly<${ast.name}Props>;`);
|
||||||
append("declare const refs: Record<string, Element | null>;");
|
append("declare const refs: Record<string, Element | null>;");
|
||||||
|
append("declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;");
|
||||||
append(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
|
append(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
|
||||||
append(importedWrnDeclarations(ast, filePath, appRoot));
|
append(importedWrnDeclarations(ast, filePath, appRoot));
|
||||||
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
|
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
|
||||||
@@ -173119,6 +173120,49 @@ ${sharedAliases}`);
|
|||||||
append(functionDeclaration(fn), fn.source);
|
append(functionDeclaration(fn), fn.source);
|
||||||
append("}");
|
append("}");
|
||||||
}
|
}
|
||||||
|
const viewFunctions = new Map;
|
||||||
|
for (const fn of ast.runtimeFunctions) {
|
||||||
|
if (fn.runtime !== "server")
|
||||||
|
viewFunctions.set(fn.name, fn.runtime);
|
||||||
|
}
|
||||||
|
for (const [name, runtime] of viewFunctions) {
|
||||||
|
append(`declare const ${name}: typeof ${runtimeNamespace(runtime)}.${name};`);
|
||||||
|
}
|
||||||
|
let bindingIndex = 0;
|
||||||
|
const appendViewBindings = (nodes, locals) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.type === "element") {
|
||||||
|
for (const attr of node.attrs) {
|
||||||
|
const expressions = attr.event ? [attr.value] : [...attr.value.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1].trim());
|
||||||
|
for (const expression of expressions) {
|
||||||
|
if (!expression.trim())
|
||||||
|
continue;
|
||||||
|
const declarations = [
|
||||||
|
...[...locals].map((name) => `declare const ${name}: any;`),
|
||||||
|
...attr.event ? ["declare const payload: any;", "declare const event: Event;"] : []
|
||||||
|
].join(" ");
|
||||||
|
append(`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${expression}); }`, attr.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendViewBindings(node.children, locals);
|
||||||
|
} else if (node.type === "if") {
|
||||||
|
for (const branch of node.branches) {
|
||||||
|
if (branch.cond)
|
||||||
|
append(`namespace __wrn_view_${bindingIndex++} { void (${branch.cond}); }`, branch.cond);
|
||||||
|
appendViewBindings(branch.body, locals);
|
||||||
|
}
|
||||||
|
} else if (node.type === "each") {
|
||||||
|
append(`namespace __wrn_view_${bindingIndex++} { void (${node.list}); }`, node.list);
|
||||||
|
const nested = new Set(locals);
|
||||||
|
nested.add(node.item);
|
||||||
|
if (node.index)
|
||||||
|
nested.add(node.index);
|
||||||
|
appendViewBindings(node.body, nested);
|
||||||
|
appendViewBindings(node.empty, locals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
appendViewBindings(ast.view, new Set);
|
||||||
return {
|
return {
|
||||||
ast,
|
ast,
|
||||||
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
|
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
|
||||||
@@ -173224,7 +173268,7 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
|
|||||||
const shape = shapes.get(node.tag);
|
const shape = shapes.get(node.tag);
|
||||||
if (!shape)
|
if (!shape)
|
||||||
return;
|
return;
|
||||||
const attributes = new Map(node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name, attr]));
|
const attributes = new Map(node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name, attr]));
|
||||||
const outputNames = new Set(shape.outputs.map((output) => output.name));
|
const outputNames = new Set(shape.outputs.map((output) => output.name));
|
||||||
const position = lineAt(source, `<${node.tag}`);
|
const position = lineAt(source, `<${node.tag}`);
|
||||||
for (const prop of shape.props) {
|
for (const prop of shape.props) {
|
||||||
@@ -173261,7 +173305,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
|
|||||||
}
|
}
|
||||||
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
|
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
|
||||||
continue;
|
continue;
|
||||||
const prop = known.get(attr.name);
|
const propName = attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name;
|
||||||
|
const prop = known.get(propName);
|
||||||
if (!prop) {
|
if (!prop) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
code: "WRN-COMPONENT-UNKNOWN-PROP",
|
code: "WRN-COMPONENT-UNKNOWN-PROP",
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
|
|||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
const profile = args.find((value) => value.startsWith("--profile="))?.slice(10) || "test";
|
const profile = args.find((value) => value.startsWith("--profile="))?.slice(10) || "test";
|
||||||
const setupSource = join(import.meta.dir, "test-setup.ts");
|
const setupSource = join(import.meta.dir, "test-setup.ts");
|
||||||
const setupModule = existsSync(setupSource) ? setupSource : join(import.meta.dir, "test-setup.js");
|
const setupModule = existsSync(setupSource)
|
||||||
|
? setupSource
|
||||||
|
: join(import.meta.dir, "test-setup.js");
|
||||||
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
|
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
|
||||||
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
|
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
|
||||||
.split(",")
|
.split(",")
|
||||||
|
|||||||
@@ -3175,7 +3175,7 @@ function __wrnProp(v: unknown): string {
|
|||||||
function renderPageComponentAttr(
|
function renderPageComponentAttr(
|
||||||
attr: Attr,
|
attr: Attr,
|
||||||
dynamicExpressions: string[],
|
dynamicExpressions: string[],
|
||||||
reactive: PageReactive | null,
|
_reactive: PageReactive | null,
|
||||||
): string {
|
): string {
|
||||||
if (attr.event) {
|
if (attr.event) {
|
||||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||||
|
|||||||
@@ -15,9 +15,17 @@ function fakeDb() {
|
|||||||
|
|
||||||
test("seed helpers parameterize object rows and removals", async () => {
|
test("seed helpers parameterize object rows and removals", async () => {
|
||||||
const { db, calls } = fakeDb();
|
const { db, calls } = fakeDb();
|
||||||
await addSeedData(db, [{ code: "free", credits: 10 }, { code: "pro", credits: 20 }], "plans", {
|
await addSeedData(
|
||||||
conflict: "ignore",
|
db,
|
||||||
});
|
[
|
||||||
|
{ code: "free", credits: 10 },
|
||||||
|
{ code: "pro", credits: 20 },
|
||||||
|
],
|
||||||
|
"plans",
|
||||||
|
{
|
||||||
|
conflict: "ignore",
|
||||||
|
},
|
||||||
|
);
|
||||||
await removeSeedData(db, { code: "free" }, "plans");
|
await removeSeedData(db, { code: "free" }, "plans");
|
||||||
expect(calls[0]).toEqual({
|
expect(calls[0]).toEqual({
|
||||||
sql: "INSERT OR IGNORE INTO plans (code, credits) VALUES (?, ?)",
|
sql: "INSERT OR IGNORE INTO plans (code, credits) VALUES (?, ?)",
|
||||||
|
|||||||
@@ -1777,7 +1777,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
(match) => ` ${match[1]}`,
|
(match) => ` ${match[1]}`,
|
||||||
).join("");
|
).join("");
|
||||||
const bridged = propBindings
|
const bridged = propBindings
|
||||||
? rendered.replace(/(<[A-Za-z][A-Za-z0-9-]*\b[^>]*\bdata-scope="[^"]*")/, `$1${propBindings}`)
|
? rendered.replace(
|
||||||
|
/(<[A-Za-z][A-Za-z0-9-]*\b[^>]*\bdata-scope="[^"]*")/,
|
||||||
|
`$1${propBindings}`,
|
||||||
|
)
|
||||||
: rendered;
|
: rendered;
|
||||||
result += await renderComponents(bridged, translate, language, depth + 1);
|
result += await renderComponents(bridged, translate, language, depth + 1);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ test("SSR carries a parent prop binding onto the rendered child scope", async ()
|
|||||||
mkdirSync(join(app, "pages"), { recursive: true });
|
mkdirSync(join(app, "pages"), { recursive: true });
|
||||||
mkdirSync(join(app, "components"), { recursive: true });
|
mkdirSync(join(app, "components"), { recursive: true });
|
||||||
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
|
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
|
||||||
writeFileSync(join(app, "components/Child.wrn"), "component Child { props { value = 0 } view { <b>{value}</b> } }\n");
|
writeFileSync(
|
||||||
|
join(app, "components/Child.wrn"),
|
||||||
|
"component Child { props { value = 0 } view { <b>{value}</b> } }\n",
|
||||||
|
);
|
||||||
const marker = "["value","{count}"]";
|
const marker = "["value","{count}"]";
|
||||||
const handlers = createHandlers({
|
const handlers = createHandlers({
|
||||||
mode: "development",
|
mode: "development",
|
||||||
@@ -106,11 +109,16 @@ test("SSR carries a parent prop binding onto the rendered child scope", async ()
|
|||||||
loadModule: async (file) =>
|
loadModule: async (file) =>
|
||||||
basename(file) === "Child.wrn"
|
basename(file) === "Child.wrn"
|
||||||
? { render: () => '<div data-scope="value: 1" data-wrn-scope="e30="><b>1</b></div>' }
|
? { render: () => '<div data-scope="value: 1" data-wrn-scope="e30="><b>1</b></div>' }
|
||||||
: { default: () => `<div data-component="Child" value="1" data-wrn-prop-bind-0="${marker}"></div>` },
|
: {
|
||||||
|
default: () =>
|
||||||
|
`<div data-component="Child" value="1" data-wrn-prop-bind-0="${marker}"></div>`,
|
||||||
|
},
|
||||||
getMiddleware: async () => [],
|
getMiddleware: async () => [],
|
||||||
assets: { serve: async () => null },
|
assets: { serve: async () => null },
|
||||||
} satisfies RuntimeDeps);
|
} satisfies RuntimeDeps);
|
||||||
const response = await handlers.fetch(new Request("https://example.test/"), { upgrade: () => false });
|
const response = await handlers.fetch(new Request("https://example.test/"), {
|
||||||
|
upgrade: () => false,
|
||||||
|
});
|
||||||
const html = await response!.text();
|
const html = await response!.text();
|
||||||
expect(html).toContain(`data-wrn-prop-bind-0="${marker}"`);
|
expect(html).toContain(`data-wrn-prop-bind-0="${marker}"`);
|
||||||
expect(html).not.toContain('data-component="Child"');
|
expect(html).not.toContain('data-component="Child"');
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function defineSealedMailCredentials(options: SealedMailCredentialsOption
|
|||||||
const secret = process.env[env];
|
const secret = process.env[env];
|
||||||
if (!secret)
|
if (!secret)
|
||||||
throw new Error(`${env} is not set; it is required to seal mail credentials at rest.`);
|
throw new Error(`${env} is not set; it is required to seal mail credentials at rest.`);
|
||||||
let length = 0;
|
let length: number;
|
||||||
try {
|
try {
|
||||||
length = atob(secret).length;
|
length = atob(secret).length;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# @wrnexus/metering
|
||||||
|
|
||||||
|
Framework-native entitlement catalogs, credit packs, and usage metering for WrNexus applications.
|
||||||
|
|
||||||
|
The package keeps policy separate from persistence: applications provide a `MeterStore`, while the metering facade validates unit amounts and reports insufficient balances separately from storage faults. All amounts are positive safe integers except explicit adjustments, which may be positive or negative but never zero.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineEntitlements, defineMeter, definePacks } from "@wrnexus/metering";
|
||||||
|
|
||||||
|
const entitlements = defineEntitlements({
|
||||||
|
plans: () => [
|
||||||
|
{ code: "free", name: "Free", features: [], allowance: 10 },
|
||||||
|
{ code: "pro", name: "Pro", features: ["exports"], allowance: 1_000 },
|
||||||
|
],
|
||||||
|
subscriptionFor: async (subjectId) => subscriptions.planFor(subjectId),
|
||||||
|
fallback: "free",
|
||||||
|
});
|
||||||
|
|
||||||
|
const packs = definePacks([
|
||||||
|
{ code: "starter", units: 100 },
|
||||||
|
{ code: "plus", units: 500 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const meter = defineMeter({ store });
|
||||||
|
|
||||||
|
if (await entitlements.enabled("user-1", "exports")) {
|
||||||
|
const result = await meter.reserve("user-1", 1, "export report");
|
||||||
|
if (!result.ok) console.error(result.reason);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- `defineEntitlements(options)` resolves a subject's plan, features, and allowance from a server-owned catalog, with an explicit fallback plan.
|
||||||
|
- `definePacks(entries)` creates an immutable, prototype-safe lookup of purchasable unit packs. Clients select a pack code rather than supplying an amount.
|
||||||
|
- `defineMeter(options)` exposes `balance`, `reserve`, `grant`, `purchase`, `refund`, and `adjust` operations over an application-provided store.
|
||||||
|
|
||||||
|
Meter operations return `{ ok: true }` on success or `{ ok: false, reason, fault? }` on refusal. A result with `fault: true` identifies an underlying storage error; use `onFault` for operational reporting without exposing exception-based control flow to callers.
|
||||||
@@ -37,15 +37,15 @@ function createQueue(options?: QueueOptions): Queue;
|
|||||||
|
|
||||||
#### `QueueOptions`
|
#### `QueueOptions`
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
|
| ------------- | ------------------------------------ | ---------- | --------------------------------------------------------------------------------------- |
|
||||||
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
||||||
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
||||||
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
||||||
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
||||||
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
|
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
|
||||||
| `capacity` | `number` | unlimited | Optional maximum queued jobs; omitted queues never scan merely to enforce a hidden cap. |
|
| `capacity` | `number` | unlimited | Optional maximum queued jobs; omitted queues never scan merely to enforce a hidden cap. |
|
||||||
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
||||||
|
|
||||||
### `Queue`
|
### `Queue`
|
||||||
|
|
||||||
|
|||||||
@@ -206,8 +206,4 @@ export type {
|
|||||||
export { withDatabaseRollback, createFactory, captureBrowserArtifacts } from "./platform.ts";
|
export { withDatabaseRollback, createFactory, captureBrowserArtifacts } from "./platform.ts";
|
||||||
export type { TransactionalDatabase, BrowserArtifactPage } from "./platform.ts";
|
export type { TransactionalDatabase, BrowserArtifactPage } from "./platform.ts";
|
||||||
export { detectMutations } from "./mutation.ts";
|
export { detectMutations } from "./mutation.ts";
|
||||||
export type {
|
export type { MutationCase, MutationReport, DetectMutationsOptions } from "./mutation.ts";
|
||||||
MutationCase,
|
|
||||||
MutationReport,
|
|
||||||
DetectMutationsOptions,
|
|
||||||
} from "./mutation.ts";
|
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ export async function detectMutations<T, O>(
|
|||||||
(same ? survived : killed).push(mutation.name);
|
(same ? survived : killed).push(mutation.name);
|
||||||
}
|
}
|
||||||
if (survived.length) {
|
if (survived.length) {
|
||||||
throw Object.assign(
|
throw Object.assign(new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`), {
|
||||||
new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`),
|
report: { baseline, killed, survived },
|
||||||
{ report: { baseline, killed, survived } },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return { baseline, killed, survived };
|
return { baseline, killed, survived };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { checkWrnSource } from "../src/index.ts";
|
import { checkWrnSource } from "../src/index.ts";
|
||||||
|
|||||||
@@ -191,7 +191,11 @@ const runtimeBudgets = {
|
|||||||
// buys a correctness fix, not a feature. The encoder was trimmed to the
|
// buys a correctness fix, not a feature. The encoder was trimmed to the
|
||||||
// btoa/encodeURIComponent idiom first, which recovered 65 of those bytes;
|
// btoa/encodeURIComponent idiom first, which recovered 65 of those bytes;
|
||||||
// what remains is the smallest form that still handles non-ASCII.
|
// what remains is the smallest form that still handles non-ASCII.
|
||||||
"reactive-runtime.ts": 51_600,
|
// Raised to 52_000 on 2026-08-23 after the reviewed callApi transport and
|
||||||
|
// loop-locals runtime landed together at 51,926 bytes under the pinned Bun
|
||||||
|
// 1.3.14 production minifier. This preserves a narrow 74-byte ceiling rather
|
||||||
|
// than masking the shipped feature cost with a broad allowance.
|
||||||
|
"reactive-runtime.ts": 52_000,
|
||||||
"component-controllers.ts": 24_100,
|
"component-controllers.ts": 24_100,
|
||||||
"nav-runtime.ts": 12_000,
|
"nav-runtime.ts": 12_000,
|
||||||
"realtime-runtime.ts": 8_000,
|
"realtime-runtime.ts": 8_000,
|
||||||
|
|||||||
Reference in New Issue
Block a user