@aventara/next 0.0.0-stage → 1.0.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ import { AvConstructionError, } from "@aventara/core";
2
+ import { io } from "next/cache.js";
3
+ function ioBeforeCall() {
4
+ try {
5
+ return io();
6
+ }
7
+ catch {
8
+ return Promise.resolve();
9
+ }
10
+ }
11
+ const handlesBeforeBuild = (member) => `Aventara: \`${member}\` builds its handles on the Framework, which is built on first use; \`await ready()\` before building them.`;
12
+ const CLIENT_MEMBERS = ["tx", "transaction"];
13
+ const APPLICATION_MEMBERS = ["appTx", "transaction"];
14
+ export function refuseMemberClashes(framework) {
15
+ const clashes = [
16
+ ...clashesOf("avClient", framework.contracts.client.resources, CLIENT_MEMBERS),
17
+ ...clashesOf("avApp", framework.contracts.application.resources, APPLICATION_MEMBERS),
18
+ ];
19
+ if (clashes.length > 0) {
20
+ throw new AvConstructionError("contract-compilation", clashes.map(({ tree, member }) => ({
21
+ code: "COMPILER_RESERVED_RESOURCE_NAME",
22
+ path: `adapter.model.resources.${member}`,
23
+ message: `resource "${member}" clashes with ${tree}.${member}; rename the Resource or expose it under another name`,
24
+ })));
25
+ }
26
+ }
27
+ function clashesOf(tree, resources, members) {
28
+ return members
29
+ .filter((member) => Object.hasOwn(resources, member))
30
+ .map((member) => ({ tree, member }));
31
+ }
32
+ export function createServerOperations(source, requestState) {
33
+ const withRequestState = async (options) => {
34
+ if (options?.state !== undefined) {
35
+ return options;
36
+ }
37
+ const state = await requestState();
38
+ return state === undefined ? options : { ...options, state };
39
+ };
40
+ const asGiven = async (options) => options;
41
+ const clientTransaction = transactionOf(source, withRequestState);
42
+ const applicationTransaction = transactionOf(source, asGiven);
43
+ return {
44
+ avClient: operationTree("avClient", source, (framework) => framework.serverClient, withRequestState, {
45
+ tx: () => builders(source, "avClient.tx").serverClientTx,
46
+ transaction: () => clientTransaction,
47
+ }),
48
+ avApp: operationTree("avApp", source, (framework) => framework.application, asGiven, {
49
+ appTx: () => builders(source, "avApp.appTx").appTx,
50
+ transaction: () => applicationTransaction,
51
+ }),
52
+ };
53
+ }
54
+ function builders(source, member) {
55
+ const framework = source.current();
56
+ if (framework === undefined) {
57
+ throw new Error(handlesBeforeBuild(member));
58
+ }
59
+ return framework;
60
+ }
61
+ function transactionOf(source, fill) {
62
+ return async (operations, options) => {
63
+ await ioBeforeCall();
64
+ const framework = await source.built();
65
+ return framework.transaction(operations, await fill(options));
66
+ };
67
+ }
68
+ const NEVER_A_RESOURCE = new Set(["then", "__esModule"]);
69
+ function operationTree(name, source, layer, fill, members) {
70
+ const leaf = (path) => async (args, options) => {
71
+ await ioBeforeCall();
72
+ const framework = await source.built();
73
+ const call = memberAt(layer(framework), path);
74
+ if (typeof call !== "function") {
75
+ throw new TypeError(`${name}.${path.join(".")} is not a function`);
76
+ }
77
+ return call(args, await fill(options));
78
+ };
79
+ const resources = new Map();
80
+ return new Proxy(Object.freeze(Object.create(null)), {
81
+ get: (_target, key) => {
82
+ if (typeof key !== "string" || NEVER_A_RESOURCE.has(key)) {
83
+ return undefined;
84
+ }
85
+ const member = Object.hasOwn(members, key) ? members[key] : undefined;
86
+ if (member !== undefined) {
87
+ return member();
88
+ }
89
+ let resource = resources.get(key);
90
+ if (resource === undefined) {
91
+ resource = keyedNodes((family) => keyedNodes((variant) => leaf([key, family, variant])));
92
+ resources.set(key, resource);
93
+ }
94
+ return resource;
95
+ },
96
+ });
97
+ }
98
+ function keyedNodes(child) {
99
+ const children = new Map();
100
+ return new Proxy(Object.freeze(Object.create(null)), {
101
+ get: (_target, key) => {
102
+ if (typeof key !== "string" || NEVER_A_RESOURCE.has(key)) {
103
+ return undefined;
104
+ }
105
+ if (!children.has(key)) {
106
+ children.set(key, child(key));
107
+ }
108
+ return children.get(key);
109
+ },
110
+ });
111
+ }
112
+ function memberAt(root, path) {
113
+ let node = root;
114
+ for (const key of path) {
115
+ if (typeof node !== "object" ||
116
+ node === null ||
117
+ !Object.hasOwn(node, key)) {
118
+ return undefined;
119
+ }
120
+ node = node[key];
121
+ }
122
+ return node;
123
+ }
@@ -0,0 +1,25 @@
1
+ import type { AvFramework } from "@aventara/core";
2
+ /** One `api` path's build, shared by every copy of the module that calls `Aventara()` with the same options. */
3
+ export type FrameworkSlot = {
4
+ readonly fingerprint: string;
5
+ /** The build: once started, kept, a construction error included. */
6
+ framework: Promise<AvFramework> | undefined;
7
+ /** The Framework once built. */
8
+ built: AvFramework | undefined;
9
+ };
10
+ /** The `globalThis` key the build for `api` is held under. */
11
+ export declare const frameworkKey: (api: string) => symbol;
12
+ /**
13
+ * The slot for `api` in this process: the one a copy of the same module
14
+ * already holds (the same `fingerprint`), else a new one. A different
15
+ * fingerprint is refused in production and replaces the slot otherwise.
16
+ *
17
+ * @throws Error when another configuration holds `api` in production.
18
+ */
19
+ export declare function frameworkSlot(api: string, fingerprint: string): FrameworkSlot;
20
+ /**
21
+ * The options' fingerprint: their plain data in a stable order; a function or
22
+ * any other object than plain data and arrays counts only as present, since
23
+ * bundlers spell them differently in each layer of one app.
24
+ */
25
+ export declare function optionsFingerprint(options: unknown): string;
@@ -0,0 +1,65 @@
1
+ const KEY_PREFIX = "aventara.next.framework:";
2
+ const environment = () => globalThis.process?.env;
3
+ export const frameworkKey = (api) => Symbol.for(`${KEY_PREFIX}${api}`);
4
+ function claimed(api) {
5
+ return `Aventara: another configuration already serves the \`api\` path ${api === "" ? "/" : api} in this process; give each Aventara() its own \`api\` path.`;
6
+ }
7
+ export function frameworkSlot(api, fingerprint) {
8
+ const key = frameworkKey(api);
9
+ const global = globalThis;
10
+ const held = global[key];
11
+ if (held !== undefined) {
12
+ if (held.fingerprint === fingerprint) {
13
+ return held;
14
+ }
15
+ if (environment()?.NODE_ENV === "production") {
16
+ throw new Error(claimed(api));
17
+ }
18
+ }
19
+ const slot = {
20
+ fingerprint,
21
+ framework: undefined,
22
+ built: undefined,
23
+ };
24
+ global[key] = slot;
25
+ return slot;
26
+ }
27
+ export function optionsFingerprint(options) {
28
+ const seen = new Set();
29
+ const walk = (value) => {
30
+ switch (typeof value) {
31
+ case "function":
32
+ return "fn";
33
+ case "bigint":
34
+ return `${value}n`;
35
+ case "object":
36
+ break;
37
+ default:
38
+ return String(JSON.stringify(value));
39
+ }
40
+ if (value === null) {
41
+ return "null";
42
+ }
43
+ if (seen.has(value)) {
44
+ return "[cycle]";
45
+ }
46
+ seen.add(value);
47
+ if (Array.isArray(value)) {
48
+ let text = "[";
49
+ for (const member of value) {
50
+ text += `${walk(member)},`;
51
+ }
52
+ return `${text}]`;
53
+ }
54
+ const prototype = Object.getPrototypeOf(value);
55
+ if (prototype !== Object.prototype && prototype !== null) {
56
+ return "obj";
57
+ }
58
+ let text = "{";
59
+ for (const key of Object.keys(value).sort()) {
60
+ text += `${JSON.stringify(key)}:${walk(value[key])},`;
61
+ }
62
+ return `${text}}`;
63
+ };
64
+ return walk(options);
65
+ }
@@ -0,0 +1,25 @@
1
+ import type { StateSeed } from "./protocol-route.table.js";
2
+ import { type ProtocolHost } from "./route-handlers.factory.js";
3
+ /** The `globalThis` key the in-process path is registered under. */
4
+ export declare const UNIVERSAL_PATH_KEY: unique symbol;
5
+ /** The request the generated client's transport hands to its `fetch`. */
6
+ export type UniversalRequest = {
7
+ readonly method: string;
8
+ readonly headers: Readonly<Record<string, string>>;
9
+ readonly body: string;
10
+ };
11
+ /** What the generated client's transport reads of a `fetch` answer. */
12
+ export type UniversalAnswer = {
13
+ readonly status: number;
14
+ text(): Promise<string>;
15
+ };
16
+ /** The in-process path: the generated client's request in, the protocol's answer out. */
17
+ export type UniversalPath = (url: string, init: UniversalRequest) => Promise<UniversalAnswer>;
18
+ /**
19
+ * The in-process path over the protocol `host` resolves to: each request is
20
+ * answered by the bound member its route names, with the state `state` seeds,
21
+ * or by the protocol's absence answer.
22
+ */
23
+ export declare function universalPath(host: () => Promise<ProtocolHost>, state: StateSeed | undefined): UniversalPath;
24
+ /** Registers `path` as this server's in-process path for `@/aventara/universal`. */
25
+ export declare function registerUniversalPath(path: UniversalPath): void;
@@ -0,0 +1,31 @@
1
+ import { routeKey } from "./route-handlers.factory.js";
2
+ export const UNIVERSAL_PATH_KEY = Symbol.for("aventara.next.universal");
3
+ function outsideApi(url, api) {
4
+ return `Aventara: @/aventara/universal called ${url} on the server, which is not under the configured \`api\` path ${api === "" ? "/" : api}; run aventara-build --client again (npm run aventara:prepare).`;
5
+ }
6
+ export function universalPath(host, state) {
7
+ return async (url, init) => {
8
+ const { binding, api, routes } = await host();
9
+ if (url !== api && !url.startsWith(`${api}/`)) {
10
+ throw new Error(outsideApi(url, api));
11
+ }
12
+ const path = url.slice(api.length);
13
+ const request = {
14
+ method: init.method,
15
+ path,
16
+ headers: Object.fromEntries(new Headers(init.headers)),
17
+ body: { kind: "raw", content: init.body },
18
+ };
19
+ const answer = routes.get(routeKey(init.method, path));
20
+ const response = answer === undefined
21
+ ? binding.answerAbsent(request)
22
+ : await answer(request, state);
23
+ if (response === undefined) {
24
+ throw new Error(outsideApi(url, api));
25
+ }
26
+ return { status: response.status, text: async () => response.body };
27
+ };
28
+ }
29
+ export function registerUniversalPath(path) {
30
+ globalThis[UNIVERSAL_PATH_KEY] = path;
31
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Makes data an Aventara client answered safe to pass from the server to the
3
+ * browser: each `Date`, `bigint`, `Uint8Array` and decimal becomes a tagged
4
+ * plain object, which `avHydrate` (from `@/aventara/client`) revives in the
5
+ * browser, a decimal as the browser client's own `AvDecimal`. Arrays and
6
+ * plain objects are copied with their members dehydrated; any other value is
7
+ * returned as it is.
8
+ *
9
+ * Give it to TanStack Query as `dehydrate(queryClient, { serializeData: avDehydrate })`,
10
+ * or call it on an SWR `fallback` in a Server Component.
11
+ *
12
+ * @throws TypeError for a `Date` with no wire form (invalid, or a year past four digits).
13
+ */
14
+ export declare function avDehydrate(data: unknown): unknown;
@@ -0,0 +1,72 @@
1
+ import { AvProtocol } from "@aventara/core/protocol";
2
+ import { HYDRATION_FORMAT } from "./hydration-tag.format.js";
3
+ const { tagKey, valueKey, objectTag } = HYDRATION_FORMAT;
4
+ const DECIMAL = new RegExp(AvProtocol.scalarFormats.decimal);
5
+ const DATETIME = new RegExp(AvProtocol.scalarFormats.datetime);
6
+ const tagged = (tag, wire) => ({
7
+ [tagKey]: tag,
8
+ [valueKey]: wire,
9
+ });
10
+ function isPlainObject(value) {
11
+ const prototype = Object.getPrototypeOf(value);
12
+ return prototype === Object.prototype || prototype === null;
13
+ }
14
+ function decimalString(value) {
15
+ const methods = value;
16
+ if (typeof methods.toString !== "function" ||
17
+ typeof methods.toJSON !== "function") {
18
+ return undefined;
19
+ }
20
+ const digits = methods.toString.call(value);
21
+ return typeof digits === "string" &&
22
+ DECIMAL.test(digits) &&
23
+ methods.toJSON.call(value) === digits
24
+ ? digits
25
+ : undefined;
26
+ }
27
+ function base64Of(bytes) {
28
+ let binary = "";
29
+ for (const byte of bytes) {
30
+ binary += String.fromCharCode(byte);
31
+ }
32
+ return btoa(binary);
33
+ }
34
+ function datetimeOf(date) {
35
+ const wire = Number.isNaN(date.getTime()) ? "" : date.toISOString();
36
+ if (!DATETIME.test(wire)) {
37
+ throw new TypeError("avDehydrate: a Date that is invalid, or whose year is not four digits, has no wire form.");
38
+ }
39
+ return wire;
40
+ }
41
+ function dehydratedMembers(record) {
42
+ return Object.fromEntries(Object.entries(record).map(([key, member]) => [key, avDehydrate(member)]));
43
+ }
44
+ export function avDehydrate(data) {
45
+ switch (typeof data) {
46
+ case "bigint":
47
+ return tagged("bigint", data.toString(10));
48
+ case "object":
49
+ break;
50
+ default:
51
+ return data;
52
+ }
53
+ if (data === null) {
54
+ return data;
55
+ }
56
+ if (Array.isArray(data)) {
57
+ return data.map((member) => avDehydrate(member));
58
+ }
59
+ if (data instanceof Date) {
60
+ return tagged("datetime", datetimeOf(data));
61
+ }
62
+ if (data instanceof Uint8Array) {
63
+ return tagged("bytes", base64Of(data));
64
+ }
65
+ if (isPlainObject(data)) {
66
+ return Object.hasOwn(data, tagKey)
67
+ ? tagged(objectTag, dehydratedMembers(data))
68
+ : dehydratedMembers(data);
69
+ }
70
+ const decimal = decimalString(data);
71
+ return decimal === undefined ? data : tagged("decimal", decimal);
72
+ }
@@ -0,0 +1,15 @@
1
+ import type { AvProtocolRequest, AvProtocolResponse } from "@aventara/core/protocol";
2
+ import type { McpRequest } from "./mcp-binding.loader.js";
3
+ /** Zero bytes: the body of a request whose body the protocol does not read. */
4
+ export declare const NO_BYTES: Uint8Array<ArrayBuffer>;
5
+ /**
6
+ * A request URL's pathname, as the `Request` carries it: the query dropped and
7
+ * nothing URL-decoded.
8
+ */
9
+ export declare function pathnameOf(request: Request): string;
10
+ /** The request as the protocol reads it: path relative to the mount path, its headers, raw body. */
11
+ export declare function protocolRequest(request: Request, path: string, content: Uint8Array): AvProtocolRequest;
12
+ /** The request as `@aventara/mcp` reads it: absolute URL at the origin the caller used, its headers, raw body, no peer address. */
13
+ export declare function mcpRequest(request: Request, content: Uint8Array): McpRequest;
14
+ /** The protocol's answer as the route handler's `Response`: status, headers and body as they are. */
15
+ export declare function webResponse(answer: AvProtocolResponse): Response;
@@ -0,0 +1,37 @@
1
+ export const NO_BYTES = new Uint8Array();
2
+ export function pathnameOf(request) {
3
+ return new URL(request.url).pathname;
4
+ }
5
+ export function protocolRequest(request, path, content) {
6
+ return {
7
+ method: request.method,
8
+ path,
9
+ headers: Object.fromEntries(request.headers),
10
+ body: { kind: "raw", content },
11
+ };
12
+ }
13
+ export function mcpRequest(request, content) {
14
+ return {
15
+ method: request.method,
16
+ url: calledUrl(request),
17
+ headers: Object.fromEntries(request.headers),
18
+ body: content,
19
+ remoteAddress: undefined,
20
+ };
21
+ }
22
+ function firstOf(request, name) {
23
+ const value = request.headers.get(name)?.split(",")[0]?.trim();
24
+ return value === undefined || value === "" ? undefined : value;
25
+ }
26
+ function calledUrl(request) {
27
+ const url = new URL(request.url);
28
+ const host = firstOf(request, "x-forwarded-host") ?? firstOf(request, "host");
29
+ const scheme = firstOf(request, "x-forwarded-proto");
30
+ return `${scheme ?? url.protocol.slice(0, -1)}://${host ?? url.host}${url.pathname}${url.search}`;
31
+ }
32
+ export function webResponse(answer) {
33
+ return new Response(answer.body === "" ? null : answer.body, {
34
+ status: answer.status,
35
+ headers: answer.headers,
36
+ });
37
+ }
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@aventara/next",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-rc.9",
4
+ "license": "SEE LICENSE IN LICENSE",
5
+ "description": "Next.js App Router host integration for Aventara.",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": "^22.18.0 || >=24.2.0"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "LICENSE-ADDITIONAL-PERMISSION.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "dependencies": {
25
+ "@aventara/core": "1.0.0-rc.9",
26
+ "server-only": "0.0.1"
27
+ },
28
+ "peerDependencies": {
29
+ "@aventara/mcp": "1.0.0-rc.9",
30
+ "next": ">=16.4 <17",
31
+ "react": "^19.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@aventara/mcp": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "aventara": {
39
+ "host": {
40
+ "alias": "next",
41
+ "label": "Next.js",
42
+ "framework": "next",
43
+ "layout": {
44
+ "config": "src/aventara/config",
45
+ "adapter": "src/aventara/adapter",
46
+ "generated": "src/aventara/generated",
47
+ "client": "src/aventara/client.ts",
48
+ "universal": "src/aventara/universal.ts"
49
+ }
50
+ }
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "devDependencies": {
56
+ "@aventara/build": "1.0.0-rc.9",
57
+ "@aventara/client": "1.0.0-rc.9",
58
+ "@aventara/mcp": "1.0.0-rc.9",
59
+ "@aventara/testing": "1.0.0-rc.9",
60
+ "@modelcontextprotocol/sdk": "1.30.1",
61
+ "@tanstack/react-query": "5.104.2",
62
+ "@types/react": "19.3.0",
63
+ "jsdom": "30.1.2",
64
+ "next": "16.4.2",
65
+ "react": "19.3.0",
66
+ "react-dom": "19.3.0",
67
+ "swr": "2.5.1",
68
+ "typescript-6": "npm:typescript@6.0.3"
69
+ },
70
+ "scripts": {
71
+ "build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments --declaration false && tsc -p tsconfig.json --emitDeclarationOnly && node ../../scripts/build/declaration-comments.sanitizer.ts dist",
72
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
73
+ "test": "vitest run --typecheck --config ./vitest.config.mts --root .",
74
+ "lint": "biome lint src"
75
+ }
6
76
  }