chore: restore production release gate
This commit is contained in:
@@ -72,7 +72,9 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
|
||||
const watch = args.includes("--watch");
|
||||
const profile = args.find((value) => value.startsWith("--profile="))?.slice(10) || "test";
|
||||
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 browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
|
||||
.split(",")
|
||||
|
||||
@@ -3175,7 +3175,7 @@ function __wrnProp(v: unknown): string {
|
||||
function renderPageComponentAttr(
|
||||
attr: Attr,
|
||||
dynamicExpressions: string[],
|
||||
reactive: PageReactive | null,
|
||||
_reactive: PageReactive | null,
|
||||
): string {
|
||||
if (attr.event) {
|
||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
|
||||
@@ -15,9 +15,17 @@ function fakeDb() {
|
||||
|
||||
test("seed helpers parameterize object rows and removals", async () => {
|
||||
const { db, calls } = fakeDb();
|
||||
await addSeedData(db, [{ code: "free", credits: 10 }, { code: "pro", credits: 20 }], "plans", {
|
||||
conflict: "ignore",
|
||||
});
|
||||
await addSeedData(
|
||||
db,
|
||||
[
|
||||
{ code: "free", credits: 10 },
|
||||
{ code: "pro", credits: 20 },
|
||||
],
|
||||
"plans",
|
||||
{
|
||||
conflict: "ignore",
|
||||
},
|
||||
);
|
||||
await removeSeedData(db, { code: "free" }, "plans");
|
||||
expect(calls[0]).toEqual({
|
||||
sql: "INSERT OR IGNORE INTO plans (code, credits) VALUES (?, ?)",
|
||||
|
||||
@@ -1777,7 +1777,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
(match) => ` ${match[1]}`,
|
||||
).join("");
|
||||
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;
|
||||
result += await renderComponents(bridged, translate, language, depth + 1);
|
||||
} 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, "components"), { recursive: true });
|
||||
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 handlers = createHandlers({
|
||||
mode: "development",
|
||||
@@ -106,11 +109,16 @@ test("SSR carries a parent prop binding onto the rendered child scope", async ()
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Child.wrn"
|
||||
? { 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 () => [],
|
||||
assets: { serve: async () => null },
|
||||
} 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();
|
||||
expect(html).toContain(`data-wrn-prop-bind-0="${marker}"`);
|
||||
expect(html).not.toContain('data-component="Child"');
|
||||
|
||||
@@ -117,7 +117,7 @@ export function defineSealedMailCredentials(options: SealedMailCredentialsOption
|
||||
const secret = process.env[env];
|
||||
if (!secret)
|
||||
throw new Error(`${env} is not set; it is required to seal mail credentials at rest.`);
|
||||
let length = 0;
|
||||
let length: number;
|
||||
try {
|
||||
length = atob(secret).length;
|
||||
} 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`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
|
||||
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
||||
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
||||
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
||||
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
||||
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------------------------------------ | ---------- | --------------------------------------------------------------------------------------- |
|
||||
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
||||
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
||||
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
||||
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
||||
| `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. |
|
||||
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
||||
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
||||
|
||||
### `Queue`
|
||||
|
||||
|
||||
@@ -206,8 +206,4 @@ export type {
|
||||
export { withDatabaseRollback, createFactory, captureBrowserArtifacts } from "./platform.ts";
|
||||
export type { TransactionalDatabase, BrowserArtifactPage } from "./platform.ts";
|
||||
export { detectMutations } from "./mutation.ts";
|
||||
export type {
|
||||
MutationCase,
|
||||
MutationReport,
|
||||
DetectMutationsOptions,
|
||||
} from "./mutation.ts";
|
||||
export type { MutationCase, MutationReport, DetectMutationsOptions } from "./mutation.ts";
|
||||
|
||||
@@ -43,10 +43,9 @@ export async function detectMutations<T, O>(
|
||||
(same ? survived : killed).push(mutation.name);
|
||||
}
|
||||
if (survived.length) {
|
||||
throw Object.assign(
|
||||
new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`),
|
||||
{ report: { baseline, killed, survived } },
|
||||
);
|
||||
throw Object.assign(new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`), {
|
||||
report: { baseline, killed, survived },
|
||||
});
|
||||
}
|
||||
return { baseline, killed, survived };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { join } from "node:path";
|
||||
import { checkWrnSource } from "../src/index.ts";
|
||||
|
||||
Reference in New Issue
Block a user